Detailed changes
@@ -19,6 +19,8 @@ rustflags = [
"windows_slim_errors", # This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
"-C",
"target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows
+ "-C",
+ "link-arg=-fuse-ld=lld",
]
[env]
@@ -33,7 +33,6 @@ workspace-members = [
"zed_emmet",
"zed_glsl",
"zed_html",
- "perplexity",
"zed_proto",
"zed_ruff",
"slash_commands_example",
@@ -21,6 +21,9 @@ env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
+ DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
+ DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
+ ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
jobs:
job_spec:
@@ -52,9 +55,10 @@ jobs:
fi
# Specify anything which should skip full CI in this regex:
# - docs/
+ # - script/update_top_ranking_issues/
# - .github/ISSUE_TEMPLATE/
# - .github/workflows/ (except .github/workflows/ci.yml)
- SKIP_REGEX='^(docs/|\.github/(ISSUE_TEMPLATE|workflows/(?!ci)))'
+ SKIP_REGEX='^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!ci)))'
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -vP "$SKIP_REGEX") ]]; then
echo "run_tests=true" >> $GITHUB_OUTPUT
else
@@ -65,13 +69,13 @@ jobs:
else
echo "run_docs=false" >> $GITHUB_OUTPUT
fi
- if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep '^Cargo.lock') ]]; then
+ if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -P '^(Cargo.lock|script/.*licenses)') ]]; then
echo "run_license=true" >> $GITHUB_OUTPUT
else
echo "run_license=false" >> $GITHUB_OUTPUT
fi
NIX_REGEX='^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)'
- if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep "$NIX_REGEX") ]]; then
+ if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -P "$NIX_REGEX") ]]; then
echo "run_nix=true" >> $GITHUB_OUTPUT
else
echo "run_nix=false" >> $GITHUB_OUTPUT
@@ -390,7 +394,7 @@ jobs:
windows_tests:
timeout-minutes: 60
- name: (Windows) Run Tests
+ name: (Windows) Run Clippy and tests
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
@@ -411,11 +415,10 @@ jobs:
with:
clean: false
- - name: Setup Cargo and Rustup
+ - name: Configure CI
run: |
- mkdir -p ${{ env.CARGO_HOME }} -ErrorAction Ignore
- cp ./.cargo/ci-config.toml ${{ env.CARGO_HOME }}/config.toml
- .\script\install-rustup.ps1
+ New-Item -ItemType Directory -Path "./../.cargo" -Force
+ Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml"
- name: cargo clippy
run: |
@@ -430,18 +433,9 @@ jobs:
- name: Limit target directory size
run: ./script/clear-target-dir-if-larger-than.ps1 250
- # - name: Check dev drive space
- # working-directory: ${{ env.ZED_WORKSPACE }}
- # # `setup-dev-driver.ps1` creates a 100GB drive, with CI taking up ~45GB of the drive.
- # run: ./script/exit-ci-if-dev-drive-is-full.ps1 95
-
- # Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug.
- name: Clean CI config file
if: always()
- run: |
- if (Test-Path "${{ env.CARGO_HOME }}/config.toml") {
- Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force
- }
+ run: Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue
tests_pass:
name: Tests Pass
@@ -502,9 +496,6 @@ jobs:
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 }}
- ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
- DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
- DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -587,10 +578,6 @@ jobs:
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
needs: [linux_tests]
- env:
- ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
- DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
- DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -644,10 +631,6 @@ jobs:
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
needs: [linux_tests]
- env:
- ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
- DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
- DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -700,16 +683,12 @@ jobs:
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
needs: [linux_tests]
name: Build Zed on FreeBSD
- # env:
- # MYTOKEN : ${{ secrets.MYTOKEN }}
- # MYTOKEN2: "value2"
steps:
- uses: actions/checkout@v4
- name: Build FreeBSD remote-server
id: freebsd-build
uses: vmactions/freebsd-vm@c3ae29a132c8ef1924775414107a97cac042aad5 # v1.2.0
with:
- # envs: "MYTOKEN MYTOKEN2"
usesh: true
release: 13.5
copyback: true
@@ -763,12 +742,63 @@ jobs:
# excludes the final package to only cache dependencies
cachix-filter: "-zed-editor-[0-9.]*-nightly"
+ bundle-windows-x64:
+ timeout-minutes: 120
+ name: Create a Windows installer
+ runs-on: [self-hosted, Windows, X64]
+ if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
+ needs: [windows_tests]
+ env:
+ 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: Checkout repo
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ clean: false
+
+ - name: Determine version and release channel
+ working-directory: ${{ env.ZED_WORKSPACE }}
+ if: ${{ startsWith(github.ref, 'refs/tags/v') }}
+ run: |
+ # This exports RELEASE_CHANNEL into env (GITHUB_ENV)
+ script/determine-release-channel.ps1
+
+ - name: Build Zed installer
+ working-directory: ${{ env.ZED_WORKSPACE }}
+ run: script/bundle-windows.ps1
+
+ - name: Upload installer (x86_64) to Workflow - zed (run-bundling)
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
+ with:
+ name: ZedEditorUserSetup-x64-${{ github.event.pull_request.head.sha || github.sha }}.exe
+ path: ${{ env.SETUP_PATH }}
+
+ - name: Upload Artifacts to release
+ uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
+ # Re-enable when we are ready to publish windows preview releases
+ if: false && ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) && env.RELEASE_CHANNEL == 'preview' }} # upload only preview
+ with:
+ draft: true
+ prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
+ files: ${{ env.SETUP_PATH }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
auto-release-preview:
name: Auto release preview
if: |
startsWith(github.ref, 'refs/tags/v')
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
- needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, freebsd]
+ needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64, freebsd]
runs-on:
- self-hosted
- bundle
@@ -12,6 +12,9 @@ env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
+ ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
+ DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
+ DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
jobs:
style:
@@ -51,6 +54,32 @@ jobs:
- name: Run tests
uses: ./.github/actions/run_tests
+ windows-tests:
+ timeout-minutes: 60
+ name: Run tests on Windows
+ if: github.repository_owner == 'zed-industries'
+ runs-on: [self-hosted, Windows, X64]
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ clean: false
+
+ - name: Configure CI
+ run: |
+ New-Item -ItemType Directory -Path "./../.cargo" -Force
+ Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml"
+
+ - name: Run tests
+ uses: ./.github/actions/run_tests_windows
+
+ - name: Limit target directory size
+ run: ./script/clear-target-dir-if-larger-than.ps1 1024
+
+ - name: Clean CI config file
+ if: always()
+ run: Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue
+
bundle-mac:
timeout-minutes: 60
name: Create a macOS bundle
@@ -65,9 +94,6 @@ jobs:
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 }}
- DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
- DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
- ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
steps:
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -99,10 +125,6 @@ jobs:
runs-on:
- buildjet-16vcpu-ubuntu-2004
needs: tests
- env:
- DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
- DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
- ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -138,10 +160,6 @@ jobs:
runs-on:
- buildjet-16vcpu-ubuntu-2204-arm
needs: tests
- env:
- DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
- DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
- ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -172,9 +190,6 @@ jobs:
if: github.repository_owner == 'zed-industries'
runs-on: github-8vcpu-ubuntu-2404
needs: tests
- env:
- DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
- DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
name: Build Zed on FreeBSD
# env:
# MYTOKEN : ${{ secrets.MYTOKEN }}
@@ -213,10 +228,49 @@ jobs:
bundle-nix:
name: Build and cache Nix package
+ if: false
needs: tests
secrets: inherit
uses: ./.github/workflows/nix.yml
+ bundle-windows-x64:
+ timeout-minutes: 60
+ name: Create a Windows installer
+ if: github.repository_owner == 'zed-industries'
+ runs-on: [self-hosted, Windows, X64]
+ needs: windows-tests
+ env:
+ 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: Checkout repo
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ clean: false
+
+ - name: Set release channel to nightly
+ working-directory: ${{ env.ZED_WORKSPACE }}
+ 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"
+
+ - name: Build Zed installer
+ working-directory: ${{ env.ZED_WORKSPACE }}
+ run: script/bundle-windows.ps1
+
+ - name: Upload Zed Nightly
+ working-directory: ${{ env.ZED_WORKSPACE }}
+ run: script/upload-nightly.ps1 windows
+
update-nightly-tag:
name: Update nightly tag
if: github.repository_owner == 'zed-industries'
@@ -225,6 +279,7 @@ jobs:
- bundle-mac
- bundle-linux-x86
- bundle-linux-arm
+ - bundle-windows-x64
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -5,9 +5,7 @@
"build": {
"label": "Build Zed",
"command": "cargo",
- "args": [
- "build"
- ]
+ "args": ["build"]
}
},
{
@@ -16,9 +14,7 @@
"build": {
"label": "Build Zed",
"command": "cargo",
- "args": [
- "build"
- ]
+ "args": ["build"]
}
- },
+ }
]
@@ -40,7 +40,7 @@
},
"file_types": {
"Dockerfile": ["Dockerfile*[!dockerignore]"],
- "JSONC": ["assets/**/*.json", "renovate.json"],
+ "JSONC": ["**/assets/**/*.json", "renovate.json"],
"Git Ignore": ["dockerignore"]
},
"hard_tabs": false,
@@ -2,6 +2,34 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "acp"
+version = "0.1.0"
+dependencies = [
+ "agent_servers",
+ "agentic-coding-protocol",
+ "anyhow",
+ "assistant_tool",
+ "async-pipe",
+ "buffer_diff",
+ "editor",
+ "env_logger 0.11.8",
+ "futures 0.3.31",
+ "gpui",
+ "indoc",
+ "itertools 0.14.0",
+ "language",
+ "markdown",
+ "project",
+ "serde_json",
+ "settings",
+ "smol",
+ "tempfile",
+ "ui",
+ "util",
+ "workspace-hack",
+]
+
[[package]]
name = "activity_indicator"
version = "0.1.0"
@@ -107,6 +135,24 @@ dependencies = [
"zstd",
]
+[[package]]
+name = "agent_servers"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "collections",
+ "futures 0.3.31",
+ "gpui",
+ "paths",
+ "project",
+ "schemars",
+ "serde",
+ "settings",
+ "util",
+ "which 6.0.3",
+ "workspace-hack",
+]
+
[[package]]
name = "agent_settings"
version = "0.1.0"
@@ -130,8 +176,11 @@ dependencies = [
name = "agent_ui"
version = "0.1.0"
dependencies = [
+ "acp",
"agent",
+ "agent_servers",
"agent_settings",
+ "agentic-coding-protocol",
"anyhow",
"assistant_context",
"assistant_slash_command",
@@ -191,6 +240,7 @@ dependencies = [
"settings",
"smol",
"streaming_diff",
+ "task",
"telemetry",
"telemetry_events",
"terminal",
@@ -212,6 +262,24 @@ dependencies = [
"zed_llm_client",
]
+[[package]]
+name = "agentic-coding-protocol"
+version = "0.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e276b798eddd02562a339340a96919d90bbfcf78de118fdddc932524646fac7"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "derive_more 2.0.1",
+ "futures 0.3.31",
+ "log",
+ "parking_lot",
+ "schemars",
+ "semver",
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "ahash"
version = "0.7.8"
@@ -538,6 +606,8 @@ dependencies = [
"anyhow",
"futures 0.3.31",
"gpui",
+ "net",
+ "parking_lot",
"smol",
"tempfile",
"util",
@@ -608,7 +678,7 @@ dependencies = [
"anyhow",
"async-trait",
"collections",
- "derive_more",
+ "derive_more 0.99.19",
"extension",
"futures 0.3.31",
"gpui",
@@ -671,7 +741,7 @@ dependencies = [
"clock",
"collections",
"ctor",
- "derive_more",
+ "derive_more 0.99.19",
"futures 0.3.31",
"gpui",
"icons",
@@ -707,7 +777,7 @@ dependencies = [
"clock",
"collections",
"component",
- "derive_more",
+ "derive_more 0.99.19",
"editor",
"feature_flags",
"fs",
@@ -1164,7 +1234,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
- "derive_more",
+ "derive_more 0.99.19",
"gpui",
"parking_lot",
"rodio",
@@ -2857,7 +2927,7 @@ dependencies = [
"cocoa 0.26.0",
"collections",
"credentials_provider",
- "derive_more",
+ "derive_more 0.99.19",
"feature_flags",
"fs",
"futures 0.3.31",
@@ -3041,10 +3111,11 @@ dependencies = [
"context_server",
"ctor",
"dap",
+ "dap-types",
"dap_adapters",
"dashmap 6.1.0",
"debugger_ui",
- "derive_more",
+ "derive_more 0.99.19",
"editor",
"envy",
"extension",
@@ -3249,7 +3320,7 @@ name = "command_palette_hooks"
version = "0.1.0"
dependencies = [
"collections",
- "derive_more",
+ "derive_more 0.99.19",
"gpui",
"workspace-hack",
]
@@ -4324,11 +4395,15 @@ dependencies = [
"futures 0.3.31",
"fuzzy",
"gpui",
+ "hex",
+ "indoc",
"itertools 0.14.0",
"language",
"log",
"menu",
+ "notifications",
"parking_lot",
+ "parse_int",
"paths",
"picker",
"pretty_assertions",
@@ -4344,6 +4419,7 @@ dependencies = [
"tasks_ui",
"telemetry",
"terminal_view",
+ "text",
"theme",
"tree-sitter",
"tree-sitter-go",
@@ -4451,6 +4527,27 @@ dependencies = [
"syn 2.0.101",
]
+[[package]]
+name = "derive_more"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+ "unicode-xid",
+]
+
[[package]]
name = "derive_refineable"
version = "0.1.0"
@@ -4830,6 +4927,7 @@ dependencies = [
"tree-sitter-python",
"tree-sitter-rust",
"tree-sitter-typescript",
+ "tree-sitter-yaml",
"ui",
"unicode-script",
"unicode-segmentation",
@@ -5186,6 +5284,16 @@ dependencies = [
"libc",
]
+[[package]]
+name = "explorer_command_injector"
+version = "0.1.0"
+dependencies = [
+ "windows 0.61.1",
+ "windows-core 0.61.0",
+ "windows-registry 0.5.1",
+ "workspace-hack",
+]
+
[[package]]
name = "exr"
version = "1.73.0"
@@ -6138,7 +6246,7 @@ dependencies = [
"askpass",
"async-trait",
"collections",
- "derive_more",
+ "derive_more 0.99.19",
"futures 0.3.31",
"git2",
"gpui",
@@ -7155,7 +7263,7 @@ dependencies = [
"core-video",
"cosmic-text",
"ctor",
- "derive_more",
+ "derive_more 0.99.19",
"embed-resource",
"env_logger 0.11.8",
"etagere",
@@ -7701,7 +7809,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bytes 1.10.1",
- "derive_more",
+ "derive_more 0.99.19",
"futures 0.3.31",
"http 1.3.1",
"log",
@@ -8139,7 +8247,7 @@ dependencies = [
"async-trait",
"cargo_metadata",
"collections",
- "derive_more",
+ "derive_more 0.99.19",
"extension",
"fs",
"futures 0.3.31",
@@ -8898,6 +9006,7 @@ dependencies = [
"gpui",
"language",
"lsp",
+ "project",
"serde",
"serde_json",
"util",
@@ -9022,7 +9131,6 @@ dependencies = [
"itertools 0.14.0",
"language",
"lsp",
- "picker",
"project",
"release_channel",
"serde_json",
@@ -9577,12 +9685,11 @@ dependencies = [
[[package]]
name = "lsp-types"
version = "0.95.1"
-source = "git+https://github.com/zed-industries/lsp-types?rev=c9c189f1c5dd53c624a419ce35bc77ad6a908d18#c9c189f1c5dd53c624a419ce35bc77ad6a908d18"
+source = "git+https://github.com/zed-industries/lsp-types?rev=6add7052b598ea1f40f7e8913622c3958b009b60#6add7052b598ea1f40f7e8913622c3958b009b60"
dependencies = [
"bitflags 1.3.2",
"serde",
"serde_json",
- "serde_repr",
"url",
]
@@ -10229,6 +10336,18 @@ dependencies = [
"jni-sys",
]
+[[package]]
+name = "net"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-io",
+ "smol",
+ "tempfile",
+ "windows 0.61.1",
+ "workspace-hack",
+]
+
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
@@ -11182,6 +11301,15 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[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"
@@ -11334,14 +11462,6 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
-[[package]]
-name = "perplexity"
-version = "0.1.0"
-dependencies = [
- "serde",
- "zed_extension_api 0.6.0",
-]
-
[[package]]
name = "pest"
version = "2.8.0"
@@ -12233,6 +12353,7 @@ dependencies = [
"anyhow",
"askpass",
"async-trait",
+ "base64 0.22.1",
"buffer_diff",
"circular-buffer",
"client",
@@ -12278,6 +12399,7 @@ dependencies = [
"sha2",
"shellexpand 2.1.2",
"shlex",
+ "smallvec",
"smol",
"snippet",
"snippet_provider",
@@ -12533,6 +12655,7 @@ dependencies = [
"prost 0.9.0",
"prost-build 0.9.0",
"serde",
+ "typed-path",
"workspace-hack",
]
@@ -13196,6 +13319,7 @@ dependencies = [
"fs",
"futures 0.3.31",
"git",
+ "git2",
"git_hosting_providers",
"gpui",
"gpui_tokio",
@@ -14010,7 +14134,7 @@ dependencies = [
[[package]]
name = "scap"
version = "0.0.8"
-source = "git+https://github.com/zed-industries/scap?rev=08f0a01417505cc0990b9931a37e5120db92e0d0#08f0a01417505cc0990b9931a37e5120db92e0d0"
+source = "git+https://github.com/zed-industries/scap?rev=270538dc780f5240723233ff901e1054641ed318#270538dc780f5240723233ff901e1054641ed318"
dependencies = [
"anyhow",
"cocoa 0.25.0",
@@ -14057,10 +14181,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
dependencies = [
+ "chrono",
"dyn-clone",
"indexmap",
"ref-cast",
"schemars_derive",
+ "semver",
"serde",
"serde_json",
]
@@ -14582,15 +14708,19 @@ dependencies = [
"language",
"log",
"menu",
+ "notifications",
"paths",
"project",
"schemars",
"search",
"serde",
+ "serde_json",
"settings",
"theme",
"tree-sitter-json",
+ "tree-sitter-rust",
"ui",
+ "ui_input",
"util",
"workspace",
"workspace-hack",
@@ -16018,7 +16148,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
- "derive_more",
+ "derive_more 0.99.19",
"fs",
"futures 0.3.31",
"gpui",
@@ -17033,6 +17163,12 @@ dependencies = [
"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"
@@ -17346,6 +17482,7 @@ dependencies = [
"rand 0.8.5",
"regex",
"rust-embed",
+ "schemars",
"serde",
"serde_json",
"serde_json_lenient",
@@ -18355,7 +18492,6 @@ dependencies = [
"language",
"picker",
"project",
- "schemars",
"serde",
"settings",
"telemetry",
@@ -19550,6 +19686,8 @@ dependencies = [
"rustix 1.0.7",
"rustls 0.23.26",
"rustls-webpki 0.103.1",
+ "scap",
+ "schemars",
"scopeguard",
"sea-orm",
"sea-query-binder",
@@ -19596,7 +19734,9 @@ dependencies = [
"wasmtime-cranelift",
"wasmtime-environ",
"winapi",
+ "windows 0.61.1",
"windows-core 0.61.0",
+ "windows-future",
"windows-numerics",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
@@ -19943,10 +20083,11 @@ dependencies = [
[[package]]
name = "zed"
-version = "0.195.0"
+version = "0.196.0"
dependencies = [
"activity_indicator",
"agent",
+ "agent_servers",
"agent_settings",
"agent_ui",
"anyhow",
@@ -20141,9 +20282,9 @@ dependencies = [
[[package]]
name = "zed_llm_client"
-version = "0.8.5"
+version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c740e29260b8797ad252c202ea09a255b3cbc13f30faaf92fb6b2490336106e0"
+checksum = "6607f74dee2a18a9ce0f091844944a0e59881359ab62e0768fb0618f55d4c1dc"
dependencies = [
"anyhow",
"serde",
@@ -2,9 +2,11 @@
resolver = "2"
members = [
"crates/activity_indicator",
+ "crates/acp",
"crates/agent_ui",
"crates/agent",
"crates/agent_settings",
+ "crates/agent_servers",
"crates/anthropic",
"crates/askpass",
"crates/assets",
@@ -45,6 +47,7 @@ members = [
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/editor",
+ "crates/explorer_command_injector",
"crates/eval",
"crates/extension",
"crates/extension_api",
@@ -99,6 +102,7 @@ members = [
"crates/migrator",
"crates/mistral",
"crates/multi_buffer",
+ "crates/net",
"crates/node_runtime",
"crates/notifications",
"crates/ollama",
@@ -188,7 +192,6 @@ members = [
"extensions/emmet",
"extensions/glsl",
"extensions/html",
- "extensions/perplexity",
"extensions/proto",
"extensions/ruff",
"extensions/slash-commands-example",
@@ -215,10 +218,12 @@ edition = "2024"
# Workspace member crates
#
-activity_indicator = { path = "crates/activity_indicator" }
+acp = { path = "crates/acp" }
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 = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" }
askpass = { path = "crates/askpass" }
@@ -276,7 +281,6 @@ go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui", default-features = false, features = [
"http_client",
- "screen-capture",
] }
gpui_macros = { path = "crates/gpui_macros" }
gpui_tokio = { path = "crates/gpui_tokio" }
@@ -312,6 +316,7 @@ menu = { path = "crates/menu" }
migrator = { path = "crates/migrator" }
mistral = { path = "crates/mistral" }
multi_buffer = { path = "crates/multi_buffer" }
+net = { path = "crates/net" }
node_runtime = { path = "crates/node_runtime" }
notifications = { path = "crates/notifications" }
ollama = { path = "crates/ollama" }
@@ -399,6 +404,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
+agentic-coding-protocol = { version = "0.0.9" }
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -486,7 +492,7 @@ 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 = "c9c189f1c5dd53c624a419ce35bc77ad6a908d18" }
+lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "6add7052b598ea1f40f7e8913622c3958b009b60" }
markup5ever_rcdom = "0.3.0"
metal = "0.29"
moka = { version = "0.12.10", features = ["sync"] }
@@ -501,6 +507,7 @@ 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"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
@@ -540,7 +547,7 @@ rustc-demangle = "0.1.23"
rustc-hash = "2.1.0"
rustls = { version = "0.23.26" }
rustls-platform-verifier = "0.5.0"
-scap = { git = "https://github.com/zed-industries/scap", rev = "08f0a01417505cc0990b9931a37e5120db92e0d0", default-features = false }
+scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false }
schemars = { version = "1.0", features = ["indexmap2"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
@@ -625,8 +632,10 @@ wasmtime = { version = "29", default-features = false, features = [
] }
wasmtime-wasi = "29"
which = "6.0.0"
+windows-core = "0.61"
+wit-component = "0.221"
workspace-hack = "0.1.0"
-zed_llm_client = "= 0.8.5"
+zed_llm_client = "= 0.8.6"
zstd = "0.11"
[workspace.dependencies.async-stripe]
@@ -661,6 +670,7 @@ features = [
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Imaging_D2D",
+ "Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Credentials",
"Win32_Storage_FileSystem",
@@ -0,0 +1 @@
+<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Gemini</title><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>
@@ -1,3 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.75776 5.50003H8.49988C8.70769 5.50003 8.89518 5.62971 8.95455 5.82346C9.04049 6.01876 8.9858 6.23906 8.82956 6.37656L4.82971 9.87643C4.65315 10.0295 4.39488 10.042 4.20614 9.90455C4.01724 9.76705 3.94849 9.51706 4.04052 9.30301L5.24219 6.49999H3.48601C3.2918 6.49999 3.10524 6.37031 3.03197 6.17657C2.9587 5.98126 3.014 5.76096 3.1708 5.62346L7.17018 2.12375C7.34674 1.97001 7.60454 1.95829 7.7936 2.09547C7.98265 2.23275 8.0514 2.48218 7.95922 2.69695L6.75776 5.50003Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6.98749 1.67322C7.08029 1.71878 7.15543 1.79374 7.20121 1.88643C7.24699 1.97912 7.26084 2.08434 7.24061 2.18572L6.72812 4.75007H9.28122C9.37107 4.75006 9.45903 4.77588 9.53463 4.82445C9.61022 4.87302 9.67027 4.94229 9.70761 5.02402C9.74495 5.10574 9.75801 5.19648 9.74524 5.28542C9.73247 5.37437 9.69441 5.45776 9.63559 5.52569L5.57313 10.2131C5.50536 10.2912 5.41366 10.3447 5.31233 10.3653C5.211 10.3858 5.10571 10.3723 5.01285 10.3268C4.92 10.2813 4.8448 10.2064 4.79896 10.1137C4.75311 10.021 4.7392 9.9158 4.75939 9.81439L5.27188 7.25004H2.71878C2.62893 7.25005 2.54097 7.22423 2.46537 7.17566C2.38978 7.12709 2.32973 7.05782 2.29239 6.97609C2.25505 6.89437 2.24199 6.80363 2.25476 6.71469C2.26753 6.62574 2.30559 6.54235 2.36441 6.47443L6.42687 1.78697C6.49466 1.70879 6.58641 1.65524 6.68782 1.63467C6.78923 1.61409 6.89459 1.62765 6.98749 1.67322Z" fill="black"/>
</svg>
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clipboard"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg>
@@ -1 +1,12 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bug"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.49219 2.29071L6.41455 3.1933" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.61816 3.1933L10.508 2.29071" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.7042 5.89221V5.15749C5.69033 4.85975 5.73943 4.56239 5.84856 4.28336C5.95768 4.00434 6.12456 3.74943 6.33913 3.53402C6.55369 3.31862 6.81149 3.14718 7.09697 3.03005C7.38245 2.91292 7.68969 2.85254 8.00014 2.85254C8.3106 2.85254 8.61784 2.91292 8.90332 3.03005C9.18879 3.14718 9.44659 3.31862 9.66116 3.53402C9.87572 3.74943 10.0426 4.00434 10.1517 4.28336C10.2609 4.56239 10.31 4.85975 10.2961 5.15749V5.89221" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.00006 13.0426C6.13263 13.0426 4.60474 11.6005 4.60474 9.83792V8.23558C4.60474 7.66895 4.84322 7.12554 5.26772 6.72487C5.69221 6.32421 6.26796 6.09912 6.86829 6.09912H9.13184C9.73217 6.09912 10.3079 6.32421 10.7324 6.72487C11.1569 7.12554 11.3954 7.66895 11.3954 8.23558V9.83792C11.3954 11.6005 9.86749 13.0426 8.00006 13.0426Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.60452 6.25196C3.51235 6.13878 2.60693 5.17677 2.60693 3.9884" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.60462 8.81659H2.34106" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.4541 13.3186C2.4541 12.1302 3.41611 11.1116 4.60448 11.0551" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.0761 3.9884C13.0761 5.17677 12.1706 6.13878 11.0955 6.25196" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.6591 8.81659H11.3955" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.3955 11.0551C12.5839 11.1116 13.5459 12.1302 13.5459 13.3186" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -1,5 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.0001 1.33334H4.00008C3.64646 1.33334 3.30732 1.47382 3.05727 1.72387C2.80722 1.97392 2.66675 2.31305 2.66675 2.66668V13.3333C2.66675 13.687 2.80722 14.0261 3.05727 14.2762C3.30732 14.5262 3.64646 14.6667 4.00008 14.6667H12.0001C12.3537 14.6667 12.6928 14.5262 12.9429 14.2762C13.1929 14.0261 13.3334 13.687 13.3334 13.3333V4.66668L10.0001 1.33334Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.66659 6.5L6.33325 9.83333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6.33325 6.5L9.66659 9.83333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M3.03125 3V3.03125M3.03125 3.03125V9M3.03125 3.03125C3.03125 5 6 5 6 5M3.03125 9C3.03125 11 6 11 6 11M3.03125 9V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
- <rect x="8" y="2.5" width="6" height="5" rx="1.5" fill="black"/>
- <rect x="8" y="8.46875" width="6" height="5.0625" rx="1.5" fill="black"/>
+<path d="M3 3V3.03125M3 3.03125V9M3 3.03125C3 5 5.96875 5 5.96875 5M3 9C3 11 5.96875 11 5.96875 11M3 9V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<rect x="8" y="3" width="5.5" height="4" rx="1.5" fill="black"/>
+<rect x="8" y="9" width="5.5" height="4" rx="1.5" fill="black"/>
</svg>
@@ -1,6 +1,7 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 3.25C4.02614 3.25 4.25 3.02614 4.25 2.75C4.25 2.47386 4.02614 2.25 3.75 2.25C3.47386 2.25 3.25 2.47386 3.25 2.75C3.25 3.02614 3.47386 3.25 3.75 3.25ZM3.75 4.25C4.57843 4.25 5.25 3.57843 5.25 2.75C5.25 1.92157 4.57843 1.25 3.75 1.25C2.92157 1.25 2.25 1.92157 2.25 2.75C2.25 3.57843 2.92157 4.25 3.75 4.25Z" fill="black"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M8.25 3.25C8.52614 3.25 8.75 3.02614 8.75 2.75C8.75 2.47386 8.52614 2.25 8.25 2.25C7.97386 2.25 7.75 2.47386 7.75 2.75C7.75 3.02614 7.97386 3.25 8.25 3.25ZM8.25 4.25C9.07843 4.25 9.75 3.57843 9.75 2.75C9.75 1.92157 9.07843 1.25 8.25 1.25C7.42157 1.25 6.75 1.92157 6.75 2.75C6.75 3.57843 7.42157 4.25 8.25 4.25Z" fill="black"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 9.75C4.02614 9.75 4.25 9.52614 4.25 9.25C4.25 8.97386 4.02614 8.75 3.75 8.75C3.47386 8.75 3.25 8.97386 3.25 9.25C3.25 9.52614 3.47386 9.75 3.75 9.75ZM3.75 10.75C4.57843 10.75 5.25 10.0784 5.25 9.25C5.25 8.42157 4.57843 7.75 3.75 7.75C2.92157 7.75 2.25 8.42157 2.25 9.25C2.25 10.0784 2.92157 10.75 3.75 10.75Z" fill="black"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M3.25 3.75H4.25V5.59609C4.67823 5.35824 5.24991 5.25 6 5.25H7.25017C7.5262 5.25 7.75 5.02625 7.75 4.75V3.75H8.75V4.75C8.75 5.57832 8.07871 6.25 7.25017 6.25H6C5.14559 6.25 4.77639 6.41132 4.59684 6.56615C4.42571 6.71373 4.33877 6.92604 4.25 7.30651V8.25H3.25V3.75Z" fill="black"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="5" cy="12" r="1.25" stroke="black" stroke-width="1.5"/>
+<path d="M5 11V5" stroke="black" stroke-width="1.5"/>
+<path d="M5 10C5 10 5.5 8 7 8C7.73103 8 8.69957 8 9.50049 8C10.3289 8 11 7.32843 11 6.5V5" stroke="black" stroke-width="1.5"/>
+<circle cx="5" cy="4" r="1.25" stroke="black" stroke-width="1.5"/>
+<circle cx="11" cy="4" r="1.25" stroke="black" stroke-width="1.5"/>
</svg>
@@ -1 +1,7 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-tree"><path d="M21 12h-8"/><path d="M21 6H8"/><path d="M21 18h-8"/><path d="M3 6v4c0 1.1.9 2 2 2h3"/><path d="M3 10v6c0 1.1.9 2 2 2h3"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.5 8H9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.5 4L6.5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.5 12H9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 3.5V6.33333C3 7.25 3.72 8 4.6 8H7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 6V10.5C3 11.325 3.72 12 4.6 12H7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-location-edit-icon lucide-location-edit"><path d="M17.97 9.304A8 8 0 0 0 2 10c0 4.69 4.887 9.562 7.022 11.468"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/><circle cx="10" cy="10" r="3"/></svg>
@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5 4L10 7L5 10V4Z" fill="black" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-code"><path d="m13 13.5 2-2.5-2-2.5"/><path d="m21 21-4.3-4.3"/><path d="M9 8.5 7 11l2 2.5"/><circle cx="11" cy="11" r="8"/></svg>
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.8889 3H4.11111C3.49746 3 3 3.49746 3 4.11111V11.8889C3 12.5025 3.49746 13 4.11111 13H11.8889C12.5025 13 13 12.5025 13 11.8889V4.11111C13 3.49746 12.5025 3 11.8889 3Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.37939 10.3243H10.3794" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.64966 9.32837L7.64966 7.32837L5.64966 5.32837" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.4174 10.2159C10.5454 9.58974 10.4174 9.57261 11.3762 8.46959C11.9337 7.82822 12.335 7.09214 12.335 6.27818C12.335 5.28184 11.9309 4.32631 11.2118 3.62179C10.4926 2.91728 9.5171 2.52148 8.50001 2.52148C7.48291 2.52148 6.50748 2.91728 5.78828 3.62179C5.06909 4.32631 4.66504 5.28184 4.66504 6.27818C4.66504 6.9043 4.79288 7.65565 5.62379 8.46959C6.58253 9.59098 6.45474 9.58974 6.58257 10.2159M10.4174 10.2159L10.4174 12.2989C10.4174 12.9504 9.87836 13.4786 9.21329 13.4786H7.78674C7.12167 13.4786 6.58253 12.9504 6.58253 12.2989L6.58257 10.2159M10.4174 10.2159H8.50001H6.58257" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.5 2.5H6.5C6.22386 2.5 6 2.83579 6 3.25V4.75C6 5.16421 6.22386 5.5 6.5 5.5H9.5C9.77614 5.5 10 5.16421 10 4.75V3.25C10 2.83579 9.77614 2.5 9.5 2.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 3.5H11C11.2652 3.5 11.5196 3.61706 11.7071 3.82544C11.8946 4.03381 12 4.31643 12 4.61111V12.3889C12 12.6836 11.8946 12.9662 11.7071 13.1746C11.5196 13.3829 11.2652 13.5 11 13.5H5C4.73478 13.5 4.48043 13.3829 4.29289 13.1746C4.10536 12.9662 4 12.6836 4 12.3889V4.61111C4 4.31643 4.10536 4.03381 4.29289 3.82544C4.48043 3.61706 4.73478 3.5 5 3.5H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.50002 2.5H5C4.73478 2.5 4.48043 2.6159 4.29289 2.82219C4.10535 3.02848 4 3.30826 4 3.6V12.3999C4 12.6917 4.10535 12.9715 4.29289 13.1778C4.48043 13.3841 4.73478 13.5 5 13.5H11C11.2652 13.5 11.5195 13.3841 11.7071 13.1778C11.8946 12.9715 12 12.6917 12 12.3999V5.25L9.50002 2.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.3427 6.82379L6.65698 9.5095" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.65698 6.82379L9.3427 9.5095" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.7244 11.5299L9.01711 3.2922C8.91447 3.11109 8.76562 2.96045 8.58576 2.85564C8.4059 2.75084 8.20145 2.69562 7.99328 2.69562C7.7851 2.69562 7.58066 2.75084 7.40079 2.85564C7.22093 2.96045 7.07209 3.11109 6.96945 3.2922L2.26218 11.5299C2.15844 11.7096 2.10404 11.9135 2.1045 12.121C2.10495 12.3285 2.16026 12.5321 2.2648 12.7113C2.36934 12.8905 2.5194 13.0389 2.69978 13.1415C2.88015 13.244 3.08443 13.297 3.2919 13.2951H12.7064C12.9129 13.2949 13.1157 13.2404 13.2944 13.137C13.4731 13.0336 13.6215 12.8851 13.7247 12.7062C13.8278 12.5273 13.8821 12.3245 13.882 12.118C13.882 11.9115 13.8276 11.7087 13.7244 11.5299Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.99927 6.23425V8.58788" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.99927 10.9415H8.00492" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.4 12.5C12.6917 12.5 12.9715 12.3884 13.1778 12.1899C13.3841 11.9913 13.5 11.722 13.5 11.4412V6.14706C13.5 5.86624 13.3841 5.59693 13.1778 5.39836C12.9715 5.19979 12.6917 5.08824 12.4 5.08824H8.055C7.87103 5.08997 7.68955 5.04726 7.52717 4.96402C7.36478 4.88078 7.22668 4.75967 7.1255 4.61176L6.68 3.97647C6.57984 3.83007 6.44349 3.7099 6.28317 3.62674C6.12286 3.54358 5.94361 3.50003 5.7615 3.5H3.6C3.30826 3.5 3.02847 3.61155 2.82218 3.81012C2.61589 4.00869 2.5 4.27801 2.5 4.55882V11.4412C2.5 11.722 2.61589 11.9913 2.82218 12.1899C3.02847 12.3884 3.30826 12.5 3.6 12.5H12.4Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9 8.5L4.94864 12.6222C4.71647 12.8544 4.40157 12.9848 4.07323 12.9848C3.74488 12.9848 3.42999 12.8544 3.19781 12.6222C2.96564 12.39 2.83521 12.0751 2.83521 11.7468C2.83521 11.4185 2.96564 11.1036 3.19781 10.8714L7.5 6.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.8352 9.98474L13.8352 6.98474" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8352 7.42495L11.7634 6.4298C11.5533 6.23484 11.4353 5.97039 11.4352 5.69462V5.08526L10.1696 3.91022C9.54495 3.33059 8.69961 3.00261 7.81649 2.99722L5.83521 2.98474L6.35041 3.41108C6.71634 3.71233 7.00935 4.08216 7.21013 4.4962C7.4109 4.91024 7.51488 5.35909 7.51521 5.81316L7.5 6.5L9 8.5L9.5 8C9.5 8 9.87337 7.79457 10.0834 7.98959L11.1552 8.98474" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.5 12C6.65203 12.304 6.87068 12.5565 7.13399 12.7321C7.39729 12.9076 7.69597 13 8 13C8.30403 13 8.60271 12.9076 8.86601 12.7321C9.12932 12.5565 9.34797 12.304 9.5 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.63088 9.21874C3.56556 9.28556 3.52246 9.36865 3.50681 9.45791C3.49116 9.54718 3.50364 9.63876 3.54273 9.72152C3.58183 9.80429 3.64585 9.87467 3.72701 9.92409C3.80817 9.97352 3.90298 9.99987 3.99989 9.99994H12.0001C12.097 9.99997 12.1918 9.97372 12.273 9.92439C12.3542 9.87505 12.4183 9.80476 12.4575 9.72205C12.4967 9.63934 12.5093 9.54778 12.4938 9.45851C12.4783 9.36924 12.4353 9.2861 12.3701 9.21921C11.705 8.57941 11 7.89947 11 5.79994C11 5.05733 10.684 4.34514 10.1213 3.82004C9.55872 3.29494 8.79564 2.99994 7.99997 2.99994C7.20431 2.99994 6.44123 3.29494 5.87861 3.82004C5.31599 4.34514 4.99991 5.05733 4.99991 5.79994C4.99991 7.89947 4.2944 8.57941 3.63088 9.21874Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.5871 5.40582C12.8514 5.14152 13 4.78304 13 4.40922C13 4.03541 12.8516 3.67688 12.5873 3.41252C12.323 3.14816 11.9645 2.99962 11.5907 2.99957C11.2169 2.99953 10.8584 3.14798 10.594 3.41227L3.92098 10.0869C3.80488 10.2027 3.71903 10.3452 3.67097 10.5019L3.01047 12.678C2.99754 12.7212 2.99657 12.7672 3.00764 12.8109C3.01872 12.8547 3.04143 12.8946 3.07337 12.9265C3.1053 12.9584 3.14528 12.981 3.18905 12.992C3.23282 13.003 3.27875 13.002 3.32197 12.989L5.49849 12.329C5.65508 12.2813 5.79758 12.196 5.91349 12.0805L12.5871 5.40582Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 5L11 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,7 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3 5.66667V4.33333C3 3.97971 3.14048 3.64057 3.39052 3.39052C3.64057 3.14048 3.97971 3 4.33333 3H5.66667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.3333 3H11.6666C12.0202 3 12.3593 3.14048 12.6094 3.39052C12.8594 3.64057 12.9999 3.97971 12.9999 4.33333V5.66667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.9999 10.3333V11.6666C12.9999 12.0203 12.8594 12.3594 12.6094 12.6095C12.3593 12.8595 12.0202 13 11.6666 13H10.3333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.66667 13H4.33333C3.97971 13 3.64057 12.8595 3.39052 12.6095C3.14048 12.3594 3 12.0203 3 11.6666V10.3333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.5 8H10.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.57132 13.7143C5.20251 13.7143 5.71418 13.2026 5.71418 12.5714C5.71418 11.9403 5.20251 11.4286 4.57132 11.4286C3.94014 11.4286 3.42847 11.9403 3.42847 12.5714C3.42847 13.2026 3.94014 13.7143 4.57132 13.7143Z" fill="black"/>
+<path d="M10.2856 2.85712V5.71426M10.2856 5.71426V8.5714M10.2856 5.71426H13.1428M10.2856 5.71426H7.42847M10.2856 5.71426L12.1904 3.80949M10.2856 5.71426L8.38084 7.61906M10.2856 5.71426L12.1904 7.61906M10.2856 5.71426L8.38084 3.80949" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+</svg>
@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13 13L11 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.5 12C9.98528 12 12 9.98528 12 7.5C12 5.01472 9.98528 3 7.5 3C5.01472 3 3 5.01472 3 7.5C3 9.98528 5.01472 12 7.5 12Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.99487 8.44023L7.32821 7.10689L5.99487 5.77356" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.33838 10.2264H10.005" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.8889 3H4.11111C3.49746 3 3 3.49746 3 4.11111V11.8889C3 12.5025 3.49746 13 4.11111 13H11.8889C12.5025 13 13 12.5025 13 11.8889V4.11111C13 3.49746 12.5025 3 11.8889 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.99993 13.4804C11.0267 13.4804 13.4803 11.0267 13.4803 7.99999C13.4803 4.97325 11.0267 2.51959 7.99993 2.51959C4.97319 2.51959 2.51953 4.97325 2.51953 7.99999C2.51953 11.0267 4.97319 13.4804 7.99993 13.4804Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 3C6.71611 4.34807 6 6.13836 6 7.99999C6 9.86163 6.71611 11.6519 8 13C9.28387 11.6519 10 9.86163 10 7.99999C10 6.13836 9.28387 4.34807 8 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.24121 7.04827C4.52425 8.27022 6.22817 8.95178 7.99999 8.95178C9.77182 8.95178 11.4757 8.27022 12.7588 7.04827" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -1,3 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.9 8.00002C7.44656 8.00002 8.7 6.74637 8.7 5.20002C8.7 3.65368 7.44656 2.40002 5.9 2.40002C4.35344 2.40002 3.1 3.65368 3.1 5.20002C3.1 6.74637 4.35344 8.00002 5.9 8.00002ZM7.00906 9.05002H4.79094C2.69684 9.05002 1 10.7475 1 12.841C1 13.261 1.3395 13.6 1.75819 13.6H10.0409C10.4609 13.6 10.8 13.261 10.8 12.841C10.8 10.7475 9.1025 9.05002 7.00906 9.05002ZM11.4803 9.40002H9.86484C10.87 10.2247 11.5 11.4585 11.5 12.841C11.5 13.121 11.4169 13.3791 11.2812 13.6H14.3C14.6872 13.6 15 13.285 15 12.8803C15 10.9663 13.4338 9.40002 11.4803 9.40002ZM10.45 8.00002C11.8041 8.00002 12.9 6.90409 12.9 5.55002C12.9 4.19596 11.8041 3.10002 10.45 3.10002C9.90072 3.10002 9.39913 3.28716 8.9905 3.59243C9.2425 4.07631 9.4 4.61815 9.4 5.20002C9.4 5.97702 9.13903 6.69059 8.70897 7.27181C9.15281 7.72002 9.7675 8.00002 10.45 8.00002Z" fill="white"/>
+<path d="M6.79118 8.27005C8.27568 8.27005 9.4791 7.06663 9.4791 5.58214C9.4791 4.09765 8.27568 2.89423 6.79118 2.89423C5.30669 2.89423 4.10327 4.09765 4.10327 5.58214C4.10327 7.06663 5.30669 8.27005 6.79118 8.27005Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.79112 8.60443C4.19441 8.60443 2.08936 10.7095 2.08936 13.3062H11.4929C11.4929 10.7095 9.38784 8.60443 6.79112 8.60443Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M14.6984 12.9263C14.6984 10.8893 13.4895 8.99736 12.2806 8.09067C12.6779 7.79254 12.9957 7.40104 13.2057 6.95083C13.4157 6.50062 13.5115 6.00558 13.4846 5.50952C13.4577 5.01346 13.309 4.53168 13.0515 4.10681C12.7941 3.68194 12.4358 3.3271 12.0085 3.07367" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 2L6.72534 5.87534C6.6601 6.07367 6.5492 6.25392 6.40155 6.40155C6.25392 6.5492 6.07367 6.6601 5.87534 6.72534L2 8L5.87534 9.27466C6.07367 9.3399 6.25392 9.4508 6.40155 9.59845C6.5492 9.74608 6.6601 9.92633 6.72534 10.1247L8 14L9.27466 10.1247C9.3399 9.92633 9.4508 9.74608 9.59845 9.59845C9.74608 9.4508 9.92633 9.3399 10.1247 9.27466L14 8L10.1247 6.72534C9.92633 6.6601 9.74608 6.5492 9.59845 6.40155C9.4508 6.25392 9.3399 6.07367 9.27466 5.87534L8 2Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 2.93652L6.9243 6.20697C6.86924 6.37435 6.77565 6.52646 6.65105 6.65105C6.52646 6.77565 6.37435 6.86924 6.20697 6.9243L2.93652 8L6.20697 9.0757C6.37435 9.13076 6.52646 9.22435 6.65105 9.34895C6.77565 9.47354 6.86924 9.62565 6.9243 9.79306L8 13.0635L9.0757 9.79306C9.13076 9.62565 9.22435 9.47354 9.34895 9.34895C9.47354 9.22435 9.62565 9.13076 9.79306 9.0757L13.0635 8L9.79306 6.9243C9.62565 6.86924 9.47354 6.77565 9.34895 6.65105C9.22435 6.52646 9.13076 6.37435 9.0757 6.20697L8 2.93652Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -268,6 +268,14 @@
"ctrl-alt-t": "agent::NewThread"
}
},
+ {
+ "context": "AgentPanel && acp_thread",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-n": "agent::NewAcpThread",
+ "ctrl-alt-t": "agent::NewThread"
+ }
+ },
{
"context": "MessageEditor > Editor",
"bindings": {
@@ -306,6 +314,16 @@
"enter": "agent::AcceptSuggestedContext"
}
},
+ {
+ "context": "AcpThread > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "agent::Chat",
+ "up": "agent::PreviousHistoryMessage",
+ "down": "agent::NextHistoryMessage",
+ "shift-ctrl-r": "agent::OpenAgentDiff"
+ }
+ },
{
"context": "ThreadHistory",
"bindings": {
@@ -457,8 +475,8 @@
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-u": "editor::UndoSelection",
"ctrl-shift-u": "editor::RedoSelection",
- "f8": "editor::GoToDiagnostic",
- "shift-f8": "editor::GoToPreviousDiagnostic",
+ "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",
@@ -605,7 +623,9 @@
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
// or by tag:
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
- "f5": "debugger::Rerun"
+ "f5": "debugger::Rerun",
+ "ctrl-f4": "workspace::CloseActiveDock",
+ "ctrl-w": "workspace::CloseActiveDock"
}
},
{
@@ -836,6 +856,7 @@
"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",
@@ -978,6 +999,7 @@
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"bindings": {
+ "ctrl-p": "file_finder::Toggle",
"ctrl-shift-a": "file_finder::ToggleSplitMenu",
"ctrl-shift-i": "file_finder::ToggleFilterMenu"
}
@@ -1093,7 +1115,10 @@
"context": "KeymapEditor",
"use_key_equivalents": true,
"bindings": {
- "ctrl-f": "search::FocusSearch"
+ "ctrl-f": "search::FocusSearch",
+ "alt-find": "keymap_editor::ToggleKeystrokeSearch",
+ "alt-ctrl-f": "keymap_editor::ToggleKeystrokeSearch",
+ "alt-c": "keymap_editor::ToggleConflictFilter"
}
}
]
@@ -8,7 +8,7 @@
"shift-cmd-f5": "debugger::RerunSession",
"f6": "debugger::Pause",
"f7": "debugger::StepOver",
- "f11": "debugger::StepInto",
+ "ctrl-f11": "debugger::StepInto",
"shift-f11": "debugger::StepOut",
"home": "menu::SelectFirst",
"shift-pageup": "menu::SelectFirst",
@@ -309,6 +309,14 @@
"cmd-alt-t": "agent::NewThread"
}
},
+ {
+ "context": "AgentPanel && acp_thread",
+ "use_key_equivalents": true,
+ "bindings": {
+ "cmd-n": "agent::NewAcpThread",
+ "cmd-alt-t": "agent::NewThread"
+ }
+ },
{
"context": "MessageEditor > Editor",
"use_key_equivalents": true,
@@ -357,6 +365,16 @@
"ctrl--": "pane::GoBack"
}
},
+ {
+ "context": "AcpThread > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "agent::Chat",
+ "up": "agent::PreviousHistoryMessage",
+ "down": "agent::NextHistoryMessage",
+ "shift-ctrl-r": "agent::OpenAgentDiff"
+ }
+ },
{
"context": "ThreadHistory",
"bindings": {
@@ -510,8 +528,8 @@
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
"cmd-u": "editor::UndoSelection",
"cmd-shift-u": "editor::RedoSelection",
- "f8": "editor::GoToDiagnostic",
- "shift-f8": "editor::GoToPreviousDiagnostic",
+ "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",
@@ -659,7 +677,8 @@
"cmd-k shift-up": "workspace::SwapPaneUp",
"cmd-k shift-down": "workspace::SwapPaneDown",
"cmd-shift-x": "zed::Extensions",
- "f5": "debugger::Rerun"
+ "f5": "debugger::Rerun",
+ "cmd-w": "workspace::CloseActiveDock"
}
},
{
@@ -911,6 +930,7 @@
"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",
@@ -1078,6 +1098,7 @@
"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",
@@ -1193,7 +1214,8 @@
"context": "KeymapEditor",
"use_key_equivalents": true,
"bindings": {
- "cmd-f": "search::FocusSearch"
+ "cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch",
+ "cmd-alt-c": "keymap_editor::ToggleConflictFilter"
}
}
]
@@ -189,6 +189,8 @@
"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
@@ -218,35 +220,18 @@
"context": "vim_mode == normal",
"bindings": {
"ctrl-[": "editor::Cancel",
- "escape": "editor::Cancel",
":": "command_palette::Toggle",
"c": "vim::PushChange",
"shift-c": "vim::ChangeToEndOfLine",
"d": "vim::PushDelete",
"delete": "vim::DeleteRight",
- "shift-d": "vim::DeleteToEndOfLine",
- "shift-j": "vim::JoinLines",
"g shift-j": "vim::JoinLinesNoWhitespace",
"y": "vim::PushYank",
- "shift-y": "vim::YankLine",
- "i": "vim::InsertBefore",
- "shift-i": "vim::InsertFirstNonWhitespace",
- "a": "vim::InsertAfter",
- "shift-a": "vim::InsertEndOfLine",
"x": "vim::DeleteRight",
"shift-x": "vim::DeleteLeft",
- "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",
"ctrl-r": "vim::Redo",
- "r": "vim::PushReplace",
- "s": "vim::Substitute",
- "shift-s": "vim::SubstituteLine",
">": "vim::PushIndent",
"<": "vim::PushOutdent",
"=": "vim::PushAutoIndent",
@@ -256,11 +241,8 @@
"g ~": "vim::PushOppositeCase",
"g ?": "vim::PushRot13",
// "g ?": "vim::PushRot47",
- "\"": "vim::PushRegister",
"g w": "vim::PushRewrap",
"g q": "vim::PushRewrap",
- "ctrl-pagedown": "pane::ActivateNextItem",
- "ctrl-pageup": "pane::ActivatePreviousItem",
"insert": "vim::InsertBefore",
// tree-sitter related commands
"[ x": "vim::SelectLargerSyntaxNode",
@@ -327,6 +309,7 @@
"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",
@@ -363,18 +346,11 @@
}
},
{
- "context": "vim_mode == helix_normal && !menu",
+ "context": "(vim_mode == normal || vim_mode == helix_normal) && !menu",
"bindings": {
"escape": "editor::Cancel",
- "ctrl-[": "editor::Cancel",
- ":": "command_palette::Toggle",
- "left": "vim::WrappingLeft",
- "right": "vim::WrappingRight",
- "h": "vim::WrappingLeft",
- "l": "vim::WrappingRight",
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
- "y": "editor::Copy",
"shift-y": "vim::YankLine",
"i": "vim::InsertBefore",
"shift-i": "vim::InsertFirstNonWhitespace",
@@ -388,27 +364,40 @@
"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": "vim_mode == helix_normal && !menu",
+ "bindings": {
+ "ctrl-[": "editor::Cancel",
+ ":": "command_palette::Toggle",
+ "left": "vim::WrappingLeft",
+ "right": "vim::WrappingRight",
+ "h": "vim::WrappingLeft",
+ "l": "vim::WrappingRight",
+ "y": "editor::Copy",
+ "alt-;": "vim::OtherEnd",
"ctrl-r": "vim::Redo",
"f": ["vim::PushFindForward", { "before": false, "multiline": true }],
"t": ["vim::PushFindForward", { "before": true, "multiline": true }],
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true }],
"shift-t": ["vim::PushFindBackward", { "after": true, "multiline": true }],
- "r": "vim::PushReplace",
- "s": "vim::Substitute",
- "shift-s": "vim::SubstituteLine",
">": "vim::Indent",
"<": "vim::Outdent",
"=": "vim::AutoIndent",
"g u": "vim::PushLowercase",
"g shift-u": "vim::PushUppercase",
"g ~": "vim::PushOppositeCase",
- "\"": "vim::PushRegister",
"g q": "vim::PushRewrap",
"g w": "vim::PushRewrap",
- "ctrl-pagedown": "pane::ActivateNextItem",
- "ctrl-pageup": "pane::ActivatePreviousItem",
"insert": "vim::InsertBefore",
- ".": "vim::Repeat",
"alt-.": "vim::RepeatFind",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
@@ -428,7 +417,6 @@
"g h": "vim::StartOfLine",
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
"g e": "vim::EndOfDocument",
- "g y": "editor::GoToTypeDefinition",
"g r": "editor::FindAllReferences", // zed specific
"g t": "vim::WindowTop",
"g c": "vim::WindowMiddle",
@@ -478,7 +466,7 @@
}
},
{
- "context": "vim_mode == insert && showing_signature_help && !showing_completions",
+ "context": "(vim_mode == insert || vim_mode == normal) && showing_signature_help && !showing_completions",
"bindings": {
"ctrl-p": "editor::SignatureHelpPrevious",
"ctrl-n": "editor::SignatureHelpNext"
@@ -853,6 +841,7 @@
"i": "git_panel::FocusEditor",
"x": "git::ToggleStaged",
"shift-x": "git::StageAll",
+ "g x": "git::StageRange",
"shift-u": "git::UnstageAll"
}
},
@@ -228,7 +228,12 @@
// 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": true,
+ "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`
@@ -357,7 +362,9 @@
// 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
+ "show_sign_in": true,
+ // Whether to show the menus in the titlebar.
+ "show_menus": false
},
// Scrollbar related settings
"scrollbar": {
@@ -617,6 +624,8 @@
// 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.
@@ -746,8 +755,6 @@
"default_width": 380
},
"agent": {
- // Version of this setting.
- "version": "2",
// Whether the agent is enabled.
"enabled": true,
/// What completion mode to start new threads in, if available. Can be 'normal' or 'burn'.
@@ -810,6 +817,7 @@
"edit_file": true,
"fetch": true,
"list_directory": true,
+ "project_notifications": true,
"move_path": true,
"now": true,
"find_path": true,
@@ -829,6 +837,7 @@
"diagnostics": true,
"fetch": true,
"list_directory": true,
+ "project_notifications": true,
"now": true,
"find_path": true,
"read_file": true,
@@ -855,7 +864,15 @@
// its response, or needs user input.
// Default: false
- "play_sound_when_agent_done": 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
},
// The settings for slash commands.
"slash_commands": {
@@ -1118,6 +1135,7 @@
"**/.svn",
"**/.hg",
"**/.jj",
+ "**/.repo",
"**/CVS",
"**/.DS_Store",
"**/Thumbs.db",
@@ -1140,16 +1158,14 @@
// Control whether the git blame information is shown inline,
// in the currently focused line.
"inline_blame": {
- "enabled": true
+ "enabled": true,
// Sets a delay after which the inline blame information is shown.
// Delay is restarted with every cursor movement.
- // "delay_ms": 600
- //
+ "delay_ms": 0,
// Whether or not to display the git commit summary on the same line.
- // "show_commit_summary": false
- //
+ "show_commit_summary": false,
// The minimum column number to show the inline blame information at
- // "min_column": 0
+ "min_column": 0
},
// How git hunks are displayed visually in the editor.
// This setting can take two values:
@@ -1292,6 +1308,8 @@
// 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": false,
// 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
@@ -1348,7 +1366,7 @@
// 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,
@@ -1360,11 +1378,26 @@
// 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
+ "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,
+ "max_scroll_history_lines": 10000,
+ // 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
},
"code_actions_on_format": {},
// Settings related to running tasks.
@@ -1576,6 +1609,9 @@
"use_on_type_format": false,
"allow_rewrap": "anywhere",
"soft_wrap": "editor_width",
+ "completions": {
+ "words": "disabled"
+ },
"prettier": {
"allowed": true
}
@@ -1589,6 +1625,9 @@
}
},
"Plain Text": {
+ "completions": {
+ "words": "disabled"
+ },
"allow_rewrap": "anywhere"
},
"Python": {
@@ -1656,7 +1695,6 @@
// Different settings for specific language models.
"language_models": {
"anthropic": {
- "version": "1",
"api_url": "https://api.anthropic.com"
},
"google": {
@@ -1666,7 +1704,6 @@
"api_url": "http://localhost:11434"
},
"openai": {
- "version": "1",
"api_url": "https://api.openai.com/v1"
},
"open_router": {
@@ -1819,6 +1856,8 @@
"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,
@@ -3,13 +3,6 @@
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[
- {
- "label": "Debug active PHP file",
- "adapter": "PHP",
- "program": "$ZED_FILE",
- "request": "launch",
- "cwd": "$ZED_WORKTREE_ROOT"
- },
{
"label": "Debug active Python file",
"adapter": "Debugpy",
@@ -0,0 +1,47 @@
+[package]
+name = "acp"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/acp.rs"
+doctest = false
+
+[features]
+test-support = ["gpui/test-support", "project/test-support"]
+gemini = []
+
+[dependencies]
+agent_servers.workspace = true
+agentic-coding-protocol.workspace = true
+anyhow.workspace = true
+assistant_tool.workspace = true
+buffer_diff.workspace = true
+editor.workspace = true
+futures.workspace = true
+gpui.workspace = true
+itertools.workspace = true
+language.workspace = true
+markdown.workspace = true
+project.workspace = true
+settings.workspace = true
+smol.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace-hack.workspace = true
+
+[dev-dependencies]
+async-pipe.workspace = true
+env_logger.workspace = true
+gpui = { workspace = true, "features" = ["test-support"] }
+indoc.workspace = true
+project = { workspace = true, "features" = ["test-support"] }
+serde_json.workspace = true
+tempfile.workspace = true
+util.workspace = true
+settings.workspace = true
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -0,0 +1,1926 @@
+pub use acp::ToolCallId;
+use agent_servers::AgentServer;
+use agentic_coding_protocol::{self as acp, UserMessageChunk};
+use anyhow::{Context as _, Result, anyhow};
+use assistant_tool::ActionLog;
+use buffer_diff::BufferDiff;
+use editor::{MultiBuffer, PathKey};
+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, Capability, LanguageRegistry, OffsetRangeExt as _, Point,
+ text_diff,
+};
+use markdown::Markdown;
+use project::{AgentLocation, Project};
+use std::collections::HashMap;
+use std::error::Error;
+use std::fmt::{Formatter, Write};
+use std::{
+ fmt::Display,
+ mem,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+use ui::{App, IconName};
+use util::ResultExt;
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct UserMessage {
+ pub content: Entity<Markdown>,
+}
+
+impl UserMessage {
+ pub fn from_acp(
+ message: &acp::SendUserMessageParams,
+ language_registry: Arc<LanguageRegistry>,
+ cx: &mut App,
+ ) -> Self {
+ let mut md_source = String::new();
+
+ for chunk in &message.chunks {
+ match chunk {
+ UserMessageChunk::Text { text } => md_source.push_str(&text),
+ UserMessageChunk::Path { path } => {
+ write!(&mut md_source, "{}", MentionPath(&path)).unwrap()
+ }
+ }
+ }
+
+ Self {
+ content: cx
+ .new(|cx| Markdown::new(md_source.into(), Some(language_registry), None, cx)),
+ }
+ }
+
+ fn to_markdown(&self, cx: &App) -> String {
+ format!("## User\n\n{}\n\n", self.content.read(cx).source())
+ }
+}
+
+#[derive(Debug)]
+pub struct MentionPath<'a>(&'a Path);
+
+impl<'a> MentionPath<'a> {
+ const PREFIX: &'static str = "@file:";
+
+ pub fn new(path: &'a Path) -> Self {
+ MentionPath(path)
+ }
+
+ pub fn try_parse(url: &'a str) -> Option<Self> {
+ let path = url.strip_prefix(Self::PREFIX)?;
+ Some(MentionPath(Path::new(path)))
+ }
+
+ pub fn path(&self) -> &Path {
+ self.0
+ }
+}
+
+impl Display for MentionPath<'_> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "[@{}]({}{})",
+ self.0.file_name().unwrap_or_default().display(),
+ Self::PREFIX,
+ self.0.display()
+ )
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct AssistantMessage {
+ pub chunks: Vec<AssistantMessageChunk>,
+}
+
+impl AssistantMessage {
+ 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(Clone, Debug, Eq, PartialEq)]
+pub enum AssistantMessageChunk {
+ Text { chunk: Entity<Markdown> },
+ Thought { chunk: Entity<Markdown> },
+}
+
+impl AssistantMessageChunk {
+ pub fn from_acp(
+ chunk: acp::AssistantMessageChunk,
+ language_registry: Arc<LanguageRegistry>,
+ cx: &mut App,
+ ) -> Self {
+ match chunk {
+ acp::AssistantMessageChunk::Text { text } => Self::Text {
+ chunk: cx.new(|cx| Markdown::new(text.into(), Some(language_registry), None, cx)),
+ },
+ acp::AssistantMessageChunk::Thought { thought } => Self::Thought {
+ chunk: cx
+ .new(|cx| Markdown::new(thought.into(), Some(language_registry), None, cx)),
+ },
+ }
+ }
+
+ pub fn from_str(chunk: &str, language_registry: Arc<LanguageRegistry>, cx: &mut App) -> Self {
+ Self::Text {
+ chunk: cx.new(|cx| {
+ Markdown::new(chunk.to_owned().into(), Some(language_registry), None, cx)
+ }),
+ }
+ }
+
+ fn to_markdown(&self, cx: &App) -> String {
+ match self {
+ Self::Text { chunk } => chunk.read(cx).source().to_string(),
+ Self::Thought { chunk } => {
+ format!("<thinking>\n{}\n</thinking>", chunk.read(cx).source())
+ }
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum AgentThreadEntry {
+ UserMessage(UserMessage),
+ AssistantMessage(AssistantMessage),
+ ToolCall(ToolCall),
+}
+
+impl AgentThreadEntry {
+ 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(too_call) => too_call.to_markdown(cx),
+ }
+ }
+
+ pub fn diff(&self) -> Option<&Diff> {
+ if let AgentThreadEntry::ToolCall(ToolCall {
+ content: Some(ToolCallContent::Diff { diff }),
+ ..
+ }) = self
+ {
+ Some(&diff)
+ } else {
+ None
+ }
+ }
+
+ pub fn locations(&self) -> Option<&[acp::ToolCallLocation]> {
+ if let AgentThreadEntry::ToolCall(ToolCall { locations, .. }) = self {
+ Some(locations)
+ } else {
+ None
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct ToolCall {
+ pub id: acp::ToolCallId,
+ pub label: Entity<Markdown>,
+ pub icon: IconName,
+ pub content: Option<ToolCallContent>,
+ pub status: ToolCallStatus,
+ pub locations: Vec<acp::ToolCallLocation>,
+}
+
+impl ToolCall {
+ fn to_markdown(&self, cx: &App) -> String {
+ let mut markdown = format!(
+ "**Tool Call: {}**\nStatus: {}\n\n",
+ self.label.read(cx).source(),
+ self.status
+ );
+ if let Some(content) = &self.content {
+ markdown.push_str(content.to_markdown(cx).as_str());
+ markdown.push_str("\n\n");
+ }
+ markdown
+ }
+}
+
+#[derive(Debug)]
+pub enum ToolCallStatus {
+ WaitingForConfirmation {
+ confirmation: ToolCallConfirmation,
+ respond_tx: oneshot::Sender<acp::ToolCallConfirmationOutcome>,
+ },
+ Allowed {
+ status: acp::ToolCallStatus,
+ },
+ Rejected,
+ Canceled,
+}
+
+impl Display for ToolCallStatus {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ ToolCallStatus::WaitingForConfirmation { .. } => "Waiting for confirmation",
+ ToolCallStatus::Allowed { status } => match status {
+ acp::ToolCallStatus::Running => "Running",
+ acp::ToolCallStatus::Finished => "Finished",
+ acp::ToolCallStatus::Error => "Error",
+ },
+ ToolCallStatus::Rejected => "Rejected",
+ ToolCallStatus::Canceled => "Canceled",
+ }
+ )
+ }
+}
+
+#[derive(Debug)]
+pub enum ToolCallConfirmation {
+ Edit {
+ description: Option<Entity<Markdown>>,
+ },
+ Execute {
+ command: String,
+ root_command: String,
+ description: Option<Entity<Markdown>>,
+ },
+ Mcp {
+ server_name: String,
+ tool_name: String,
+ tool_display_name: String,
+ description: Option<Entity<Markdown>>,
+ },
+ Fetch {
+ urls: Vec<SharedString>,
+ description: Option<Entity<Markdown>>,
+ },
+ Other {
+ description: Entity<Markdown>,
+ },
+}
+
+impl ToolCallConfirmation {
+ pub fn from_acp(
+ confirmation: acp::ToolCallConfirmation,
+ language_registry: Arc<LanguageRegistry>,
+ cx: &mut App,
+ ) -> Self {
+ let to_md = |description: String, cx: &mut App| -> Entity<Markdown> {
+ cx.new(|cx| {
+ Markdown::new(
+ description.into(),
+ Some(language_registry.clone()),
+ None,
+ cx,
+ )
+ })
+ };
+
+ match confirmation {
+ acp::ToolCallConfirmation::Edit { description } => Self::Edit {
+ description: description.map(|description| to_md(description, cx)),
+ },
+ acp::ToolCallConfirmation::Execute {
+ command,
+ root_command,
+ description,
+ } => Self::Execute {
+ command,
+ root_command,
+ description: description.map(|description| to_md(description, cx)),
+ },
+ acp::ToolCallConfirmation::Mcp {
+ server_name,
+ tool_name,
+ tool_display_name,
+ description,
+ } => Self::Mcp {
+ server_name,
+ tool_name,
+ tool_display_name,
+ description: description.map(|description| to_md(description, cx)),
+ },
+ acp::ToolCallConfirmation::Fetch { urls, description } => Self::Fetch {
+ urls: urls.iter().map(|url| url.into()).collect(),
+ description: description.map(|description| to_md(description, cx)),
+ },
+ acp::ToolCallConfirmation::Other { description } => Self::Other {
+ description: to_md(description, cx),
+ },
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum ToolCallContent {
+ Markdown { markdown: Entity<Markdown> },
+ Diff { diff: Diff },
+}
+
+impl ToolCallContent {
+ pub fn from_acp(
+ content: acp::ToolCallContent,
+ language_registry: Arc<LanguageRegistry>,
+ cx: &mut App,
+ ) -> Self {
+ match content {
+ acp::ToolCallContent::Markdown { markdown } => Self::Markdown {
+ markdown: cx.new(|cx| Markdown::new_text(markdown.into(), cx)),
+ },
+ acp::ToolCallContent::Diff { diff } => Self::Diff {
+ diff: Diff::from_acp(diff, language_registry, cx),
+ },
+ }
+ }
+
+ fn to_markdown(&self, cx: &App) -> String {
+ match self {
+ Self::Markdown { markdown } => markdown.read(cx).source().to_string(),
+ Self::Diff { diff } => diff.to_markdown(cx),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct Diff {
+ pub multibuffer: Entity<MultiBuffer>,
+ pub path: PathBuf,
+ pub new_buffer: Entity<Buffer>,
+ pub old_buffer: Entity<Buffer>,
+ _task: Task<Result<()>>,
+}
+
+impl Diff {
+ pub fn from_acp(
+ diff: acp::Diff,
+ language_registry: Arc<LanguageRegistry>,
+ cx: &mut App,
+ ) -> Self {
+ let acp::Diff {
+ path,
+ old_text,
+ new_text,
+ } = diff;
+
+ let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly));
+
+ let new_buffer = cx.new(|cx| Buffer::local(new_text, cx));
+ let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx));
+ let new_buffer_snapshot = new_buffer.read(cx).text_snapshot();
+ let old_buffer_snapshot = old_buffer.read(cx).snapshot();
+ let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx));
+ let diff_task = buffer_diff.update(cx, |diff, cx| {
+ diff.set_base_text(
+ old_buffer_snapshot,
+ Some(language_registry.clone()),
+ new_buffer_snapshot,
+ cx,
+ )
+ });
+
+ let task = cx.spawn({
+ let multibuffer = multibuffer.clone();
+ let path = path.clone();
+ let new_buffer = new_buffer.clone();
+ async move |cx| {
+ diff_task.await?;
+
+ multibuffer
+ .update(cx, |multibuffer, cx| {
+ let hunk_ranges = {
+ let buffer = new_buffer.read(cx);
+ let diff = buffer_diff.read(cx);
+ diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx)
+ .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer))
+ .collect::<Vec<_>>()
+ };
+
+ multibuffer.set_excerpts_for_path(
+ PathKey::for_buffer(&new_buffer, cx),
+ new_buffer.clone(),
+ hunk_ranges,
+ editor::DEFAULT_MULTIBUFFER_CONTEXT,
+ cx,
+ );
+ multibuffer.add_diff(buffer_diff.clone(), cx);
+ })
+ .log_err();
+
+ if let Some(language) = language_registry
+ .language_for_file_path(&path)
+ .await
+ .log_err()
+ {
+ new_buffer.update(cx, |buffer, cx| buffer.set_language(Some(language), cx))?;
+ }
+
+ anyhow::Ok(())
+ }
+ });
+
+ Self {
+ multibuffer,
+ path,
+ new_buffer,
+ old_buffer,
+ _task: task,
+ }
+ }
+
+ fn to_markdown(&self, cx: &App) -> String {
+ let buffer_text = self
+ .multibuffer
+ .read(cx)
+ .all_buffers()
+ .iter()
+ .map(|buffer| buffer.read(cx).text())
+ .join("\n");
+ format!("Diff: {}\n```\n{}\n```\n", self.path.display(), buffer_text)
+ }
+}
+
+pub struct AcpThread {
+ entries: Vec<AgentThreadEntry>,
+ title: SharedString,
+ project: Entity<Project>,
+ action_log: Entity<ActionLog>,
+ shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>,
+ send_task: Option<Task<()>>,
+ connection: Arc<acp::AgentConnection>,
+ child_status: Option<Task<Result<()>>>,
+ _io_task: Task<()>,
+}
+
+pub enum AcpThreadEvent {
+ NewEntry,
+ EntryUpdated(usize),
+}
+
+impl EventEmitter<AcpThreadEvent> for AcpThread {}
+
+#[derive(PartialEq, Eq)]
+pub enum ThreadStatus {
+ Idle,
+ WaitingForToolConfirmation,
+ Generating,
+}
+
+#[derive(Debug, Clone)]
+pub enum LoadError {
+ Unsupported { current_version: SharedString },
+ Exited(i32),
+ Other(SharedString),
+}
+
+impl Display for LoadError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ LoadError::Unsupported { current_version } => {
+ write!(
+ f,
+ "Your installed version of Gemini {} doesn't support the Agentic Coding Protocol (ACP).",
+ current_version
+ )
+ }
+ LoadError::Exited(status) => write!(f, "Server exited with status {}", status),
+ LoadError::Other(msg) => write!(f, "{}", msg),
+ }
+ }
+}
+
+impl Error for LoadError {}
+
+impl AcpThread {
+ pub async fn spawn(
+ server: impl AgentServer + 'static,
+ root_dir: &Path,
+ project: Entity<Project>,
+ cx: &mut AsyncApp,
+ ) -> Result<Entity<Self>> {
+ let command = match server.command(&project, cx).await {
+ Ok(command) => command,
+ Err(e) => return Err(anyhow!(LoadError::Other(format!("{e}").into()))),
+ };
+
+ let mut child = util::command::new_smol_command(&command.path)
+ .args(command.args.iter())
+ .current_dir(root_dir)
+ .stdin(std::process::Stdio::piped())
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::inherit())
+ .kill_on_drop(true)
+ .spawn()?;
+
+ let stdin = child.stdin.take().unwrap();
+ let stdout = child.stdout.take().unwrap();
+
+ cx.new(|cx| {
+ let foreground_executor = cx.foreground_executor().clone();
+
+ let (connection, io_fut) = acp::AgentConnection::connect_to_agent(
+ AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async()),
+ stdin,
+ stdout,
+ move |fut| foreground_executor.spawn(fut).detach(),
+ );
+
+ let io_task = cx.background_spawn(async move {
+ io_fut.await.log_err();
+ });
+
+ let child_status = cx.background_spawn(async move {
+ match child.status().await {
+ Err(e) => Err(anyhow!(e)),
+ Ok(result) if result.success() => Ok(()),
+ Ok(result) => {
+ if let Some(version) = server.version(&command).await.log_err()
+ && !version.supported
+ {
+ Err(anyhow!(LoadError::Unsupported {
+ current_version: version.current_version
+ }))
+ } else {
+ Err(anyhow!(LoadError::Exited(result.code().unwrap_or(-127))))
+ }
+ }
+ }
+ });
+
+ let action_log = cx.new(|_| ActionLog::new(project.clone()));
+
+ Self {
+ action_log,
+ shared_buffers: Default::default(),
+ entries: Default::default(),
+ title: "ACP Thread".into(),
+ project,
+ send_task: None,
+ connection: Arc::new(connection),
+ child_status: Some(child_status),
+ _io_task: io_task,
+ }
+ })
+ }
+
+ pub fn action_log(&self) -> &Entity<ActionLog> {
+ &self.action_log
+ }
+
+ pub fn project(&self) -> &Entity<Project> {
+ &self.project
+ }
+
+ #[cfg(test)]
+ pub fn fake(
+ stdin: async_pipe::PipeWriter,
+ stdout: async_pipe::PipeReader,
+ project: Entity<Project>,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let foreground_executor = cx.foreground_executor().clone();
+
+ let (connection, io_fut) = acp::AgentConnection::connect_to_agent(
+ AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async()),
+ stdin,
+ stdout,
+ move |fut| {
+ foreground_executor.spawn(fut).detach();
+ },
+ );
+
+ let io_task = cx.background_spawn({
+ async move {
+ io_fut.await.log_err();
+ }
+ });
+
+ let action_log = cx.new(|_| ActionLog::new(project.clone()));
+
+ Self {
+ action_log,
+ shared_buffers: Default::default(),
+ entries: Default::default(),
+ title: "ACP Thread".into(),
+ project,
+ send_task: None,
+ connection: Arc::new(connection),
+ child_status: None,
+ _io_task: io_task,
+ }
+ }
+
+ pub fn title(&self) -> SharedString {
+ self.title.clone()
+ }
+
+ pub fn entries(&self) -> &[AgentThreadEntry] {
+ &self.entries
+ }
+
+ pub fn status(&self) -> ThreadStatus {
+ if self.send_task.is_some() {
+ if self.waiting_for_tool_confirmation() {
+ ThreadStatus::WaitingForToolConfirmation
+ } else {
+ ThreadStatus::Generating
+ }
+ } else {
+ ThreadStatus::Idle
+ }
+ }
+
+ pub fn has_pending_edit_tool_calls(&self) -> bool {
+ for entry in self.entries.iter().rev() {
+ match entry {
+ AgentThreadEntry::UserMessage(_) => return false,
+ AgentThreadEntry::ToolCall(ToolCall {
+ status:
+ ToolCallStatus::Allowed {
+ status: acp::ToolCallStatus::Running,
+ ..
+ },
+ content: Some(ToolCallContent::Diff { .. }),
+ ..
+ }) => return true,
+ AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {}
+ }
+ }
+
+ false
+ }
+
+ pub fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
+ self.entries.push(entry);
+ cx.emit(AcpThreadEvent::NewEntry);
+ }
+
+ pub fn push_assistant_chunk(
+ &mut self,
+ chunk: acp::AssistantMessageChunk,
+ cx: &mut Context<Self>,
+ ) {
+ let entries_len = self.entries.len();
+ if let Some(last_entry) = self.entries.last_mut()
+ && let AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) = last_entry
+ {
+ cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1));
+
+ match (chunks.last_mut(), &chunk) {
+ (
+ Some(AssistantMessageChunk::Text { chunk: old_chunk }),
+ acp::AssistantMessageChunk::Text { text: new_chunk },
+ )
+ | (
+ Some(AssistantMessageChunk::Thought { chunk: old_chunk }),
+ acp::AssistantMessageChunk::Thought { thought: new_chunk },
+ ) => {
+ old_chunk.update(cx, |old_chunk, cx| {
+ old_chunk.append(&new_chunk, cx);
+ });
+ }
+ _ => {
+ chunks.push(AssistantMessageChunk::from_acp(
+ chunk,
+ self.project.read(cx).languages().clone(),
+ cx,
+ ));
+ }
+ }
+ } else {
+ let chunk = AssistantMessageChunk::from_acp(
+ chunk,
+ self.project.read(cx).languages().clone(),
+ cx,
+ );
+
+ self.push_entry(
+ AgentThreadEntry::AssistantMessage(AssistantMessage {
+ chunks: vec![chunk],
+ }),
+ cx,
+ );
+ }
+ }
+
+ pub fn request_tool_call(
+ &mut self,
+ tool_call: acp::RequestToolCallConfirmationParams,
+ cx: &mut Context<Self>,
+ ) -> ToolCallRequest {
+ let (tx, rx) = oneshot::channel();
+
+ let status = ToolCallStatus::WaitingForConfirmation {
+ confirmation: ToolCallConfirmation::from_acp(
+ tool_call.confirmation,
+ self.project.read(cx).languages().clone(),
+ cx,
+ ),
+ respond_tx: tx,
+ };
+
+ let id = self.insert_tool_call(tool_call.tool_call, status, cx);
+ ToolCallRequest { id, outcome: rx }
+ }
+
+ pub fn push_tool_call(
+ &mut self,
+ request: acp::PushToolCallParams,
+ cx: &mut Context<Self>,
+ ) -> acp::ToolCallId {
+ let status = ToolCallStatus::Allowed {
+ status: acp::ToolCallStatus::Running,
+ };
+
+ self.insert_tool_call(request, status, cx)
+ }
+
+ fn insert_tool_call(
+ &mut self,
+ tool_call: acp::PushToolCallParams,
+ status: ToolCallStatus,
+ cx: &mut Context<Self>,
+ ) -> acp::ToolCallId {
+ let language_registry = self.project.read(cx).languages().clone();
+ let id = acp::ToolCallId(self.entries.len() as u64);
+ let call = ToolCall {
+ id,
+ label: cx.new(|cx| {
+ Markdown::new(
+ tool_call.label.into(),
+ Some(language_registry.clone()),
+ None,
+ cx,
+ )
+ }),
+ icon: acp_icon_to_ui_icon(tool_call.icon),
+ content: tool_call
+ .content
+ .map(|content| ToolCallContent::from_acp(content, language_registry, cx)),
+ locations: tool_call.locations,
+ status,
+ };
+
+ self.push_entry(AgentThreadEntry::ToolCall(call), cx);
+
+ id
+ }
+
+ pub fn authorize_tool_call(
+ &mut self,
+ id: acp::ToolCallId,
+ outcome: acp::ToolCallConfirmationOutcome,
+ cx: &mut Context<Self>,
+ ) {
+ let Some((ix, call)) = self.tool_call_mut(id) else {
+ return;
+ };
+
+ let new_status = if outcome == acp::ToolCallConfirmationOutcome::Reject {
+ ToolCallStatus::Rejected
+ } else {
+ ToolCallStatus::Allowed {
+ status: acp::ToolCallStatus::Running,
+ }
+ };
+
+ let curr_status = mem::replace(&mut call.status, new_status);
+
+ if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status {
+ respond_tx.send(outcome).log_err();
+ } else if cfg!(debug_assertions) {
+ panic!("tried to authorize an already authorized tool call");
+ }
+
+ cx.emit(AcpThreadEvent::EntryUpdated(ix));
+ }
+
+ pub fn update_tool_call(
+ &mut self,
+ id: acp::ToolCallId,
+ new_status: acp::ToolCallStatus,
+ new_content: Option<acp::ToolCallContent>,
+ cx: &mut Context<Self>,
+ ) -> Result<()> {
+ let language_registry = self.project.read(cx).languages().clone();
+ let (ix, call) = self.tool_call_mut(id).context("Entry not found")?;
+
+ call.content = new_content
+ .map(|new_content| ToolCallContent::from_acp(new_content, language_registry, cx));
+
+ match &mut call.status {
+ ToolCallStatus::Allowed { status } => {
+ *status = new_status;
+ }
+ ToolCallStatus::WaitingForConfirmation { .. } => {
+ anyhow::bail!("Tool call hasn't been authorized yet")
+ }
+ ToolCallStatus::Rejected => {
+ anyhow::bail!("Tool call was rejected and therefore can't be updated")
+ }
+ ToolCallStatus::Canceled => {
+ call.status = ToolCallStatus::Allowed { status: new_status };
+ }
+ }
+
+ cx.emit(AcpThreadEvent::EntryUpdated(ix));
+ Ok(())
+ }
+
+ fn tool_call_mut(&mut self, id: acp::ToolCallId) -> Option<(usize, &mut ToolCall)> {
+ let entry = self.entries.get_mut(id.0 as usize);
+ debug_assert!(
+ entry.is_some(),
+ "We shouldn't give out ids to entries that don't exist"
+ );
+ match entry {
+ Some(AgentThreadEntry::ToolCall(call)) if call.id == id => Some((id.0 as usize, call)),
+ _ => {
+ if cfg!(debug_assertions) {
+ panic!("entry is not a tool call");
+ }
+ None
+ }
+ }
+ }
+
+ /// Returns true if the last turn is awaiting tool authorization
+ pub fn waiting_for_tool_confirmation(&self) -> bool {
+ for entry in self.entries.iter().rev() {
+ match &entry {
+ AgentThreadEntry::ToolCall(call) => match call.status {
+ ToolCallStatus::WaitingForConfirmation { .. } => return true,
+ ToolCallStatus::Allowed { .. }
+ | ToolCallStatus::Rejected
+ | ToolCallStatus::Canceled => continue,
+ },
+ AgentThreadEntry::UserMessage(_) | AgentThreadEntry::AssistantMessage(_) => {
+ // Reached the beginning of the turn
+ return false;
+ }
+ }
+ }
+ false
+ }
+
+ pub fn initialize(
+ &self,
+ ) -> impl use<> + Future<Output = Result<acp::InitializeResponse, acp::Error>> {
+ let connection = self.connection.clone();
+ async move { connection.initialize().await }
+ }
+
+ pub fn authenticate(&self) -> impl use<> + Future<Output = Result<(), acp::Error>> {
+ let connection = self.connection.clone();
+ async move { connection.request(acp::AuthenticateParams).await }
+ }
+
+ #[cfg(test)]
+ pub fn send_raw(
+ &mut self,
+ message: &str,
+ cx: &mut Context<Self>,
+ ) -> BoxFuture<'static, Result<(), acp::Error>> {
+ self.send(
+ acp::SendUserMessageParams {
+ chunks: vec![acp::UserMessageChunk::Text {
+ text: message.to_string(),
+ }],
+ },
+ cx,
+ )
+ }
+
+ pub fn send(
+ &mut self,
+ message: acp::SendUserMessageParams,
+ cx: &mut Context<Self>,
+ ) -> BoxFuture<'static, Result<(), acp::Error>> {
+ let agent = self.connection.clone();
+ self.push_entry(
+ AgentThreadEntry::UserMessage(UserMessage::from_acp(
+ &message,
+ self.project.read(cx).languages().clone(),
+ cx,
+ )),
+ cx,
+ );
+
+ let (tx, rx) = oneshot::channel();
+ let cancel = self.cancel(cx);
+
+ self.send_task = Some(cx.spawn(async move |this, cx| {
+ cancel.await.log_err();
+
+ let result = agent.request(message).await;
+ tx.send(result).log_err();
+ this.update(cx, |this, _cx| this.send_task.take()).log_err();
+ }));
+
+ async move {
+ match rx.await {
+ Ok(Err(e)) => Err(e)?,
+ _ => Ok(()),
+ }
+ }
+ .boxed()
+ }
+
+ pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<(), acp::Error>> {
+ let agent = self.connection.clone();
+
+ if self.send_task.take().is_some() {
+ cx.spawn(async move |this, cx| {
+ agent.request(acp::CancelSendMessageParams).await?;
+
+ this.update(cx, |this, _cx| {
+ for entry in this.entries.iter_mut() {
+ if let AgentThreadEntry::ToolCall(call) = entry {
+ let cancel = matches!(
+ call.status,
+ ToolCallStatus::WaitingForConfirmation { .. }
+ | ToolCallStatus::Allowed {
+ status: acp::ToolCallStatus::Running
+ }
+ );
+
+ if cancel {
+ let curr_status =
+ mem::replace(&mut call.status, ToolCallStatus::Canceled);
+
+ if let ToolCallStatus::WaitingForConfirmation {
+ respond_tx, ..
+ } = curr_status
+ {
+ respond_tx
+ .send(acp::ToolCallConfirmationOutcome::Cancel)
+ .ok();
+ }
+ }
+ }
+ }
+ })?;
+ Ok(())
+ })
+ } else {
+ Task::ready(Ok(()))
+ }
+ }
+
+ pub fn read_text_file(
+ &self,
+ request: acp::ReadTextFileParams,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<String>> {
+ 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(&request.path, cx)
+ .context("invalid path")?;
+ anyhow::Ok(project.open_buffer(path, cx))
+ });
+ let buffer = load??.await?;
+
+ action_log.update(cx, |action_log, cx| {
+ action_log.buffer_read(buffer.clone(), cx);
+ })?;
+ project.update(cx, |project, cx| {
+ let position = buffer
+ .read(cx)
+ .snapshot()
+ .anchor_before(Point::new(request.line.unwrap_or_default(), 0));
+ project.set_agent_location(
+ Some(AgentLocation {
+ buffer: buffer.downgrade(),
+ position,
+ }),
+ cx,
+ );
+ })?;
+ let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
+ this.update(cx, |this, _| {
+ let text = snapshot.text();
+ this.shared_buffers.insert(buffer.clone(), snapshot);
+ text
+ })
+ })
+ }
+
+ pub fn write_text_file(
+ &self,
+ path: PathBuf,
+ content: String,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ 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::<Vec<_>>()
+ })
+ .await;
+ cx.update(|cx| {
+ project.update(cx, |project, cx| {
+ project.set_agent_location(
+ Some(AgentLocation {
+ buffer: buffer.downgrade(),
+ position: edits
+ .last()
+ .map(|(range, _)| range.end)
+ .unwrap_or(Anchor::MIN),
+ }),
+ cx,
+ );
+ });
+
+ action_log.update(cx, |action_log, cx| {
+ action_log.buffer_read(buffer.clone(), cx);
+ });
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit(edits, None, cx);
+ });
+ 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 child_status(&mut self) -> Option<Task<Result<()>>> {
+ self.child_status.take()
+ }
+
+ pub fn to_markdown(&self, cx: &App) -> String {
+ self.entries.iter().map(|e| e.to_markdown(cx)).collect()
+ }
+}
+
+struct AcpClientDelegate {
+ thread: WeakEntity<AcpThread>,
+ cx: AsyncApp,
+ // sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
+}
+
+impl AcpClientDelegate {
+ fn new(thread: WeakEntity<AcpThread>, cx: AsyncApp) -> Self {
+ Self { thread, cx }
+ }
+}
+
+impl acp::Client for AcpClientDelegate {
+ async fn stream_assistant_message_chunk(
+ &self,
+ params: acp::StreamAssistantMessageChunkParams,
+ ) -> Result<(), acp::Error> {
+ let cx = &mut self.cx.clone();
+
+ cx.update(|cx| {
+ self.thread
+ .update(cx, |thread, cx| {
+ thread.push_assistant_chunk(params.chunk, cx)
+ })
+ .ok();
+ })?;
+
+ Ok(())
+ }
+
+ async fn request_tool_call_confirmation(
+ &self,
+ request: acp::RequestToolCallConfirmationParams,
+ ) -> Result<acp::RequestToolCallConfirmationResponse, acp::Error> {
+ let cx = &mut self.cx.clone();
+ let ToolCallRequest { id, outcome } = cx
+ .update(|cx| {
+ self.thread
+ .update(cx, |thread, cx| thread.request_tool_call(request, cx))
+ })?
+ .context("Failed to update thread")?;
+
+ Ok(acp::RequestToolCallConfirmationResponse {
+ id,
+ outcome: outcome.await.map_err(acp::Error::into_internal_error)?,
+ })
+ }
+
+ async fn push_tool_call(
+ &self,
+ request: acp::PushToolCallParams,
+ ) -> Result<acp::PushToolCallResponse, acp::Error> {
+ let cx = &mut self.cx.clone();
+ let id = cx
+ .update(|cx| {
+ self.thread
+ .update(cx, |thread, cx| thread.push_tool_call(request, cx))
+ })?
+ .context("Failed to update thread")?;
+
+ Ok(acp::PushToolCallResponse { id })
+ }
+
+ async fn update_tool_call(&self, request: acp::UpdateToolCallParams) -> Result<(), acp::Error> {
+ let cx = &mut self.cx.clone();
+
+ cx.update(|cx| {
+ self.thread.update(cx, |thread, cx| {
+ thread.update_tool_call(request.tool_call_id, request.status, request.content, cx)
+ })
+ })?
+ .context("Failed to update thread")??;
+
+ Ok(())
+ }
+
+ async fn read_text_file(
+ &self,
+ request: acp::ReadTextFileParams,
+ ) -> Result<acp::ReadTextFileResponse, acp::Error> {
+ let content = self
+ .cx
+ .update(|cx| {
+ self.thread
+ .update(cx, |thread, cx| thread.read_text_file(request, cx))
+ })?
+ .context("Failed to update thread")?
+ .await?;
+ Ok(acp::ReadTextFileResponse { content })
+ }
+
+ async fn write_text_file(&self, request: acp::WriteTextFileParams) -> Result<(), acp::Error> {
+ self.cx
+ .update(|cx| {
+ self.thread.update(cx, |thread, cx| {
+ thread.write_text_file(request.path, request.content, cx)
+ })
+ })?
+ .context("Failed to update thread")?
+ .await?;
+
+ Ok(())
+ }
+}
+
+fn acp_icon_to_ui_icon(icon: acp::Icon) -> IconName {
+ match icon {
+ acp::Icon::FileSearch => IconName::ToolSearch,
+ acp::Icon::Folder => IconName::ToolFolder,
+ acp::Icon::Globe => IconName::ToolWeb,
+ acp::Icon::Hammer => IconName::ToolHammer,
+ acp::Icon::LightBulb => IconName::ToolBulb,
+ acp::Icon::Pencil => IconName::ToolPencil,
+ acp::Icon::Regex => IconName::ToolRegex,
+ acp::Icon::Terminal => IconName::ToolTerminal,
+ }
+}
+
+pub struct ToolCallRequest {
+ pub id: acp::ToolCallId,
+ pub outcome: oneshot::Receiver<acp::ToolCallConfirmationOutcome>,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use agent_servers::{AgentServerCommand, AgentServerVersion};
+ use async_pipe::{PipeReader, PipeWriter};
+ use futures::{channel::mpsc, future::LocalBoxFuture, select};
+ use gpui::{AsyncApp, TestAppContext};
+ use indoc::indoc;
+ use project::FakeFs;
+ use serde_json::json;
+ use settings::SettingsStore;
+ use smol::{future::BoxedLocal, stream::StreamExt as _};
+ use std::{cell::RefCell, env, path::Path, rc::Rc, 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);
+ Project::init_settings(cx);
+ language::init(cx);
+ });
+ }
+
+ #[gpui::test]
+ async fn test_thinking_concatenation(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ let project = Project::test(fs, [], cx).await;
+ let (thread, fake_server) = fake_acp_thread(project, cx);
+
+ fake_server.update(cx, |fake_server, _| {
+ fake_server.on_user_message(move |_, server, mut cx| async move {
+ server
+ .update(&mut cx, |server, _| {
+ server.send_to_zed(acp::StreamAssistantMessageChunkParams {
+ chunk: acp::AssistantMessageChunk::Thought {
+ thought: "Thinking ".into(),
+ },
+ })
+ })?
+ .await
+ .unwrap();
+ server
+ .update(&mut cx, |server, _| {
+ server.send_to_zed(acp::StreamAssistantMessageChunkParams {
+ chunk: acp::AssistantMessageChunk::Thought {
+ thought: "hard!".into(),
+ },
+ })
+ })?
+ .await
+ .unwrap();
+
+ Ok(())
+ })
+ });
+
+ 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>
+ Thinking hard!
+ </thinking>
+
+ "#}
+ );
+ }
+
+ #[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 (thread, fake_server) = fake_acp_thread(project.clone(), cx);
+ let (worktree, pathbuf) = project
+ .update(cx, |project, cx| {
+ project.find_or_create_worktree(path!("/tmp/foo"), true, cx)
+ })
+ .await
+ .unwrap();
+ let buffer = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree.read(cx).id(), pathbuf), cx)
+ })
+ .await
+ .unwrap();
+
+ let (read_file_tx, read_file_rx) = oneshot::channel::<()>();
+ let read_file_tx = Rc::new(RefCell::new(Some(read_file_tx)));
+
+ fake_server.update(cx, |fake_server, _| {
+ fake_server.on_user_message(move |_, server, mut cx| {
+ let read_file_tx = read_file_tx.clone();
+ async move {
+ let content = server
+ .update(&mut cx, |server, _| {
+ server.send_to_zed(acp::ReadTextFileParams {
+ path: path!("/tmp/foo").into(),
+ line: None,
+ limit: None,
+ })
+ })?
+ .await
+ .unwrap();
+ assert_eq!(content.content, "one\ntwo\nthree\n");
+ read_file_tx.take().unwrap().send(()).unwrap();
+ server
+ .update(&mut cx, |server, _| {
+ server.send_to_zed(acp::WriteTextFileParams {
+ path: path!("/tmp/foo").into(),
+ content: "one\ntwo\nthree\nfour\nfive\n".to_string(),
+ })
+ })?
+ .await
+ .unwrap();
+ Ok(())
+ }
+ })
+ });
+
+ let request = thread.update(cx, |thread, cx| {
+ thread.send_raw("Extend the count in /tmp/foo", cx)
+ });
+ 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_succeeding_canceled_toolcall(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ let project = Project::test(fs, [], cx).await;
+ let (thread, fake_server) = fake_acp_thread(project, cx);
+
+ let (end_turn_tx, end_turn_rx) = oneshot::channel::<()>();
+
+ let tool_call_id = Rc::new(RefCell::new(None));
+ let end_turn_rx = Rc::new(RefCell::new(Some(end_turn_rx)));
+ fake_server.update(cx, |fake_server, _| {
+ let tool_call_id = tool_call_id.clone();
+ fake_server.on_user_message(move |_, server, mut cx| {
+ let end_turn_rx = end_turn_rx.clone();
+ let tool_call_id = tool_call_id.clone();
+ async move {
+ let tool_call_result = server
+ .update(&mut cx, |server, _| {
+ server.send_to_zed(acp::PushToolCallParams {
+ label: "Fetch".to_string(),
+ icon: acp::Icon::Globe,
+ content: None,
+ locations: vec![],
+ })
+ })?
+ .await
+ .unwrap();
+ *tool_call_id.clone().borrow_mut() = Some(tool_call_result.id);
+ end_turn_rx.take().unwrap().await.ok();
+
+ Ok(())
+ }
+ })
+ });
+
+ 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::Allowed {
+ status: acp::ToolCallStatus::Running,
+ ..
+ },
+ ..
+ })
+ ));
+ });
+
+ cx.run_until_parked();
+
+ thread
+ .update(cx, |thread, cx| thread.cancel(cx))
+ .await
+ .unwrap();
+
+ thread.read_with(cx, |thread, _| {
+ assert!(matches!(
+ &thread.entries[1],
+ AgentThreadEntry::ToolCall(ToolCall {
+ status: ToolCallStatus::Canceled,
+ ..
+ })
+ ));
+ });
+
+ fake_server
+ .update(cx, |fake_server, _| {
+ fake_server.send_to_zed(acp::UpdateToolCallParams {
+ tool_call_id: tool_call_id.borrow().unwrap(),
+ status: acp::ToolCallStatus::Finished,
+ content: None,
+ })
+ })
+ .await
+ .unwrap();
+
+ drop(end_turn_tx);
+ request.await.unwrap();
+
+ thread.read_with(cx, |thread, _| {
+ assert!(matches!(
+ thread.entries[1],
+ AgentThreadEntry::ToolCall(ToolCall {
+ status: ToolCallStatus::Allowed {
+ status: acp::ToolCallStatus::Finished,
+ ..
+ },
+ ..
+ })
+ ));
+ });
+ }
+
+ #[gpui::test]
+ #[cfg_attr(not(feature = "gemini"), ignore)]
+ async fn test_gemini_basic(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ cx.executor().allow_parking();
+
+ let fs = FakeFs::new(cx.executor());
+ let project = Project::test(fs, [], cx).await;
+ let thread = gemini_acp_thread(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_eq!(thread.entries.len(), 2);
+ assert!(matches!(
+ thread.entries[0],
+ AgentThreadEntry::UserMessage(_)
+ ));
+ assert!(matches!(
+ thread.entries[1],
+ AgentThreadEntry::AssistantMessage(_)
+ ));
+ });
+ }
+
+ #[gpui::test]
+ #[cfg_attr(not(feature = "gemini"), ignore)]
+ async fn test_gemini_path_mentions(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ cx.executor().allow_parking();
+ 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 = gemini_acp_thread(project.clone(), tempdir.path(), cx).await;
+ thread
+ .update(cx, |thread, cx| {
+ thread.send(
+ acp::SendUserMessageParams {
+ chunks: vec![
+ acp::UserMessageChunk::Text {
+ text: "Read the file ".into(),
+ },
+ acp::UserMessageChunk::Path {
+ path: Path::new("foo.rs").into(),
+ },
+ acp::UserMessageChunk::Text {
+ text: " and tell me what the content of the println! is".into(),
+ },
+ ],
+ },
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+
+ thread.read_with(cx, |thread, cx| {
+ assert_eq!(thread.entries.len(), 3);
+ assert!(matches!(
+ thread.entries[0],
+ AgentThreadEntry::UserMessage(_)
+ ));
+ assert!(matches!(thread.entries[1], AgentThreadEntry::ToolCall(_)));
+ let AgentThreadEntry::AssistantMessage(assistant_message) = &thread.entries[2] else {
+ panic!("Expected AssistantMessage")
+ };
+ assert!(
+ assistant_message.to_markdown(cx).contains("Hello, world!"),
+ "unexpected assistant message: {:?}",
+ assistant_message.to_markdown(cx)
+ );
+ });
+ }
+
+ #[gpui::test]
+ #[cfg_attr(not(feature = "gemini"), ignore)]
+ async fn test_gemini_tool_call(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ cx.executor().allow_parking();
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/private/tmp"),
+ json!({"foo": "Lorem ipsum dolor", "bar": "bar", "baz": "baz"}),
+ )
+ .await;
+ let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
+ let thread = gemini_acp_thread(project.clone(), "/private/tmp", cx).await;
+ thread
+ .update(cx, |thread, cx| {
+ thread.send_raw(
+ "Read the '/private/tmp/foo' file and tell me what you see.",
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+ thread.read_with(cx, |thread, _cx| {
+ assert!(matches!(
+ &thread.entries()[2],
+ AgentThreadEntry::ToolCall(ToolCall {
+ status: ToolCallStatus::Allowed { .. },
+ ..
+ })
+ ));
+
+ assert!(matches!(
+ thread.entries[3],
+ AgentThreadEntry::AssistantMessage(_)
+ ));
+ });
+ }
+
+ #[gpui::test]
+ #[cfg_attr(not(feature = "gemini"), ignore)]
+ async fn test_gemini_tool_call_with_confirmation(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ cx.executor().allow_parking();
+
+ let fs = FakeFs::new(cx.executor());
+ let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
+ let thread = gemini_acp_thread(project.clone(), "/private/tmp", cx).await;
+ let full_turn = thread.update(cx, |thread, cx| {
+ thread.send_raw(r#"Run `echo "Hello, world!"`"#, cx)
+ });
+
+ run_until_first_tool_call(&thread, cx).await;
+
+ let tool_call_id = thread.read_with(cx, |thread, _cx| {
+ let AgentThreadEntry::ToolCall(ToolCall {
+ id,
+ status:
+ ToolCallStatus::WaitingForConfirmation {
+ confirmation: ToolCallConfirmation::Execute { root_command, .. },
+ ..
+ },
+ ..
+ }) = &thread.entries()[2]
+ else {
+ panic!();
+ };
+
+ assert_eq!(root_command, "echo");
+
+ *id
+ });
+
+ thread.update(cx, |thread, cx| {
+ thread.authorize_tool_call(tool_call_id, acp::ToolCallConfirmationOutcome::Allow, cx);
+
+ assert!(matches!(
+ &thread.entries()[2],
+ AgentThreadEntry::ToolCall(ToolCall {
+ status: ToolCallStatus::Allowed { .. },
+ ..
+ })
+ ));
+ });
+
+ full_turn.await.unwrap();
+
+ thread.read_with(cx, |thread, cx| {
+ let AgentThreadEntry::ToolCall(ToolCall {
+ content: Some(ToolCallContent::Markdown { markdown }),
+ status: ToolCallStatus::Allowed { .. },
+ ..
+ }) = &thread.entries()[2]
+ else {
+ panic!();
+ };
+
+ markdown.read_with(cx, |md, _cx| {
+ assert!(
+ md.source().contains("Hello, world!"),
+ r#"Expected '{}' to contain "Hello, world!""#,
+ md.source()
+ );
+ });
+ });
+ }
+
+ #[gpui::test]
+ #[cfg_attr(not(feature = "gemini"), ignore)]
+ async fn test_gemini_cancel(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ cx.executor().allow_parking();
+
+ let fs = FakeFs::new(cx.executor());
+ let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
+ let thread = gemini_acp_thread(project.clone(), "/private/tmp", cx).await;
+ let full_turn = thread.update(cx, |thread, cx| {
+ thread.send_raw(r#"Run `echo "Hello, world!"`"#, cx)
+ });
+
+ let first_tool_call_ix = run_until_first_tool_call(&thread, cx).await;
+
+ thread.read_with(cx, |thread, _cx| {
+ let AgentThreadEntry::ToolCall(ToolCall {
+ id,
+ status:
+ ToolCallStatus::WaitingForConfirmation {
+ confirmation: ToolCallConfirmation::Execute { root_command, .. },
+ ..
+ },
+ ..
+ }) = &thread.entries()[first_tool_call_ix]
+ else {
+ panic!("{:?}", thread.entries()[1]);
+ };
+
+ assert_eq!(root_command, "echo");
+
+ *id
+ });
+
+ thread
+ .update(cx, |thread, cx| thread.cancel(cx))
+ .await
+ .unwrap();
+ full_turn.await.unwrap();
+ thread.read_with(cx, |thread, _| {
+ 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(..),
+ ))
+ });
+ }
+
+ async fn run_until_first_tool_call(
+ thread: &Entity<AcpThread>,
+ cx: &mut TestAppContext,
+ ) -> usize {
+ let (mut tx, mut rx) = mpsc::channel::<usize>(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()
+ }
+ }
+ }
+
+ pub async fn gemini_acp_thread(
+ project: Entity<Project>,
+ current_dir: impl AsRef<Path>,
+ cx: &mut TestAppContext,
+ ) -> Entity<AcpThread> {
+ struct DevGemini;
+
+ impl agent_servers::AgentServer for DevGemini {
+ async fn command(
+ &self,
+ _project: &Entity<Project>,
+ _cx: &mut AsyncApp,
+ ) -> Result<agent_servers::AgentServerCommand> {
+ let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("../../../gemini-cli/packages/cli")
+ .to_string_lossy()
+ .to_string();
+
+ Ok(AgentServerCommand {
+ path: "node".into(),
+ args: vec![cli_path, "--acp".into()],
+ env: None,
+ })
+ }
+
+ async fn version(
+ &self,
+ _command: &agent_servers::AgentServerCommand,
+ ) -> Result<AgentServerVersion> {
+ Ok(AgentServerVersion {
+ current_version: "0.1.0".into(),
+ supported: true,
+ })
+ }
+ }
+
+ let thread = AcpThread::spawn(DevGemini, current_dir.as_ref(), project, &mut cx.to_async())
+ .await
+ .unwrap();
+
+ thread
+ .update(cx, |thread, _| thread.initialize())
+ .await
+ .unwrap();
+ thread
+ }
+
+ pub fn fake_acp_thread(
+ project: Entity<Project>,
+ cx: &mut TestAppContext,
+ ) -> (Entity<AcpThread>, Entity<FakeAcpServer>) {
+ let (stdin_tx, stdin_rx) = async_pipe::pipe();
+ let (stdout_tx, stdout_rx) = async_pipe::pipe();
+ let thread = cx.update(|cx| cx.new(|cx| AcpThread::fake(stdin_tx, stdout_rx, project, cx)));
+ let agent = cx.update(|cx| cx.new(|cx| FakeAcpServer::new(stdin_rx, stdout_tx, cx)));
+ (thread, agent)
+ }
+
+ pub struct FakeAcpServer {
+ connection: acp::ClientConnection,
+ _io_task: Task<()>,
+ on_user_message: Option<
+ Rc<
+ dyn Fn(
+ acp::SendUserMessageParams,
+ Entity<FakeAcpServer>,
+ AsyncApp,
+ ) -> LocalBoxFuture<'static, Result<(), acp::Error>>,
+ >,
+ >,
+ }
+
+ #[derive(Clone)]
+ struct FakeAgent {
+ server: Entity<FakeAcpServer>,
+ cx: AsyncApp,
+ }
+
+ impl acp::Agent for FakeAgent {
+ async fn initialize(
+ &self,
+ params: acp::InitializeParams,
+ ) -> Result<acp::InitializeResponse, acp::Error> {
+ Ok(acp::InitializeResponse {
+ protocol_version: params.protocol_version,
+ is_authenticated: true,
+ })
+ }
+
+ async fn authenticate(&self) -> Result<(), acp::Error> {
+ Ok(())
+ }
+
+ async fn cancel_send_message(&self) -> Result<(), acp::Error> {
+ Ok(())
+ }
+
+ async fn send_user_message(
+ &self,
+ request: acp::SendUserMessageParams,
+ ) -> Result<(), acp::Error> {
+ let mut cx = self.cx.clone();
+ let handler = self
+ .server
+ .update(&mut cx, |server, _| server.on_user_message.clone())
+ .ok()
+ .flatten();
+ if let Some(handler) = handler {
+ handler(request, self.server.clone(), self.cx.clone()).await
+ } else {
+ Err(anyhow::anyhow!("No handler for on_user_message").into())
+ }
+ }
+ }
+
+ impl FakeAcpServer {
+ fn new(stdin: PipeReader, stdout: PipeWriter, cx: &Context<Self>) -> Self {
+ let agent = FakeAgent {
+ server: cx.entity(),
+ cx: cx.to_async(),
+ };
+ let foreground_executor = cx.foreground_executor().clone();
+
+ let (connection, io_fut) = acp::ClientConnection::connect_to_client(
+ agent.clone(),
+ stdout,
+ stdin,
+ move |fut| {
+ foreground_executor.spawn(fut).detach();
+ },
+ );
+ FakeAcpServer {
+ connection: connection,
+ on_user_message: None,
+ _io_task: cx.background_spawn(async move {
+ io_fut.await.log_err();
+ }),
+ }
+ }
+
+ fn on_user_message<F>(
+ &mut self,
+ handler: impl for<'a> Fn(acp::SendUserMessageParams, Entity<FakeAcpServer>, AsyncApp) -> F
+ + 'static,
+ ) where
+ F: Future<Output = Result<(), acp::Error>> + 'static,
+ {
+ self.on_user_message
+ .replace(Rc::new(move |request, server, cx| {
+ handler(request, server, cx).boxed_local()
+ }));
+ }
+
+ fn send_to_zed<T: acp::ClientRequest + 'static>(
+ &self,
+ message: T,
+ ) -> BoxedLocal<Result<T::Response>> {
+ self.connection
+ .request(message)
+ .map(|f| f.map_err(|err| anyhow!(err)))
+ .boxed_local()
+ }
+ }
+}
@@ -31,7 +31,13 @@ use workspace::{StatusItemView, Workspace, item::ItemHandle};
const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0);
-actions!(activity_indicator, [ShowErrorMessage]);
+actions!(
+ activity_indicator,
+ [
+ /// Displays error messages from language servers in the status bar.
+ ShowErrorMessage
+ ]
+);
pub enum Event {
ShowStatus {
@@ -442,7 +448,7 @@ impl ActivityIndicator {
.into_any_element(),
),
message: format!("Debug: {}", session.read(cx).adapter()),
- tooltip_message: Some(session.read(cx).label().to_string()),
+ tooltip_message: session.read(cx).label().map(|label| label.to_string()),
on_click: None,
});
}
@@ -1,7 +1,7 @@
use std::sync::Arc;
use agent_settings::{AgentProfileId, AgentProfileSettings, AgentSettings};
-use assistant_tool::{Tool, ToolSource, ToolWorkingSet};
+use assistant_tool::{Tool, ToolSource, ToolWorkingSet, UniqueToolName};
use collections::IndexMap;
use convert_case::{Case, Casing};
use fs::Fs;
@@ -72,7 +72,7 @@ impl AgentProfile {
&self.id
}
- pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
+ pub fn enabled_tools(&self, cx: &App) -> Vec<(UniqueToolName, Arc<dyn Tool>)> {
let Some(settings) = AgentSettings::get_global(cx).profiles.get(&self.id) else {
return Vec::new();
};
@@ -81,7 +81,7 @@ impl AgentProfile {
.read(cx)
.tools(cx)
.into_iter()
- .filter(|tool| Self::is_enabled(settings, tool.source(), tool.name()))
+ .filter(|(_, tool)| Self::is_enabled(settings, tool.source(), tool.name()))
.collect()
}
@@ -137,7 +137,7 @@ mod tests {
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
.into_iter()
- .map(|tool| tool.name())
+ .map(|(_, tool)| tool.name())
.collect::<Vec<_>>();
enabled_tools.sort();
@@ -174,7 +174,7 @@ mod tests {
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
.into_iter()
- .map(|tool| tool.name())
+ .map(|(_, tool)| tool.name())
.collect::<Vec<_>>();
enabled_tools.sort();
@@ -207,7 +207,7 @@ mod tests {
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
.into_iter()
- .map(|tool| tool.name())
+ .map(|(_, tool)| tool.name())
.collect::<Vec<_>>();
enabled_tools.sort();
@@ -267,10 +267,10 @@ mod tests {
}
fn default_tool_set(cx: &mut TestAppContext) -> Entity<ToolWorkingSet> {
- cx.new(|_| {
+ cx.new(|cx| {
let mut tool_set = ToolWorkingSet::default();
- tool_set.insert(Arc::new(FakeTool::new("enabled_mcp_tool", "mcp")));
- tool_set.insert(Arc::new(FakeTool::new("disabled_mcp_tool", "mcp")));
+ tool_set.insert(Arc::new(FakeTool::new("enabled_mcp_tool", "mcp")), cx);
+ tool_set.insert(Arc::new(FakeTool::new("disabled_mcp_tool", "mcp")), cx);
tool_set
})
}
@@ -0,0 +1,3 @@
+[The following is an auto-generated notification; do not reply]
+
+These files have changed since the last read:
@@ -13,7 +13,7 @@ use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
use client::{ModelRequestUsage, RequestUsage};
-use collections::{HashMap, HashSet};
+use collections::HashMap;
use feature_flags::{self, FeatureFlagAppExt};
use futures::{FutureExt, StreamExt as _, future::Shared};
use git::repository::DiffType;
@@ -23,10 +23,11 @@ use gpui::{
};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
- LanguageModelId, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
- LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent,
- LanguageModelToolUseId, MessageContent, ModelRequestLimitReachedError, PaymentRequiredError,
- Role, SelectedModel, StopReason, TokenUsage,
+ LanguageModelExt as _, LanguageModelId, LanguageModelRegistry, LanguageModelRequest,
+ LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
+ LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, MessageContent,
+ ModelRequestLimitReachedError, PaymentRequiredError, Role, SelectedModel, StopReason,
+ TokenUsage,
};
use postage::stream::Stream as _;
use project::{
@@ -45,7 +46,7 @@ use std::{
time::{Duration, Instant},
};
use thiserror::Error;
-use util::{ResultExt as _, post_inc};
+use util::{ResultExt as _, debug_panic, post_inc};
use uuid::Uuid;
use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
@@ -960,13 +961,14 @@ impl Thread {
model: Arc<dyn LanguageModel>,
) -> Vec<LanguageModelRequestTool> {
if model.supports_tools() {
- resolve_tool_name_conflicts(self.profile.enabled_tools(cx).as_slice())
+ self.profile
+ .enabled_tools(cx)
.into_iter()
.filter_map(|(name, tool)| {
// Skip tools that cannot be supported
let input_schema = tool.input_schema(model.tool_input_format()).ok()?;
Some(LanguageModelRequestTool {
- name,
+ name: name.into(),
description: tool.description(),
input_schema,
})
@@ -1247,6 +1249,8 @@ impl Thread {
self.remaining_turns -= 1;
+ self.flush_notifications(model.clone(), intent, cx);
+
let request = self.to_completion_request(model.clone(), intent, cx);
self.stream_completion(request, model, intent, window, cx);
@@ -1280,6 +1284,7 @@ impl Thread {
tool_choice: None,
stop: Vec::new(),
temperature: AgentSettings::temperature_for_model(&model, cx),
+ thinking_allowed: true,
};
let available_tools = self.available_tools(cx, model.clone());
@@ -1445,6 +1450,7 @@ impl Thread {
tool_choice: None,
stop: Vec::new(),
temperature: AgentSettings::temperature_for_model(model, cx),
+ thinking_allowed: false,
};
for message in &self.messages {
@@ -1480,6 +1486,111 @@ impl Thread {
request
}
+ /// Insert auto-generated notifications (if any) to the thread
+ fn flush_notifications(
+ &mut self,
+ model: Arc<dyn LanguageModel>,
+ intent: CompletionIntent,
+ cx: &mut Context<Self>,
+ ) {
+ match intent {
+ CompletionIntent::UserPrompt | CompletionIntent::ToolResults => {
+ if let Some(pending_tool_use) = self.attach_tracked_files_state(model, cx) {
+ cx.emit(ThreadEvent::ToolFinished {
+ tool_use_id: pending_tool_use.id.clone(),
+ pending_tool_use: Some(pending_tool_use),
+ });
+ }
+ }
+ CompletionIntent::ThreadSummarization
+ | CompletionIntent::ThreadContextSummarization
+ | CompletionIntent::CreateFile
+ | CompletionIntent::EditFile
+ | CompletionIntent::InlineAssist
+ | CompletionIntent::TerminalInlineAssist
+ | CompletionIntent::GenerateGitCommitMessage => {}
+ };
+ }
+
+ fn attach_tracked_files_state(
+ &mut self,
+ model: Arc<dyn LanguageModel>,
+ cx: &mut App,
+ ) -> Option<PendingToolUse> {
+ let action_log = self.action_log.read(cx);
+
+ action_log.unnotified_stale_buffers(cx).next()?;
+
+ // Represent notification as a simulated `project_notifications` tool call
+ let tool_name = Arc::from("project_notifications");
+ let Some(tool) = self.tools.read(cx).tool(&tool_name, cx) else {
+ debug_panic!("`project_notifications` tool not found");
+ return None;
+ };
+
+ if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) {
+ return None;
+ }
+
+ let input = serde_json::json!({});
+ let request = Arc::new(LanguageModelRequest::default()); // unused
+ let window = None;
+ let tool_result = tool.run(
+ input,
+ request,
+ self.project.clone(),
+ self.action_log.clone(),
+ model.clone(),
+ window,
+ cx,
+ );
+
+ let tool_use_id =
+ LanguageModelToolUseId::from(format!("project_notifications_{}", self.messages.len()));
+
+ let tool_use = LanguageModelToolUse {
+ id: tool_use_id.clone(),
+ name: tool_name.clone(),
+ raw_input: "{}".to_string(),
+ input: serde_json::json!({}),
+ is_input_complete: true,
+ };
+
+ let tool_output = cx.background_executor().block(tool_result.output);
+
+ // Attach a project_notification tool call to the latest existing
+ // Assistant message. We cannot create a new Assistant message
+ // because thinking models require a `thinking` block that we
+ // cannot mock. We cannot send a notification as a normal
+ // (non-tool-use) User message because this distracts Agent
+ // too much.
+ let tool_message_id = self
+ .messages
+ .iter()
+ .enumerate()
+ .rfind(|(_, message)| message.role == Role::Assistant)
+ .map(|(_, message)| message.id)?;
+
+ let tool_use_metadata = ToolUseMetadata {
+ model: model.clone(),
+ thread_id: self.id.clone(),
+ prompt_id: self.last_prompt_id.clone(),
+ };
+
+ self.tool_use
+ .request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx);
+
+ let pending_tool_use = self.tool_use.insert_tool_output(
+ tool_use_id.clone(),
+ tool_name,
+ tool_output,
+ self.configured_model.as_ref(),
+ self.completion_mode,
+ );
+
+ pending_tool_use
+ }
+
pub fn stream_completion(
&mut self,
request: LanguageModelRequest,
@@ -1503,6 +1614,10 @@ impl Thread {
prompt_id: prompt_id.clone(),
};
+ let completion_mode = request
+ .mode
+ .unwrap_or(zed_llm_client::CompletionMode::Normal);
+
self.last_received_chunk_at = Some(Instant::now());
let task = cx.spawn(async move |thread, cx| {
@@ -1852,7 +1967,11 @@ impl Thread {
.unwrap_or(0)
// We know the context window was exceeded in practice, so if our estimate was
// lower than max tokens, the estimate was wrong; return that we exceeded by 1.
- .max(model.max_token_count().saturating_add(1))
+ .max(
+ model
+ .max_token_count_for_mode(completion_mode)
+ .saturating_add(1),
+ )
});
thread.exceeded_window_error = Some(ExceededWindowError {
model_id: model.id(),
@@ -2386,7 +2505,7 @@ impl Thread {
let tool_list = available_tools
.iter()
- .map(|tool| format!("- {}: {}", tool.name(), tool.description()))
+ .map(|(name, tool)| format!("- {}: {}", name, tool.description()))
.collect::<Vec<_>>()
.join("\n");
@@ -2400,6 +2519,7 @@ impl Thread {
hallucinated_tool_name,
Err(anyhow!("Missing tool call: {error_message}")),
self.configured_model.as_ref(),
+ self.completion_mode,
);
cx.emit(ThreadEvent::MissingToolUse {
@@ -2426,6 +2546,7 @@ impl Thread {
tool_name,
Err(anyhow!("Error parsing input JSON: {error}")),
self.configured_model.as_ref(),
+ self.completion_mode,
);
let ui_text = if let Some(pending_tool_use) = &pending_tool_use {
pending_tool_use.ui_text.clone()
@@ -2501,6 +2622,7 @@ impl Thread {
tool_name,
output,
thread.configured_model.as_ref(),
+ thread.completion_mode,
);
thread.tool_finished(tool_use_id, pending_tool_use, false, window, cx);
})
@@ -2606,7 +2728,7 @@ impl Thread {
.profile
.enabled_tools(cx)
.iter()
- .map(|tool| tool.name())
+ .map(|(name, _)| name.clone().into())
.collect();
self.message_feedback.insert(message_id, feedback);
@@ -2977,7 +3099,9 @@ impl Thread {
return TotalTokenUsage::default();
};
- let max = model.model.max_token_count();
+ let max = model
+ .model
+ .max_token_count_for_mode(self.completion_mode().into());
let index = self
.messages
@@ -3004,7 +3128,9 @@ impl Thread {
pub fn total_token_usage(&self) -> Option<TotalTokenUsage> {
let model = self.configured_model.as_ref()?;
- let max = model.model.max_token_count();
+ let max = model
+ .model
+ .max_token_count_for_mode(self.completion_mode().into());
if let Some(exceeded_error) = &self.exceeded_window_error {
if model.model.id() == exceeded_error.model_id {
@@ -3070,6 +3196,7 @@ impl Thread {
tool_name,
err,
self.configured_model.as_ref(),
+ self.completion_mode,
);
self.tool_finished(tool_use_id.clone(), None, true, window, cx);
}
@@ -3144,85 +3271,6 @@ struct PendingCompletion {
_task: Task<()>,
}
-/// Resolves tool name conflicts by ensuring all tool names are unique.
-///
-/// When multiple tools have the same name, this function applies the following rules:
-/// 1. Native tools always keep their original name
-/// 2. Context server tools get prefixed with their server ID and an underscore
-/// 3. All tool names are truncated to MAX_TOOL_NAME_LENGTH (64 characters)
-/// 4. If conflicts still exist after prefixing, the conflicting tools are filtered out
-///
-/// Note: This function assumes that built-in tools occur before MCP tools in the tools list.
-fn resolve_tool_name_conflicts(tools: &[Arc<dyn Tool>]) -> Vec<(String, Arc<dyn Tool>)> {
- fn resolve_tool_name(tool: &Arc<dyn Tool>) -> String {
- let mut tool_name = tool.name();
- tool_name.truncate(MAX_TOOL_NAME_LENGTH);
- tool_name
- }
-
- const MAX_TOOL_NAME_LENGTH: usize = 64;
-
- let mut duplicated_tool_names = HashSet::default();
- let mut seen_tool_names = HashSet::default();
- for tool in tools {
- let tool_name = resolve_tool_name(tool);
- if seen_tool_names.contains(&tool_name) {
- debug_assert!(
- tool.source() != assistant_tool::ToolSource::Native,
- "There are two built-in tools with the same name: {}",
- tool_name
- );
- duplicated_tool_names.insert(tool_name);
- } else {
- seen_tool_names.insert(tool_name);
- }
- }
-
- if duplicated_tool_names.is_empty() {
- return tools
- .into_iter()
- .map(|tool| (resolve_tool_name(tool), tool.clone()))
- .collect();
- }
-
- tools
- .into_iter()
- .filter_map(|tool| {
- let mut tool_name = resolve_tool_name(tool);
- if !duplicated_tool_names.contains(&tool_name) {
- return Some((tool_name, tool.clone()));
- }
- match tool.source() {
- assistant_tool::ToolSource::Native => {
- // Built-in tools always keep their original name
- Some((tool_name, tool.clone()))
- }
- assistant_tool::ToolSource::ContextServer { id } => {
- // Context server tools are prefixed with the context server ID, and truncated if necessary
- tool_name.insert(0, '_');
- if tool_name.len() + id.len() > MAX_TOOL_NAME_LENGTH {
- let len = MAX_TOOL_NAME_LENGTH - tool_name.len();
- let mut id = id.to_string();
- id.truncate(len);
- tool_name.insert_str(0, &id);
- } else {
- tool_name.insert_str(0, &id);
- }
-
- tool_name.truncate(MAX_TOOL_NAME_LENGTH);
-
- if seen_tool_names.contains(&tool_name) {
- log::error!("Cannot resolve tool name conflict for tool {}", tool.name());
- None
- } else {
- Some((tool_name, tool.clone()))
- }
- }
- }
- })
- .collect()
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -3234,11 +3282,13 @@ mod tests {
const TEST_RATE_LIMIT_RETRY_SECS: u64 = 30;
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelParameters};
use assistant_tool::ToolRegistry;
+ use assistant_tools;
use futures::StreamExt;
use futures::future::BoxFuture;
use futures::stream::BoxStream;
use gpui::TestAppContext;
- use icons::IconName;
+ use http_client;
+ use indoc::indoc;
use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider};
use language_model::{
LanguageModelCompletionError, LanguageModelName, LanguageModelProviderId,
@@ -3566,6 +3616,134 @@ fn main() {{
);
}
+ #[gpui::test]
+ async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
+ init_test_settings(cx);
+
+ let project = create_test_project(
+ cx,
+ json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
+ )
+ .await;
+
+ let (_workspace, _thread_store, thread, context_store, model) =
+ setup_test_environment(cx, project.clone()).await;
+
+ // Add a buffer to the context. This will be a tracked buffer
+ let buffer = add_file_to_context(&project, &context_store, "test/code.rs", cx)
+ .await
+ .unwrap();
+
+ let context = context_store
+ .read_with(cx, |store, _| store.context().next().cloned())
+ .unwrap();
+ let loaded_context = cx
+ .update(|cx| load_context(vec![context], &project, &None, cx))
+ .await;
+
+ // Insert user message and assistant response
+ thread.update(cx, |thread, cx| {
+ thread.insert_user_message("Explain this code", loaded_context, None, Vec::new(), cx);
+ thread.insert_assistant_message(
+ vec![MessageSegment::Text("This code prints 42.".into())],
+ cx,
+ );
+ });
+
+ // We shouldn't have a stale buffer notification yet
+ let notifications = thread.read_with(cx, |thread, _| {
+ find_tool_uses(thread, "project_notifications")
+ });
+ assert!(
+ notifications.is_empty(),
+ "Should not have stale buffer notification before buffer is modified"
+ );
+
+ // Modify the buffer
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit(
+ [(1..1, "\n println!(\"Added a new line\");\n")],
+ None,
+ cx,
+ );
+ });
+
+ // Insert another user message
+ thread.update(cx, |thread, cx| {
+ thread.insert_user_message(
+ "What does the code do now?",
+ ContextLoadResult::default(),
+ None,
+ Vec::new(),
+ cx,
+ )
+ });
+
+ // Check for the stale buffer warning
+ thread.update(cx, |thread, cx| {
+ thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx)
+ });
+
+ let notifications = thread.read_with(cx, |thread, _cx| {
+ find_tool_uses(thread, "project_notifications")
+ });
+
+ let [notification] = notifications.as_slice() else {
+ panic!("Should have a `project_notifications` tool use");
+ };
+
+ let Some(notification_content) = notification.content.to_str() else {
+ panic!("`project_notifications` should return text");
+ };
+
+ let expected_content = indoc! {"[The following is an auto-generated notification; do not reply]
+
+ These files have changed since the last read:
+ - code.rs
+ "};
+ assert_eq!(notification_content, expected_content);
+
+ // Insert another user message and flush notifications again
+ thread.update(cx, |thread, cx| {
+ thread.insert_user_message(
+ "Can you tell me more?",
+ ContextLoadResult::default(),
+ None,
+ Vec::new(),
+ cx,
+ )
+ });
+
+ thread.update(cx, |thread, cx| {
+ thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx)
+ });
+
+ // There should be no new notifications (we already flushed one)
+ let notifications = thread.read_with(cx, |thread, _cx| {
+ find_tool_uses(thread, "project_notifications")
+ });
+
+ assert_eq!(
+ notifications.len(),
+ 1,
+ "Should still have only one notification after second flush - no duplicates"
+ );
+ }
+
+ fn find_tool_uses(thread: &Thread, tool_name: &str) -> Vec<LanguageModelToolResult> {
+ thread
+ .messages()
+ .flat_map(|message| {
+ thread
+ .tool_results_for_message(message.id)
+ .into_iter()
+ .filter(|result| result.tool_name == tool_name.into())
+ .cloned()
+ .collect::<Vec<_>>()
+ })
+ .collect()
+ }
+
#[gpui::test]
async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) {
init_test_settings(cx);
@@ -3883,148 +4061,6 @@ fn main() {{
});
}
- #[gpui::test]
- fn test_resolve_tool_name_conflicts() {
- use assistant_tool::{Tool, ToolSource};
-
- assert_resolve_tool_name_conflicts(
- vec![
- TestTool::new("tool1", ToolSource::Native),
- TestTool::new("tool2", ToolSource::Native),
- TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
- ],
- vec!["tool1", "tool2", "tool3"],
- );
-
- assert_resolve_tool_name_conflicts(
- vec![
- TestTool::new("tool1", ToolSource::Native),
- TestTool::new("tool2", ToolSource::Native),
- TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
- TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
- ],
- vec!["tool1", "tool2", "mcp-1_tool3", "mcp-2_tool3"],
- );
-
- assert_resolve_tool_name_conflicts(
- vec![
- TestTool::new("tool1", ToolSource::Native),
- TestTool::new("tool2", ToolSource::Native),
- TestTool::new("tool3", ToolSource::Native),
- TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
- TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
- ],
- vec!["tool1", "tool2", "tool3", "mcp-1_tool3", "mcp-2_tool3"],
- );
-
- // Test that tool with very long name is always truncated
- assert_resolve_tool_name_conflicts(
- vec![TestTool::new(
- "tool-with-more-then-64-characters-blah-blah-blah-blah-blah-blah-blah-blah",
- ToolSource::Native,
- )],
- vec!["tool-with-more-then-64-characters-blah-blah-blah-blah-blah-blah-"],
- );
-
- // Test deduplication of tools with very long names, in this case the mcp server name should be truncated
- assert_resolve_tool_name_conflicts(
- vec![
- TestTool::new("tool-with-very-very-very-long-name", ToolSource::Native),
- TestTool::new(
- "tool-with-very-very-very-long-name",
- ToolSource::ContextServer {
- id: "mcp-with-very-very-very-long-name".into(),
- },
- ),
- ],
- vec![
- "tool-with-very-very-very-long-name",
- "mcp-with-very-very-very-long-_tool-with-very-very-very-long-name",
- ],
- );
-
- fn assert_resolve_tool_name_conflicts(
- tools: Vec<TestTool>,
- expected: Vec<impl Into<String>>,
- ) {
- let tools: Vec<Arc<dyn Tool>> = tools
- .into_iter()
- .map(|t| Arc::new(t) as Arc<dyn Tool>)
- .collect();
- let tools = resolve_tool_name_conflicts(&tools);
- assert_eq!(tools.len(), expected.len());
- for (i, expected_name) in expected.into_iter().enumerate() {
- let expected_name = expected_name.into();
- let actual_name = &tools[i].0;
- assert_eq!(
- actual_name, &expected_name,
- "Expected '{}' got '{}' at index {}",
- expected_name, actual_name, i
- );
- }
- }
-
- struct TestTool {
- name: String,
- source: ToolSource,
- }
-
- impl TestTool {
- fn new(name: impl Into<String>, source: ToolSource) -> Self {
- Self {
- name: name.into(),
- source,
- }
- }
- }
-
- impl Tool for TestTool {
- fn name(&self) -> String {
- self.name.clone()
- }
-
- fn icon(&self) -> IconName {
- IconName::Ai
- }
-
- fn may_perform_edits(&self) -> bool {
- false
- }
-
- fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
- true
- }
-
- fn source(&self) -> ToolSource {
- self.source.clone()
- }
-
- fn description(&self) -> String {
- "Test tool".to_string()
- }
-
- fn ui_text(&self, _input: &serde_json::Value) -> String {
- "Test tool".to_string()
- }
-
- fn run(
- self: Arc<Self>,
- _input: serde_json::Value,
- _request: Arc<LanguageModelRequest>,
- _project: Entity<Project>,
- _action_log: Entity<ActionLog>,
- _model: Arc<dyn LanguageModel>,
- _window: Option<AnyWindowHandle>,
- _cx: &mut App,
- ) -> assistant_tool::ToolResult {
- assistant_tool::ToolResult {
- output: Task::ready(Err(anyhow::anyhow!("No content"))),
- card: None,
- }
- }
- }
- }
-
// Helper to create a model that returns errors
enum TestError {
Overloaded,
@@ -5273,6 +5309,14 @@ fn main() {{
language_model::init_settings(cx);
ThemeSettings::register(cx);
ToolRegistry::default_global(cx);
+ assistant_tool::init(cx);
+
+ let http_client = Arc::new(http_client::HttpClientWithUrl::new(
+ http_client::FakeHttpClient::with_200_response(),
+ "http://localhost".to_string(),
+ None,
+ ));
+ assistant_tools::init(http_client, cx);
});
}
@@ -6,7 +6,7 @@ use crate::{
};
use agent_settings::{AgentProfileId, CompletionMode};
use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ToolId, ToolWorkingSet};
+use assistant_tool::{Tool, ToolId, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
use context_server::ContextServerId;
@@ -537,8 +537,8 @@ impl ThreadStore {
}
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
- tool_working_set.update(cx, |tool_working_set, _| {
- tool_working_set.remove(&tool_ids);
+ tool_working_set.update(cx, |tool_working_set, cx| {
+ tool_working_set.remove(&tool_ids, cx);
});
}
}
@@ -569,19 +569,17 @@ impl ThreadStore {
.log_err()
{
let tool_ids = tool_working_set
- .update(cx, |tool_working_set, _| {
- response
- .tools
- .into_iter()
- .map(|tool| {
- log::info!("registering context server tool: {:?}", tool.name);
- tool_working_set.insert(Arc::new(ContextServerTool::new(
+ .update(cx, |tool_working_set, cx| {
+ tool_working_set.extend(
+ response.tools.into_iter().map(|tool| {
+ Arc::new(ContextServerTool::new(
context_server_store.clone(),
server.id(),
tool,
- )))
- })
- .collect::<Vec<_>>()
+ )) as Arc<dyn Tool>
+ }),
+ cx,
+ )
})
.log_err();
@@ -2,6 +2,7 @@ use crate::{
thread::{MessageId, PromptId, ThreadId},
thread_store::SerializedMessage,
};
+use agent_settings::CompletionMode;
use anyhow::Result;
use assistant_tool::{
AnyToolCard, Tool, ToolResultContent, ToolResultOutput, ToolUseStatus, ToolWorkingSet,
@@ -11,8 +12,9 @@ use futures::{FutureExt as _, future::Shared};
use gpui::{App, Entity, SharedString, Task, Window};
use icons::IconName;
use language_model::{
- ConfiguredModel, LanguageModel, LanguageModelRequest, LanguageModelToolResult,
- LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, Role,
+ ConfiguredModel, LanguageModel, LanguageModelExt, LanguageModelRequest,
+ LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolUse,
+ LanguageModelToolUseId, Role,
};
use project::Project;
use std::sync::Arc;
@@ -400,6 +402,7 @@ impl ToolUseState {
tool_name: Arc<str>,
output: Result<ToolResultOutput>,
configured_model: Option<&ConfiguredModel>,
+ completion_mode: CompletionMode,
) -> Option<PendingToolUse> {
let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id);
@@ -426,7 +429,10 @@ impl ToolUseState {
// Protect from overly large output
let tool_output_limit = configured_model
- .map(|model| model.model.max_token_count() as usize * BYTES_PER_TOKEN_ESTIMATE)
+ .map(|model| {
+ model.model.max_token_count_for_mode(completion_mode.into()) as usize
+ * BYTES_PER_TOKEN_ESTIMATE
+ })
.unwrap_or(usize::MAX);
let content = match tool_result {
@@ -0,0 +1,27 @@
+[package]
+name = "agent_servers"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/agent_servers.rs"
+doctest = false
+
+[dependencies]
+anyhow.workspace = true
+collections.workspace = true
+futures.workspace = true
+gpui.workspace = true
+paths.workspace = true
+project.workspace = true
+schemars.workspace = true
+serde.workspace = true
+settings.workspace = true
+util.workspace = true
+which.workspace = true
+workspace-hack.workspace = true
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -0,0 +1,231 @@
+use std::{
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+
+use anyhow::{Context as _, Result};
+use collections::HashMap;
+use gpui::{App, AsyncApp, Entity, SharedString};
+use project::Project;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsSources, SettingsStore};
+use util::{ResultExt, paths};
+
+pub fn init(cx: &mut App) {
+ AllAgentServersSettings::register(cx);
+}
+
+#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
+pub struct AllAgentServersSettings {
+ gemini: Option<AgentServerSettings>,
+}
+
+#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
+pub struct AgentServerSettings {
+ #[serde(flatten)]
+ command: AgentServerCommand,
+}
+
+#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
+pub struct AgentServerCommand {
+ #[serde(rename = "command")]
+ pub path: PathBuf,
+ #[serde(default)]
+ pub args: Vec<String>,
+ pub env: Option<HashMap<String, String>>,
+}
+
+pub struct Gemini;
+
+pub struct AgentServerVersion {
+ pub current_version: SharedString,
+ pub supported: bool,
+}
+
+pub trait AgentServer: Send {
+ fn command(
+ &self,
+ project: &Entity<Project>,
+ cx: &mut AsyncApp,
+ ) -> impl Future<Output = Result<AgentServerCommand>>;
+
+ fn version(
+ &self,
+ command: &AgentServerCommand,
+ ) -> impl Future<Output = Result<AgentServerVersion>> + Send;
+}
+
+const GEMINI_ACP_ARG: &str = "--acp";
+
+impl AgentServer for Gemini {
+ async fn command(
+ &self,
+ project: &Entity<Project>,
+ cx: &mut AsyncApp,
+ ) -> Result<AgentServerCommand> {
+ let custom_command = cx.read_global(|settings: &SettingsStore, _| {
+ let settings = settings.get::<AllAgentServersSettings>(None);
+ settings
+ .gemini
+ .as_ref()
+ .map(|gemini_settings| AgentServerCommand {
+ path: gemini_settings.command.path.clone(),
+ args: gemini_settings
+ .command
+ .args
+ .iter()
+ .cloned()
+ .chain(std::iter::once(GEMINI_ACP_ARG.into()))
+ .collect(),
+ env: gemini_settings.command.env.clone(),
+ })
+ })?;
+
+ if let Some(custom_command) = custom_command {
+ return Ok(custom_command);
+ }
+
+ if let Some(path) = find_bin_in_path("gemini", project, cx).await {
+ return Ok(AgentServerCommand {
+ path,
+ args: vec![GEMINI_ACP_ARG.into()],
+ env: None,
+ });
+ }
+
+ let (fs, node_runtime) = project.update(cx, |project, _| {
+ (project.fs().clone(), project.node_runtime().cloned())
+ })?;
+ let node_runtime = node_runtime.context("gemini not found on path")?;
+
+ let directory = ::paths::agent_servers_dir().join("gemini");
+ fs.create_dir(&directory).await?;
+ node_runtime
+ .npm_install_packages(&directory, &[("@google/gemini-cli", "latest")])
+ .await?;
+ let path = directory.join("node_modules/.bin/gemini");
+
+ Ok(AgentServerCommand {
+ path,
+ args: vec![GEMINI_ACP_ARG.into()],
+ env: None,
+ })
+ }
+
+ async fn version(&self, command: &AgentServerCommand) -> Result<AgentServerVersion> {
+ let version_fut = util::command::new_smol_command(&command.path)
+ .args(command.args.iter())
+ .arg("--version")
+ .kill_on_drop(true)
+ .output();
+
+ let help_fut = util::command::new_smol_command(&command.path)
+ .args(command.args.iter())
+ .arg("--help")
+ .kill_on_drop(true)
+ .output();
+
+ let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
+
+ let current_version = String::from_utf8(version_output?.stdout)?.into();
+ let supported = String::from_utf8(help_output?.stdout)?.contains(GEMINI_ACP_ARG);
+
+ Ok(AgentServerVersion {
+ current_version,
+ supported,
+ })
+ }
+}
+
+async fn find_bin_in_path(
+ bin_name: &'static str,
+ project: &Entity<Project>,
+ cx: &mut AsyncApp,
+) -> Option<PathBuf> {
+ let (env_task, root_dir) = project
+ .update(cx, |project, cx| {
+ let worktree = project.visible_worktrees(cx).next();
+ match worktree {
+ Some(worktree) => {
+ let env_task = project.environment().update(cx, |env, cx| {
+ env.get_worktree_environment(worktree.clone(), cx)
+ });
+
+ let path = worktree.read(cx).abs_path();
+ (env_task, path)
+ }
+ None => {
+ let path: Arc<Path> = paths::home_dir().as_path().into();
+ let env_task = project.environment().update(cx, |env, cx| {
+ env.get_directory_environment(path.clone(), cx)
+ });
+ (env_task, path)
+ }
+ }
+ })
+ .log_err()?;
+
+ cx.background_executor()
+ .spawn(async move {
+ let which_result = if cfg!(windows) {
+ which::which(bin_name)
+ } else {
+ let env = env_task.await.unwrap_or_default();
+ let shell_path = env.get("PATH").cloned();
+ which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref())
+ };
+
+ if let Err(which::Error::CannotFindBinaryPath) = which_result {
+ return None;
+ }
+
+ which_result.log_err()
+ })
+ .await
+}
+
+impl std::fmt::Debug for AgentServerCommand {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let filtered_env = self.env.as_ref().map(|env| {
+ env.iter()
+ .map(|(k, v)| {
+ (
+ k,
+ if util::redact::should_redact(k) {
+ "[REDACTED]"
+ } else {
+ v
+ },
+ )
+ })
+ .collect::<Vec<_>>()
+ });
+
+ f.debug_struct("AgentServerCommand")
+ .field("path", &self.path)
+ .field("args", &self.args)
+ .field("env", &filtered_env)
+ .finish()
+ }
+}
+
+impl settings::Settings for AllAgentServersSettings {
+ const KEY: Option<&'static str> = Some("agent_servers");
+
+ type FileContent = Self;
+
+ fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
+ let mut settings = AllAgentServersSettings::default();
+
+ for value in sources.defaults_and_customizations() {
+ if value.gemini.is_some() {
+ settings.gemini = value.gemini.clone();
+ }
+ }
+
+ Ok(settings)
+ }
+
+ fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+}
@@ -67,6 +67,8 @@ pub struct AgentSettings {
pub model_parameters: Vec<LanguageModelParameters>,
pub preferred_completion_mode: CompletionMode,
pub enable_feedback: bool,
+ pub expand_edit_card: bool,
+ pub expand_terminal_card: bool,
}
impl AgentSettings {
@@ -291,6 +293,14 @@ pub struct AgentSettingsContent {
///
/// Default: true
enable_feedback: Option<bool>,
+ /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
+ ///
+ /// Default: true
+ expand_edit_card: Option<bool>,
+ /// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
+ ///
+ /// Default: true
+ expand_terminal_card: Option<bool>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
@@ -441,6 +451,11 @@ impl Settings for AgentSettings {
value.preferred_completion_mode,
);
merge(&mut settings.enable_feedback, value.enable_feedback);
+ merge(&mut settings.expand_edit_card, value.expand_edit_card);
+ merge(
+ &mut settings.expand_terminal_card,
+ value.expand_terminal_card,
+ );
settings
.model_parameters
@@ -13,14 +13,14 @@ path = "src/agent_ui.rs"
doctest = false
[features]
-test-support = [
- "gpui/test-support",
- "language/test-support",
-]
+test-support = ["gpui/test-support", "language/test-support"]
[dependencies]
+acp.workspace = true
agent.workspace = true
+agentic-coding-protocol.workspace = true
agent_settings.workspace = true
+agent_servers.workspace = true
anyhow.workspace = true
assistant_context.workspace = true
assistant_slash_command.workspace = true
@@ -76,6 +76,7 @@ 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
@@ -0,0 +1,6 @@
+mod completion_provider;
+mod message_history;
+mod thread_view;
+
+pub use message_history::MessageHistory;
+pub use thread_view::AcpThreadView;
@@ -0,0 +1,574 @@
+use std::ops::Range;
+use std::path::Path;
+use std::sync::Arc;
+use std::sync::atomic::AtomicBool;
+
+use anyhow::Result;
+use collections::HashMap;
+use editor::display_map::CreaseId;
+use editor::{CompletionProvider, Editor, ExcerptId};
+use file_icons::FileIcons;
+use gpui::{App, Entity, Task, WeakEntity};
+use language::{Buffer, CodeLabel, HighlightId};
+use lsp::CompletionContext;
+use parking_lot::Mutex;
+use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, WorktreeId};
+use rope::Point;
+use text::{Anchor, ToPoint};
+use ui::prelude::*;
+use workspace::Workspace;
+
+use crate::context_picker::MentionLink;
+use crate::context_picker::file_context_picker::{extract_file_name_and_directory, search_files};
+
+#[derive(Default)]
+pub struct MentionSet {
+ paths_by_crease_id: HashMap<CreaseId, ProjectPath>,
+}
+
+impl MentionSet {
+ pub fn insert(&mut self, crease_id: CreaseId, path: ProjectPath) {
+ self.paths_by_crease_id.insert(crease_id, path);
+ }
+
+ pub fn path_for_crease_id(&self, crease_id: CreaseId) -> Option<ProjectPath> {
+ self.paths_by_crease_id.get(&crease_id).cloned()
+ }
+
+ pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
+ self.paths_by_crease_id.drain().map(|(id, _)| id)
+ }
+}
+
+pub struct ContextPickerCompletionProvider {
+ workspace: WeakEntity<Workspace>,
+ editor: WeakEntity<Editor>,
+ mention_set: Arc<Mutex<MentionSet>>,
+}
+
+impl ContextPickerCompletionProvider {
+ pub fn new(
+ mention_set: Arc<Mutex<MentionSet>>,
+ workspace: WeakEntity<Workspace>,
+ editor: WeakEntity<Editor>,
+ ) -> Self {
+ Self {
+ mention_set,
+ workspace,
+ editor,
+ }
+ }
+
+ fn completion_for_path(
+ project_path: ProjectPath,
+ path_prefix: &str,
+ is_recent: bool,
+ is_directory: bool,
+ excerpt_id: ExcerptId,
+ source_range: Range<Anchor>,
+ editor: Entity<Editor>,
+ mention_set: Arc<Mutex<MentionSet>>,
+ cx: &App,
+ ) -> Completion {
+ let (file_name, directory) =
+ extract_file_name_and_directory(&project_path.path, path_prefix);
+
+ let label =
+ build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
+ let full_path = if let Some(directory) = directory {
+ format!("{}{}", directory, file_name)
+ } else {
+ file_name.to_string()
+ };
+
+ let crease_icon_path = if is_directory {
+ FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
+ } else {
+ FileIcons::get_icon(Path::new(&full_path), cx)
+ .unwrap_or_else(|| IconName::File.path().into())
+ };
+ let completion_icon_path = if is_recent {
+ IconName::HistoryRerun.path().into()
+ } else {
+ crease_icon_path.clone()
+ };
+
+ let new_text = format!("{} ", MentionLink::for_file(&file_name, &full_path));
+ let new_text_len = new_text.len();
+ Completion {
+ replace_range: source_range.clone(),
+ new_text,
+ label,
+ documentation: None,
+ source: project::CompletionSource::Custom,
+ icon_path: Some(completion_icon_path),
+ insert_text_mode: None,
+ confirm: Some(confirm_completion_callback(
+ crease_icon_path,
+ file_name,
+ project_path,
+ excerpt_id,
+ source_range.start,
+ new_text_len - 1,
+ editor,
+ mention_set,
+ )),
+ }
+ }
+}
+
+fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
+ let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
+ let mut label = CodeLabel::default();
+
+ label.push_str(&file_name, None);
+ label.push_str(" ", None);
+
+ if let Some(directory) = directory {
+ label.push_str(&directory, comment_id);
+ }
+
+ label.filter_range = 0..label.text().len();
+
+ label
+}
+
+impl CompletionProvider for ContextPickerCompletionProvider {
+ fn completions(
+ &self,
+ excerpt_id: ExcerptId,
+ buffer: &Entity<Buffer>,
+ buffer_position: Anchor,
+ _trigger: CompletionContext,
+ _window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) -> Task<Result<Vec<CompletionResponse>>> {
+ let state = buffer.update(cx, |buffer, _cx| {
+ let position = buffer_position.to_point(buffer);
+ let line_start = Point::new(position.row, 0);
+ let offset_to_line = buffer.point_to_offset(line_start);
+ let mut lines = buffer.text_for_range(line_start..position).lines();
+ let line = lines.next()?;
+ MentionCompletion::try_parse(line, offset_to_line)
+ });
+ let Some(state) = state else {
+ return Task::ready(Ok(Vec::new()));
+ };
+
+ let Some(workspace) = self.workspace.upgrade() else {
+ return Task::ready(Ok(Vec::new()));
+ };
+
+ let snapshot = buffer.read(cx).snapshot();
+ let source_range = snapshot.anchor_before(state.source_range.start)
+ ..snapshot.anchor_after(state.source_range.end);
+
+ let editor = self.editor.clone();
+ let mention_set = self.mention_set.clone();
+ let MentionCompletion { argument, .. } = state;
+ let query = argument.unwrap_or_else(|| "".to_string());
+
+ let search_task = search_files(query.clone(), Arc::<AtomicBool>::default(), &workspace, cx);
+
+ cx.spawn(async move |_, cx| {
+ let matches = search_task.await;
+ let Some(editor) = editor.upgrade() else {
+ return Ok(Vec::new());
+ };
+
+ let completions = cx.update(|cx| {
+ matches
+ .into_iter()
+ .map(|mat| {
+ let path_match = &mat.mat;
+ let project_path = ProjectPath {
+ worktree_id: WorktreeId::from_usize(path_match.worktree_id),
+ path: path_match.path.clone(),
+ };
+
+ Self::completion_for_path(
+ project_path,
+ &path_match.path_prefix,
+ mat.is_recent,
+ path_match.is_dir,
+ excerpt_id,
+ source_range.clone(),
+ editor.clone(),
+ mention_set.clone(),
+ cx,
+ )
+ })
+ .collect()
+ })?;
+
+ Ok(vec![CompletionResponse {
+ completions,
+ // Since this does its own filtering (see `filter_completions()` returns false),
+ // there is no benefit to computing whether this set of completions is incomplete.
+ is_incomplete: true,
+ }])
+ })
+ }
+
+ fn is_completion_trigger(
+ &self,
+ buffer: &Entity<language::Buffer>,
+ position: language::Anchor,
+ _text: &str,
+ _trigger_in_words: bool,
+ _menu_is_open: bool,
+ cx: &mut Context<Editor>,
+ ) -> bool {
+ let buffer = buffer.read(cx);
+ let position = position.to_point(buffer);
+ let line_start = Point::new(position.row, 0);
+ let offset_to_line = buffer.point_to_offset(line_start);
+ let mut lines = buffer.text_for_range(line_start..position).lines();
+ if let Some(line) = lines.next() {
+ MentionCompletion::try_parse(line, offset_to_line)
+ .map(|completion| {
+ completion.source_range.start <= offset_to_line + position.column as usize
+ && completion.source_range.end >= offset_to_line + position.column as usize
+ })
+ .unwrap_or(false)
+ } else {
+ false
+ }
+ }
+
+ fn sort_completions(&self) -> bool {
+ false
+ }
+
+ fn filter_completions(&self) -> bool {
+ false
+ }
+}
+
+fn confirm_completion_callback(
+ crease_icon_path: SharedString,
+ crease_text: SharedString,
+ project_path: ProjectPath,
+ excerpt_id: ExcerptId,
+ start: Anchor,
+ content_len: usize,
+ editor: Entity<Editor>,
+ mention_set: Arc<Mutex<MentionSet>>,
+) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
+ Arc::new(move |_, window, cx| {
+ let crease_text = crease_text.clone();
+ let crease_icon_path = crease_icon_path.clone();
+ let editor = editor.clone();
+ let project_path = project_path.clone();
+ let mention_set = mention_set.clone();
+ window.defer(cx, move |window, cx| {
+ let crease_id = crate::context_picker::insert_crease_for_mention(
+ excerpt_id,
+ start,
+ content_len,
+ crease_text.clone(),
+ crease_icon_path,
+ editor.clone(),
+ window,
+ cx,
+ );
+ if let Some(crease_id) = crease_id {
+ mention_set.lock().insert(crease_id, project_path);
+ }
+ });
+ false
+ })
+}
+
+#[derive(Debug, Default, PartialEq)]
+struct MentionCompletion {
+ source_range: Range<usize>,
+ argument: Option<String>,
+}
+
+impl MentionCompletion {
+ fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
+ let last_mention_start = line.rfind('@')?;
+ if last_mention_start >= line.len() {
+ return Some(Self::default());
+ }
+ if last_mention_start > 0
+ && line
+ .chars()
+ .nth(last_mention_start - 1)
+ .map_or(false, |c| !c.is_whitespace())
+ {
+ return None;
+ }
+
+ let rest_of_line = &line[last_mention_start + 1..];
+ let mut argument = None;
+
+ let mut parts = rest_of_line.split_whitespace();
+ let mut end = last_mention_start + 1;
+ if let Some(argument_text) = parts.next() {
+ end += argument_text.len();
+ argument = Some(argument_text.to_string());
+ }
+
+ Some(Self {
+ source_range: last_mention_start + offset_to_line..end + offset_to_line,
+ argument,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
+ use project::{Project, ProjectPath};
+ use serde_json::json;
+ use settings::SettingsStore;
+ use std::{ops::Deref, rc::Rc};
+ use util::path;
+ use workspace::{AppState, Item};
+
+ #[test]
+ fn test_mention_completion_parse() {
+ assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None);
+
+ assert_eq!(
+ MentionCompletion::try_parse("Lorem @", 0),
+ Some(MentionCompletion {
+ source_range: 6..7,
+ argument: None,
+ })
+ );
+
+ assert_eq!(
+ MentionCompletion::try_parse("Lorem @main", 0),
+ Some(MentionCompletion {
+ source_range: 6..11,
+ argument: Some("main".to_string()),
+ })
+ );
+
+ assert_eq!(MentionCompletion::try_parse("test@", 0), None);
+ }
+
+ struct AtMentionEditor(Entity<Editor>);
+
+ impl Item for AtMentionEditor {
+ type Event = ();
+
+ fn include_in_nav_history() -> bool {
+ false
+ }
+
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+ "Test".into()
+ }
+ }
+
+ impl EventEmitter<()> for AtMentionEditor {}
+
+ impl Focusable for AtMentionEditor {
+ fn focus_handle(&self, cx: &App) -> FocusHandle {
+ self.0.read(cx).focus_handle(cx).clone()
+ }
+ }
+
+ impl Render for AtMentionEditor {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+ self.0.clone().into_any_element()
+ }
+ }
+
+ #[gpui::test]
+ async fn test_context_completion_provider(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let app_state = cx.update(AppState::test);
+
+ cx.update(|cx| {
+ language::init(cx);
+ editor::init(cx);
+ workspace::init(app_state.clone(), cx);
+ Project::init_settings(cx);
+ });
+
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ path!("/dir"),
+ json!({
+ "editor": "",
+ "a": {
+ "one.txt": "",
+ "two.txt": "",
+ "three.txt": "",
+ "four.txt": ""
+ },
+ "b": {
+ "five.txt": "",
+ "six.txt": "",
+ "seven.txt": "",
+ "eight.txt": "",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
+ let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let workspace = window.root(cx).unwrap();
+
+ let worktree = project.update(cx, |project, cx| {
+ let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
+ assert_eq!(worktrees.len(), 1);
+ worktrees.pop().unwrap()
+ });
+ let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
+
+ let mut cx = VisualTestContext::from_window(*window.deref(), cx);
+
+ let paths = vec![
+ path!("a/one.txt"),
+ path!("a/two.txt"),
+ path!("a/three.txt"),
+ path!("a/four.txt"),
+ path!("b/five.txt"),
+ path!("b/six.txt"),
+ path!("b/seven.txt"),
+ path!("b/eight.txt"),
+ ];
+
+ let mut opened_editors = Vec::new();
+ for path in paths {
+ let buffer = workspace
+ .update_in(&mut cx, |workspace, window, cx| {
+ workspace.open_path(
+ ProjectPath {
+ worktree_id,
+ path: Path::new(path).into(),
+ },
+ None,
+ false,
+ window,
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+ opened_editors.push(buffer);
+ }
+
+ let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
+ let editor = cx.new(|cx| {
+ Editor::new(
+ editor::EditorMode::full(),
+ multi_buffer::MultiBuffer::build_simple("", cx),
+ None,
+ window,
+ cx,
+ )
+ });
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.add_item(
+ Box::new(cx.new(|_| AtMentionEditor(editor.clone()))),
+ true,
+ true,
+ None,
+ window,
+ cx,
+ );
+ });
+ editor
+ });
+
+ let mention_set = Arc::new(Mutex::new(MentionSet::default()));
+
+ let editor_entity = editor.downgrade();
+ editor.update_in(&mut cx, |editor, window, cx| {
+ window.focus(&editor.focus_handle(cx));
+ editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
+ mention_set.clone(),
+ workspace.downgrade(),
+ editor_entity,
+ ))));
+ });
+
+ cx.simulate_input("Lorem ");
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(editor.text(cx), "Lorem ");
+ assert!(!editor.has_visible_completions_menu());
+ });
+
+ cx.simulate_input("@");
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(editor.text(cx), "Lorem @");
+ assert!(editor.has_visible_completions_menu());
+ assert_eq!(
+ current_completion_labels(editor),
+ &[
+ "eight.txt dir/b/",
+ "seven.txt dir/b/",
+ "six.txt dir/b/",
+ "five.txt dir/b/",
+ "four.txt dir/a/",
+ "three.txt dir/a/",
+ "two.txt dir/a/",
+ "one.txt dir/a/",
+ "dir ",
+ "a dir/",
+ "four.txt dir/a/",
+ "one.txt dir/a/",
+ "three.txt dir/a/",
+ "two.txt dir/a/",
+ "b dir/",
+ "eight.txt dir/b/",
+ "five.txt dir/b/",
+ "seven.txt dir/b/",
+ "six.txt dir/b/",
+ "editor dir/"
+ ]
+ );
+ });
+
+ // Select and confirm "File"
+ editor.update_in(&mut cx, |editor, window, cx| {
+ assert!(editor.has_visible_completions_menu());
+ editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+ editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+ editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+ editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+ editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+ });
+
+ cx.run_until_parked();
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(editor.text(cx), "Lorem [@four.txt](@file:dir/a/four.txt) ");
+ });
+ }
+
+ fn current_completion_labels(editor: &Editor) -> Vec<String> {
+ let completions = editor.current_completions().expect("Missing completions");
+ completions
+ .into_iter()
+ .map(|completion| completion.label.text.to_string())
+ .collect::<Vec<_>>()
+ }
+
+ pub(crate) fn init_test(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let store = SettingsStore::test(cx);
+ cx.set_global(store);
+ theme::init(theme::LoadThemes::JustBase, cx);
+ client::init_settings(cx);
+ language::init(cx);
+ Project::init_settings(cx);
+ workspace::init_settings(cx);
+ editor::init_settings(cx);
+ });
+ }
+}
@@ -0,0 +1,87 @@
+pub struct MessageHistory<T> {
+ items: Vec<T>,
+ current: Option<usize>,
+}
+
+impl<T> Default for MessageHistory<T> {
+ fn default() -> Self {
+ MessageHistory {
+ items: Vec::new(),
+ current: None,
+ }
+ }
+}
+
+impl<T> MessageHistory<T> {
+ pub fn push(&mut self, message: T) {
+ self.current.take();
+ self.items.push(message);
+ }
+
+ pub fn reset_position(&mut self) {
+ self.current.take();
+ }
+
+ pub fn prev(&mut self) -> Option<&T> {
+ if self.items.is_empty() {
+ return None;
+ }
+
+ let new_ix = self
+ .current
+ .get_or_insert(self.items.len())
+ .saturating_sub(1);
+
+ self.current = Some(new_ix);
+ self.items.get(new_ix)
+ }
+
+ pub fn next(&mut self) -> Option<&T> {
+ let current = self.current.as_mut()?;
+ *current += 1;
+
+ self.items.get(*current).or_else(|| {
+ self.current.take();
+ None
+ })
+ }
+}
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_prev_next() {
+ let mut history = MessageHistory::default();
+
+ // Test empty history
+ assert_eq!(history.prev(), None);
+ assert_eq!(history.next(), None);
+
+ // Add some messages
+ history.push("first");
+ history.push("second");
+ history.push("third");
+
+ // Test prev navigation
+ assert_eq!(history.prev(), Some(&"third"));
+ assert_eq!(history.prev(), Some(&"second"));
+ assert_eq!(history.prev(), Some(&"first"));
+ assert_eq!(history.prev(), Some(&"first"));
+
+ assert_eq!(history.next(), Some(&"second"));
+
+ // Test mixed navigation
+ history.push("fourth");
+ assert_eq!(history.prev(), Some(&"fourth"));
+ assert_eq!(history.prev(), Some(&"third"));
+ assert_eq!(history.next(), Some(&"fourth"));
+ assert_eq!(history.next(), None);
+
+ // Test that push resets navigation
+ history.prev();
+ history.prev();
+ history.push("fifth");
+ assert_eq!(history.prev(), Some(&"fifth"));
+ }
+}
@@ -0,0 +1,2502 @@
+use std::cell::RefCell;
+use std::collections::BTreeMap;
+use std::path::Path;
+use std::rc::Rc;
+use std::sync::Arc;
+use std::time::Duration;
+
+use agentic_coding_protocol::{self as acp};
+use assistant_tool::ActionLog;
+use buffer_diff::BufferDiff;
+use collections::{HashMap, HashSet};
+use editor::{
+ AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
+ EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
+};
+use file_icons::FileIcons;
+use futures::channel::oneshot;
+use gpui::{
+ Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
+ FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
+ Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
+ Window, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*,
+ pulsating_between,
+};
+use language::language_settings::SoftWrap;
+use language::{Buffer, Language};
+use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
+use parking_lot::Mutex;
+use project::Project;
+use settings::Settings as _;
+use text::Anchor;
+use theme::ThemeSettings;
+use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*};
+use util::ResultExt;
+use workspace::{CollaboratorId, Workspace};
+use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
+
+use ::acp::{
+ AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, Diff,
+ LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallConfirmation, ToolCallContent,
+ ToolCallId, ToolCallStatus,
+};
+
+use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
+use crate::acp::message_history::MessageHistory;
+use crate::agent_diff::AgentDiff;
+use crate::{AgentDiffPane, Follow, KeepAll, OpenAgentDiff, RejectAll};
+
+const RESPONSE_PADDING_X: Pixels = px(19.);
+
+pub struct AcpThreadView {
+ workspace: WeakEntity<Workspace>,
+ project: Entity<Project>,
+ thread_state: ThreadState,
+ diff_editors: HashMap<EntityId, Entity<Editor>>,
+ message_editor: Entity<Editor>,
+ message_set_from_history: bool,
+ _message_editor_subscription: Subscription,
+ mention_set: Arc<Mutex<MentionSet>>,
+ last_error: Option<Entity<Markdown>>,
+ list_state: ListState,
+ auth_task: Option<Task<()>>,
+ expanded_tool_calls: HashSet<ToolCallId>,
+ expanded_thinking_blocks: HashSet<(usize, usize)>,
+ edits_expanded: bool,
+ message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
+}
+
+enum ThreadState {
+ Loading {
+ _task: Task<()>,
+ },
+ Ready {
+ thread: Entity<AcpThread>,
+ _subscription: [Subscription; 2],
+ },
+ LoadError(LoadError),
+ Unauthenticated {
+ thread: Entity<AcpThread>,
+ },
+}
+
+impl AcpThreadView {
+ pub fn new(
+ workspace: WeakEntity<Workspace>,
+ project: Entity<Project>,
+ message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let language = Language::new(
+ language::LanguageConfig {
+ completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
+ ..Default::default()
+ },
+ None,
+ );
+
+ let mention_set = Arc::new(Mutex::new(MentionSet::default()));
+
+ let message_editor = cx.new(|cx| {
+ let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
+ let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+
+ let mut editor = Editor::new(
+ editor::EditorMode::AutoHeight {
+ min_lines: 4,
+ max_lines: None,
+ },
+ buffer,
+ None,
+ window,
+ cx,
+ );
+ editor.set_placeholder_text("Message the agent - @ to include files", cx);
+ editor.set_show_indent_guides(false, cx);
+ editor.set_soft_wrap();
+ editor.set_use_modal_editing(true);
+ editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
+ mention_set.clone(),
+ workspace.clone(),
+ cx.weak_entity(),
+ ))));
+ editor.set_context_menu_options(ContextMenuOptions {
+ min_entries_visible: 12,
+ max_entries_visible: 12,
+ placement: Some(ContextMenuPlacement::Above),
+ });
+ editor
+ });
+
+ let message_editor_subscription = cx.subscribe(&message_editor, |this, _, event, _| {
+ if let editor::EditorEvent::BufferEdited = &event {
+ if !this.message_set_from_history {
+ this.message_history.borrow_mut().reset_position();
+ }
+ this.message_set_from_history = false;
+ }
+ });
+
+ let mention_set = mention_set.clone();
+
+ let list_state = ListState::new(
+ 0,
+ gpui::ListAlignment::Bottom,
+ px(2048.0),
+ cx.processor({
+ move |this: &mut Self, index: usize, window, cx| {
+ let Some((entry, len)) = this.thread().and_then(|thread| {
+ let entries = &thread.read(cx).entries();
+ Some((entries.get(index)?, entries.len()))
+ }) else {
+ return Empty.into_any();
+ };
+ this.render_entry(index, len, entry, window, cx)
+ }
+ }),
+ );
+
+ Self {
+ workspace: workspace.clone(),
+ project: project.clone(),
+ thread_state: Self::initial_state(workspace, project, window, cx),
+ message_editor,
+ message_set_from_history: false,
+ _message_editor_subscription: message_editor_subscription,
+ mention_set,
+ diff_editors: Default::default(),
+ list_state: list_state,
+ last_error: None,
+ auth_task: None,
+ expanded_tool_calls: HashSet::default(),
+ expanded_thinking_blocks: HashSet::default(),
+ edits_expanded: false,
+ message_history,
+ }
+ }
+
+ fn initial_state(
+ workspace: WeakEntity<Workspace>,
+ project: Entity<Project>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> ThreadState {
+ let root_dir = project
+ .read(cx)
+ .visible_worktrees(cx)
+ .next()
+ .map(|worktree| worktree.read(cx).abs_path())
+ .unwrap_or_else(|| paths::home_dir().as_path().into());
+
+ let load_task = cx.spawn_in(window, async move |this, cx| {
+ let thread = match AcpThread::spawn(agent_servers::Gemini, &root_dir, project, cx).await
+ {
+ Ok(thread) => thread,
+ Err(err) => {
+ this.update(cx, |this, cx| {
+ this.handle_load_error(err, cx);
+ cx.notify();
+ })
+ .log_err();
+ return;
+ }
+ };
+
+ let init_response = async {
+ let resp = thread
+ .read_with(cx, |thread, _cx| thread.initialize())?
+ .await?;
+ anyhow::Ok(resp)
+ };
+
+ let result = match init_response.await {
+ Err(e) => {
+ let mut cx = cx.clone();
+ if e.downcast_ref::<oneshot::Canceled>().is_some() {
+ let child_status = thread
+ .update(&mut cx, |thread, _| thread.child_status())
+ .ok()
+ .flatten();
+ if let Some(child_status) = child_status {
+ match child_status.await {
+ Ok(_) => Err(e),
+ Err(e) => Err(e),
+ }
+ } else {
+ Err(e)
+ }
+ } else {
+ Err(e)
+ }
+ }
+ Ok(response) => {
+ if !response.is_authenticated {
+ this.update(cx, |this, _| {
+ this.thread_state = ThreadState::Unauthenticated { thread };
+ })
+ .ok();
+ return;
+ };
+ Ok(())
+ }
+ };
+
+ this.update_in(cx, |this, window, cx| {
+ match result {
+ Ok(()) => {
+ let thread_subscription =
+ cx.subscribe_in(&thread, window, Self::handle_thread_event);
+
+ let action_log = thread.read(cx).action_log().clone();
+ let action_log_subscription =
+ cx.observe(&action_log, |_, _, cx| cx.notify());
+
+ this.list_state
+ .splice(0..0, thread.read(cx).entries().len());
+
+ AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
+
+ this.thread_state = ThreadState::Ready {
+ thread,
+ _subscription: [thread_subscription, action_log_subscription],
+ };
+
+ cx.notify();
+ }
+ Err(err) => {
+ this.handle_load_error(err, cx);
+ }
+ };
+ })
+ .log_err();
+ });
+
+ ThreadState::Loading { _task: load_task }
+ }
+
+ fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context<Self>) {
+ if let Some(load_err) = err.downcast_ref::<LoadError>() {
+ self.thread_state = ThreadState::LoadError(load_err.clone());
+ } else {
+ self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into()))
+ }
+ cx.notify();
+ }
+
+ pub fn thread(&self) -> Option<&Entity<AcpThread>> {
+ match &self.thread_state {
+ ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
+ Some(thread)
+ }
+ ThreadState::Loading { .. } | ThreadState::LoadError(..) => None,
+ }
+ }
+
+ pub fn title(&self, cx: &App) -> SharedString {
+ match &self.thread_state {
+ ThreadState::Ready { thread, .. } => thread.read(cx).title(),
+ ThreadState::Loading { .. } => "Loading…".into(),
+ ThreadState::LoadError(_) => "Failed to load".into(),
+ ThreadState::Unauthenticated { .. } => "Not authenticated".into(),
+ }
+ }
+
+ pub fn cancel(&mut self, cx: &mut Context<Self>) {
+ self.last_error.take();
+
+ if let Some(thread) = self.thread() {
+ thread.update(cx, |thread, cx| thread.cancel(cx)).detach();
+ }
+ }
+
+ fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
+ self.last_error.take();
+
+ let mut ix = 0;
+ let mut chunks: Vec<acp::UserMessageChunk> = Vec::new();
+ let project = self.project.clone();
+ self.message_editor.update(cx, |editor, cx| {
+ let text = editor.text(cx);
+ editor.display_map.update(cx, |map, cx| {
+ let snapshot = map.snapshot(cx);
+ for (crease_id, crease) in snapshot.crease_snapshot.creases() {
+ if let Some(project_path) =
+ self.mention_set.lock().path_for_crease_id(crease_id)
+ {
+ let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
+ if crease_range.start > ix {
+ chunks.push(acp::UserMessageChunk::Text {
+ text: text[ix..crease_range.start].to_string(),
+ });
+ }
+ if let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) {
+ chunks.push(acp::UserMessageChunk::Path { path: abs_path });
+ }
+ ix = crease_range.end;
+ }
+ }
+
+ if ix < text.len() {
+ let last_chunk = text[ix..].trim();
+ if !last_chunk.is_empty() {
+ chunks.push(acp::UserMessageChunk::Text {
+ text: last_chunk.into(),
+ });
+ }
+ }
+ })
+ });
+
+ if chunks.is_empty() {
+ return;
+ }
+
+ let Some(thread) = self.thread() else { return };
+ let message = acp::SendUserMessageParams { chunks };
+ let task = thread.update(cx, |thread, cx| thread.send(message.clone(), cx));
+
+ cx.spawn(async move |this, cx| {
+ let result = task.await;
+
+ this.update(cx, |this, cx| {
+ if let Err(err) = result {
+ this.last_error =
+ Some(cx.new(|cx| Markdown::new(err.to_string().into(), None, None, cx)))
+ }
+ })
+ })
+ .detach();
+
+ let mention_set = self.mention_set.clone();
+
+ self.message_editor.update(cx, |editor, cx| {
+ editor.clear(window, cx);
+ editor.remove_creases(mention_set.lock().drain(), cx)
+ });
+
+ self.message_history.borrow_mut().push(message);
+ }
+
+ fn previous_history_message(
+ &mut self,
+ _: &PreviousHistoryMessage,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.message_set_from_history = Self::set_draft_message(
+ self.message_editor.clone(),
+ self.mention_set.clone(),
+ self.project.clone(),
+ self.message_history.borrow_mut().prev(),
+ window,
+ cx,
+ );
+ }
+
+ fn next_history_message(
+ &mut self,
+ _: &NextHistoryMessage,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.message_set_from_history = Self::set_draft_message(
+ self.message_editor.clone(),
+ self.mention_set.clone(),
+ self.project.clone(),
+ self.message_history.borrow_mut().next(),
+ window,
+ cx,
+ );
+ }
+
+ fn set_draft_message(
+ message_editor: Entity<Editor>,
+ mention_set: Arc<Mutex<MentionSet>>,
+ project: Entity<Project>,
+ message: Option<&acp::SendUserMessageParams>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> bool {
+ cx.notify();
+
+ let Some(message) = message else {
+ return false;
+ };
+
+ let mut text = String::new();
+ let mut mentions = Vec::new();
+
+ for chunk in &message.chunks {
+ match chunk {
+ acp::UserMessageChunk::Text { text: chunk } => {
+ text.push_str(&chunk);
+ }
+ acp::UserMessageChunk::Path { path } => {
+ let start = text.len();
+ let content = MentionPath::new(path).to_string();
+ text.push_str(&content);
+ let end = text.len();
+ if let Some(project_path) =
+ project.read(cx).project_path_for_absolute_path(path, cx)
+ {
+ let filename: SharedString = path
+ .file_name()
+ .unwrap_or_default()
+ .to_string_lossy()
+ .to_string()
+ .into();
+ mentions.push((start..end, project_path, filename));
+ }
+ }
+ }
+ }
+
+ let snapshot = message_editor.update(cx, |editor, cx| {
+ editor.set_text(text, window, cx);
+ editor.buffer().read(cx).snapshot(cx)
+ });
+
+ for (range, project_path, filename) in mentions {
+ let crease_icon_path = if project_path.path.is_dir() {
+ FileIcons::get_folder_icon(false, cx)
+ .unwrap_or_else(|| IconName::Folder.path().into())
+ } else {
+ FileIcons::get_icon(Path::new(project_path.path.as_ref()), cx)
+ .unwrap_or_else(|| IconName::File.path().into())
+ };
+
+ let anchor = snapshot.anchor_before(range.start);
+ let crease_id = crate::context_picker::insert_crease_for_mention(
+ anchor.excerpt_id,
+ anchor.text_anchor,
+ range.end - range.start,
+ filename,
+ crease_icon_path,
+ message_editor.clone(),
+ window,
+ cx,
+ );
+ if let Some(crease_id) = crease_id {
+ mention_set.lock().insert(crease_id, project_path);
+ }
+ }
+
+ true
+ }
+
+ fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
+ if let Some(thread) = self.thread() {
+ AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
+ }
+ }
+
+ fn open_edited_buffer(
+ &mut self,
+ buffer: &Entity<Buffer>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ 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_thread_event(
+ &mut self,
+ thread: &Entity<AcpThread>,
+ event: &AcpThreadEvent,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let count = self.list_state.item_count();
+ match event {
+ AcpThreadEvent::NewEntry => {
+ let index = thread.read(cx).entries().len() - 1;
+ self.sync_thread_entry_view(index, window, cx);
+ self.list_state.splice(count..count, 1);
+ }
+ AcpThreadEvent::EntryUpdated(index) => {
+ let index = *index;
+ self.sync_thread_entry_view(index, window, cx);
+ self.list_state.splice(index..index + 1, 1);
+ }
+ }
+ cx.notify();
+ }
+
+ fn sync_thread_entry_view(
+ &mut self,
+ entry_ix: usize,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(multibuffer) = self.entry_diff_multibuffer(entry_ix, cx) else {
+ return;
+ };
+
+ if self.diff_editors.contains_key(&multibuffer.entity_id()) {
+ return;
+ }
+
+ let editor = cx.new(|cx| {
+ let mut editor = Editor::new(
+ EditorMode::Full {
+ scale_ui_elements_with_buffer_font_size: false,
+ show_active_line_background: false,
+ sized_by_content: true,
+ },
+ multibuffer.clone(),
+ None,
+ window,
+ cx,
+ );
+ editor.set_show_gutter(false, cx);
+ editor.disable_inline_diagnostics();
+ editor.disable_expand_excerpt_buttons(cx);
+ editor.set_show_vertical_scrollbar(false, cx);
+ editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
+ editor.set_soft_wrap_mode(SoftWrap::None, cx);
+ editor.scroll_manager.set_forbid_vertical_scroll(true);
+ editor.set_show_indent_guides(false, cx);
+ editor.set_read_only(true);
+ editor.set_show_breakpoints(false, cx);
+ editor.set_show_code_actions(false, cx);
+ editor.set_show_git_diff_gutter(false, cx);
+ editor.set_expand_all_diff_hunks(cx);
+ editor.set_text_style_refinement(TextStyleRefinement {
+ font_size: Some(
+ TextSize::Small
+ .rems(cx)
+ .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
+ .into(),
+ ),
+ ..Default::default()
+ });
+ editor
+ });
+ let entity_id = multibuffer.entity_id();
+ cx.observe_release(&multibuffer, move |this, _, _| {
+ this.diff_editors.remove(&entity_id);
+ })
+ .detach();
+
+ self.diff_editors.insert(entity_id, editor);
+ }
+
+ fn entry_diff_multibuffer(&self, entry_ix: usize, cx: &App) -> Option<Entity<MultiBuffer>> {
+ let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
+ entry.diff().map(|diff| diff.multibuffer.clone())
+ }
+
+ fn authenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ let Some(thread) = self.thread().cloned() else {
+ return;
+ };
+
+ self.last_error.take();
+ let authenticate = thread.read(cx).authenticate();
+ self.auth_task = Some(cx.spawn_in(window, {
+ let project = self.project.clone();
+ async move |this, cx| {
+ let result = authenticate.await;
+
+ this.update_in(cx, |this, window, cx| {
+ if let Err(err) = result {
+ this.last_error = Some(cx.new(|cx| {
+ Markdown::new(format!("Error: {err}").into(), None, None, cx)
+ }))
+ } else {
+ this.thread_state =
+ Self::initial_state(this.workspace.clone(), project.clone(), window, cx)
+ }
+ this.auth_task.take()
+ })
+ .ok();
+ }
+ }));
+ }
+
+ fn authorize_tool_call(
+ &mut self,
+ id: ToolCallId,
+ outcome: acp::ToolCallConfirmationOutcome,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(thread) = self.thread() else {
+ return;
+ };
+ thread.update(cx, |thread, cx| {
+ thread.authorize_tool_call(id, outcome, cx);
+ });
+ cx.notify();
+ }
+
+ fn render_entry(
+ &self,
+ index: usize,
+ total_entries: usize,
+ entry: &AgentThreadEntry,
+ window: &mut Window,
+ cx: &Context<Self>,
+ ) -> AnyElement {
+ match &entry {
+ AgentThreadEntry::UserMessage(message) => div()
+ .py_4()
+ .px_2()
+ .child(
+ v_flex()
+ .p_3()
+ .gap_1p5()
+ .rounded_lg()
+ .shadow_md()
+ .bg(cx.theme().colors().editor_background)
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .text_xs()
+ .child(self.render_markdown(
+ message.content.clone(),
+ user_message_markdown_style(window, cx),
+ )),
+ )
+ .into_any(),
+ AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
+ let style = default_markdown_style(false, window, cx);
+ let message_body = v_flex()
+ .w_full()
+ .gap_2p5()
+ .children(chunks.iter().enumerate().map(|(chunk_ix, chunk)| {
+ match chunk {
+ AssistantMessageChunk::Text { chunk } => self
+ .render_markdown(chunk.clone(), style.clone())
+ .into_any_element(),
+ AssistantMessageChunk::Thought { chunk } => self.render_thinking_block(
+ index,
+ chunk_ix,
+ chunk.clone(),
+ window,
+ cx,
+ ),
+ }
+ }))
+ .into_any();
+
+ v_flex()
+ .px_5()
+ .py_1()
+ .when(index + 1 == total_entries, |this| this.pb_4())
+ .w_full()
+ .text_ui(cx)
+ .child(message_body)
+ .into_any()
+ }
+ AgentThreadEntry::ToolCall(tool_call) => div()
+ .py_1p5()
+ .px_5()
+ .child(self.render_tool_call(index, tool_call, window, cx))
+ .into_any(),
+ }
+ }
+
+ fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
+ cx.theme()
+ .colors()
+ .element_background
+ .blend(cx.theme().colors().editor_foreground.opacity(0.025))
+ }
+
+ fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
+ cx.theme().colors().border.opacity(0.6)
+ }
+
+ fn tool_name_font_size(&self) -> Rems {
+ rems_from_px(13.)
+ }
+
+ fn render_thinking_block(
+ &self,
+ entry_ix: usize,
+ chunk_ix: usize,
+ chunk: Entity<Markdown>,
+ window: &Window,
+ cx: &Context<Self>,
+ ) -> AnyElement {
+ let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
+ let key = (entry_ix, chunk_ix);
+ let is_open = self.expanded_thinking_blocks.contains(&key);
+
+ v_flex()
+ .child(
+ h_flex()
+ .id(header_id)
+ .group("disclosure-header")
+ .w_full()
+ .justify_between()
+ .opacity(0.8)
+ .hover(|style| style.opacity(1.))
+ .child(
+ h_flex()
+ .gap_1p5()
+ .child(
+ Icon::new(IconName::ToolBulb)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .child(
+ div()
+ .text_size(self.tool_name_font_size())
+ .child("Thinking"),
+ ),
+ )
+ .child(
+ div().visible_on_hover("disclosure-header").child(
+ Disclosure::new("thinking-disclosure", is_open)
+ .opened_icon(IconName::ChevronUp)
+ .closed_icon(IconName::ChevronDown)
+ .on_click(cx.listener({
+ move |this, _event, _window, cx| {
+ if is_open {
+ this.expanded_thinking_blocks.remove(&key);
+ } else {
+ this.expanded_thinking_blocks.insert(key);
+ }
+ cx.notify();
+ }
+ })),
+ ),
+ )
+ .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()
+ .relative()
+ .mt_1p5()
+ .ml(px(7.))
+ .pl_4()
+ .border_l_1()
+ .border_color(self.tool_card_border_color(cx))
+ .text_ui_sm(cx)
+ .child(
+ self.render_markdown(chunk, default_markdown_style(false, window, cx)),
+ ),
+ )
+ })
+ .into_any_element()
+ }
+
+ fn render_tool_call(
+ &self,
+ entry_ix: usize,
+ tool_call: &ToolCall,
+ window: &Window,
+ cx: &Context<Self>,
+ ) -> Div {
+ let header_id = SharedString::from(format!("tool-call-header-{}", entry_ix));
+
+ let status_icon = match &tool_call.status {
+ ToolCallStatus::WaitingForConfirmation { .. } => None,
+ ToolCallStatus::Allowed {
+ status: acp::ToolCallStatus::Running,
+ ..
+ } => Some(
+ Icon::new(IconName::ArrowCircle)
+ .color(Color::Accent)
+ .size(IconSize::Small)
+ .with_animation(
+ "running",
+ Animation::new(Duration::from_secs(2)).repeat(),
+ |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+ )
+ .into_any(),
+ ),
+ ToolCallStatus::Allowed {
+ status: acp::ToolCallStatus::Finished,
+ ..
+ } => None,
+ ToolCallStatus::Rejected
+ | ToolCallStatus::Canceled
+ | ToolCallStatus::Allowed {
+ status: acp::ToolCallStatus::Error,
+ ..
+ } => Some(
+ Icon::new(IconName::X)
+ .color(Color::Error)
+ .size(IconSize::Small)
+ .into_any_element(),
+ ),
+ };
+
+ let needs_confirmation = match &tool_call.status {
+ ToolCallStatus::WaitingForConfirmation { .. } => true,
+ _ => tool_call
+ .content
+ .iter()
+ .any(|content| matches!(content, ToolCallContent::Diff { .. })),
+ };
+
+ let is_collapsible = tool_call.content.is_some() && !needs_confirmation;
+ let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id);
+
+ let content = if is_open {
+ match &tool_call.status {
+ ToolCallStatus::WaitingForConfirmation { confirmation, .. } => {
+ Some(self.render_tool_call_confirmation(
+ tool_call.id,
+ confirmation,
+ tool_call.content.as_ref(),
+ window,
+ cx,
+ ))
+ }
+ ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
+ tool_call.content.as_ref().map(|content| {
+ div()
+ .py_1p5()
+ .child(self.render_tool_call_content(content, window, cx))
+ .into_any_element()
+ })
+ }
+ ToolCallStatus::Rejected => None,
+ }
+ } else {
+ None
+ };
+
+ v_flex()
+ .when(needs_confirmation, |this| {
+ this.rounded_lg()
+ .border_1()
+ .border_color(self.tool_card_border_color(cx))
+ .bg(cx.theme().colors().editor_background)
+ .overflow_hidden()
+ })
+ .child(
+ h_flex()
+ .id(header_id)
+ .w_full()
+ .gap_1()
+ .justify_between()
+ .map(|this| {
+ if needs_confirmation {
+ this.px_2()
+ .py_1()
+ .rounded_t_md()
+ .bg(self.tool_card_header_bg(cx))
+ .border_b_1()
+ .border_color(self.tool_card_border_color(cx))
+ } else {
+ this.opacity(0.8).hover(|style| style.opacity(1.))
+ }
+ })
+ .child(
+ h_flex()
+ .id("tool-call-header")
+ .overflow_x_scroll()
+ .map(|this| {
+ if needs_confirmation {
+ this.text_xs()
+ } else {
+ this.text_size(self.tool_name_font_size())
+ }
+ })
+ .gap_1p5()
+ .child(
+ Icon::new(tool_call.icon)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .child(if tool_call.locations.len() == 1 {
+ let name = tool_call.locations[0]
+ .path
+ .file_name()
+ .unwrap_or_default()
+ .display()
+ .to_string();
+
+ h_flex()
+ .id(("open-tool-call-location", entry_ix))
+ .child(name)
+ .w_full()
+ .max_w_full()
+ .pr_1()
+ .gap_0p5()
+ .cursor_pointer()
+ .rounded_sm()
+ .opacity(0.8)
+ .hover(|label| {
+ label.opacity(1.).bg(cx
+ .theme()
+ .colors()
+ .element_hover
+ .opacity(0.5))
+ })
+ .tooltip(Tooltip::text("Jump to File"))
+ .on_click(cx.listener(move |this, _, window, cx| {
+ this.open_tool_call_location(entry_ix, 0, window, cx);
+ }))
+ .into_any_element()
+ } else {
+ self.render_markdown(
+ tool_call.label.clone(),
+ default_markdown_style(needs_confirmation, window, cx),
+ )
+ .into_any()
+ }),
+ )
+ .child(
+ h_flex()
+ .gap_0p5()
+ .when(is_collapsible, |this| {
+ this.child(
+ Disclosure::new(("expand", tool_call.id.0), is_open)
+ .opened_icon(IconName::ChevronUp)
+ .closed_icon(IconName::ChevronDown)
+ .on_click(cx.listener({
+ let id = tool_call.id;
+ move |this: &mut Self, _, _, cx: &mut Context<Self>| {
+ if is_open {
+ this.expanded_tool_calls.remove(&id);
+ } else {
+ this.expanded_tool_calls.insert(id);
+ }
+ cx.notify();
+ }
+ })),
+ )
+ })
+ .children(status_icon),
+ )
+ .on_click(cx.listener({
+ let id = tool_call.id;
+ move |this: &mut Self, _, _, cx: &mut Context<Self>| {
+ if is_open {
+ this.expanded_tool_calls.remove(&id);
+ } else {
+ this.expanded_tool_calls.insert(id);
+ }
+ cx.notify();
+ }
+ })),
+ )
+ .when(is_open, |this| {
+ this.child(
+ div()
+ .text_xs()
+ .when(is_collapsible, |this| {
+ this.mt_1()
+ .border_1()
+ .border_color(self.tool_card_border_color(cx))
+ .bg(cx.theme().colors().editor_background)
+ .rounded_lg()
+ })
+ .children(content),
+ )
+ })
+ }
+
+ fn render_tool_call_content(
+ &self,
+ content: &ToolCallContent,
+ window: &Window,
+ cx: &Context<Self>,
+ ) -> AnyElement {
+ match content {
+ ToolCallContent::Markdown { markdown } => {
+ div()
+ .p_2()
+ .child(self.render_markdown(
+ markdown.clone(),
+ default_markdown_style(false, window, cx),
+ ))
+ .into_any_element()
+ }
+ ToolCallContent::Diff {
+ diff: Diff { multibuffer, .. },
+ ..
+ } => self.render_diff_editor(multibuffer),
+ }
+ }
+
+ fn render_tool_call_confirmation(
+ &self,
+ tool_call_id: ToolCallId,
+ confirmation: &ToolCallConfirmation,
+ content: Option<&ToolCallContent>,
+ window: &Window,
+ cx: &Context<Self>,
+ ) -> AnyElement {
+ let confirmation_container = v_flex().mt_1().py_1p5();
+
+ let button_container = h_flex()
+ .pt_1p5()
+ .px_1p5()
+ .gap_1()
+ .justify_end()
+ .border_t_1()
+ .border_color(self.tool_card_border_color(cx));
+
+ match confirmation {
+ ToolCallConfirmation::Edit { description } => confirmation_container
+ .child(
+ div()
+ .px_2()
+ .children(description.clone().map(|description| {
+ self.render_markdown(
+ description,
+ default_markdown_style(false, window, cx),
+ )
+ })),
+ )
+ .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
+ .child(
+ button_container
+ .child(
+ Button::new(("always_allow", tool_call_id.0), "Always Allow Edits")
+ .icon(IconName::CheckDouble)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Success)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::AlwaysAllow,
+ cx,
+ );
+ }
+ })),
+ )
+ .child(
+ Button::new(("allow", tool_call_id.0), "Allow")
+ .icon(IconName::Check)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Success)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::Allow,
+ cx,
+ );
+ }
+ })),
+ )
+ .child(
+ Button::new(("reject", tool_call_id.0), "Reject")
+ .icon(IconName::X)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Error)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::Reject,
+ cx,
+ );
+ }
+ })),
+ ),
+ )
+ .into_any(),
+ ToolCallConfirmation::Execute {
+ command,
+ root_command,
+ description,
+ } => confirmation_container
+ .child(v_flex().px_2().pb_1p5().child(command.clone()).children(
+ description.clone().map(|description| {
+ self.render_markdown(description, default_markdown_style(false, window, cx))
+ .on_url_click({
+ let workspace = self.workspace.clone();
+ move |text, window, cx| {
+ Self::open_link(text, &workspace, window, cx);
+ }
+ })
+ }),
+ ))
+ .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
+ .child(
+ button_container
+ .child(
+ Button::new(
+ ("always_allow", tool_call_id.0),
+ format!("Always Allow {root_command}"),
+ )
+ .icon(IconName::CheckDouble)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Success)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::AlwaysAllow,
+ cx,
+ );
+ }
+ })),
+ )
+ .child(
+ Button::new(("allow", tool_call_id.0), "Allow")
+ .icon(IconName::Check)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Success)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::Allow,
+ cx,
+ );
+ }
+ })),
+ )
+ .child(
+ Button::new(("reject", tool_call_id.0), "Reject")
+ .icon(IconName::X)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Error)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::Reject,
+ cx,
+ );
+ }
+ })),
+ ),
+ )
+ .into_any(),
+ ToolCallConfirmation::Mcp {
+ server_name,
+ tool_name: _,
+ tool_display_name,
+ description,
+ } => confirmation_container
+ .child(
+ v_flex()
+ .px_2()
+ .pb_1p5()
+ .child(format!("{server_name} - {tool_display_name}"))
+ .children(description.clone().map(|description| {
+ self.render_markdown(
+ description,
+ default_markdown_style(false, window, cx),
+ )
+ })),
+ )
+ .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
+ .child(
+ button_container
+ .child(
+ Button::new(
+ ("always_allow_server", tool_call_id.0),
+ format!("Always Allow {server_name}"),
+ )
+ .icon(IconName::CheckDouble)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Success)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
+ cx,
+ );
+ }
+ })),
+ )
+ .child(
+ Button::new(
+ ("always_allow_tool", tool_call_id.0),
+ format!("Always Allow {tool_display_name}"),
+ )
+ .icon(IconName::CheckDouble)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Success)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::AlwaysAllowTool,
+ cx,
+ );
+ }
+ })),
+ )
+ .child(
+ Button::new(("allow", tool_call_id.0), "Allow")
+ .icon(IconName::Check)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Success)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::Allow,
+ cx,
+ );
+ }
+ })),
+ )
+ .child(
+ Button::new(("reject", tool_call_id.0), "Reject")
+ .icon(IconName::X)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Error)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::Reject,
+ cx,
+ );
+ }
+ })),
+ ),
+ )
+ .into_any(),
+ ToolCallConfirmation::Fetch { description, urls } => confirmation_container
+ .child(
+ v_flex()
+ .px_2()
+ .pb_1p5()
+ .gap_1()
+ .children(urls.iter().map(|url| {
+ h_flex().child(
+ Button::new(url.clone(), url)
+ .icon(IconName::ArrowUpRight)
+ .icon_color(Color::Muted)
+ .icon_size(IconSize::XSmall)
+ .on_click({
+ let url = url.clone();
+ move |_, _, cx| cx.open_url(&url)
+ }),
+ )
+ }))
+ .children(description.clone().map(|description| {
+ self.render_markdown(
+ description,
+ default_markdown_style(false, window, cx),
+ )
+ })),
+ )
+ .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
+ .child(
+ button_container
+ .child(
+ Button::new(("always_allow", tool_call_id.0), "Always Allow")
+ .icon(IconName::CheckDouble)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Success)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::AlwaysAllow,
+ cx,
+ );
+ }
+ })),
+ )
+ .child(
+ Button::new(("allow", tool_call_id.0), "Allow")
+ .icon(IconName::Check)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Success)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::Allow,
+ cx,
+ );
+ }
+ })),
+ )
+ .child(
+ Button::new(("reject", tool_call_id.0), "Reject")
+ .icon(IconName::X)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Error)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::Reject,
+ cx,
+ );
+ }
+ })),
+ ),
+ )
+ .into_any(),
+ ToolCallConfirmation::Other { description } => confirmation_container
+ .child(v_flex().px_2().pb_1p5().child(self.render_markdown(
+ description.clone(),
+ default_markdown_style(false, window, cx),
+ )))
+ .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
+ .child(
+ button_container
+ .child(
+ Button::new(("always_allow", tool_call_id.0), "Always Allow")
+ .icon(IconName::CheckDouble)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Success)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::AlwaysAllow,
+ cx,
+ );
+ }
+ })),
+ )
+ .child(
+ Button::new(("allow", tool_call_id.0), "Allow")
+ .icon(IconName::Check)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Success)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::Allow,
+ cx,
+ );
+ }
+ })),
+ )
+ .child(
+ Button::new(("reject", tool_call_id.0), "Reject")
+ .icon(IconName::X)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Error)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener({
+ let id = tool_call_id;
+ move |this, _, _, cx| {
+ this.authorize_tool_call(
+ id,
+ acp::ToolCallConfirmationOutcome::Reject,
+ cx,
+ );
+ }
+ })),
+ ),
+ )
+ .into_any(),
+ }
+ }
+
+ fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
+ v_flex()
+ .h_full()
+ .child(
+ if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) {
+ editor.clone().into_any_element()
+ } else {
+ Empty.into_any()
+ },
+ )
+ .into_any()
+ }
+
+ fn render_gemini_logo(&self) -> AnyElement {
+ Icon::new(IconName::AiGemini)
+ .color(Color::Muted)
+ .size(IconSize::XLarge)
+ .into_any_element()
+ }
+
+ fn render_error_gemini_logo(&self) -> AnyElement {
+ let logo = Icon::new(IconName::AiGemini)
+ .color(Color::Muted)
+ .size(IconSize::XLarge)
+ .into_any_element();
+
+ h_flex()
+ .relative()
+ .justify_center()
+ .child(div().opacity(0.3).child(logo))
+ .child(
+ h_flex().absolute().right_1().bottom_0().child(
+ Icon::new(IconName::XCircle)
+ .color(Color::Error)
+ .size(IconSize::Small),
+ ),
+ )
+ .into_any_element()
+ }
+
+ fn render_empty_state(&self, loading: bool, cx: &App) -> AnyElement {
+ v_flex()
+ .size_full()
+ .items_center()
+ .justify_center()
+ .child(
+ if loading {
+ h_flex()
+ .justify_center()
+ .child(self.render_gemini_logo())
+ .with_animation(
+ "pulsating_icon",
+ Animation::new(Duration::from_secs(2))
+ .repeat()
+ .with_easing(pulsating_between(0.4, 1.0)),
+ |icon, delta| icon.opacity(delta),
+ ).into_any()
+ } else {
+ self.render_gemini_logo().into_any_element()
+ }
+ )
+ .child(
+ h_flex()
+ .mt_4()
+ .mb_1()
+ .justify_center()
+ .child(Headline::new(if loading {
+ "Connecting to Gemini…"
+ } else {
+ "Welcome to Gemini"
+ }).size(HeadlineSize::Medium)),
+ )
+ .child(
+ div()
+ .max_w_1_2()
+ .text_sm()
+ .text_center()
+ .map(|this| if loading {
+ this.invisible()
+ } else {
+ this.text_color(cx.theme().colors().text_muted)
+ })
+ .child("Ask questions, edit files, run commands.\nBe specific for the best results.")
+ )
+ .into_any()
+ }
+
+ fn render_pending_auth_state(&self) -> AnyElement {
+ v_flex()
+ .items_center()
+ .justify_center()
+ .child(self.render_error_gemini_logo())
+ .child(
+ h_flex()
+ .mt_4()
+ .mb_1()
+ .justify_center()
+ .child(Headline::new("Not Authenticated").size(HeadlineSize::Medium)),
+ )
+ .into_any()
+ }
+
+ fn render_error_state(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
+ let mut container = v_flex()
+ .items_center()
+ .justify_center()
+ .child(self.render_error_gemini_logo())
+ .child(
+ v_flex()
+ .mt_4()
+ .mb_2()
+ .gap_0p5()
+ .text_center()
+ .items_center()
+ .child(Headline::new("Failed to launch").size(HeadlineSize::Medium))
+ .child(
+ Label::new(e.to_string())
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ ),
+ );
+
+ if matches!(e, LoadError::Unsupported { .. }) {
+ container =
+ container.child(Button::new("upgrade", "Upgrade Gemini to Latest").on_click(
+ cx.listener(|this, _, window, cx| {
+ this.workspace
+ .update(cx, |workspace, cx| {
+ let project = workspace.project().read(cx);
+ let cwd = project.first_project_directory(cx);
+ let shell = project.terminal_settings(&cwd, cx).shell.clone();
+ let command =
+ "npm install -g @google/gemini-cli@latest".to_string();
+ let spawn_in_terminal = task::SpawnInTerminal {
+ id: task::TaskId("install".to_string()),
+ full_label: command.clone(),
+ label: command.clone(),
+ command: Some(command.clone()),
+ args: Vec::new(),
+ command_label: command.clone(),
+ cwd,
+ env: Default::default(),
+ use_new_terminal: true,
+ allow_concurrent_runs: true,
+ reveal: Default::default(),
+ reveal_target: Default::default(),
+ hide: Default::default(),
+ shell,
+ show_summary: true,
+ show_command: true,
+ show_rerun: false,
+ };
+ workspace
+ .spawn_in_terminal(spawn_in_terminal, window, cx)
+ .detach();
+ })
+ .ok();
+ }),
+ ));
+ }
+
+ container.into_any()
+ }
+
+ fn render_edits_bar(
+ &self,
+ thread_entity: &Entity<AcpThread>,
+ window: &mut Window,
+ cx: &Context<Self>,
+ ) -> Option<AnyElement> {
+ let thread = thread_entity.read(cx);
+ let action_log = thread.action_log();
+ let changed_buffers = action_log.read(cx).changed_buffers(cx);
+
+ if changed_buffers.is_empty() {
+ return None;
+ }
+
+ let editor_bg_color = cx.theme().colors().editor_background;
+ let active_color = cx.theme().colors().element_selected;
+ let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
+
+ let pending_edits = thread.has_pending_edit_tool_calls();
+ let expanded = self.edits_expanded;
+
+ v_flex()
+ .mt_1()
+ .mx_2()
+ .bg(bg_edit_files_disclosure)
+ .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.),
+ }])
+ .child(self.render_edits_bar_summary(
+ action_log,
+ &changed_buffers,
+ expanded,
+ pending_edits,
+ window,
+ cx,
+ ))
+ .when(expanded, |parent| {
+ parent.child(self.render_edits_bar_files(
+ action_log,
+ &changed_buffers,
+ pending_edits,
+ cx,
+ ))
+ })
+ .into_any()
+ .into()
+ }
+
+ fn render_edits_bar_summary(
+ &self,
+ action_log: &Entity<ActionLog>,
+ changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
+ expanded: bool,
+ pending_edits: bool,
+ window: &mut Window,
+ cx: &Context<Self>,
+ ) -> 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()
+ .when(expanded, |this| {
+ this.border_b_1().border_color(cx.theme().colors().border)
+ })
+ .child(
+ h_flex()
+ .id("edits-container")
+ .cursor_pointer()
+ .w_full()
+ .gap_1()
+ .child(Disclosure::new("edits-disclosure", expanded))
+ .map(|this| {
+ 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,
+ window,
+ 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(),
+ window,
+ cx,
+ )
+ .map(|kb| kb.size(rems_from_px(10.))),
+ )
+ .on_click({
+ let action_log = action_log.clone();
+ cx.listener(move |_, _, _, cx| {
+ action_log.update(cx, |action_log, cx| {
+ action_log.reject_all_edits(cx).detach();
+ })
+ })
+ }),
+ )
+ .child(
+ Button::new("keep-all-changes", "Keep All")
+ .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, window, cx)
+ .map(|kb| kb.size(rems_from_px(10.))),
+ )
+ .on_click({
+ let action_log = action_log.clone();
+ cx.listener(move |_, _, _, cx| {
+ action_log.update(cx, |action_log, cx| {
+ action_log.keep_all_edits(cx);
+ })
+ })
+ }),
+ ),
+ )
+ }
+
+ fn render_edits_bar_files(
+ &self,
+ action_log: &Entity<ActionLog>,
+ changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
+ pending_edits: bool,
+ cx: &Context<Self>,
+ ) -> Div {
+ let editor_bg_color = cx.theme().colors().editor_background;
+
+ v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
+ |(index, (buffer, _diff))| {
+ let file = buffer.read(cx).file()?;
+ let path = file.path();
+
+ let file_path = path.parent().and_then(|parent| {
+ let parent_str = parent.to_string_lossy();
+
+ if parent_str.is_empty() {
+ None
+ } else {
+ Some(
+ Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
+ .color(Color::Muted)
+ .size(LabelSize::XSmall)
+ .buffer_font(cx),
+ )
+ }
+ });
+
+ let file_name = path.file_name().map(|name| {
+ Label::new(name.to_string_lossy().to_string())
+ .size(LabelSize::XSmall)
+ .buffer_font(cx)
+ });
+
+ let file_icon = FileIcons::get_icon(&path, cx)
+ .map(Icon::from_path)
+ .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
+ .unwrap_or_else(|| {
+ 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))
+ .relative()
+ .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", index))
+ .pr_8()
+ .gap_1p5()
+ .max_w_full()
+ .overflow_x_scroll()
+ .child(file_icon)
+ .child(h_flex().gap_0p5().children(file_name).children(file_path))
+ .on_click({
+ let buffer = buffer.clone();
+ cx.listener(move |this, _, window, cx| {
+ this.open_edited_buffer(&buffer, window, cx);
+ })
+ }),
+ )
+ .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();
+ move |_, _, cx| {
+ action_log.update(cx, |action_log, cx| {
+ action_log
+ .reject_edits_in_ranges(
+ buffer.clone(),
+ vec![Anchor::MIN..Anchor::MAX],
+ 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();
+ move |_, _, cx| {
+ action_log.update(cx, |action_log, cx| {
+ action_log.keep_edits_in_range(
+ buffer.clone(),
+ Anchor::MIN..Anchor::MAX,
+ cx,
+ );
+ })
+ }
+ }),
+ ),
+ )
+ .child(
+ div()
+ .id("gradient-overlay")
+ .absolute()
+ .h_full()
+ .w_12()
+ .top_0()
+ .bottom_0()
+ .right(px(152.))
+ .bg(overlay_gradient),
+ );
+
+ Some(element)
+ },
+ ))
+ }
+
+ fn render_message_editor(&mut self, cx: &mut Context<Self>) -> AnyElement {
+ let settings = ThemeSettings::get_global(cx);
+ let font_size = TextSize::Small
+ .rems(cx)
+ .to_pixels(settings.agent_font_size(cx));
+ let line_height = settings.buffer_line_height.value() * font_size;
+
+ let text_style = TextStyle {
+ color: cx.theme().colors().text,
+ font_family: settings.buffer_font.family.clone(),
+ font_fallbacks: settings.buffer_font.fallbacks.clone(),
+ font_features: settings.buffer_font.features.clone(),
+ font_size: font_size.into(),
+ line_height: line_height.into(),
+ ..Default::default()
+ };
+
+ EditorElement::new(
+ &self.message_editor,
+ EditorStyle {
+ background: cx.theme().colors().editor_background,
+ local_player: cx.theme().players().local(),
+ text: text_style,
+ syntax: cx.theme().syntax().clone(),
+ ..Default::default()
+ },
+ )
+ .into_any()
+ }
+
+ fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
+ if self.thread().map_or(true, |thread| {
+ thread.read(cx).status() == ThreadStatus::Idle
+ }) {
+ let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
+ IconButton::new("send-message", IconName::Send)
+ .icon_color(Color::Accent)
+ .style(ButtonStyle::Filled)
+ .disabled(self.thread().is_none() || is_editor_empty)
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.chat(&Chat, window, cx);
+ }))
+ .when(!is_editor_empty, |button| {
+ button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx))
+ })
+ .when(is_editor_empty, |button| {
+ button.tooltip(Tooltip::text("Type a message to submit"))
+ })
+ .into_any_element()
+ } else {
+ IconButton::new("stop-generation", IconName::StopFilled)
+ .icon_color(Color::Error)
+ .style(ButtonStyle::Tinted(ui::TintColor::Error))
+ .tooltip(move |window, cx| {
+ Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
+ })
+ .on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
+ .into_any_element()
+ }
+ }
+
+ fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
+ let following = self
+ .workspace
+ .read_with(cx, |workspace, _| {
+ workspace.is_being_followed(CollaboratorId::Agent)
+ })
+ .unwrap_or(false);
+
+ IconButton::new("follow-agent", IconName::Crosshair)
+ .icon_size(IconSize::Small)
+ .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("Stop Following Agent", &Follow, window, cx)
+ } else {
+ Tooltip::with_meta(
+ "Follow Agent",
+ Some(&Follow),
+ "Track the agent's location as it reads and edits files.",
+ window,
+ cx,
+ )
+ }
+ })
+ .on_click(cx.listener(move |this, _, window, cx| {
+ this.workspace
+ .update(cx, |workspace, cx| {
+ if following {
+ workspace.unfollow(CollaboratorId::Agent, window, cx);
+ } else {
+ workspace.follow(CollaboratorId::Agent, window, cx);
+ }
+ })
+ .ok();
+ }))
+ }
+
+ fn render_markdown(&self, markdown: Entity<Markdown>, 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<Workspace>,
+ window: &mut Window,
+ cx: &mut App,
+ ) {
+ let Some(workspace) = workspace.upgrade() else {
+ cx.open_url(&url);
+ return;
+ };
+
+ if let Some(mention_path) = MentionPath::try_parse(&url) {
+ workspace.update(cx, |workspace, cx| {
+ let project = workspace.project();
+ let Some((path, entry)) = project.update(cx, |project, cx| {
+ let path = project.find_project_path(mention_path.path(), cx)?;
+ let entry = project.entry_for_path(&path, cx)?;
+ Some((path, entry))
+ }) else {
+ return;
+ };
+
+ if entry.is_dir() {
+ project.update(cx, |_, cx| {
+ cx.emit(project::Event::RevealInProjectPanel(entry.id));
+ });
+ } else {
+ workspace
+ .open_path(path, None, true, window, cx)
+ .detach_and_log_err(cx);
+ }
+ })
+ } else {
+ cx.open_url(&url);
+ }
+ }
+
+ fn open_tool_call_location(
+ &self,
+ entry_ix: usize,
+ location_ix: usize,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Option<()> {
+ let location = self
+ .thread()?
+ .read(cx)
+ .entries()
+ .get(entry_ix)?
+ .locations()?
+ .get(location_ix)?;
+
+ let project_path = self
+ .project
+ .read(cx)
+ .find_project_path(&location.path, cx)?;
+
+ let open_task = self
+ .workspace
+ .update(cx, |worskpace, cx| {
+ worskpace.open_path(project_path, None, true, window, cx)
+ })
+ .log_err()?;
+
+ window
+ .spawn(cx, async move |cx| {
+ let item = open_task.await?;
+
+ let Some(active_editor) = item.downcast::<Editor>() else {
+ return anyhow::Ok(());
+ };
+
+ active_editor.update_in(cx, |editor, window, cx| {
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ let first_hunk = editor
+ .diff_hunks_in_ranges(
+ &[editor::Anchor::min()..editor::Anchor::max()],
+ &snapshot,
+ )
+ .next();
+ if let Some(first_hunk) = first_hunk {
+ let first_hunk_start = first_hunk.multi_buffer_range().start;
+ editor.change_selections(Default::default(), window, cx, |selections| {
+ selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
+ })
+ }
+ })?;
+
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+
+ None
+ }
+
+ pub fn open_thread_as_markdown(
+ &self,
+ workspace: Entity<Workspace>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Task<anyhow::Result<()>> {
+ let markdown_language_task = workspace
+ .read(cx)
+ .app_state()
+ .languages
+ .language_for_name("Markdown");
+
+ let (thread_summary, markdown) = match &self.thread_state {
+ ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
+ let thread = thread.read(cx);
+ (thread.title().to_string(), thread.to_markdown(cx))
+ }
+ ThreadState::Loading { .. } | ThreadState::LoadError(..) => return Task::ready(Ok(())),
+ };
+
+ window.spawn(cx, async move |cx| {
+ let markdown_language = markdown_language_task.await?;
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ let project = workspace.project().clone();
+
+ if !project.read(cx).is_local() {
+ anyhow::bail!("failed to open active thread as markdown in remote project");
+ }
+
+ let buffer = project.update(cx, |project, cx| {
+ project.create_local_buffer(&markdown, Some(markdown_language), cx)
+ });
+ let buffer = cx.new(|cx| {
+ MultiBuffer::singleton(buffer, cx).with_title(thread_summary.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_summary);
+ editor
+ })),
+ None,
+ true,
+ window,
+ cx,
+ );
+
+ anyhow::Ok(())
+ })??;
+ anyhow::Ok(())
+ })
+ }
+
+ fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
+ self.list_state.scroll_to(ListOffset::default());
+ cx.notify();
+ }
+}
+
+impl Focusable for AcpThreadView {
+ fn focus_handle(&self, cx: &App) -> FocusHandle {
+ self.message_editor.focus_handle(cx)
+ }
+}
+
+impl Render for AcpThreadView {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let open_as_markdown = IconButton::new("open-as-markdown", IconName::DocumentText)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Ignored)
+ .tooltip(Tooltip::text("Open Thread as Markdown"))
+ .on_click(cx.listener(move |this, _, window, cx| {
+ 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::ArrowUpAlt)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Ignored)
+ .tooltip(Tooltip::text("Scroll To Top"))
+ .on_click(cx.listener(move |this, _, _, cx| {
+ this.scroll_to_top(cx);
+ }));
+
+ v_flex()
+ .size_full()
+ .key_context("AcpThread")
+ .on_action(cx.listener(Self::chat))
+ .on_action(cx.listener(Self::previous_history_message))
+ .on_action(cx.listener(Self::next_history_message))
+ .on_action(cx.listener(Self::open_agent_diff))
+ .child(match &self.thread_state {
+ ThreadState::Unauthenticated { .. } => v_flex()
+ .p_2()
+ .flex_1()
+ .items_center()
+ .justify_center()
+ .child(self.render_pending_auth_state())
+ .child(h_flex().mt_1p5().justify_center().child(
+ Button::new("sign-in", "Sign in to Gemini").on_click(
+ cx.listener(|this, _, window, cx| this.authenticate(window, cx)),
+ ),
+ )),
+ ThreadState::Loading { .. } => {
+ v_flex().flex_1().child(self.render_empty_state(true, cx))
+ }
+ ThreadState::LoadError(e) => v_flex()
+ .p_2()
+ .flex_1()
+ .items_center()
+ .justify_center()
+ .child(self.render_error_state(e, cx)),
+ ThreadState::Ready { thread, .. } => v_flex().flex_1().map(|this| {
+ if self.list_state.item_count() > 0 {
+ this.child(
+ list(self.list_state.clone())
+ .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
+ .flex_grow()
+ .into_any(),
+ )
+ .child(
+ h_flex()
+ .group("controls")
+ .mt_1()
+ .mr_1()
+ .py_2()
+ .px(RESPONSE_PADDING_X)
+ .opacity(0.4)
+ .hover(|style| style.opacity(1.))
+ .gap_1()
+ .flex_wrap()
+ .justify_end()
+ .child(open_as_markdown)
+ .child(scroll_to_top)
+ .into_any_element(),
+ )
+ .children(match thread.read(cx).status() {
+ ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => None,
+ ThreadStatus::Generating => div()
+ .px_5()
+ .py_2()
+ .child(LoadingLabel::new("").size(LabelSize::Small))
+ .into(),
+ })
+ .children(self.render_edits_bar(&thread, window, cx))
+ } else {
+ this.child(self.render_empty_state(false, cx))
+ }
+ }),
+ })
+ .when_some(self.last_error.clone(), |el, error| {
+ el.child(
+ div()
+ .p_2()
+ .text_xs()
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .bg(cx.theme().status().error_background)
+ .child(
+ self.render_markdown(error, default_markdown_style(false, window, cx)),
+ ),
+ )
+ })
+ .child(
+ v_flex()
+ .p_2()
+ .pt_3()
+ .gap_1()
+ .bg(cx.theme().colors().editor_background)
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .child(self.render_message_editor(cx))
+ .child(
+ h_flex()
+ .justify_between()
+ .child(self.render_follow_toggle(cx))
+ .child(self.render_send_button(cx)),
+ ),
+ )
+ }
+}
+
+fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
+ let mut style = default_markdown_style(false, window, cx);
+ let mut text_style = window.text_style();
+ let theme_settings = ThemeSettings::get_global(cx);
+
+ let buffer_font = theme_settings.buffer_font.family.clone();
+ let buffer_font_size = TextSize::Small.rems(cx);
+
+ text_style.refine(&TextStyleRefinement {
+ font_family: Some(buffer_font),
+ font_size: Some(buffer_font_size.into()),
+ ..Default::default()
+ });
+
+ style.base_text_style = text_style;
+ style.link_callback = Some(Rc::new(move |url, cx| {
+ if MentionPath::try_parse(url).is_some() {
+ let colors = cx.theme().colors();
+ Some(TextStyleRefinement {
+ background_color: Some(colors.element_background),
+ ..Default::default()
+ })
+ } else {
+ None
+ }
+ }));
+ style
+}
+
+fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle {
+ let theme_settings = ThemeSettings::get_global(cx);
+ let colors = cx.theme().colors();
+
+ let buffer_font_size = TextSize::Small.rems(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 {
+ TextSize::Small.rems(cx)
+ } else {
+ TextSize::Default.rems(cx)
+ };
+
+ 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(cx.theme().colors().text),
+ ..Default::default()
+ });
+
+ MarkdownStyle {
+ base_text_style: text_style.clone(),
+ syntax: cx.theme().syntax().clone(),
+ selection_background_color: cx.theme().colors().element_selection_background,
+ code_block_overflow_x_scroll: true,
+ table_overflow_x_scroll: true,
+ heading_level_styles: Some(HeadingLevelStyles {
+ 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(Pixels(8.)))),
+ left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
+ right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
+ bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
+ },
+ margin: EdgesRefinement {
+ top: Some(Length::Definite(Pixels(8.).into())),
+ left: Some(Length::Definite(Pixels(0.).into())),
+ right: Some(Length::Definite(Pixels(0.).into())),
+ bottom: Some(Length::Definite(Pixels(12.).into())),
+ },
+ border_style: Some(BorderStyle::Solid),
+ border_widths: EdgesRefinement {
+ top: Some(AbsoluteLength::Pixels(Pixels(1.))),
+ left: Some(AbsoluteLength::Pixels(Pixels(1.))),
+ right: Some(AbsoluteLength::Pixels(Pixels(1.))),
+ bottom: Some(AbsoluteLength::Pixels(Pixels(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)),
+ underline: Some(UnderlineStyle {
+ color: Some(colors.text_accent.opacity(0.5)),
+ thickness: px(1.),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ ..Default::default()
+ }
+}
@@ -1,9 +1,7 @@
use crate::context_picker::{ContextPicker, MentionLink};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::message_editor::{extract_message_creases, insert_message_creases};
-use crate::ui::{
- AddedContext, AgentNotification, AgentNotificationEvent, AnimatedLabel, ContextPill,
-};
+use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
use crate::{AgentPanel, ModelUsageContext};
use agent::{
ContextStore, LastRestoreCheckpoint, MessageCrease, MessageId, MessageSegment, TextThreadStore,
@@ -789,6 +787,15 @@ impl ActiveThread {
.unwrap()
}
});
+
+ let workspace_subscription = if let Some(workspace) = workspace.upgrade() {
+ Some(cx.observe_release(&workspace, |this, _, cx| {
+ this.dismiss_notifications(cx);
+ }))
+ } else {
+ None
+ };
+
let mut this = Self {
language_registry,
thread_store,
@@ -836,6 +843,10 @@ impl ActiveThread {
}
}
+ if let Some(subscription) = workspace_subscription {
+ this._subscriptions.push(subscription);
+ }
+
this
}
@@ -1026,6 +1037,7 @@ impl ActiveThread {
}
}
ThreadEvent::MessageAdded(message_id) => {
+ self.clear_last_error();
if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
thread.message(*message_id).map(|message| {
RenderedMessage::from_segments(
@@ -1042,6 +1054,7 @@ impl ActiveThread {
cx.notify();
}
ThreadEvent::MessageEdited(message_id) => {
+ self.clear_last_error();
if let Some(index) = self.messages.iter().position(|id| id == message_id) {
if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
thread.message(*message_id).map(|message| {
@@ -1461,6 +1474,7 @@ impl ActiveThread {
&configured_model.model,
cx,
),
+ thinking_allowed: true,
};
Some(configured_model.model.count_tokens(request, cx))
@@ -1818,7 +1832,7 @@ impl ActiveThread {
.my_3()
.mx_5()
.when(is_generating_stale || message.is_hidden, |this| {
- this.child(AnimatedLabel::new("").size(LabelSize::Small))
+ this.child(LoadingLabel::new("").size(LabelSize::Small))
})
});
@@ -2580,11 +2594,11 @@ impl ActiveThread {
h_flex()
.gap_1p5()
.child(
- Icon::new(IconName::LightBulb)
- .size(IconSize::XSmall)
+ Icon::new(IconName::ToolBulb)
+ .size(IconSize::Small)
.color(Color::Muted),
)
- .child(AnimatedLabel::new("Thinking").size(LabelSize::Small)),
+ .child(LoadingLabel::new("Thinking").size(LabelSize::Small)),
)
.child(
h_flex()
@@ -2994,7 +3008,7 @@ impl ActiveThread {
.overflow_x_scroll()
.child(
Icon::new(tool_use.icon)
- .size(IconSize::XSmall)
+ .size(IconSize::Small)
.color(Color::Muted),
)
.child(
@@ -3153,7 +3167,7 @@ impl ActiveThread {
.border_color(self.tool_card_border_color(cx))
.rounded_b_lg()
.child(
- AnimatedLabel::new("Waiting for Confirmation").size(LabelSize::Small)
+ LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small)
)
.child(
h_flex()
@@ -26,8 +26,8 @@ use project::{
};
use settings::{Settings, update_settings_file};
use ui::{
- ContextMenu, Disclosure, ElevationIndex, Indicator, PopoverMenu, Scrollbar, ScrollbarState,
- Switch, SwitchColor, Tooltip, prelude::*,
+ ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
+ Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip, prelude::*,
};
use util::ResultExt as _;
use workspace::Workspace;
@@ -172,19 +172,29 @@ impl AgentConfiguration {
.unwrap_or(false);
v_flex()
- .py_2()
- .gap_1p5()
- .border_t_1()
- .border_color(cx.theme().colors().border.opacity(0.6))
+ .when(is_expanded, |this| this.mb_2())
+ .child(
+ div()
+ .opacity(0.6)
+ .px_2()
+ .child(Divider::horizontal().color(DividerColor::Border)),
+ )
.child(
h_flex()
+ .map(|this| {
+ if is_expanded {
+ this.mt_2().mb_1()
+ } else {
+ this.my_2()
+ }
+ })
.w_full()
- .gap_1()
.justify_between()
.child(
h_flex()
.id(provider_id_string.clone())
.cursor_pointer()
+ .px_2()
.py_0p5()
.w_full()
.justify_between()
@@ -247,12 +257,16 @@ impl AgentConfiguration {
)
}),
)
- .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}",
- ))),
- })
+ .child(
+ div()
+ .px_2()
+ .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}",
+ ))),
+ }),
+ )
}
fn render_provider_configuration_section(
@@ -262,12 +276,11 @@ impl AgentConfiguration {
let providers = LanguageModelRegistry::read_global(cx).providers();
v_flex()
- .p(DynamicSpacing::Base16.rems(cx))
- .pr(DynamicSpacing::Base20.rems(cx))
- .border_b_1()
- .border_color(cx.theme().colors().border)
.child(
v_flex()
+ .p(DynamicSpacing::Base16.rems(cx))
+ .pr(DynamicSpacing::Base20.rems(cx))
+ .pb_0()
.mb_2p5()
.gap_0p5()
.child(Headline::new("LLM Providers"))
@@ -276,10 +289,15 @@ impl AgentConfiguration {
.color(Color::Muted),
),
)
- .children(
- providers
- .into_iter()
- .map(|provider| self.render_provider_configuration_block(&provider, cx)),
+ .child(
+ div()
+ .pl(DynamicSpacing::Base08.rems(cx))
+ .pr(DynamicSpacing::Base20.rems(cx))
+ .children(
+ providers.into_iter().map(|provider| {
+ self.render_provider_configuration_block(&provider, cx)
+ }),
+ ),
)
}
@@ -418,7 +436,7 @@ impl AgentConfiguration {
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
- let context_server_ids = self.context_server_store.read(cx).all_server_ids().clone();
+ let context_server_ids = self.context_server_store.read(cx).configured_server_ids();
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
@@ -379,6 +379,14 @@ impl ConfigureContextServerModal {
};
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({
@@ -399,13 +407,21 @@ impl ConfigureContextServerModal {
})
.detach();
- // When we write the settings to the file, the context server will be restarted.
- workspace.update(cx, |workspace, cx| {
- let fs = workspace.app_state().fs.clone();
- update_settings_file::<ProjectSettings>(fs.clone(), cx, |project_settings, _| {
- project_settings.context_servers.insert(id.0, settings);
+ let 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();
+ update_settings_file::<ProjectSettings>(fs.clone(), cx, |project_settings, _| {
+ project_settings.context_servers.insert(id.0, settings);
+ });
});
- });
+ } 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<Self>) {
@@ -724,7 +740,9 @@ fn wait_for_context_server(
});
cx.spawn(async move |_cx| {
- let result = rx.await.unwrap();
+ let result = rx
+ .await
+ .map_err(|_| Arc::from("Context server store was dropped"))?;
drop(subscription);
result
})
@@ -1,7 +1,9 @@
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
-use agent::{Thread, ThreadEvent};
+use acp::{AcpThread, AcpThreadEvent};
+use agent::{Thread, ThreadEvent, ThreadSummary};
use agent_settings::AgentSettings;
use anyhow::Result;
+use assistant_tool::ActionLog;
use buffer_diff::DiffHunkStatus;
use collections::{HashMap, HashSet};
use editor::{
@@ -41,16 +43,108 @@ use zed_actions::assistant::ToggleFocus;
pub struct AgentDiffPane {
multibuffer: Entity<MultiBuffer>,
editor: Entity<Editor>,
- thread: Entity<Thread>,
+ thread: AgentDiffThread,
focus_handle: FocusHandle,
workspace: WeakEntity<Workspace>,
title: SharedString,
_subscriptions: Vec<Subscription>,
}
+#[derive(PartialEq, Eq, Clone)]
+pub enum AgentDiffThread {
+ Native(Entity<Thread>),
+ AcpThread(Entity<AcpThread>),
+}
+
+impl AgentDiffThread {
+ fn project(&self, cx: &App) -> Entity<Project> {
+ match self {
+ AgentDiffThread::Native(thread) => thread.read(cx).project().clone(),
+ AgentDiffThread::AcpThread(thread) => thread.read(cx).project().clone(),
+ }
+ }
+ fn action_log(&self, cx: &App) -> Entity<ActionLog> {
+ match self {
+ AgentDiffThread::Native(thread) => thread.read(cx).action_log().clone(),
+ AgentDiffThread::AcpThread(thread) => thread.read(cx).action_log().clone(),
+ }
+ }
+
+ fn summary(&self, cx: &App) -> ThreadSummary {
+ match self {
+ AgentDiffThread::Native(thread) => thread.read(cx).summary().clone(),
+ AgentDiffThread::AcpThread(thread) => ThreadSummary::Ready(thread.read(cx).title()),
+ }
+ }
+
+ fn is_generating(&self, cx: &App) -> bool {
+ match self {
+ AgentDiffThread::Native(thread) => thread.read(cx).is_generating(),
+ AgentDiffThread::AcpThread(thread) => {
+ thread.read(cx).status() == acp::ThreadStatus::Generating
+ }
+ }
+ }
+
+ fn has_pending_edit_tool_uses(&self, cx: &App) -> bool {
+ match self {
+ AgentDiffThread::Native(thread) => thread.read(cx).has_pending_edit_tool_uses(),
+ AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(),
+ }
+ }
+
+ fn downgrade(&self) -> WeakAgentDiffThread {
+ match self {
+ AgentDiffThread::Native(thread) => WeakAgentDiffThread::Native(thread.downgrade()),
+ AgentDiffThread::AcpThread(thread) => {
+ WeakAgentDiffThread::AcpThread(thread.downgrade())
+ }
+ }
+ }
+}
+
+impl From<Entity<Thread>> for AgentDiffThread {
+ fn from(entity: Entity<Thread>) -> Self {
+ AgentDiffThread::Native(entity)
+ }
+}
+
+impl From<Entity<AcpThread>> for AgentDiffThread {
+ fn from(entity: Entity<AcpThread>) -> Self {
+ AgentDiffThread::AcpThread(entity)
+ }
+}
+
+#[derive(PartialEq, Eq, Clone)]
+pub enum WeakAgentDiffThread {
+ Native(WeakEntity<Thread>),
+ AcpThread(WeakEntity<AcpThread>),
+}
+
+impl WeakAgentDiffThread {
+ pub fn upgrade(&self) -> Option<AgentDiffThread> {
+ match self {
+ WeakAgentDiffThread::Native(weak) => weak.upgrade().map(AgentDiffThread::Native),
+ WeakAgentDiffThread::AcpThread(weak) => weak.upgrade().map(AgentDiffThread::AcpThread),
+ }
+ }
+}
+
+impl From<WeakEntity<Thread>> for WeakAgentDiffThread {
+ fn from(entity: WeakEntity<Thread>) -> Self {
+ WeakAgentDiffThread::Native(entity)
+ }
+}
+
+impl From<WeakEntity<AcpThread>> for WeakAgentDiffThread {
+ fn from(entity: WeakEntity<AcpThread>) -> Self {
+ WeakAgentDiffThread::AcpThread(entity)
+ }
+}
+
impl AgentDiffPane {
pub fn deploy(
- thread: Entity<Thread>,
+ thread: impl Into<AgentDiffThread>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
@@ -61,14 +155,16 @@ impl AgentDiffPane {
}
pub fn deploy_in_workspace(
- thread: Entity<Thread>,
+ thread: impl Into<AgentDiffThread>,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<Self> {
+ let thread = thread.into();
let existing_diff = workspace
.items_of_type::<AgentDiffPane>(cx)
.find(|diff| diff.read(cx).thread == thread);
+
if let Some(existing_diff) = existing_diff {
workspace.activate_item(&existing_diff, true, true, window, cx);
existing_diff
@@ -81,7 +177,7 @@ impl AgentDiffPane {
}
pub fn new(
- thread: Entity<Thread>,
+ thread: AgentDiffThread,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -89,7 +185,7 @@ impl AgentDiffPane {
let focus_handle = cx.focus_handle();
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
- let project = thread.read(cx).project().clone();
+ let project = thread.project(cx).clone();
let editor = cx.new(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
@@ -100,16 +196,27 @@ impl AgentDiffPane {
editor
});
- let action_log = thread.read(cx).action_log().clone();
+ let action_log = thread.action_log(cx).clone();
+
let mut this = Self {
- _subscriptions: vec![
- cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
- this.update_excerpts(window, cx)
- }),
- cx.subscribe(&thread, |this, _thread, event, cx| {
- this.handle_thread_event(event, cx)
- }),
- ],
+ _subscriptions: [
+ Some(
+ cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
+ this.update_excerpts(window, cx)
+ }),
+ ),
+ match &thread {
+ AgentDiffThread::Native(thread) => {
+ Some(cx.subscribe(&thread, |this, _thread, event, cx| {
+ this.handle_thread_event(event, cx)
+ }))
+ }
+ AgentDiffThread::AcpThread(_) => None,
+ },
+ ]
+ .into_iter()
+ .flatten()
+ .collect(),
title: SharedString::default(),
multibuffer,
editor,
@@ -123,8 +230,7 @@ impl AgentDiffPane {
}
fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- let thread = self.thread.read(cx);
- let changed_buffers = thread.action_log().read(cx).changed_buffers(cx);
+ let changed_buffers = self.thread.action_log(cx).read(cx).changed_buffers(cx);
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
for (buffer, diff_handle) in changed_buffers {
@@ -211,7 +317,7 @@ impl AgentDiffPane {
}
fn update_title(&mut self, cx: &mut Context<Self>) {
- let new_title = self.thread.read(cx).summary().unwrap_or("Agent Changes");
+ let new_title = self.thread.summary(cx).unwrap_or("Agent Changes");
if new_title != self.title {
self.title = new_title;
cx.emit(EditorEvent::TitleChanged);
@@ -275,14 +381,15 @@ impl AgentDiffPane {
fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
self.thread
- .update(cx, |thread, cx| thread.keep_all_edits(cx));
+ .action_log(cx)
+ .update(cx, |action_log, cx| action_log.keep_all_edits(cx))
}
}
fn keep_edits_in_selection(
editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot,
- thread: &Entity<Thread>,
+ thread: &AgentDiffThread,
window: &mut Window,
cx: &mut Context<Editor>,
) {
@@ -297,7 +404,7 @@ fn keep_edits_in_selection(
fn reject_edits_in_selection(
editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot,
- thread: &Entity<Thread>,
+ thread: &AgentDiffThread,
window: &mut Window,
cx: &mut Context<Editor>,
) {
@@ -311,7 +418,7 @@ fn reject_edits_in_selection(
fn keep_edits_in_ranges(
editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot,
- thread: &Entity<Thread>,
+ thread: &AgentDiffThread,
ranges: Vec<Range<editor::Anchor>>,
window: &mut Window,
cx: &mut Context<Editor>,
@@ -326,8 +433,8 @@ fn keep_edits_in_ranges(
for hunk in &diff_hunks_in_ranges {
let buffer = multibuffer.read(cx).buffer(hunk.buffer_id);
if let Some(buffer) = buffer {
- thread.update(cx, |thread, cx| {
- thread.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
+ thread.action_log(cx).update(cx, |action_log, cx| {
+ action_log.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
});
}
}
@@ -336,7 +443,7 @@ fn keep_edits_in_ranges(
fn reject_edits_in_ranges(
editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot,
- thread: &Entity<Thread>,
+ thread: &AgentDiffThread,
ranges: Vec<Range<editor::Anchor>>,
window: &mut Window,
cx: &mut Context<Editor>,
@@ -362,8 +469,9 @@ fn reject_edits_in_ranges(
for (buffer, ranges) in ranges_by_buffer {
thread
- .update(cx, |thread, cx| {
- thread.reject_edits_in_ranges(buffer, ranges, cx)
+ .action_log(cx)
+ .update(cx, |action_log, cx| {
+ action_log.reject_edits_in_ranges(buffer, ranges, cx)
})
.detach_and_log_err(cx);
}
@@ -461,7 +569,7 @@ impl Item for AgentDiffPane {
}
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
- let summary = self.thread.read(cx).summary().unwrap_or("Agent Changes");
+ let summary = self.thread.summary(cx).unwrap_or("Agent Changes");
Label::new(format!("Review: {}", summary))
.color(if params.selected {
Color::Default
@@ -641,7 +749,7 @@ impl Render for AgentDiffPane {
}
}
-fn diff_hunk_controls(thread: &Entity<Thread>) -> editor::RenderDiffHunkControlsFn {
+fn diff_hunk_controls(thread: &AgentDiffThread) -> editor::RenderDiffHunkControlsFn {
let thread = thread.clone();
Arc::new(
@@ -676,7 +784,7 @@ fn render_diff_hunk_controls(
hunk_range: Range<editor::Anchor>,
is_created_file: bool,
line_height: Pixels,
- thread: &Entity<Thread>,
+ thread: &AgentDiffThread,
editor: &Entity<Editor>,
window: &mut Window,
cx: &mut App,
@@ -1112,11 +1220,8 @@ impl Render for AgentDiffToolbar {
return Empty.into_any();
};
- let has_pending_edit_tool_use = agent_diff
- .read(cx)
- .thread
- .read(cx)
- .has_pending_edit_tool_uses();
+ let has_pending_edit_tool_use =
+ agent_diff.read(cx).thread.has_pending_edit_tool_uses(cx);
if has_pending_edit_tool_use {
return div().px_2().child(spinner_icon).into_any();
@@ -1187,8 +1292,8 @@ pub enum EditorState {
}
struct WorkspaceThread {
- thread: WeakEntity<Thread>,
- _thread_subscriptions: [Subscription; 2],
+ thread: WeakAgentDiffThread,
+ _thread_subscriptions: (Subscription, Subscription),
singleton_editors: HashMap<WeakEntity<Buffer>, HashMap<WeakEntity<Editor>, Subscription>>,
_settings_subscription: Subscription,
_workspace_subscription: Option<Subscription>,
@@ -1212,23 +1317,23 @@ impl AgentDiff {
pub fn set_active_thread(
workspace: &WeakEntity<Workspace>,
- thread: &Entity<Thread>,
+ thread: impl Into<AgentDiffThread>,
window: &mut Window,
cx: &mut App,
) {
Self::global(cx).update(cx, |this, cx| {
- this.register_active_thread_impl(workspace, thread, window, cx);
+ this.register_active_thread_impl(workspace, thread.into(), window, cx);
});
}
fn register_active_thread_impl(
&mut self,
workspace: &WeakEntity<Workspace>,
- thread: &Entity<Thread>,
+ thread: AgentDiffThread,
window: &mut Window,
cx: &mut Context<Self>,
) {
- let action_log = thread.read(cx).action_log().clone();
+ let action_log = thread.action_log(cx).clone();
let action_log_subscription = cx.observe_in(&action_log, window, {
let workspace = workspace.clone();
@@ -1237,17 +1342,25 @@ impl AgentDiff {
}
});
- let thread_subscription = cx.subscribe_in(&thread, window, {
- let workspace = workspace.clone();
- move |this, _thread, event, window, cx| {
- this.handle_thread_event(&workspace, event, window, cx)
- }
- });
+ let thread_subscription = match &thread {
+ AgentDiffThread::Native(thread) => cx.subscribe_in(&thread, window, {
+ let workspace = workspace.clone();
+ move |this, _thread, event, window, cx| {
+ this.handle_native_thread_event(&workspace, event, window, cx)
+ }
+ }),
+ AgentDiffThread::AcpThread(thread) => cx.subscribe_in(&thread, window, {
+ let workspace = workspace.clone();
+ move |this, thread, event, window, cx| {
+ this.handle_acp_thread_event(&workspace, thread, event, window, cx)
+ }
+ }),
+ };
if let Some(workspace_thread) = self.workspace_threads.get_mut(&workspace) {
// replace thread and action log subscription, but keep editors
workspace_thread.thread = thread.downgrade();
- workspace_thread._thread_subscriptions = [action_log_subscription, thread_subscription];
+ workspace_thread._thread_subscriptions = (action_log_subscription, thread_subscription);
self.update_reviewing_editors(&workspace, window, cx);
return;
}
@@ -1272,7 +1385,7 @@ impl AgentDiff {
workspace.clone(),
WorkspaceThread {
thread: thread.downgrade(),
- _thread_subscriptions: [action_log_subscription, thread_subscription],
+ _thread_subscriptions: (action_log_subscription, thread_subscription),
singleton_editors: HashMap::default(),
_settings_subscription: settings_subscription,
_workspace_subscription: workspace_subscription,
@@ -1319,7 +1432,7 @@ impl AgentDiff {
fn register_review_action<T: Action>(
workspace: &mut Workspace,
- review: impl Fn(&Entity<Editor>, &Entity<Thread>, &mut Window, &mut App) -> PostReviewState
+ review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState
+ 'static,
this: &Entity<AgentDiff>,
) {
@@ -1338,7 +1451,7 @@ impl AgentDiff {
});
}
- fn handle_thread_event(
+ fn handle_native_thread_event(
&mut self,
workspace: &WeakEntity<Workspace>,
event: &ThreadEvent,
@@ -1380,6 +1493,40 @@ impl AgentDiff {
}
}
+ fn handle_acp_thread_event(
+ &mut self,
+ workspace: &WeakEntity<Workspace>,
+ thread: &Entity<AcpThread>,
+ event: &AcpThreadEvent,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ match event {
+ AcpThreadEvent::NewEntry => {
+ if thread
+ .read(cx)
+ .entries()
+ .last()
+ .and_then(|entry| entry.diff())
+ .is_some()
+ {
+ self.update_reviewing_editors(workspace, window, cx);
+ }
+ }
+ AcpThreadEvent::EntryUpdated(ix) => {
+ if thread
+ .read(cx)
+ .entries()
+ .get(*ix)
+ .and_then(|entry| entry.diff())
+ .is_some()
+ {
+ self.update_reviewing_editors(workspace, window, cx);
+ }
+ }
+ }
+ }
+
fn handle_workspace_event(
&mut self,
workspace: &Entity<Workspace>,
@@ -1485,7 +1632,7 @@ impl AgentDiff {
return;
};
- let action_log = thread.read(cx).action_log();
+ let action_log = thread.action_log(cx);
let changed_buffers = action_log.read(cx).changed_buffers(cx);
let mut unaffected = self.reviewing_editors.clone();
@@ -1510,7 +1657,7 @@ impl AgentDiff {
multibuffer.add_diff(diff_handle.clone(), cx);
});
- let new_state = if thread.read(cx).is_generating() {
+ let new_state = if thread.is_generating(cx) {
EditorState::Generating
} else {
EditorState::Reviewing
@@ -1606,7 +1753,7 @@ impl AgentDiff {
fn keep_all(
editor: &Entity<Editor>,
- thread: &Entity<Thread>,
+ thread: &AgentDiffThread,
window: &mut Window,
cx: &mut App,
) -> PostReviewState {
@@ -1626,7 +1773,7 @@ impl AgentDiff {
fn reject_all(
editor: &Entity<Editor>,
- thread: &Entity<Thread>,
+ thread: &AgentDiffThread,
window: &mut Window,
cx: &mut App,
) -> PostReviewState {
@@ -1646,7 +1793,7 @@ impl AgentDiff {
fn keep(
editor: &Entity<Editor>,
- thread: &Entity<Thread>,
+ thread: &AgentDiffThread,
window: &mut Window,
cx: &mut App,
) -> PostReviewState {
@@ -1659,7 +1806,7 @@ impl AgentDiff {
fn reject(
editor: &Entity<Editor>,
- thread: &Entity<Thread>,
+ thread: &AgentDiffThread,
window: &mut Window,
cx: &mut App,
) -> PostReviewState {
@@ -1682,7 +1829,7 @@ impl AgentDiff {
fn review_in_active_editor(
&mut self,
workspace: &mut Workspace,
- review: impl Fn(&Entity<Editor>, &Entity<Thread>, &mut Window, &mut App) -> PostReviewState,
+ review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> {
@@ -1703,7 +1850,7 @@ impl AgentDiff {
if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx) {
if let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
- let changed_buffers = thread.read(cx).action_log().read(cx).changed_buffers(cx);
+ let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx);
let mut keys = changed_buffers.keys().cycle();
keys.find(|k| *k == &curr_buffer);
@@ -1801,8 +1948,9 @@ mod tests {
})
.await
.unwrap();
- let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
- let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
+ let thread =
+ AgentDiffThread::Native(thread_store.update(cx, |store, cx| store.create_thread(cx)));
+ let action_log = cx.read(|cx| thread.action_log(cx));
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
@@ -1988,8 +2136,9 @@ mod tests {
});
// Set the active thread
+ let thread = AgentDiffThread::Native(thread);
cx.update(|window, cx| {
- AgentDiff::set_active_thread(&workspace.downgrade(), &thread, window, cx)
+ AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx)
});
let buffer1 = project
@@ -1,3 +1,4 @@
+use std::cell::RefCell;
use std::ops::Range;
use std::path::Path;
use std::rc::Rc;
@@ -7,12 +8,15 @@ use std::time::Duration;
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize};
+use crate::NewAcpThread;
+use crate::agent_diff::AgentDiffThread;
use crate::language_model_selector::ToggleModelSelector;
use crate::{
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell,
ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
+ acp::AcpThreadView,
active_thread::{self, ActiveThread, ActiveThreadEvent},
agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
agent_diff::AgentDiff,
@@ -38,6 +42,7 @@ use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use client::{UserStore, zed_urls};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
+use feature_flags::{self, FeatureFlagAppExt};
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
@@ -109,6 +114,12 @@ pub fn init(cx: &mut App) {
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
}
})
+ .register_action(|workspace, _: &NewAcpThread, window, cx| {
+ if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+ workspace.focus_panel::<AgentPanel>(window, cx);
+ panel.update(cx, |panel, cx| panel.new_gemini_thread(window, cx));
+ }
+ })
.register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
@@ -125,7 +136,8 @@ pub fn init(cx: &mut App) {
let thread = thread.read(cx).thread().clone();
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
}
- ActiveView::TextThread { .. }
+ ActiveView::AcpThread { .. }
+ | ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
}
@@ -188,6 +200,9 @@ enum ActiveView {
message_editor: Entity<MessageEditor>,
_subscriptions: Vec<gpui::Subscription>,
},
+ AcpThread {
+ thread_view: Entity<AcpThreadView>,
+ },
TextThread {
context_editor: Entity<TextThreadEditor>,
title_editor: Entity<Editor>,
@@ -207,7 +222,9 @@ enum WhichFontSize {
impl ActiveView {
pub fn which_font_size_used(&self) -> WhichFontSize {
match self {
- ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont,
+ ActiveView::Thread { .. } | ActiveView::AcpThread { .. } | ActiveView::History => {
+ WhichFontSize::AgentFont
+ }
ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
ActiveView::Configuration => WhichFontSize::None,
}
@@ -238,6 +255,7 @@ impl ActiveView {
thread.scroll_to_bottom(cx);
});
}
+ ActiveView::AcpThread { .. } => {}
ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
@@ -416,11 +434,14 @@ pub struct AgentPanel {
configuration_subscription: Option<Subscription>,
local_timezone: UtcOffset,
active_view: ActiveView,
+ acp_message_history:
+ Rc<RefCell<crate::acp::MessageHistory<agentic_coding_protocol::SendUserMessageParams>>>,
previous_view: Option<ActiveView>,
history_store: Entity<HistoryStore>,
history: Entity<ThreadHistory>,
hovered_recent_history_item: Option<usize>,
- assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
+ new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
+ agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
assistant_navigation_menu: Option<Entity<ContextMenu>>,
width: Option<Pixels>,
@@ -607,7 +628,7 @@ impl AgentPanel {
}
};
- AgentDiff::set_active_thread(&workspace, &thread, window, cx);
+ AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
let weak_panel = weak_self.clone();
@@ -653,7 +674,8 @@ impl AgentPanel {
.clone()
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
}
- ActiveView::TextThread { .. }
+ ActiveView::AcpThread { .. }
+ | ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
},
@@ -680,10 +702,12 @@ impl AgentPanel {
.unwrap(),
inline_assist_context_store,
previous_view: None,
+ acp_message_history: Default::default(),
history_store: history_store.clone(),
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
hovered_recent_history_item: None,
- assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
+ new_thread_menu_handle: PopoverMenuHandle::default(),
+ agent_panel_menu_handle: PopoverMenuHandle::default(),
assistant_navigation_menu_handle: PopoverMenuHandle::default(),
assistant_navigation_menu: None,
width: None,
@@ -733,6 +757,9 @@ impl AgentPanel {
ActiveView::Thread { thread, .. } => {
thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
}
+ ActiveView::AcpThread { thread_view, .. } => {
+ thread_view.update(cx, |thread_element, cx| thread_element.cancel(cx));
+ }
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
}
}
@@ -740,18 +767,18 @@ impl AgentPanel {
fn active_message_editor(&self) -> Option<&Entity<MessageEditor>> {
match &self.active_view {
ActiveView::Thread { message_editor, .. } => Some(message_editor),
- ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
+ ActiveView::AcpThread { .. }
+ | ActiveView::TextThread { .. }
+ | ActiveView::History
+ | ActiveView::Configuration => None,
}
}
fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
- // Preserve chat box text when using creating new thread from summary'
- let preserved_text = if action.from_thread_id.is_some() {
- self.active_message_editor()
- .map(|editor| editor.read(cx).get_text(cx).trim().to_string())
- } else {
- None
- };
+ // Preserve chat box text when using creating new thread
+ let preserved_text = self
+ .active_message_editor()
+ .map(|editor| editor.read(cx).get_text(cx).trim().to_string());
let thread = self
.thread_store
@@ -823,7 +850,7 @@ impl AgentPanel {
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
self.set_active_view(thread_view, window, cx);
- AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
+ AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
}
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -862,6 +889,37 @@ impl AgentPanel {
context_editor.focus_handle(cx).focus(window);
}
+ fn new_gemini_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ let workspace = self.workspace.clone();
+ let project = self.project.clone();
+ let message_history = self.acp_message_history.clone();
+
+ cx.spawn_in(window, async move |this, cx| {
+ let thread_view = cx.new_window_entity(|window, cx| {
+ crate::acp::AcpThreadView::new(
+ workspace.clone(),
+ project,
+ message_history,
+ window,
+ cx,
+ )
+ })?;
+ this.update_in(cx, |this, window, cx| {
+ this.set_active_view(
+ ActiveView::AcpThread {
+ thread_view: thread_view.clone(),
+ },
+ window,
+ cx,
+ );
+ })
+ .log_err();
+
+ anyhow::Ok(())
+ })
+ .detach();
+ }
+
fn deploy_rules_library(
&mut self,
action: &OpenRulesLibrary,
@@ -994,6 +1052,7 @@ impl AgentPanel {
cx,
)
});
+
let message_editor = cx.new(|cx| {
MessageEditor::new(
self.fs.clone(),
@@ -1012,7 +1071,7 @@ impl AgentPanel {
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
self.set_active_view(thread_view, window, cx);
- AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
+ AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
}
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
@@ -1025,6 +1084,9 @@ impl AgentPanel {
ActiveView::Thread { message_editor, .. } => {
message_editor.focus_handle(cx).focus(window);
}
+ ActiveView::AcpThread { thread_view } => {
+ thread_view.focus_handle(cx).focus(window);
+ }
ActiveView::TextThread { context_editor, .. } => {
context_editor.focus_handle(cx).focus(window);
}
@@ -1052,7 +1114,7 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
- self.assistant_dropdown_menu_handle.toggle(window, cx);
+ self.agent_panel_menu_handle.toggle(window, cx);
}
pub fn increase_font_size(
@@ -1140,11 +1202,19 @@ impl AgentPanel {
let thread = thread.read(cx).thread().clone();
self.workspace
.update(cx, |workspace, cx| {
- AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
+ AgentDiffPane::deploy_in_workspace(
+ AgentDiffThread::Native(thread),
+ workspace,
+ window,
+ cx,
+ )
})
.log_err();
}
- ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
+ ActiveView::AcpThread { .. }
+ | ActiveView::TextThread { .. }
+ | ActiveView::History
+ | ActiveView::Configuration => {}
}
}
@@ -1197,6 +1267,13 @@ impl AgentPanel {
)
.detach_and_log_err(cx);
}
+ ActiveView::AcpThread { thread_view } => {
+ thread_view
+ .update(cx, |thread_view, cx| {
+ thread_view.open_thread_as_markdown(workspace, window, cx)
+ })
+ .detach_and_log_err(cx);
+ }
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
}
}
@@ -1351,7 +1428,8 @@ impl AgentPanel {
}
})
}
- _ => {}
+ ActiveView::AcpThread { .. } => {}
+ ActiveView::History | ActiveView::Configuration => {}
}
if current_is_special && !new_is_special {
@@ -1365,6 +1443,8 @@ impl AgentPanel {
self.active_view = new_view;
}
+ self.acp_message_history.borrow_mut().reset_position();
+
self.focus_handle(cx).focus(window);
}
@@ -1437,6 +1517,7 @@ impl Focusable for AgentPanel {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.active_view {
ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
+ ActiveView::AcpThread { thread_view, .. } => thread_view.focus_handle(cx),
ActiveView::History => self.history.focus_handle(cx),
ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
ActiveView::Configuration => {
@@ -1593,6 +1674,9 @@ impl AgentPanel {
.into_any_element(),
}
}
+ ActiveView::AcpThread { thread_view } => Label::new(thread_view.read(cx).title(cx))
+ .truncate()
+ .into_any_element(),
ActiveView::TextThread {
title_editor,
context_editor,
@@ -1727,32 +1811,26 @@ impl AgentPanel {
let active_thread = match &self.active_view {
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
- ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
+ ActiveView::AcpThread { .. }
+ | ActiveView::TextThread { .. }
+ | ActiveView::History
+ | ActiveView::Configuration => None,
};
- let agent_extra_menu = PopoverMenu::new("agent-options-menu")
+ let new_thread_menu = PopoverMenu::new("new_thread_menu")
.trigger_with_tooltip(
- IconButton::new("agent-options-menu", IconName::Ellipsis)
- .icon_size(IconSize::Small),
- {
- let focus_handle = focus_handle.clone();
- move |window, cx| {
- Tooltip::for_action_in(
- "Toggle Agent Menu",
- &ToggleOptionsMenu,
- &focus_handle,
- window,
- cx,
- )
- }
- },
+ IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
+ Tooltip::text("New Thread…"),
)
.anchor(Corner::TopRight)
- .with_handle(self.assistant_dropdown_menu_handle.clone())
+ .with_handle(self.new_thread_menu_handle.clone())
.menu(move |window, cx| {
let active_thread = active_thread.clone();
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
menu = menu
+ .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
+ this.header("Zed Agent")
+ })
.action("New Thread", NewThread::default().boxed_clone())
.action("New Text Thread", NewTextThread.boxed_clone())
.when_some(active_thread, |this, active_thread| {
@@ -1768,21 +1846,36 @@ impl AgentPanel {
this
}
})
- .separator();
+ .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
+ this.separator()
+ .header("External Agents")
+ .action("New Gemini Thread", NewAcpThread.boxed_clone())
+ });
+ menu
+ }))
+ });
- menu = menu
- .header("MCP Servers")
- .action(
- "View Server Extensions",
- Box::new(zed_actions::Extensions {
- category_filter: Some(
- zed_actions::ExtensionCategoryFilter::ContextServers,
- ),
- }),
+ let agent_panel_menu = PopoverMenu::new("agent-options-menu")
+ .trigger_with_tooltip(
+ IconButton::new("agent-options-menu", IconName::Ellipsis)
+ .icon_size(IconSize::Small),
+ {
+ let focus_handle = focus_handle.clone();
+ move |window, cx| {
+ Tooltip::for_action_in(
+ "Toggle Agent Menu",
+ &ToggleOptionsMenu,
+ &focus_handle,
+ window,
+ cx,
)
- .action("Add Custom Server…", Box::new(AddContextServer))
- .separator();
-
+ }
+ },
+ )
+ .anchor(Corner::TopRight)
+ .with_handle(self.agent_panel_menu_handle.clone())
+ .menu(move |window, cx| {
+ Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
if let Some(usage) = usage {
menu = menu
.header_with_link("Prompt Usage", "Manage", account_url.clone())
@@ -1820,6 +1913,19 @@ impl AgentPanel {
.separator()
}
+ menu = menu
+ .header("MCP Servers")
+ .action(
+ "View Server Extensions",
+ Box::new(zed_actions::Extensions {
+ category_filter: Some(
+ zed_actions::ExtensionCategoryFilter::ContextServers,
+ ),
+ }),
+ )
+ .action("Add Custom Server…", Box::new(AddContextServer))
+ .separator();
+
menu = menu
.action("Rules…", Box::new(OpenRulesLibrary::default()))
.action("Settings", Box::new(OpenConfiguration))
@@ -1861,27 +1967,8 @@ impl AgentPanel {
.px(DynamicSpacing::Base08.rems(cx))
.border_l_1()
.border_color(cx.theme().colors().border)
- .child(
- IconButton::new("new", IconName::Plus)
- .icon_size(IconSize::Small)
- .style(ButtonStyle::Subtle)
- .tooltip(move |window, cx| {
- Tooltip::for_action_in(
- "New Thread",
- &NewThread::default(),
- &focus_handle,
- window,
- cx,
- )
- })
- .on_click(move |_event, window, cx| {
- window.dispatch_action(
- NewThread::default().boxed_clone(),
- cx,
- );
- }),
- )
- .child(agent_extra_menu),
+ .child(new_thread_menu)
+ .child(agent_panel_menu),
),
)
}
@@ -1893,6 +1980,9 @@ impl AgentPanel {
message_editor,
..
} => (thread.read(cx), message_editor.read(cx)),
+ ActiveView::AcpThread { .. } => {
+ return None;
+ }
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return None;
}
@@ -2031,6 +2121,9 @@ impl AgentPanel {
return false;
}
}
+ ActiveView::AcpThread { .. } => {
+ return false;
+ }
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return false;
}
@@ -2615,6 +2708,9 @@ impl AgentPanel {
) -> Option<AnyElement> {
let active_thread = match &self.active_view {
ActiveView::Thread { thread, .. } => thread,
+ ActiveView::AcpThread { .. } => {
+ return None;
+ }
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return None;
}
@@ -2961,6 +3057,9 @@ impl AgentPanel {
.detach();
});
}
+ ActiveView::AcpThread { .. } => {
+ unimplemented!()
+ }
ActiveView::TextThread { context_editor, .. } => {
context_editor.update(cx, |context_editor, cx| {
TextThreadEditor::insert_dragged_files(
@@ -2979,8 +3078,10 @@ impl AgentPanel {
fn key_context(&self) -> KeyContext {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("AgentPanel");
- if matches!(self.active_view, ActiveView::TextThread { .. }) {
- key_context.add("prompt_editor");
+ match &self.active_view {
+ ActiveView::AcpThread { .. } => key_context.add("acp_thread"),
+ ActiveView::TextThread { .. } => key_context.add("prompt_editor"),
+ ActiveView::Thread { .. } | ActiveView::History | ActiveView::Configuration => {}
}
key_context
}
@@ -3034,6 +3135,7 @@ impl Render for AgentPanel {
});
this.continue_conversation(window, cx);
}
+ ActiveView::AcpThread { .. } => {}
ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
@@ -3075,6 +3177,10 @@ impl Render for AgentPanel {
})
.child(h_flex().child(message_editor.clone()))
.child(self.render_drag_target(cx)),
+ ActiveView::AcpThread { thread_view, .. } => parent
+ .relative()
+ .child(thread_view.clone())
+ .child(self.render_drag_target(cx)),
ActiveView::History => parent.child(self.history.clone()),
ActiveView::TextThread {
context_editor,
@@ -1,3 +1,4 @@
+mod acp;
mod active_thread;
mod agent_configuration;
mod agent_diff;
@@ -54,42 +55,76 @@ pub use ui::preview::{all_agent_previews, get_agent_preview};
actions!(
agent,
[
+ /// Creates a new text-based conversation thread.
NewTextThread,
+ /// Creates a new external agent conversation thread.
+ NewAcpThread,
+ /// Toggles the context picker interface for adding files, symbols, or other context.
ToggleContextPicker,
+ /// Toggles the navigation menu for switching between threads and views.
ToggleNavigationMenu,
+ /// Toggles the options menu for agent settings and preferences.
ToggleOptionsMenu,
+ /// Deletes the recently opened thread from history.
DeleteRecentlyOpenThread,
+ /// Toggles the profile selector for switching between agent profiles.
ToggleProfileSelector,
+ /// Removes all added context from the current conversation.
RemoveAllContext,
+ /// Expands the message editor to full size.
ExpandMessageEditor,
+ /// Opens the conversation history view.
OpenHistory,
+ /// Adds a context server to the configuration.
AddContextServer,
+ /// Removes the currently selected thread.
RemoveSelectedThread,
- Chat,
+ /// Starts a chat conversation with follow-up enabled.
ChatWithFollow,
+ /// Cycles to the next inline assist suggestion.
CycleNextInlineAssist,
+ /// Cycles to the previous inline assist suggestion.
CyclePreviousInlineAssist,
+ /// Moves focus up in the interface.
FocusUp,
+ /// Moves focus down in the interface.
FocusDown,
+ /// Moves focus left in the interface.
FocusLeft,
+ /// Moves focus right in the interface.
FocusRight,
+ /// Removes the currently focused context item.
RemoveFocusedContext,
+ /// Accepts the suggested context item.
AcceptSuggestedContext,
+ /// Opens the active thread as a markdown file.
OpenActiveThreadAsMarkdown,
+ /// Opens the agent diff view to review changes.
OpenAgentDiff,
+ /// Keeps the current suggestion or change.
Keep,
+ /// Rejects the current suggestion or change.
Reject,
+ /// Rejects all suggestions or changes.
RejectAll,
+ /// Keeps all suggestions or changes.
KeepAll,
+ /// Follows the agent's suggestions.
Follow,
+ /// Resets the trial upsell notification.
ResetTrialUpsell,
+ /// Resets the trial end upsell notification.
ResetTrialEndUpsell,
+ /// Continues the current thread.
ContinueThread,
+ /// Continues the thread with burn mode enabled.
ContinueWithBurnMode,
+ /// Toggles burn mode for faster responses.
ToggleBurnMode,
]
);
+/// Creates a new conversation thread, optionally based on an existing thread.
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = agent)]
#[serde(deny_unknown_fields)]
@@ -98,6 +133,7 @@ pub struct NewThread {
from_thread_id: Option<ThreadId>,
}
+/// Opens the profile management interface for configuring agent tools and settings.
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = agent)]
#[serde(deny_unknown_fields)]
@@ -475,6 +475,7 @@ impl CodegenAlternative {
stop: Vec::new(),
temperature,
messages: vec![request_message],
+ thinking_allowed: false,
}
}))
}
@@ -1,6 +1,6 @@
mod completion_provider;
mod fetch_context_picker;
-mod file_context_picker;
+pub(crate) mod file_context_picker;
mod rules_context_picker;
mod symbol_context_picker;
mod thread_context_picker;
@@ -426,6 +426,7 @@ impl ContextPicker {
this.add_recent_file(project_path.clone(), window, cx);
})
},
+ None,
)
}
RecentEntry::Thread(thread) => {
@@ -443,6 +444,7 @@ impl ContextPicker {
.detach_and_log_err(cx);
})
},
+ None,
)
}
}
@@ -686,6 +686,7 @@ impl ContextPickerCompletionProvider {
let mut label = CodeLabel::plain(symbol.name.clone(), None);
label.push_str(" ", None);
label.push_str(&file_name, comment_id);
+ label.push_str(&format!(" L{}", symbol.range.start.0.row + 1), comment_id);
let new_text = format!("{} ", MentionLink::for_symbol(&symbol.name, &full_path));
let new_text_len = new_text.len();
@@ -660,7 +660,6 @@ impl InlineAssistant {
height: Some(prompt_editor_height),
render: build_assist_editor_renderer(prompt_editor),
priority: 0,
- render_in_minimap: false,
},
BlockProperties {
style: BlockStyle::Sticky,
@@ -675,7 +674,6 @@ impl InlineAssistant {
.into_any_element()
}),
priority: 0,
- render_in_minimap: false,
},
];
@@ -1451,7 +1449,6 @@ impl InlineAssistant {
.into_any_element()
}),
priority: 0,
- render_in_minimap: false,
});
}
@@ -18,6 +18,7 @@ use ui::{ListItem, ListItemSpacing, prelude::*};
actions!(
agent,
[
+ /// Toggles the language model selector dropdown.
#[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])]
ToggleModelSelector
]
@@ -2,6 +2,7 @@ use std::collections::BTreeMap;
use std::rc::Rc;
use std::sync::Arc;
+use crate::agent_diff::AgentDiffThread;
use crate::agent_model_selector::AgentModelSelector;
use crate::language_model_selector::ToggleModelSelector;
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
@@ -47,13 +48,14 @@ use ui::{
};
use util::ResultExt as _;
use workspace::{CollaboratorId, Workspace};
+use zed_actions::agent::Chat;
use zed_llm_client::CompletionIntent;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector;
use crate::{
- ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
+ ActiveThread, AgentDiffPane, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode,
ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
};
@@ -474,9 +476,12 @@ impl MessageEditor {
window: &mut Window,
cx: &mut Context<Self>,
) {
- if let Ok(diff) =
- AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx)
- {
+ if let Ok(diff) = AgentDiffPane::deploy(
+ AgentDiffThread::Native(self.thread.clone()),
+ self.workspace.clone(),
+ window,
+ cx,
+ ) {
let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);
diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));
}
@@ -1160,7 +1165,7 @@ impl MessageEditor {
})
.child(
h_flex()
- .id("file-name")
+ .id(("file-name", index))
.pr_8()
.gap_1p5()
.max_w_full()
@@ -1171,9 +1176,16 @@ impl MessageEditor {
.gap_0p5()
.children(file_name)
.children(file_path),
- ), // TODO: Implement line diff
- // .child(Label::new("+").color(Color::Created))
- // .child(Label::new("-").color(Color::Deleted)),
+ )
+ .on_click({
+ let buffer = buffer.clone();
+ cx.listener(move |this, _, window, cx| {
+ this.handle_file_click(buffer.clone(), window, cx);
+ })
+ }), // TODO: Implement line diff
+ // .child(Label::new("+").color(Color::Created))
+ // .child(Label::new("-").color(Color::Deleted)),
+ //
)
.child(
h_flex()
@@ -1446,6 +1458,7 @@ impl MessageEditor {
tool_choice: None,
stop: vec![],
temperature: AgentSettings::temperature_for_model(&model.model, cx),
+ thinking_allowed: true,
};
Some(model.model.count_tokens(request, cx))
@@ -1613,6 +1626,7 @@ impl Render for MessageEditor {
v_flex()
.size_full()
+ .bg(cx.theme().colors().panel_background)
.when(changed_buffers.len() > 0, |parent| {
parent.child(self.render_edits_bar(&changed_buffers, window, cx))
})
@@ -297,6 +297,7 @@ impl TerminalInlineAssistant {
tool_choice: None,
stop: Vec::new(),
temperature,
+ thinking_allowed: false,
}
}))
}
@@ -38,8 +38,8 @@ use language::{
language_settings::{SoftWrap, all_language_settings},
};
use language_model::{
- ConfigurationError, LanguageModelImage, LanguageModelProviderTosView, LanguageModelRegistry,
- Role,
+ ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelProviderTosView,
+ LanguageModelRegistry, Role,
};
use multi_buffer::MultiBufferRow;
use picker::{Picker, popover_menu::PickerPopoverMenu};
@@ -85,16 +85,24 @@ use assistant_context::{
actions!(
assistant,
[
+ /// Sends the current message to the assistant.
Assist,
+ /// Confirms and executes the entered slash command.
ConfirmCommand,
+ /// Copies code from the assistant's response to the clipboard.
CopyCode,
+ /// Cycles between user and assistant message roles.
CycleMessageRole,
+ /// Inserts the selected text into the active editor.
InsertIntoEditor,
+ /// Quotes the current selection in the assistant conversation.
QuoteSelection,
+ /// Splits the conversation at the current cursor position.
Split,
]
);
+/// Inserts files that were dragged and dropped into the assistant conversation.
#[derive(PartialEq, Clone, Action)]
#[action(namespace = assistant, no_json, no_register)]
pub enum InsertDraggedFiles {
@@ -1248,7 +1256,6 @@ impl TextThreadEditor {
),
priority: usize::MAX,
render: render_block(MessageMetadata::from(message)),
- render_in_minimap: false,
};
let mut new_blocks = vec![];
let mut block_index_to_message = vec![];
@@ -1850,7 +1857,6 @@ impl TextThreadEditor {
.into_any_element()
}),
priority: 0,
- render_in_minimap: false,
})
})
.collect::<Vec<_>>();
@@ -3055,7 +3061,7 @@ fn token_state(context: &Entity<AssistantContext>, cx: &App) -> Option<TokenStat
.default_model()?
.model;
let token_count = context.read(cx).token_count()?;
- let max_token_count = model.max_token_count();
+ let max_token_count = model.max_token_count_for_mode(context.read(cx).completion_mode().into());
let token_state = if max_token_count.saturating_sub(token_count) == 0 {
TokenState::NoTokensLeft {
max_token_count,
@@ -42,8 +42,8 @@ impl IncompatibleToolsState {
.profile()
.enabled_tools(cx)
.iter()
- .filter(|tool| tool.input_schema(model.tool_input_format()).is_err())
- .cloned()
+ .filter(|(_, tool)| tool.input_schema(model.tool_input_format()).is_err())
+ .map(|(_, tool)| tool.clone())
.collect()
})
}
@@ -1,5 +1,4 @@
mod agent_notification;
-mod animated_label;
mod burn_mode_tooltip;
mod context_pill;
mod onboarding_modal;
@@ -7,7 +6,6 @@ pub mod preview;
mod upsell;
pub use agent_notification::*;
-pub use animated_label::*;
pub use burn_mode_tooltip::*;
pub use context_pill::*;
pub use onboarding_modal::*;
@@ -15,6 +15,8 @@ path = "src/askpass.rs"
anyhow.workspace = true
futures.workspace = true
gpui.workspace = true
+net.workspace = true
+parking_lot.workspace = true
smol.workspace = true
tempfile.workspace = true
util.workspace = true
@@ -1,21 +1,14 @@
-use std::path::{Path, PathBuf};
-use std::time::Duration;
+use std::{ffi::OsStr, time::Duration};
-#[cfg(unix)]
-use anyhow::Context as _;
+use anyhow::{Context as _, Result};
use futures::channel::{mpsc, oneshot};
-#[cfg(unix)]
-use futures::{AsyncBufReadExt as _, io::BufReader};
-#[cfg(unix)]
-use futures::{AsyncWriteExt as _, FutureExt as _, select_biased};
-use futures::{SinkExt, StreamExt};
+use futures::{
+ AsyncBufReadExt as _, AsyncWriteExt as _, FutureExt as _, SinkExt, StreamExt, io::BufReader,
+ select_biased,
+};
use gpui::{AsyncApp, BackgroundExecutor, Task};
-#[cfg(unix)]
use smol::fs;
-#[cfg(unix)]
-use smol::net::unix::UnixListener;
-#[cfg(unix)]
-use util::{ResultExt as _, fs::make_file_executable, get_shell_safe_zed_path};
+use util::ResultExt as _;
#[derive(PartialEq, Eq)]
pub enum AskPassResult {
@@ -42,41 +35,56 @@ impl AskPassDelegate {
Self { tx, _task: task }
}
- pub async fn ask_password(&mut self, prompt: String) -> anyhow::Result<String> {
+ pub async fn ask_password(&mut self, prompt: String) -> Result<String> {
let (tx, rx) = oneshot::channel();
self.tx.send((prompt, tx)).await?;
Ok(rx.await?)
}
}
-#[cfg(unix)]
pub struct AskPassSession {
- script_path: PathBuf,
+ #[cfg(not(target_os = "windows"))]
+ script_path: std::path::PathBuf,
+ #[cfg(target_os = "windows")]
+ askpass_helper: String,
+ #[cfg(target_os = "windows")]
+ secret: std::sync::Arc<parking_lot::Mutex<String>>,
_askpass_task: Task<()>,
askpass_opened_rx: Option<oneshot::Receiver<()>>,
askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
}
-#[cfg(unix)]
+#[cfg(not(target_os = "windows"))]
+const ASKPASS_SCRIPT_NAME: &str = "askpass.sh";
+#[cfg(target_os = "windows")]
+const ASKPASS_SCRIPT_NAME: &str = "askpass.ps1";
+
impl AskPassSession {
/// This will create a new AskPassSession.
/// You must retain this session until the master process exits.
#[must_use]
- pub async fn new(
- executor: &BackgroundExecutor,
- mut delegate: AskPassDelegate,
- ) -> anyhow::Result<Self> {
+ pub async fn new(executor: &BackgroundExecutor, mut delegate: AskPassDelegate) -> Result<Self> {
+ use net::async_net::UnixListener;
+ use util::fs::make_file_executable;
+
+ #[cfg(target_os = "windows")]
+ let secret = std::sync::Arc::new(parking_lot::Mutex::new(String::new()));
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
let askpass_socket = temp_dir.path().join("askpass.sock");
- let askpass_script_path = temp_dir.path().join("askpass.sh");
+ let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
- let listener =
- UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
- let zed_path = get_shell_safe_zed_path()?;
+ let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
+ #[cfg(not(target_os = "windows"))]
+ let zed_path = util::get_shell_safe_zed_path()?;
+ #[cfg(target_os = "windows")]
+ let zed_path = std::env::current_exe()
+ .context("finding current executable path for use in askpass")?;
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
let mut kill_tx = Some(askpass_kill_master_tx);
+ #[cfg(target_os = "windows")]
+ let askpass_secret = secret.clone();
let askpass_task = executor.spawn(async move {
let mut askpass_opened_tx = Some(askpass_opened_tx);
@@ -93,10 +101,14 @@ impl AskPassSession {
if let Some(password) = delegate
.ask_password(prompt.to_string())
.await
- .context("failed to get askpass password")
+ .context("getting askpass password")
.log_err()
{
stream.write_all(password.as_bytes()).await.log_err();
+ #[cfg(target_os = "windows")]
+ {
+ *askpass_secret.lock() = password;
+ }
} else {
if let Some(kill_tx) = kill_tx.take() {
kill_tx.send(()).log_err();
@@ -112,34 +124,49 @@ impl AskPassSession {
});
// Create an askpass script that communicates back to this process.
- let askpass_script = format!(
- "{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
- zed_exe = zed_path,
- askpass_socket = askpass_socket.display(),
- print_args = "printf '%s\\0' \"$@\"",
- shebang = "#!/bin/sh",
- );
- fs::write(&askpass_script_path, askpass_script).await?;
+ let askpass_script = generate_askpass_script(&zed_path, &askpass_socket);
+ fs::write(&askpass_script_path, askpass_script)
+ .await
+ .with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
make_file_executable(&askpass_script_path).await?;
+ #[cfg(target_os = "windows")]
+ let askpass_helper = format!(
+ "powershell.exe -ExecutionPolicy Bypass -File {}",
+ askpass_script_path.display()
+ );
Ok(Self {
+ #[cfg(not(target_os = "windows"))]
script_path: askpass_script_path,
+
+ #[cfg(target_os = "windows")]
+ secret,
+ #[cfg(target_os = "windows")]
+ askpass_helper,
+
_askpass_task: askpass_task,
askpass_kill_master_rx: Some(askpass_kill_master_rx),
askpass_opened_rx: Some(askpass_opened_rx),
})
}
- pub fn script_path(&self) -> &Path {
+ #[cfg(not(target_os = "windows"))]
+ pub fn script_path(&self) -> impl AsRef<OsStr> {
&self.script_path
}
+ #[cfg(target_os = "windows")]
+ pub fn script_path(&self) -> impl AsRef<OsStr> {
+ &self.askpass_helper
+ }
+
// This will run the askpass task forever, resolving as many authentication requests as needed.
// The caller is responsible for examining the result of their own commands and cancelling this
// future when this is no longer needed. Note that this can only be called once, but due to the
// drop order this takes an &mut, so you can `drop()` it after you're done with the master process.
pub async fn run(&mut self) -> AskPassResult {
- let connection_timeout = Duration::from_secs(10);
+ // This is the default timeout setting used by VSCode.
+ let connection_timeout = Duration::from_secs(17);
let askpass_opened_rx = self.askpass_opened_rx.take().expect("Only call run once");
let askpass_kill_master_rx = self
.askpass_kill_master_rx
@@ -158,14 +185,19 @@ impl AskPassSession {
}
}
}
+
+ /// This will return the password that was last set by the askpass script.
+ #[cfg(target_os = "windows")]
+ pub fn get_password(&self) -> String {
+ self.secret.lock().clone()
+ }
}
/// The main function for when Zed is running in netcat mode for use in askpass.
/// Called from both the remote server binary and the zed binary in their respective main functions.
-#[cfg(unix)]
pub fn main(socket: &str) {
+ use net::UnixStream;
use std::io::{self, Read, Write};
- use std::os::unix::net::UnixStream;
use std::process::exit;
let mut stream = match UnixStream::connect(socket) {
@@ -182,6 +214,10 @@ pub fn main(socket: &str) {
exit(1);
}
+ #[cfg(target_os = "windows")]
+ while buffer.last().map_or(false, |&b| b == b'\n' || b == b'\r') {
+ buffer.pop();
+ }
if buffer.last() != Some(&b'\0') {
buffer.push(b'\0');
}
@@ -202,28 +238,28 @@ pub fn main(socket: &str) {
exit(1);
}
}
-#[cfg(not(unix))]
-pub fn main(_socket: &str) {}
-#[cfg(not(unix))]
-pub struct AskPassSession {
- path: PathBuf,
+#[inline]
+#[cfg(not(target_os = "windows"))]
+fn generate_askpass_script(zed_path: &str, askpass_socket: &std::path::Path) -> String {
+ format!(
+ "{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
+ zed_exe = zed_path,
+ askpass_socket = askpass_socket.display(),
+ print_args = "printf '%s\\0' \"$@\"",
+ shebang = "#!/bin/sh",
+ )
}
-#[cfg(not(unix))]
-impl AskPassSession {
- pub async fn new(_: &BackgroundExecutor, _: AskPassDelegate) -> anyhow::Result<Self> {
- Ok(Self {
- path: PathBuf::new(),
- })
- }
-
- pub fn script_path(&self) -> &Path {
- &self.path
- }
-
- pub async fn run(&mut self) -> AskPassResult {
- futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(20))).await;
- AskPassResult::Timedout
- }
+#[inline]
+#[cfg(target_os = "windows")]
+fn generate_askpass_script(zed_path: &std::path::Path, askpass_socket: &std::path::Path) -> String {
+ format!(
+ r#"
+ $ErrorActionPreference = 'Stop';
+ ($args -join [char]0) | & "{zed_exe}" --askpass={askpass_socket} 2> $null
+ "#,
+ zed_exe = zed_path.display(),
+ askpass_socket = askpass_socket.display(),
+ )
}
@@ -2293,6 +2293,7 @@ impl AssistantContext {
tool_choice: None,
stop: Vec::new(),
temperature: model.and_then(|model| AgentSettings::temperature_for_model(model, cx)),
+ thinking_allowed: true,
};
for message in self.messages(cx) {
if message.status != MessageStatus::Done {
@@ -34,6 +34,11 @@ impl ExtensionSlashCommandProxy for SlashCommandRegistryProxy {
self.slash_command_registry
.register_command(ExtensionSlashCommand::new(extension, command), false)
}
+
+ fn unregister_slash_command(&self, command_name: Arc<str>) {
+ self.slash_command_registry
+ .unregister_command_by_name(&command_name)
+ }
}
/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
@@ -22,6 +22,7 @@ gpui.workspace = true
icons.workspace = true
language.workspace = true
language_model.workspace = true
+log.workspace = true
parking_lot.workspace = true
project.workspace = true
regex.workspace = true
@@ -1,5 +1,6 @@
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, Subscription, Task, WeakEntity};
@@ -7,7 +8,7 @@ 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;
+use util::{RangeExt, ResultExt as _};
/// Tracks actions performed by tools in a thread
pub struct ActionLog {
@@ -17,6 +18,8 @@ pub struct ActionLog {
edited_since_project_diagnostics_check: bool,
/// The project this action log is associated with
project: Entity<Project>,
+ /// Tracks which buffer versions have already been notified as changed externally
+ notified_versions: BTreeMap<Entity<Buffer>, clock::Global>,
}
impl ActionLog {
@@ -26,6 +29,7 @@ impl ActionLog {
tracked_buffers: BTreeMap::default(),
edited_since_project_diagnostics_check: false,
project,
+ notified_versions: BTreeMap::default(),
}
}
@@ -43,6 +47,10 @@ impl ActionLog {
self.edited_since_project_diagnostics_check
}
+ pub fn latest_snapshot(&self, buffer: &Entity<Buffer>) -> Option<text::BufferSnapshot> {
+ Some(self.tracked_buffers.get(buffer)?.snapshot.clone())
+ }
+
fn track_buffer_internal(
&mut self,
buffer: Entity<Buffer>,
@@ -51,6 +59,7 @@ impl ActionLog {
) -> &mut TrackedBuffer {
let status = if is_created {
if let Some(tracked) = self.tracked_buffers.remove(&buffer) {
+ self.notified_versions.remove(&buffer);
match tracked.status {
TrackedBufferStatus::Created {
existing_file_content,
@@ -106,7 +115,7 @@ impl ActionLog {
TrackedBuffer {
buffer: buffer.clone(),
diff_base,
- unreviewed_edits: unreviewed_edits,
+ unreviewed_edits,
snapshot: text_snapshot.clone(),
status,
version: buffer.read(cx).version(),
@@ -165,6 +174,7 @@ impl ActionLog {
// 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);
+ self.notified_versions.remove(&buffer);
}
cx.notify();
}
@@ -178,6 +188,7 @@ impl ActionLog {
// resurrected externally, we want to clear the edits we
// were tracking and reset the buffer's state.
self.tracked_buffers.remove(&buffer);
+ self.notified_versions.remove(&buffer);
self.track_buffer_internal(buffer, false, cx);
}
cx.notify();
@@ -483,6 +494,7 @@ impl ActionLog {
match tracked_buffer.status {
TrackedBufferStatus::Created { .. } => {
self.tracked_buffers.remove(&buffer);
+ self.notified_versions.remove(&buffer);
cx.notify();
}
TrackedBufferStatus::Modified => {
@@ -508,6 +520,7 @@ impl ActionLog {
match tracked_buffer.status {
TrackedBufferStatus::Deleted => {
self.tracked_buffers.remove(&buffer);
+ self.notified_versions.remove(&buffer);
cx.notify();
}
_ => {
@@ -616,6 +629,7 @@ impl ActionLog {
};
self.tracked_buffers.remove(&buffer);
+ self.notified_versions.remove(&buffer);
cx.notify();
task
}
@@ -629,6 +643,7 @@ impl ActionLog {
// Clear all tracked edits for this buffer and start over as if we just read it.
self.tracked_buffers.remove(&buffer);
+ self.notified_versions.remove(&buffer);
self.buffer_read(buffer.clone(), cx);
cx.notify();
save
@@ -704,6 +719,22 @@ impl ActionLog {
cx.notify();
}
+ pub fn reject_all_edits(&mut self, cx: &mut Context<Self>) -> Task<()> {
+ let futures = self.changed_buffers(cx).into_keys().map(|buffer| {
+ let reject = self.reject_edits_in_ranges(buffer, vec![Anchor::MIN..Anchor::MAX], cx);
+
+ async move {
+ reject.await.log_err();
+ }
+ });
+
+ let task = futures::future::join_all(futures);
+
+ cx.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<Buffer>, Entity<BufferDiff>> {
self.tracked_buffers
@@ -713,6 +744,33 @@ impl ActionLog {
.collect()
}
+ /// Returns stale buffers that haven't been notified yet
+ pub fn unnotified_stale_buffers<'a>(
+ &'a self,
+ cx: &'a App,
+ ) -> impl Iterator<Item = &'a Entity<Buffer>> {
+ self.stale_buffers(cx).filter(|buffer| {
+ let buffer_entity = buffer.read(cx);
+ self.notified_versions
+ .get(buffer)
+ .map_or(true, |notified_version| {
+ *notified_version != buffer_entity.version
+ })
+ })
+ }
+
+ /// Marks the given buffers as notified at their current versions
+ pub fn mark_buffers_as_notified(
+ &mut self,
+ buffers: impl IntoIterator<Item = Entity<Buffer>>,
+ cx: &App,
+ ) {
+ for buffer in buffers {
+ let version = buffer.read(cx).version.clone();
+ self.notified_versions.insert(buffer, version);
+ }
+ }
+
/// Iterate over buffers changed since last read or edited by the model
pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
self.tracked_buffers
@@ -25,10 +25,15 @@ fn preprocess_json_schema(json: &mut Value) -> Result<()> {
// `additionalProperties` defaults to `false` unless explicitly specified.
// This prevents models from hallucinating tool parameters.
if let Value::Object(obj) = json {
- if let Some(Value::String(type_str)) = obj.get("type") {
- if type_str == "object" && !obj.contains_key("additionalProperties") {
+ if matches!(obj.get("type"), Some(Value::String(s)) if s == "object") {
+ if !obj.contains_key("additionalProperties") {
obj.insert("additionalProperties".to_string(), Value::Bool(false));
}
+
+ // OpenAI API requires non-missing `properties`
+ if !obj.contains_key("properties") {
+ obj.insert("properties".to_string(), Value::Object(Default::default()));
+ }
}
}
Ok(())
@@ -1,18 +1,52 @@
-use std::sync::Arc;
-
-use collections::{HashMap, IndexMap};
-use gpui::App;
+use std::{borrow::Borrow, sync::Arc};
use crate::{Tool, ToolRegistry, ToolSource};
+use collections::{HashMap, HashSet, IndexMap};
+use gpui::{App, SharedString};
+use util::debug_panic;
#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
pub struct ToolId(usize);
+/// A unique identifier for a tool within a working set.
+#[derive(Clone, PartialEq, Eq, Hash, Default)]
+pub struct UniqueToolName(SharedString);
+
+impl Borrow<str> for UniqueToolName {
+ fn borrow(&self) -> &str {
+ &self.0
+ }
+}
+
+impl From<String> for UniqueToolName {
+ fn from(value: String) -> Self {
+ UniqueToolName(SharedString::new(value))
+ }
+}
+
+impl Into<String> for UniqueToolName {
+ fn into(self) -> String {
+ self.0.into()
+ }
+}
+
+impl std::fmt::Debug for UniqueToolName {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl std::fmt::Display for UniqueToolName {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0.as_ref())
+ }
+}
+
/// A working set of tools for use in one instance of the Assistant Panel.
#[derive(Default)]
pub struct ToolWorkingSet {
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
- context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
+ context_server_tools_by_name: HashMap<UniqueToolName, Arc<dyn Tool>>,
next_tool_id: ToolId,
}
@@ -24,16 +58,20 @@ impl ToolWorkingSet {
.or_else(|| ToolRegistry::global(cx).tool(name))
}
- pub fn tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
- let mut tools = ToolRegistry::global(cx).tools();
- tools.extend(self.context_server_tools_by_id.values().cloned());
+ pub fn tools(&self, cx: &App) -> Vec<(UniqueToolName, Arc<dyn Tool>)> {
+ let mut tools = ToolRegistry::global(cx)
+ .tools()
+ .into_iter()
+ .map(|tool| (UniqueToolName(tool.name().into()), tool))
+ .collect::<Vec<_>>();
+ tools.extend(self.context_server_tools_by_name.clone());
tools
}
pub fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
let mut tools_by_source = IndexMap::default();
- for tool in self.tools(cx) {
+ for (_, tool) in self.tools(cx) {
tools_by_source
.entry(tool.source())
.or_insert_with(Vec::new)
@@ -49,27 +87,324 @@ impl ToolWorkingSet {
tools_by_source
}
- pub fn insert(&mut self, tool: Arc<dyn Tool>) -> ToolId {
+ pub fn insert(&mut self, tool: Arc<dyn Tool>, cx: &App) -> ToolId {
+ let tool_id = self.register_tool(tool);
+ self.tools_changed(cx);
+ tool_id
+ }
+
+ pub fn extend(&mut self, tools: impl Iterator<Item = Arc<dyn Tool>>, cx: &App) -> Vec<ToolId> {
+ let ids = tools.map(|tool| self.register_tool(tool)).collect();
+ self.tools_changed(cx);
+ ids
+ }
+
+ pub fn remove(&mut self, tool_ids_to_remove: &[ToolId], cx: &App) {
+ self.context_server_tools_by_id
+ .retain(|id, _| !tool_ids_to_remove.contains(id));
+ self.tools_changed(cx);
+ }
+
+ fn register_tool(&mut self, tool: Arc<dyn Tool>) -> ToolId {
let tool_id = self.next_tool_id;
self.next_tool_id.0 += 1;
self.context_server_tools_by_id
.insert(tool_id, tool.clone());
- self.tools_changed();
tool_id
}
- pub fn remove(&mut self, tool_ids_to_remove: &[ToolId]) {
- self.context_server_tools_by_id
- .retain(|id, _| !tool_ids_to_remove.contains(id));
- self.tools_changed();
+ fn tools_changed(&mut self, cx: &App) {
+ self.context_server_tools_by_name = resolve_context_server_tool_name_conflicts(
+ &self
+ .context_server_tools_by_id
+ .values()
+ .cloned()
+ .collect::<Vec<_>>(),
+ &ToolRegistry::global(cx).tools(),
+ );
+ }
+}
+
+fn resolve_context_server_tool_name_conflicts(
+ context_server_tools: &[Arc<dyn Tool>],
+ native_tools: &[Arc<dyn Tool>],
+) -> HashMap<UniqueToolName, Arc<dyn Tool>> {
+ fn resolve_tool_name(tool: &Arc<dyn Tool>) -> String {
+ let mut tool_name = tool.name();
+ tool_name.truncate(MAX_TOOL_NAME_LENGTH);
+ tool_name
}
- fn tools_changed(&mut self) {
- self.context_server_tools_by_name.clear();
- self.context_server_tools_by_name.extend(
- self.context_server_tools_by_id
- .values()
- .map(|tool| (tool.name(), tool.clone())),
+ const MAX_TOOL_NAME_LENGTH: usize = 64;
+
+ let mut duplicated_tool_names = HashSet::default();
+ let mut seen_tool_names = HashSet::default();
+ seen_tool_names.extend(native_tools.iter().map(|tool| tool.name()));
+ for tool in context_server_tools {
+ let tool_name = resolve_tool_name(tool);
+ if seen_tool_names.contains(&tool_name) {
+ debug_assert!(
+ tool.source() != ToolSource::Native,
+ "Expected MCP tool but got a native tool: {}",
+ tool_name
+ );
+ duplicated_tool_names.insert(tool_name);
+ } else {
+ seen_tool_names.insert(tool_name);
+ }
+ }
+
+ if duplicated_tool_names.is_empty() {
+ return context_server_tools
+ .into_iter()
+ .map(|tool| (resolve_tool_name(tool).into(), tool.clone()))
+ .collect();
+ }
+
+ context_server_tools
+ .into_iter()
+ .filter_map(|tool| {
+ let mut tool_name = resolve_tool_name(tool);
+ if !duplicated_tool_names.contains(&tool_name) {
+ return Some((tool_name.into(), tool.clone()));
+ }
+ match tool.source() {
+ ToolSource::Native => {
+ debug_panic!("Expected MCP tool but got a native tool: {}", tool_name);
+ // Built-in tools always keep their original name
+ Some((tool_name.into(), tool.clone()))
+ }
+ ToolSource::ContextServer { id } => {
+ // Context server tools are prefixed with the context server ID, and truncated if necessary
+ tool_name.insert(0, '_');
+ if tool_name.len() + id.len() > MAX_TOOL_NAME_LENGTH {
+ let len = MAX_TOOL_NAME_LENGTH - tool_name.len();
+ let mut id = id.to_string();
+ id.truncate(len);
+ tool_name.insert_str(0, &id);
+ } else {
+ tool_name.insert_str(0, &id);
+ }
+
+ tool_name.truncate(MAX_TOOL_NAME_LENGTH);
+
+ if seen_tool_names.contains(&tool_name) {
+ log::error!("Cannot resolve tool name conflict for tool {}", tool.name());
+ None
+ } else {
+ Some((tool_name.into(), tool.clone()))
+ }
+ }
+ }
+ })
+ .collect()
+}
+#[cfg(test)]
+mod tests {
+ use gpui::{AnyWindowHandle, Entity, Task, TestAppContext};
+ use language_model::{LanguageModel, LanguageModelRequest};
+ use project::Project;
+
+ use crate::{ActionLog, ToolResult};
+
+ use super::*;
+
+ #[gpui::test]
+ fn test_unique_tool_names(cx: &mut TestAppContext) {
+ fn assert_tool(
+ tool_working_set: &ToolWorkingSet,
+ unique_name: &str,
+ expected_name: &str,
+ expected_source: ToolSource,
+ cx: &App,
+ ) {
+ let tool = tool_working_set.tool(unique_name, cx).unwrap();
+ assert_eq!(tool.name(), expected_name);
+ assert_eq!(tool.source(), expected_source);
+ }
+
+ let tool_registry = cx.update(ToolRegistry::default_global);
+ tool_registry.register_tool(TestTool::new("tool1", ToolSource::Native));
+ tool_registry.register_tool(TestTool::new("tool2", ToolSource::Native));
+
+ let mut tool_working_set = ToolWorkingSet::default();
+ cx.update(|cx| {
+ tool_working_set.extend(
+ vec![
+ Arc::new(TestTool::new(
+ "tool2",
+ ToolSource::ContextServer { id: "mcp-1".into() },
+ )) as Arc<dyn Tool>,
+ Arc::new(TestTool::new(
+ "tool2",
+ ToolSource::ContextServer { id: "mcp-2".into() },
+ )) as Arc<dyn Tool>,
+ ]
+ .into_iter(),
+ cx,
+ );
+ });
+
+ cx.update(|cx| {
+ assert_tool(&tool_working_set, "tool1", "tool1", ToolSource::Native, cx);
+ assert_tool(&tool_working_set, "tool2", "tool2", ToolSource::Native, cx);
+ assert_tool(
+ &tool_working_set,
+ "mcp-1_tool2",
+ "tool2",
+ ToolSource::ContextServer { id: "mcp-1".into() },
+ cx,
+ );
+ assert_tool(
+ &tool_working_set,
+ "mcp-2_tool2",
+ "tool2",
+ ToolSource::ContextServer { id: "mcp-2".into() },
+ cx,
+ );
+ })
+ }
+
+ #[gpui::test]
+ fn test_resolve_context_server_tool_name_conflicts() {
+ assert_resolve_context_server_tool_name_conflicts(
+ vec![
+ TestTool::new("tool1", ToolSource::Native),
+ TestTool::new("tool2", ToolSource::Native),
+ ],
+ vec![TestTool::new(
+ "tool3",
+ ToolSource::ContextServer { id: "mcp-1".into() },
+ )],
+ vec!["tool3"],
);
+
+ assert_resolve_context_server_tool_name_conflicts(
+ vec![
+ TestTool::new("tool1", ToolSource::Native),
+ TestTool::new("tool2", ToolSource::Native),
+ ],
+ vec![
+ TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
+ TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
+ ],
+ vec!["mcp-1_tool3", "mcp-2_tool3"],
+ );
+
+ assert_resolve_context_server_tool_name_conflicts(
+ vec![
+ TestTool::new("tool1", ToolSource::Native),
+ TestTool::new("tool2", ToolSource::Native),
+ TestTool::new("tool3", ToolSource::Native),
+ ],
+ vec![
+ TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }),
+ TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }),
+ ],
+ vec!["mcp-1_tool3", "mcp-2_tool3"],
+ );
+
+ // Test deduplication of tools with very long names, in this case the mcp server name should be truncated
+ assert_resolve_context_server_tool_name_conflicts(
+ vec![TestTool::new(
+ "tool-with-very-very-very-long-name",
+ ToolSource::Native,
+ )],
+ vec![TestTool::new(
+ "tool-with-very-very-very-long-name",
+ ToolSource::ContextServer {
+ id: "mcp-with-very-very-very-long-name".into(),
+ },
+ )],
+ vec!["mcp-with-very-very-very-long-_tool-with-very-very-very-long-name"],
+ );
+
+ fn assert_resolve_context_server_tool_name_conflicts(
+ builtin_tools: Vec<TestTool>,
+ context_server_tools: Vec<TestTool>,
+ expected: Vec<&'static str>,
+ ) {
+ let context_server_tools: Vec<Arc<dyn Tool>> = context_server_tools
+ .into_iter()
+ .map(|t| Arc::new(t) as Arc<dyn Tool>)
+ .collect();
+ let builtin_tools: Vec<Arc<dyn Tool>> = builtin_tools
+ .into_iter()
+ .map(|t| Arc::new(t) as Arc<dyn Tool>)
+ .collect();
+ let tools =
+ resolve_context_server_tool_name_conflicts(&context_server_tools, &builtin_tools);
+ assert_eq!(tools.len(), expected.len());
+ for (i, (name, _)) in tools.into_iter().enumerate() {
+ assert_eq!(
+ name.0.as_ref(),
+ expected[i],
+ "Expected '{}' got '{}' at index {}",
+ expected[i],
+ name,
+ i
+ );
+ }
+ }
+ }
+
+ struct TestTool {
+ name: String,
+ source: ToolSource,
+ }
+
+ impl TestTool {
+ fn new(name: impl Into<String>, source: ToolSource) -> Self {
+ Self {
+ name: name.into(),
+ source,
+ }
+ }
+ }
+
+ impl Tool for TestTool {
+ fn name(&self) -> String {
+ self.name.clone()
+ }
+
+ fn icon(&self) -> icons::IconName {
+ icons::IconName::Ai
+ }
+
+ fn may_perform_edits(&self) -> bool {
+ false
+ }
+
+ fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
+ true
+ }
+
+ fn source(&self) -> ToolSource {
+ self.source.clone()
+ }
+
+ fn description(&self) -> String {
+ "Test tool".to_string()
+ }
+
+ fn ui_text(&self, _input: &serde_json::Value) -> String {
+ "Test tool".to_string()
+ }
+
+ fn run(
+ self: Arc<Self>,
+ _input: serde_json::Value,
+ _request: Arc<LanguageModelRequest>,
+ _project: Entity<Project>,
+ _action_log: Entity<ActionLog>,
+ _model: Arc<dyn LanguageModel>,
+ _window: Option<AnyWindowHandle>,
+ _cx: &mut App,
+ ) -> ToolResult {
+ ToolResult {
+ output: Task::ready(Err(anyhow::anyhow!("No content"))),
+ card: None,
+ }
+ }
}
}
@@ -11,6 +11,7 @@ mod list_directory_tool;
mod move_path_tool;
mod now_tool;
mod open_tool;
+mod project_notifications_tool;
mod read_file_tool;
mod schema;
mod templates;
@@ -45,6 +46,7 @@ pub use edit_file_tool::{EditFileMode, EditFileToolInput};
pub use find_path_tool::FindPathToolInput;
pub use grep_tool::{GrepTool, GrepToolInput};
pub use open_tool::OpenTool;
+pub use project_notifications_tool::ProjectNotificationsTool;
pub use read_file_tool::{ReadFileTool, ReadFileToolInput};
pub use terminal_tool::TerminalTool;
@@ -61,6 +63,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(ListDirectoryTool);
registry.register_tool(NowTool);
registry.register_tool(OpenTool);
+ registry.register_tool(ProjectNotificationsTool);
registry.register_tool(FindPathTool);
registry.register_tool(ReadFileTool);
registry.register_tool(GrepTool);
@@ -57,7 +57,7 @@ impl Tool for CopyPathTool {
}
fn icon(&self) -> IconName {
- IconName::Clipboard
+ IconName::ToolCopy
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -46,7 +46,7 @@ impl Tool for CreateDirectoryTool {
}
fn icon(&self) -> IconName {
- IconName::Folder
+ IconName::ToolFolder
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -46,7 +46,7 @@ impl Tool for DeletePathTool {
}
fn icon(&self) -> IconName {
- IconName::FileDelete
+ IconName::ToolDeleteFile
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -59,7 +59,7 @@ impl Tool for DiagnosticsTool {
}
fn icon(&self) -> IconName {
- IconName::XCircle
+ IconName::ToolDiagnostics
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -719,6 +719,7 @@ impl EditAgent {
tools,
stop: Vec::new(),
temperature: None,
+ thinking_allowed: true,
};
Ok(self.model.stream_completion_text(request, cx).await?.stream)
@@ -365,17 +365,23 @@ fn eval_disable_cursor_blinking() {
// Model | Pass rate
// ============================================
//
- // claude-3.7-sonnet | 0.99 (2025-06-14)
- // claude-sonnet-4 | 0.85 (2025-06-14)
- // gemini-2.5-pro-preview-latest | 0.97 (2025-06-16)
- // gemini-2.5-flash-preview-04-17 |
- // gpt-4.1 |
+ // 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(
100,
- 0.95,
+ 0.51,
0.05,
EvalInput::from_conversation(
vec![
@@ -433,11 +439,7 @@ fn eval_disable_cursor_blinking() {
),
],
Some(input_file_content.into()),
- EvalAssertion::judge_diff(indoc! {"
- - Calls to BlinkManager in `observe_window_activation` were commented out
- - The call to `blink_manager.enable` above the call to show_cursor_names was commented out
- - All the edits have valid indentation
- "}),
+ EvalAssertion::assert_diff_any(possible_diffs),
),
);
}
@@ -1263,6 +1265,7 @@ impl EvalAssertion {
content: vec![prompt.into()],
cache: false,
}],
+ thinking_allowed: true,
..Default::default()
};
let mut response = retry_on_rate_limit(async || {
@@ -1599,6 +1602,7 @@ impl EditAgentTest {
let conversation = LanguageModelRequest {
messages,
tools,
+ thinking_allowed: true,
..Default::default()
};
@@ -9132,7 +9132,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
- self.manipulate_immutable_lines(window, cx, |lines| lines.sort())
+ self.manipulate_lines(window, cx, |lines| lines.sort())
}
pub fn sort_lines_case_insensitive(
@@ -9141,7 +9141,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
- self.manipulate_immutable_lines(window, cx, |lines| {
+ self.manipulate_lines(window, cx, |lines| {
lines.sort_by_key(|line| line.to_lowercase())
})
}
@@ -9152,7 +9152,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
- self.manipulate_immutable_lines(window, cx, |lines| {
+ self.manipulate_lines(window, cx, |lines| {
let mut seen = HashSet::default();
lines.retain(|line| seen.insert(line.to_lowercase()));
})
@@ -9164,7 +9164,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
- self.manipulate_immutable_lines(window, cx, |lines| {
+ self.manipulate_lines(window, cx, |lines| {
let mut seen = HashSet::default();
lines.retain(|line| seen.insert(*line));
})
@@ -9606,20 +9606,20 @@ impl Editor {
}
pub fn reverse_lines(&mut self, _: &ReverseLines, window: &mut Window, cx: &mut Context<Self>) {
- self.manipulate_immutable_lines(window, cx, |lines| lines.reverse())
+ self.manipulate_lines(window, cx, |lines| lines.reverse())
}
pub fn shuffle_lines(&mut self, _: &ShuffleLines, window: &mut Window, cx: &mut Context<Self>) {
- self.manipulate_immutable_lines(window, cx, |lines| lines.shuffle(&mut thread_rng()))
+ self.manipulate_lines(window, cx, |lines| lines.shuffle(&mut thread_rng()))
}
- fn manipulate_lines<M>(
+ fn manipulate_lines<Fn>(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
- mut manipulate: M,
+ mut callback: Fn,
) where
- M: FnMut(&str) -> LineManipulationResult,
+ Fn: FnMut(&mut Vec<&str>),
{
self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction);
@@ -9652,14 +9652,18 @@ impl Editor {
.text_for_range(start_point..end_point)
.collect::<String>();
- let LineManipulationResult { new_text, line_count_before, line_count_after} = manipulate(&text);
+ let mut lines = text.split('\n').collect_vec();
- edits.push((start_point..end_point, new_text));
+ 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 + line_count_after.saturating_sub(1) 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,
@@ -9668,10 +9672,10 @@ impl Editor {
reversed: selection.reversed,
});
- if line_count_after > line_count_before {
- added_lines += line_count_after - line_count_before;
- } else if line_count_before > line_count_after {
- removed_lines += line_count_before - line_count_after;
+ if lines_after > lines_before {
+ added_lines += lines_after - lines_before;
+ } else if lines_before > lines_after {
+ removed_lines += lines_before - lines_after;
}
}
@@ -9716,171 +9720,6 @@ impl Editor {
})
}
- fn manipulate_immutable_lines<Fn>(
- &mut self,
- window: &mut Window,
- cx: &mut Context<Self>,
- mut callback: Fn,
- ) where
- Fn: FnMut(&mut Vec<&str>),
- {
- self.manipulate_lines(window, cx, |text| {
- let mut lines: Vec<&str> = text.split('\n').collect();
- let line_count_before = lines.len();
-
- callback(&mut lines);
-
- LineManipulationResult {
- new_text: lines.join("\n"),
- line_count_before,
- line_count_after: lines.len(),
- }
- });
- }
-
- fn manipulate_mutable_lines<Fn>(
- &mut self,
- window: &mut Window,
- cx: &mut Context<Self>,
- mut callback: Fn,
- ) where
- Fn: FnMut(&mut Vec<Cow<'_, str>>),
- {
- self.manipulate_lines(window, cx, |text| {
- let mut lines: Vec<Cow<str>> = text.split('\n').map(Cow::from).collect();
- let line_count_before = lines.len();
-
- callback(&mut lines);
-
- LineManipulationResult {
- new_text: lines.join("\n"),
- line_count_before,
- line_count_after: lines.len(),
- }
- });
- }
-
- pub fn convert_indentation_to_spaces(
- &mut self,
- _: &ConvertIndentationToSpaces,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let settings = self.buffer.read(cx).language_settings(cx);
- let tab_size = settings.tab_size.get() as usize;
-
- self.manipulate_mutable_lines(window, cx, |lines| {
- // Allocates a reasonably sized scratch buffer once for the whole loop
- let mut reindented_line = String::with_capacity(MAX_LINE_LEN);
- // Avoids recomputing spaces that could be inserted many times
- let space_cache: Vec<Vec<char>> = (1..=tab_size)
- .map(|n| IndentSize::spaces(n as u32).chars().collect())
- .collect();
-
- for line in lines.iter_mut().filter(|line| !line.is_empty()) {
- let mut chars = line.as_ref().chars();
- let mut col = 0;
- let mut changed = false;
-
- while let Some(ch) = chars.next() {
- match ch {
- ' ' => {
- reindented_line.push(' ');
- col += 1;
- }
- '\t' => {
- // \t are converted to spaces depending on the current column
- let spaces_len = tab_size - (col % tab_size);
- reindented_line.extend(&space_cache[spaces_len - 1]);
- col += spaces_len;
- changed = true;
- }
- _ => {
- // If we dont append before break, the character is consumed
- reindented_line.push(ch);
- break;
- }
- }
- }
-
- if !changed {
- reindented_line.clear();
- continue;
- }
- // Append the rest of the line and replace old reference with new one
- reindented_line.extend(chars);
- *line = Cow::Owned(reindented_line.clone());
- reindented_line.clear();
- }
- });
- }
-
- pub fn convert_indentation_to_tabs(
- &mut self,
- _: &ConvertIndentationToTabs,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let settings = self.buffer.read(cx).language_settings(cx);
- let tab_size = settings.tab_size.get() as usize;
-
- self.manipulate_mutable_lines(window, cx, |lines| {
- // Allocates a reasonably sized buffer once for the whole loop
- let mut reindented_line = String::with_capacity(MAX_LINE_LEN);
- // Avoids recomputing spaces that could be inserted many times
- let space_cache: Vec<Vec<char>> = (1..=tab_size)
- .map(|n| IndentSize::spaces(n as u32).chars().collect())
- .collect();
-
- for line in lines.iter_mut().filter(|line| !line.is_empty()) {
- let mut chars = line.chars();
- let mut spaces_count = 0;
- let mut first_non_indent_char = None;
- let mut changed = false;
-
- while let Some(ch) = chars.next() {
- match ch {
- ' ' => {
- // Keep track of spaces. Append \t when we reach tab_size
- spaces_count += 1;
- changed = true;
- if spaces_count == tab_size {
- reindented_line.push('\t');
- spaces_count = 0;
- }
- }
- '\t' => {
- reindented_line.push('\t');
- spaces_count = 0;
- }
- _ => {
- // Dont append it yet, we might have remaining spaces
- first_non_indent_char = Some(ch);
- break;
- }
- }
- }
-
- if !changed {
- reindented_line.clear();
- continue;
- }
- // Remaining spaces that didn't make a full tab stop
- if spaces_count > 0 {
- reindented_line.extend(&space_cache[spaces_count - 1]);
- }
- // If we consume an extra character that was not indentation, add it back
- if let Some(extra_char) = first_non_indent_char {
- reindented_line.push(extra_char);
- }
- // Append the rest of the line and replace old reference with new one
- reindented_line.extend(chars);
- *line = Cow::Owned(reindented_line.clone());
- reindented_line.clear();
- }
- });
- }
-
pub fn convert_to_upper_case(
&mut self,
_: &ConvertToUpperCase,
@@ -21318,13 +21157,6 @@ pub struct LineHighlight {
pub type_id: Option<TypeId>,
}
-struct LineManipulationResult {
- pub new_text: String,
- pub line_count_before: usize,
- pub line_count_after: usize,
-}
-
-
fn render_diff_hunk_controls(
row: u32,
status: &DiffHunkStatus,
@@ -0,0 +1,28 @@
+--- 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| {
@@ -0,0 +1,29 @@
+@@ -1778,13 +1778,13 @@
+ cx.observe_global_in::<SettingsStore>(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| {
@@ -0,0 +1,34 @@
+@@ -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::<SettingsStore>(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| {
@@ -0,0 +1,33 @@
+@@ -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::<SettingsStore>(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| {
@@ -4,6 +4,7 @@ use crate::{
schema::json_schema_for,
ui::{COLLAPSED_LINES, ToolOutputPreview},
};
+use agent_settings;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput,
@@ -14,7 +15,7 @@ use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
use futures::StreamExt;
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
- TextStyleRefinement, WeakEntity, pulsating_between, px,
+ TextStyleRefinement, Transformation, WeakEntity, percentage, pulsating_between, px,
};
use indoc::formatdoc;
use language::{
@@ -138,7 +139,7 @@ impl Tool for EditFileTool {
}
fn icon(&self) -> IconName {
- IconName::Pencil
+ IconName::ToolPencil
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -515,7 +516,9 @@ pub struct EditFileToolCard {
impl EditFileToolCard {
pub fn new(path: PathBuf, project: Entity<Project>, window: &mut Window, cx: &mut App) -> Self {
+ let expand_edit_card = agent_settings::AgentSettings::get_global(cx).expand_edit_card;
let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly));
+
let editor = cx.new(|cx| {
let mut editor = Editor::new(
EditorMode::Full {
@@ -556,7 +559,7 @@ impl EditFileToolCard {
diff_task: None,
preview_expanded: true,
error_expanded: None,
- full_height_expanded: true,
+ full_height_expanded: expand_edit_card,
total_lines: None,
}
}
@@ -755,6 +758,13 @@ impl ToolCard for EditFileToolCard {
_ => None,
};
+ let running_or_pending = match status {
+ ToolUseStatus::Running | ToolUseStatus::Pending => Some(()),
+ _ => None,
+ };
+
+ let should_show_loading = running_or_pending.is_some() && !self.full_height_expanded;
+
let path_label_button = h_flex()
.id(("edit-tool-path-label-button", self.editor.entity_id()))
.w_full()
@@ -773,8 +783,8 @@ impl ToolCard for EditFileToolCard {
.child(
h_flex()
.child(
- Icon::new(IconName::Pencil)
- .size(IconSize::XSmall)
+ Icon::new(IconName::ToolPencil)
+ .size(IconSize::Small)
.color(Color::Muted),
)
.child(
@@ -863,6 +873,18 @@ impl ToolCard for EditFileToolCard {
header.bg(codeblock_header_bg)
})
.child(path_label_button)
+ .when(should_show_loading, |header| {
+ header.pr_1p5().child(
+ Icon::new(IconName::ArrowCircle)
+ .size(IconSize::XSmall)
+ .color(Color::Info)
+ .with_animation(
+ "arrow-circle",
+ Animation::new(Duration::from_secs(2)).repeat(),
+ |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+ ),
+ )
+ })
.when_some(error_message, |header, error_message| {
header.child(
h_flex()
@@ -69,10 +69,9 @@ impl FetchTool {
.to_str()
.context("invalid Content-Type header")?;
let content_type = match content_type {
- "text/html" => ContentType::Html,
- "text/plain" => ContentType::Plaintext,
+ "text/html" | "application/xhtml+xml" => ContentType::Html,
"application/json" => ContentType::Json,
- _ => ContentType::Html,
+ _ => ContentType::Plaintext,
};
match content_type {
@@ -130,7 +129,7 @@ impl Tool for FetchTool {
}
fn icon(&self) -> IconName {
- IconName::Globe
+ IconName::ToolWeb
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -68,7 +68,7 @@ impl Tool for FindPathTool {
}
fn icon(&self) -> IconName {
- IconName::SearchCode
+ IconName::ToolSearch
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -313,7 +313,7 @@ impl ToolCard for FindPathToolCard {
.mb_2()
.gap_1()
.child(
- ToolCallCardHeader::new(IconName::SearchCode, matches_label)
+ ToolCallCardHeader::new(IconName::ToolSearch, matches_label)
.with_code_path(&self.glob)
.disclosure_slot(
Disclosure::new("path-search-disclosure", self.expanded)
@@ -70,7 +70,7 @@ impl Tool for GrepTool {
}
fn icon(&self) -> IconName {
- IconName::Regex
+ IconName::ToolRegex
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -58,7 +58,7 @@ impl Tool for ListDirectoryTool {
}
fn icon(&self) -> IconName {
- IconName::Folder
+ IconName::ToolFolder
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -0,0 +1,224 @@
+use crate::schema::json_schema_for;
+use anyhow::Result;
+use assistant_tool::{ActionLog, Tool, ToolResult};
+use gpui::{AnyWindowHandle, App, Entity, Task};
+use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
+use project::Project;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::fmt::Write as _;
+use std::sync::Arc;
+use ui::IconName;
+
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct ProjectUpdatesToolInput {}
+
+pub struct ProjectNotificationsTool;
+
+impl Tool for ProjectNotificationsTool {
+ fn name(&self) -> String {
+ "project_notifications".to_string()
+ }
+
+ fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
+ false
+ }
+ fn may_perform_edits(&self) -> bool {
+ false
+ }
+ fn description(&self) -> String {
+ include_str!("./project_notifications_tool/description.md").to_string()
+ }
+
+ fn icon(&self) -> IconName {
+ IconName::ToolNotification
+ }
+
+ fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
+ json_schema_for::<ProjectUpdatesToolInput>(format)
+ }
+
+ fn ui_text(&self, _input: &serde_json::Value) -> String {
+ "Check project notifications".into()
+ }
+
+ fn run(
+ self: Arc<Self>,
+ _input: serde_json::Value,
+ _request: Arc<LanguageModelRequest>,
+ _project: Entity<Project>,
+ action_log: Entity<ActionLog>,
+ _model: Arc<dyn LanguageModel>,
+ _window: Option<AnyWindowHandle>,
+ cx: &mut App,
+ ) -> ToolResult {
+ let mut stale_files = String::new();
+ let mut notified_buffers = Vec::new();
+
+ for stale_file in action_log.read(cx).unnotified_stale_buffers(cx) {
+ if let Some(file) = stale_file.read(cx).file() {
+ writeln!(&mut stale_files, "- {}", file.path().display()).ok();
+ notified_buffers.push(stale_file.clone());
+ }
+ }
+
+ if !notified_buffers.is_empty() {
+ action_log.update(cx, |log, cx| {
+ log.mark_buffers_as_notified(notified_buffers, cx);
+ });
+ }
+
+ let response = if stale_files.is_empty() {
+ "No new notifications".to_string()
+ } else {
+ // NOTE: Changes to this prompt require a symmetric update in the LLM Worker
+ const HEADER: &str = include_str!("./project_notifications_tool/prompt_header.txt");
+ format!("{HEADER}{stale_files}").replace("\r\n", "\n")
+ };
+
+ Task::ready(Ok(response.into())).into()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use assistant_tool::ToolResultContent;
+ use gpui::{AppContext, TestAppContext};
+ use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModelProvider};
+ use project::{FakeFs, Project};
+ use serde_json::json;
+ use settings::SettingsStore;
+ use std::sync::Arc;
+ use util::path;
+
+ #[gpui::test]
+ async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/test"),
+ json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
+ let action_log = cx.new(|_| ActionLog::new(project.clone()));
+
+ let buffer_path = project
+ .read_with(cx, |project, cx| {
+ project.find_project_path("test/code.rs", cx)
+ })
+ .unwrap();
+
+ let buffer = project
+ .update(cx, |project, cx| {
+ project.open_buffer(buffer_path.clone(), cx)
+ })
+ .await
+ .unwrap();
+
+ // Start tracking the buffer
+ action_log.update(cx, |log, cx| {
+ log.buffer_read(buffer.clone(), cx);
+ });
+
+ // Run the tool before any changes
+ let tool = Arc::new(ProjectNotificationsTool);
+ let provider = Arc::new(FakeLanguageModelProvider);
+ let model: Arc<dyn LanguageModel> = Arc::new(provider.test_model());
+ let request = Arc::new(LanguageModelRequest::default());
+ let tool_input = json!({});
+
+ let result = cx.update(|cx| {
+ tool.clone().run(
+ tool_input.clone(),
+ request.clone(),
+ project.clone(),
+ action_log.clone(),
+ model.clone(),
+ None,
+ cx,
+ )
+ });
+
+ let response = result.output.await.unwrap();
+ let response_text = match &response.content {
+ ToolResultContent::Text(text) => text.clone(),
+ _ => panic!("Expected text response"),
+ };
+ assert_eq!(
+ response_text.as_str(),
+ "No new notifications",
+ "Tool should return 'No new notifications' when no stale buffers"
+ );
+
+ // Modify the buffer (makes it stale)
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit([(1..1, "\nChange!\n")], None, cx);
+ });
+
+ // Run the tool again
+ let result = cx.update(|cx| {
+ tool.clone().run(
+ tool_input.clone(),
+ request.clone(),
+ project.clone(),
+ action_log.clone(),
+ model.clone(),
+ None,
+ cx,
+ )
+ });
+
+ // This time the buffer is stale, so the tool should return a notification
+ let response = result.output.await.unwrap();
+ let response_text = match &response.content {
+ ToolResultContent::Text(text) => text.clone(),
+ _ => panic!("Expected text response"),
+ };
+
+ let expected_content = "[The following is an auto-generated notification; do not reply]\n\nThese files have changed since the last read:\n- code.rs\n";
+ assert_eq!(
+ response_text.as_str(),
+ expected_content,
+ "Tool should return the stale buffer notification"
+ );
+
+ // Run the tool once more without any changes - should get no new notifications
+ let result = cx.update(|cx| {
+ tool.run(
+ tool_input.clone(),
+ request.clone(),
+ project.clone(),
+ action_log,
+ model.clone(),
+ None,
+ cx,
+ )
+ });
+
+ let response = result.output.await.unwrap();
+ let response_text = match &response.content {
+ ToolResultContent::Text(text) => text.clone(),
+ _ => panic!("Expected text response"),
+ };
+
+ assert_eq!(
+ response_text.as_str(),
+ "No new notifications",
+ "Tool should return 'No new notifications' when running again without changes"
+ );
+ }
+
+ 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);
+ assistant_tool::init(cx);
+ });
+ }
+}
@@ -0,0 +1,3 @@
+This tool reports which files have been modified by the user since the agent last accessed them.
+
+It serves as a notification mechanism to inform the agent of recent changes. No immediate action is required in response to these updates.
@@ -0,0 +1,3 @@
+[The following is an auto-generated notification; do not reply]
+
+These files have changed since the last read:
@@ -18,7 +18,6 @@ use serde::{Deserialize, Serialize};
use settings::Settings;
use std::sync::Arc;
use ui::IconName;
-use util::markdown::MarkdownInlineCode;
/// If the model requests to read a file whose size exceeds this, then
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -68,7 +67,7 @@ impl Tool for ReadFileTool {
}
fn icon(&self) -> IconName {
- IconName::FileSearch
+ IconName::ToolRead
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -78,11 +77,21 @@ impl Tool for ReadFileTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ReadFileToolInput>(input.clone()) {
Ok(input) => {
- let path = MarkdownInlineCode(&input.path);
+ let path = &input.path;
match (input.start_line, input.end_line) {
- (Some(start), None) => format!("Read file {path} (from line {start})"),
- (Some(start), Some(end)) => format!("Read file {path} (lines {start}-{end})"),
- _ => format!("Read file {path}"),
+ (Some(start), Some(end)) => {
+ format!(
+ "[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))",
+ path, start, end, path, start, end
+ )
+ }
+ (Some(start), None) => {
+ format!(
+ "[Read file `{}` (from line {})](@selection:{}:({}-{}))",
+ path, start, path, start, start
+ )
+ }
+ _ => format!("[Read file `{}`](@file:{})", path, path),
}
}
Err(_) => "Read file".to_string(),
@@ -25,9 +25,7 @@ fn schema_to_json(
fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
let mut generator = match format {
LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
- // TODO: Gemini docs mention using a subset of OpenAPI 3, so this may benefit from using
- // `SchemaSettings::openapi3()`.
- LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::draft07()
+ LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3()
.with(|settings| {
settings.meta_schema = None;
settings.inline_subschemas = true;
@@ -2,12 +2,13 @@ use crate::{
schema::json_schema_for,
ui::{COLLAPSED_LINES, ToolOutputPreview},
};
+use agent_settings;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{FutureExt as _, future::Shared};
use gpui::{
- AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement,
- WeakEntity, Window,
+ Animation, AnimationExt, AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task,
+ TextStyleRefinement, Transformation, WeakEntity, Window, percentage,
};
use language::LineEnding;
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
@@ -89,7 +90,7 @@ impl Tool for TerminalTool {
}
fn icon(&self) -> IconName {
- IconName::Terminal
+ IconName::ToolTerminal
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -218,7 +219,7 @@ impl Tool for TerminalTool {
.update(cx, |project, cx| {
project.create_terminal(
TerminalKind::Task(task::SpawnInTerminal {
- command: program,
+ command: Some(program),
args,
cwd,
env,
@@ -247,6 +248,7 @@ impl Tool for TerminalTool {
command_markdown.clone(),
working_dir.clone(),
cx.entity_id(),
+ cx,
)
});
@@ -441,7 +443,10 @@ impl TerminalToolCard {
input_command: Entity<Markdown>,
working_dir: Option<PathBuf>,
entity_id: EntityId,
+ cx: &mut Context<Self>,
) -> Self {
+ let expand_terminal_card =
+ agent_settings::AgentSettings::get_global(cx).expand_terminal_card;
Self {
input_command,
working_dir,
@@ -453,7 +458,7 @@ impl TerminalToolCard {
finished_with_empty_output: false,
original_content_len: 0,
content_line_count: 0,
- preview_expanded: true,
+ preview_expanded: expand_terminal_card,
start_instant: Instant::now(),
elapsed_time: None,
}
@@ -518,6 +523,46 @@ impl ToolCard for TerminalToolCard {
.color(Color::Muted),
),
)
+ .when(!self.command_finished, |header| {
+ header.child(
+ Icon::new(IconName::ArrowCircle)
+ .size(IconSize::XSmall)
+ .color(Color::Info)
+ .with_animation(
+ "arrow-circle",
+ Animation::new(Duration::from_secs(2)).repeat(),
+ |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+ ),
+ )
+ })
+ .when(tool_failed || command_failed, |header| {
+ header.child(
+ div()
+ .id(("terminal-tool-error-code-indicator", self.entity_id))
+ .child(
+ Icon::new(IconName::Close)
+ .size(IconSize::Small)
+ .color(Color::Error),
+ )
+ .when(command_failed && self.exit_status.is_some(), |this| {
+ this.tooltip(Tooltip::text(format!(
+ "Exited with code {}",
+ self.exit_status
+ .and_then(|status| status.code())
+ .unwrap_or(-1),
+ )))
+ })
+ .when(
+ !command_failed && tool_failed && status.error().is_some(),
+ |this| {
+ this.tooltip(Tooltip::text(format!(
+ "Error: {}",
+ status.error().unwrap(),
+ )))
+ },
+ ),
+ )
+ })
.when(self.was_content_truncated, |header| {
let tooltip = if self.content_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
"Output exceeded terminal max lines and was \
@@ -555,34 +600,6 @@ impl ToolCard for TerminalToolCard {
.size(LabelSize::Small),
)
})
- .when(tool_failed || command_failed, |header| {
- header.child(
- div()
- .id(("terminal-tool-error-code-indicator", self.entity_id))
- .child(
- Icon::new(IconName::Close)
- .size(IconSize::Small)
- .color(Color::Error),
- )
- .when(command_failed && self.exit_status.is_some(), |this| {
- this.tooltip(Tooltip::text(format!(
- "Exited with code {}",
- self.exit_status
- .and_then(|status| status.code())
- .unwrap_or(-1),
- )))
- })
- .when(
- !command_failed && tool_failed && status.error().is_some(),
- |this| {
- this.tooltip(Tooltip::text(format!(
- "Error: {}",
- status.error().unwrap(),
- )))
- },
- ),
- )
- })
.when(!self.finished_with_empty_output, |header| {
header.child(
Disclosure::new(
@@ -634,6 +651,7 @@ impl ToolCard for TerminalToolCard {
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()
@@ -37,7 +37,7 @@ impl Tool for ThinkingTool {
}
fn icon(&self) -> IconName {
- IconName::LightBulb
+ IconName::ToolBulb
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -82,7 +82,7 @@ impl RenderOnce for ToolCallCardHeader {
.child(
h_flex().h(line_height).justify_center().child(
Icon::new(self.icon)
- .size(IconSize::XSmall)
+ .size(IconSize::Small)
.color(Color::Muted),
),
)
@@ -143,6 +143,8 @@ impl ToolCard for WebSearchToolCard {
_workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement {
+ let icon = IconName::ToolWeb;
+
let header = match self.response.as_ref() {
Some(Ok(response)) => {
let text: SharedString = if response.results.len() == 1 {
@@ -150,13 +152,12 @@ impl ToolCard for WebSearchToolCard {
} else {
format!("{} results", response.results.len()).into()
};
- ToolCallCardHeader::new(IconName::Globe, "Searched the Web")
- .with_secondary_text(text)
+ ToolCallCardHeader::new(icon, "Searched the Web").with_secondary_text(text)
}
Some(Err(error)) => {
- ToolCallCardHeader::new(IconName::Globe, "Web Search").with_error(error.to_string())
+ ToolCallCardHeader::new(icon, "Web Search").with_error(error.to_string())
}
- None => ToolCallCardHeader::new(IconName::Globe, "Searching the Web").loading(),
+ None => ToolCallCardHeader::new(icon, "Searching the Web").loading(),
};
let content = self.response.as_ref().and_then(|response| match response {
@@ -28,7 +28,17 @@ use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
-actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes,]);
+actions!(
+ auto_update,
+ [
+ /// Checks for available updates.
+ Check,
+ /// Dismisses the update error message.
+ DismissErrorMessage,
+ /// Opens the release notes for the current version in a browser.
+ ViewReleaseNotes,
+ ]
+);
#[derive(Serialize)]
struct UpdateRequestBody {
@@ -628,7 +638,7 @@ impl AutoUpdater {
let filename = match OS {
"macos" => anyhow::Ok("Zed.dmg"),
"linux" => Ok("zed.tar.gz"),
- "windows" => Ok("ZedUpdateInstaller.exe"),
+ "windows" => Ok("zed_editor_installer.exe"),
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
}?;
@@ -12,7 +12,13 @@ use workspace::Workspace;
use workspace::notifications::simple_message_notification::MessageNotification;
use workspace::notifications::{NotificationId, show_app_notification};
-actions!(auto_update, [ViewReleaseNotesLocally]);
+actions!(
+ auto_update,
+ [
+ /// Opens the release notes for the current version in a new tab.
+ ViewReleaseNotesLocally
+ ]
+);
pub fn init(cx: &mut App) {
notify_if_app_was_updated(cx);
@@ -29,7 +29,7 @@ client.workspace = true
collections.workspace = true
fs.workspace = true
futures.workspace = true
-gpui.workspace = true
+gpui = { workspace = true, features = ["screen-capture"] }
language.workspace = true
log.workspace = true
postage.workspace = true
@@ -1389,10 +1389,17 @@ impl Room {
let sources = cx.screen_capture_sources();
cx.spawn(async move |this, cx| {
- let sources = sources.await??;
- let source = sources.first().context("no display found")?;
-
- let publication = participant.publish_screenshare_track(&**source, cx).await;
+ let sources = sources
+ .await
+ .map_err(|error| error.into())
+ .and_then(|sources| sources);
+ let source =
+ sources.and_then(|sources| sources.into_iter().next().context("no display found"));
+
+ let publication = match source {
+ Ok(source) => participant.publish_screenshare_track(&*source, cx).await,
+ Err(error) => Err(error),
+ };
this.update(cx, |this, cx| {
let live_kit = this
@@ -130,6 +130,13 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
}
fn main() -> Result<()> {
+ #[cfg(all(not(debug_assertions), target_os = "windows"))]
+ unsafe {
+ use ::windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole};
+
+ let _ = AttachConsole(ATTACH_PARENT_PROCESS);
+ }
+
#[cfg(unix)]
util::prevent_root_execution();
@@ -308,19 +315,19 @@ fn main() -> Result<()> {
});
let stdin_pipe_handle: Option<JoinHandle<anyhow::Result<()>>> =
- stdin_tmp_file.map(|tmp_file| {
+ stdin_tmp_file.map(|mut tmp_file| {
thread::spawn(move || {
- let stdin = std::io::stdin().lock();
- if io::IsTerminal::is_terminal(&stdin) {
- return Ok(());
+ let mut stdin = std::io::stdin().lock();
+ if !io::IsTerminal::is_terminal(&stdin) {
+ io::copy(&mut stdin, &mut tmp_file)?;
}
- return pipe_to_tmp(stdin, tmp_file);
+ Ok(())
})
});
- let anonymous_fd_pipe_handles: Vec<JoinHandle<anyhow::Result<()>>> = anonymous_fd_tmp_files
+ let anonymous_fd_pipe_handles: Vec<_> = anonymous_fd_tmp_files
.into_iter()
- .map(|(file, tmp_file)| thread::spawn(move || pipe_to_tmp(file, tmp_file)))
+ .map(|(mut file, mut tmp_file)| thread::spawn(move || io::copy(&mut file, &mut tmp_file)))
.collect();
if args.foreground {
@@ -342,22 +349,6 @@ fn main() -> Result<()> {
Ok(())
}
-fn pipe_to_tmp(mut src: impl io::Read, mut dest: fs::File) -> Result<()> {
- let mut buffer = [0; 8 * 1024];
- loop {
- let bytes_read = match src.read(&mut buffer) {
- Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
- res => res?,
- };
- if bytes_read == 0 {
- break;
- }
- io::Write::write_all(&mut dest, &buffer[..bytes_read])?;
- }
- io::Write::flush(&mut dest)?;
- Ok(())
-}
-
fn anonymous_fd(path: &str) -> Option<fs::File> {
#[cfg(target_os = "linux")]
{
@@ -81,7 +81,17 @@ pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500);
pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(10);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20);
-actions!(client, [SignIn, SignOut, Reconnect]);
+actions!(
+ client,
+ [
+ /// Signs in to Zed account.
+ SignIn,
+ /// Signs out of Zed account.
+ SignOut,
+ /// Reconnects to the collaboration server.
+ Reconnect
+ ]
+);
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ClientSettingsContent {
@@ -35,6 +35,7 @@ dashmap.workspace = true
derive_more.workspace = true
envy = "0.4.2"
futures.workspace = true
+gpui = { workspace = true, features = ["screen-capture"] }
hex.workspace = true
http_client.workspace = true
jsonwebtoken.workspace = true
@@ -93,6 +94,7 @@ context_server.workspace = true
ctor.workspace = true
dap = { workspace = true, features = ["test-support"] }
dap_adapters = { workspace = true, features = ["test-support"] }
+dap-types.workspace = true
debugger_ui = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
extension.workspace = true
@@ -26,7 +26,7 @@ CREATE UNIQUE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id"
CREATE TABLE "access_tokens" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
- "user_id" INTEGER REFERENCES users (id),
+ "user_id" INTEGER REFERENCES users (id) ON DELETE CASCADE,
"impersonated_user_id" INTEGER REFERENCES users (id),
"hash" VARCHAR(128)
);
@@ -107,7 +107,7 @@ CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("proj
CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id");
CREATE TABLE "project_repositories" (
- "project_id" INTEGER NOT NULL,
+ "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"abs_path" VARCHAR,
"id" INTEGER NOT NULL,
"entry_ids" VARCHAR,
@@ -124,7 +124,7 @@ CREATE TABLE "project_repositories" (
CREATE INDEX "index_project_repositories_on_project_id" ON "project_repositories" ("project_id");
CREATE TABLE "project_repository_statuses" (
- "project_id" INTEGER NOT NULL,
+ "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"repository_id" INTEGER NOT NULL,
"repo_path" VARCHAR NOT NULL,
"status" INT8 NOT NULL,
@@ -0,0 +1,25 @@
+DELETE FROM project_repositories
+WHERE project_id NOT IN (SELECT id FROM projects);
+
+ALTER TABLE project_repositories
+ ADD CONSTRAINT fk_project_repositories_project_id
+ FOREIGN KEY (project_id)
+ REFERENCES projects (id)
+ ON DELETE CASCADE
+ NOT VALID;
+
+ALTER TABLE project_repositories
+ VALIDATE CONSTRAINT fk_project_repositories_project_id;
+
+DELETE FROM project_repository_statuses
+WHERE project_id NOT IN (SELECT id FROM projects);
+
+ALTER TABLE project_repository_statuses
+ ADD CONSTRAINT fk_project_repository_statuses_project_id
+ FOREIGN KEY (project_id)
+ REFERENCES projects (id)
+ ON DELETE CASCADE
+ NOT VALID;
+
+ALTER TABLE project_repository_statuses
+ VALIDATE CONSTRAINT fk_project_repository_statuses_project_id;
@@ -0,0 +1,3 @@
+ALTER TABLE access_tokens DROP CONSTRAINT access_tokens_user_id_fkey;
+ALTER TABLE access_tokens ADD CONSTRAINT access_tokens_user_id_fkey
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
@@ -1,12 +1,33 @@
{
"admins": [
"nathansobo",
- "as-cii",
"maxbrunsfeld",
- "iamnbutler",
- "mikayla-maki",
+ "as-cii",
"JosephTLyons",
- "rgbkrk"
+ "maxdeviant",
+ "SomeoneToIgnore",
+ "mikayla-maki",
+ "agu-z",
+ "osiewicz",
+ "ConradIrwin",
+ "benbrandt",
+ "bennetbo",
+ "smitbarmase",
+ "notpeter",
+ "rgbkrk",
+ "JunkuiZhang",
+ "Anthony-Eid",
+ "rtfeldman",
+ "danilo-leal",
+ "MrSubidubi",
+ "cole-miller",
+ "osyvokon",
+ "probably-neb",
+ "mgsloan",
+ "P1n3appl3",
+ "mslzed",
+ "franciskafyi",
+ "katie-z-geer"
],
"channels": ["zed"]
}
@@ -5,7 +5,7 @@ use axum::{
routing::{get, post},
};
use chrono::{DateTime, SecondsFormat, Utc};
-use collections::HashSet;
+use collections::{HashMap, HashSet};
use reqwest::StatusCode;
use sea_orm::ActiveValue;
use serde::{Deserialize, Serialize};
@@ -21,12 +21,13 @@ use stripe::{
PaymentMethod, Subscription, SubscriptionId, SubscriptionStatus,
};
use util::{ResultExt, maybe};
+use zed_llm_client::LanguageModelProvider;
use crate::api::events::SnowflakeRow;
use crate::db::billing_subscription::{
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
};
-use crate::llm::db::subscription_usage_meter::CompletionMode;
+use crate::llm::db::subscription_usage_meter::{self, CompletionMode};
use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND};
use crate::rpc::{ResultExt as _, Server};
use crate::stripe_client::{
@@ -1416,18 +1417,21 @@ async fn sync_model_request_usage_with_stripe(
let usage_meters = llm_db
.get_current_subscription_usage_meters(Utc::now())
.await?;
- let usage_meters = usage_meters
- .into_iter()
- .filter(|(_, usage)| !staff_user_ids.contains(&usage.user_id))
- .collect::<Vec<_>>();
- let user_ids = usage_meters
- .iter()
- .map(|(_, usage)| usage.user_id)
- .collect::<HashSet<UserId>>();
- let billing_subscriptions = app
- .db
- .get_active_zed_pro_billing_subscriptions(user_ids)
- .await?;
+ let mut usage_meters_by_user_id =
+ HashMap::<UserId, Vec<subscription_usage_meter::Model>>::default();
+ for (usage_meter, usage) in usage_meters {
+ let meters = usage_meters_by_user_id.entry(usage.user_id).or_default();
+ meters.push(usage_meter);
+ }
+
+ log::info!("Stripe usage sync: Retrieving Zed Pro subscriptions");
+ let get_zed_pro_subscriptions_started_at = Utc::now();
+ let billing_subscriptions = app.db.get_active_zed_pro_billing_subscriptions().await?;
+ log::info!(
+ "Stripe usage sync: Retrieved {} Zed Pro subscriptions in {}",
+ billing_subscriptions.len(),
+ Utc::now() - get_zed_pro_subscriptions_started_at
+ );
let claude_sonnet_4 = stripe_billing
.find_price_by_lookup_key("claude-sonnet-4-requests")
@@ -1451,59 +1455,90 @@ async fn sync_model_request_usage_with_stripe(
.find_price_by_lookup_key("claude-3-7-sonnet-requests-max")
.await?;
- let usage_meter_count = usage_meters.len();
+ let model_mode_combinations = [
+ ("claude-opus-4", CompletionMode::Max),
+ ("claude-opus-4", CompletionMode::Normal),
+ ("claude-sonnet-4", CompletionMode::Max),
+ ("claude-sonnet-4", CompletionMode::Normal),
+ ("claude-3-7-sonnet", CompletionMode::Max),
+ ("claude-3-7-sonnet", CompletionMode::Normal),
+ ("claude-3-5-sonnet", CompletionMode::Normal),
+ ];
- log::info!("Stripe usage sync: Syncing {usage_meter_count} usage meters");
+ let billing_subscription_count = billing_subscriptions.len();
- for (usage_meter, usage) in usage_meters {
+ log::info!("Stripe usage sync: Syncing {billing_subscription_count} Zed Pro subscriptions");
+
+ for (user_id, (billing_customer, billing_subscription)) in billing_subscriptions {
maybe!(async {
- let Some((billing_customer, billing_subscription)) =
- billing_subscriptions.get(&usage.user_id)
- else {
- bail!(
- "Attempted to sync usage meter for user who is not a Stripe customer: {}",
- usage.user_id
- );
- };
+ if staff_user_ids.contains(&user_id) {
+ return anyhow::Ok(());
+ }
let stripe_customer_id =
StripeCustomerId(billing_customer.stripe_customer_id.clone().into());
let stripe_subscription_id =
StripeSubscriptionId(billing_subscription.stripe_subscription_id.clone().into());
- let model = llm_db.model_by_id(usage_meter.model_id)?;
+ let usage_meters = usage_meters_by_user_id.get(&user_id);
- let (price, meter_event_name) = match model.name.as_str() {
- "claude-opus-4" => match usage_meter.mode {
- CompletionMode::Normal => (&claude_opus_4, "claude_opus_4/requests"),
- CompletionMode::Max => (&claude_opus_4_max, "claude_opus_4/requests/max"),
- },
- "claude-sonnet-4" => match usage_meter.mode {
- CompletionMode::Normal => (&claude_sonnet_4, "claude_sonnet_4/requests"),
- CompletionMode::Max => (&claude_sonnet_4_max, "claude_sonnet_4/requests/max"),
- },
- "claude-3-5-sonnet" => (&claude_3_5_sonnet, "claude_3_5_sonnet/requests"),
- "claude-3-7-sonnet" => match usage_meter.mode {
- CompletionMode::Normal => (&claude_3_7_sonnet, "claude_3_7_sonnet/requests"),
- CompletionMode::Max => {
- (&claude_3_7_sonnet_max, "claude_3_7_sonnet/requests/max")
+ for (model, mode) in &model_mode_combinations {
+ let Ok(model) =
+ llm_db.model(LanguageModelProvider::Anthropic, model)
+ else {
+ log::warn!("Failed to load model for user {user_id}: {model}");
+ continue;
+ };
+
+ let (price, meter_event_name) = match model.name.as_str() {
+ "claude-opus-4" => match mode {
+ CompletionMode::Normal => (&claude_opus_4, "claude_opus_4/requests"),
+ CompletionMode::Max => (&claude_opus_4_max, "claude_opus_4/requests/max"),
+ },
+ "claude-sonnet-4" => match mode {
+ CompletionMode::Normal => (&claude_sonnet_4, "claude_sonnet_4/requests"),
+ CompletionMode::Max => {
+ (&claude_sonnet_4_max, "claude_sonnet_4/requests/max")
+ }
+ },
+ "claude-3-5-sonnet" => (&claude_3_5_sonnet, "claude_3_5_sonnet/requests"),
+ "claude-3-7-sonnet" => match mode {
+ CompletionMode::Normal => {
+ (&claude_3_7_sonnet, "claude_3_7_sonnet/requests")
+ }
+ CompletionMode::Max => {
+ (&claude_3_7_sonnet_max, "claude_3_7_sonnet/requests/max")
+ }
+ },
+ model_name => {
+ bail!("Attempted to sync usage meter for unsupported model: {model_name:?}")
}
- },
- model_name => {
- bail!("Attempted to sync usage meter for unsupported model: {model_name:?}")
+ };
+
+ let model_requests = usage_meters
+ .and_then(|usage_meters| {
+ usage_meters
+ .iter()
+ .find(|meter| meter.model_id == model.id && meter.mode == *mode)
+ })
+ .map(|usage_meter| usage_meter.requests)
+ .unwrap_or(0);
+
+ if model_requests > 0 {
+ stripe_billing
+ .subscribe_to_price(&stripe_subscription_id, price)
+ .await?;
}
- };
- stripe_billing
- .subscribe_to_price(&stripe_subscription_id, price)
- .await?;
- stripe_billing
- .bill_model_request_usage(
- &stripe_customer_id,
- meter_event_name,
- usage_meter.requests,
- )
- .await?;
+ stripe_billing
+ .bill_model_request_usage(&stripe_customer_id, meter_event_name, model_requests)
+ .await
+ .with_context(|| {
+ format!(
+ "Failed to bill model request usage of {model_requests} for {stripe_customer_id}: {meter_event_name}",
+ )
+ })?;
+ }
Ok(())
})
@@ -1512,7 +1547,7 @@ async fn sync_model_request_usage_with_stripe(
}
log::info!(
- "Stripe usage sync: Synced {usage_meter_count} usage meters in {:?}",
+ "Stripe usage sync: Synced {billing_subscription_count} Zed Pro subscriptions in {}",
Utc::now() - started_at
);
@@ -199,6 +199,33 @@ impl Database {
pub async fn get_active_zed_pro_billing_subscriptions(
&self,
+ ) -> Result<HashMap<UserId, (billing_customer::Model, billing_subscription::Model)>> {
+ self.transaction(|tx| async move {
+ let mut rows = billing_subscription::Entity::find()
+ .inner_join(billing_customer::Entity)
+ .select_also(billing_customer::Entity)
+ .filter(
+ billing_subscription::Column::StripeSubscriptionStatus
+ .eq(StripeSubscriptionStatus::Active),
+ )
+ .filter(billing_subscription::Column::Kind.eq(SubscriptionKind::ZedPro))
+ .order_by_asc(billing_subscription::Column::Id)
+ .stream(&*tx)
+ .await?;
+
+ let mut subscriptions = HashMap::default();
+ while let Some(row) = rows.next().await {
+ if let (subscription, Some(customer)) = row? {
+ subscriptions.insert(customer.user_id, (customer, subscription));
+ }
+ }
+ Ok(subscriptions)
+ })
+ .await
+ }
+
+ pub async fn get_active_zed_pro_billing_subscriptions_for_users(
+ &self,
user_ids: HashSet<UserId>,
) -> Result<HashMap<UserId, (billing_customer::Model, billing_subscription::Model)>> {
self.transaction(|tx| {
@@ -142,6 +142,50 @@ impl Database {
}
}
+ loop {
+ let delete_query = Query::delete()
+ .from_table(project_repository_statuses::Entity)
+ .and_where(
+ Expr::tuple([Expr::col((
+ project_repository_statuses::Entity,
+ project_repository_statuses::Column::ProjectId,
+ ))
+ .into()])
+ .in_subquery(
+ Query::select()
+ .columns([(
+ project_repository_statuses::Entity,
+ project_repository_statuses::Column::ProjectId,
+ )])
+ .from(project_repository_statuses::Entity)
+ .inner_join(
+ project::Entity,
+ Expr::col((project::Entity, project::Column::Id)).equals((
+ project_repository_statuses::Entity,
+ project_repository_statuses::Column::ProjectId,
+ )),
+ )
+ .and_where(project::Column::HostConnectionServerId.ne(server_id))
+ .limit(10000)
+ .to_owned(),
+ ),
+ )
+ .to_owned();
+
+ let statement = Statement::from_sql_and_values(
+ tx.get_database_backend(),
+ delete_query
+ .to_string(sea_orm::sea_query::PostgresQueryBuilder)
+ .as_str(),
+ vec![],
+ );
+
+ let result = tx.execute(statement).await?;
+ if result.rows_affected() == 0 {
+ break;
+ }
+ }
+
Ok(())
})
.await
@@ -44,3 +44,53 @@ async fn test_accepted_tos(db: &Arc<Database>) {
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
assert!(user.accepted_tos_at.is_none());
}
+
+test_both_dbs!(
+ test_destroy_user_cascade_deletes_access_tokens,
+ test_destroy_user_cascade_deletes_access_tokens_postgres,
+ test_destroy_user_cascade_deletes_access_tokens_sqlite
+);
+
+async fn test_destroy_user_cascade_deletes_access_tokens(db: &Arc<Database>) {
+ let user_id = db
+ .create_user(
+ "user1@example.com",
+ Some("user1"),
+ false,
+ NewUserParams {
+ github_login: "user1".to_string(),
+ github_user_id: 12345,
+ },
+ )
+ .await
+ .unwrap()
+ .user_id;
+
+ let user = db.get_user_by_id(user_id).await.unwrap();
+ assert!(user.is_some());
+
+ let token_1_id = db
+ .create_access_token(user_id, None, "token-1", 10)
+ .await
+ .unwrap();
+
+ let token_2_id = db
+ .create_access_token(user_id, None, "token-2", 10)
+ .await
+ .unwrap();
+
+ let token_1 = db.get_access_token(token_1_id).await;
+ let token_2 = db.get_access_token(token_2_id).await;
+ assert!(token_1.is_ok());
+ assert!(token_2.is_ok());
+
+ db.destroy_user(user_id).await.unwrap();
+
+ let user = db.get_user_by_id(user_id).await.unwrap();
+ assert!(user.is_none());
+
+ let token_1 = db.get_access_token(token_1_id).await;
+ let token_2 = db.get_access_token(token_2_id).await;
+ assert!(token_1.is_err());
+ assert!(token_2.is_err());
+}
@@ -2836,60 +2836,115 @@ async fn make_update_user_plan_message(
account_too_young: Some(account_too_young),
has_overdue_invoices: billing_customer
.map(|billing_customer| billing_customer.has_overdue_invoices),
- usage: usage.map(|usage| {
- let plan = match plan {
- proto::Plan::Free => zed_llm_client::Plan::ZedFree,
- proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
- proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
+ usage: Some(
+ usage
+ .map(|usage| subscription_usage_to_proto(plan, usage, &feature_flags))
+ .unwrap_or_else(|| make_default_subscription_usage(plan, &feature_flags)),
+ ),
+ })
+}
+
+fn model_requests_limit(
+ plan: zed_llm_client::Plan,
+ feature_flags: &Vec<String>,
+) -> zed_llm_client::UsageLimit {
+ match plan.model_requests_limit() {
+ zed_llm_client::UsageLimit::Limited(limit) => {
+ let limit = if plan == zed_llm_client::Plan::ZedProTrial
+ && feature_flags
+ .iter()
+ .any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG)
+ {
+ 1_000
+ } else {
+ limit
};
- let model_requests_limit = match plan.model_requests_limit() {
- zed_llm_client::UsageLimit::Limited(limit) => {
- let limit = if plan == zed_llm_client::Plan::ZedProTrial
- && feature_flags
- .iter()
- .any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG)
- {
- 1_000
- } else {
- limit
- };
+ zed_llm_client::UsageLimit::Limited(limit)
+ }
+ zed_llm_client::UsageLimit::Unlimited => zed_llm_client::UsageLimit::Unlimited,
+ }
+}
- zed_llm_client::UsageLimit::Limited(limit)
+fn subscription_usage_to_proto(
+ plan: proto::Plan,
+ usage: crate::llm::db::subscription_usage::Model,
+ feature_flags: &Vec<String>,
+) -> proto::SubscriptionUsage {
+ let plan = match plan {
+ proto::Plan::Free => zed_llm_client::Plan::ZedFree,
+ proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
+ proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
+ };
+
+ proto::SubscriptionUsage {
+ model_requests_usage_amount: usage.model_requests as u32,
+ model_requests_usage_limit: Some(proto::UsageLimit {
+ variant: Some(match model_requests_limit(plan, feature_flags) {
+ zed_llm_client::UsageLimit::Limited(limit) => {
+ proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
+ limit: limit as u32,
+ })
}
- zed_llm_client::UsageLimit::Unlimited => zed_llm_client::UsageLimit::Unlimited,
- };
+ zed_llm_client::UsageLimit::Unlimited => {
+ proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
+ }
+ }),
+ }),
+ edit_predictions_usage_amount: usage.edit_predictions as u32,
+ edit_predictions_usage_limit: Some(proto::UsageLimit {
+ variant: Some(match plan.edit_predictions_limit() {
+ zed_llm_client::UsageLimit::Limited(limit) => {
+ proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
+ limit: limit as u32,
+ })
+ }
+ zed_llm_client::UsageLimit::Unlimited => {
+ proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
+ }
+ }),
+ }),
+ }
+}
- proto::SubscriptionUsage {
- model_requests_usage_amount: usage.model_requests as u32,
- model_requests_usage_limit: Some(proto::UsageLimit {
- variant: Some(match model_requests_limit {
- zed_llm_client::UsageLimit::Limited(limit) => {
- proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
- limit: limit as u32,
- })
- }
- zed_llm_client::UsageLimit::Unlimited => {
- proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
- }
- }),
- }),
- edit_predictions_usage_amount: usage.edit_predictions as u32,
- edit_predictions_usage_limit: Some(proto::UsageLimit {
- variant: Some(match plan.edit_predictions_limit() {
- zed_llm_client::UsageLimit::Limited(limit) => {
- proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
- limit: limit as u32,
- })
- }
- zed_llm_client::UsageLimit::Unlimited => {
- proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
- }
- }),
- }),
- }
+fn make_default_subscription_usage(
+ plan: proto::Plan,
+ feature_flags: &Vec<String>,
+) -> proto::SubscriptionUsage {
+ let plan = match plan {
+ proto::Plan::Free => zed_llm_client::Plan::ZedFree,
+ proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
+ proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
+ };
+
+ proto::SubscriptionUsage {
+ model_requests_usage_amount: 0,
+ model_requests_usage_limit: Some(proto::UsageLimit {
+ variant: Some(match model_requests_limit(plan, feature_flags) {
+ zed_llm_client::UsageLimit::Limited(limit) => {
+ proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
+ limit: limit as u32,
+ })
+ }
+ zed_llm_client::UsageLimit::Unlimited => {
+ proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
+ }
+ }),
}),
- })
+ edit_predictions_usage_amount: 0,
+ edit_predictions_usage_limit: Some(proto::UsageLimit {
+ variant: Some(match plan.edit_predictions_limit() {
+ zed_llm_client::UsageLimit::Limited(limit) => {
+ proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
+ limit: limit as u32,
+ })
+ }
+ zed_llm_client::UsageLimit::Unlimited => {
+ proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
+ }
+ }),
+ }),
+ }
}
async fn update_user_plan(session: &Session) -> Result<()> {
@@ -19,8 +19,8 @@ use crate::stripe_client::{
StripeCustomerId, StripeCustomerUpdate, StripeCustomerUpdateAddress, StripeCustomerUpdateName,
StripeMeter, StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId,
StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
- StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionItems,
- UpdateSubscriptionParams,
+ StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection,
+ UpdateSubscriptionItems, UpdateSubscriptionParams,
};
pub struct StripeBilling {
@@ -252,6 +252,7 @@ impl StripeBilling {
name: Some(StripeCustomerUpdateName::Auto),
shipping: None,
});
+ params.tax_id_collection = Some(StripeTaxIdCollection { enabled: true });
let session = self.client.create_checkout_session(params).await?;
Ok(session.url.context("no checkout session URL")?)
@@ -311,6 +312,7 @@ impl StripeBilling {
name: Some(StripeCustomerUpdateName::Auto),
shipping: None,
});
+ params.tax_id_collection = Some(StripeTaxIdCollection { enabled: true });
let session = self.client.create_checkout_session(params).await?;
Ok(session.url.context("no checkout session URL")?)
@@ -190,6 +190,7 @@ pub struct StripeCreateCheckoutSessionParams<'a> {
pub success_url: Option<&'a str>,
pub billing_address_collection: Option<StripeBillingAddressCollection>,
pub customer_update: Option<StripeCustomerUpdate>,
+ pub tax_id_collection: Option<StripeTaxIdCollection>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
@@ -218,6 +219,11 @@ pub struct StripeCreateCheckoutSessionSubscriptionData {
pub trial_settings: Option<StripeSubscriptionTrialSettings>,
}
+#[derive(Debug, PartialEq, Clone)]
+pub struct StripeTaxIdCollection {
+ pub enabled: bool,
+}
+
#[derive(Debug)]
pub struct StripeCheckoutSession {
pub url: Option<String>,
@@ -14,8 +14,8 @@ use crate::stripe_client::{
StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate,
StripeMeter, StripeMeterId, StripePrice, StripePriceId, StripeSubscription,
- StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, UpdateCustomerParams,
- UpdateSubscriptionParams,
+ StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, StripeTaxIdCollection,
+ UpdateCustomerParams, UpdateSubscriptionParams,
};
#[derive(Debug, Clone)]
@@ -38,6 +38,7 @@ pub struct StripeCreateCheckoutSessionCall {
pub success_url: Option<String>,
pub billing_address_collection: Option<StripeBillingAddressCollection>,
pub customer_update: Option<StripeCustomerUpdate>,
+ pub tax_id_collection: Option<StripeTaxIdCollection>,
}
pub struct FakeStripeClient {
@@ -236,6 +237,7 @@ impl StripeClient for FakeStripeClient {
success_url: params.success_url.map(|url| url.to_string()),
billing_address_collection: params.billing_address_collection,
customer_update: params.customer_update,
+ tax_id_collection: params.tax_id_collection,
});
Ok(StripeCheckoutSession {
@@ -27,8 +27,8 @@ use crate::stripe_client::{
StripeMeter, StripePrice, StripePriceId, StripePriceRecurring, StripeSubscription,
StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId,
StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
- StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateCustomerParams,
- UpdateSubscriptionParams,
+ StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection,
+ UpdateCustomerParams, UpdateSubscriptionParams,
};
pub struct RealStripeClient {
@@ -448,6 +448,7 @@ impl<'a> TryFrom<StripeCreateCheckoutSessionParams<'a>> for CreateCheckoutSessio
success_url: value.success_url,
billing_address_collection: value.billing_address_collection.map(Into::into),
customer_update: value.customer_update.map(Into::into),
+ tax_id_collection: value.tax_id_collection.map(Into::into),
..Default::default()
})
}
@@ -590,3 +591,11 @@ impl From<StripeCustomerUpdate> for stripe::CreateCheckoutSessionCustomerUpdate
}
}
}
+
+impl From<StripeTaxIdCollection> for stripe::CreateCheckoutSessionTaxIdCollection {
+ fn from(value: StripeTaxIdCollection) -> Self {
+ stripe::CreateCheckoutSessionTaxIdCollection {
+ enabled: value.enabled,
+ }
+ }
+}
@@ -1013,7 +1013,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// and some of which were originally opened by client B.
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| {
- pane.close_inactive_items(&Default::default(), window, cx)
+ pane.close_inactive_items(&Default::default(), None, window, cx)
.detach();
});
});
@@ -2,6 +2,7 @@ use crate::tests::TestServer;
use call::ActiveCall;
use collections::{HashMap, HashSet};
+use dap::{Capabilities, adapters::DebugTaskDefinition, transport::RequestHandling};
use debugger_ui::debugger_panel::DebugPanel;
use extension::ExtensionHostProxy;
use fs::{FakeFs, Fs as _, RemoveOptions};
@@ -22,6 +23,7 @@ use language::{
use node_runtime::NodeRuntime;
use project::{
ProjectPath,
+ debugger::session::ThreadId,
lsp_store::{FormatTrigger, LspFormatTarget},
};
use remote::SshRemoteClient;
@@ -29,7 +31,11 @@ use remote_server::{HeadlessAppState, HeadlessProject};
use rpc::proto;
use serde_json::json;
use settings::SettingsStore;
-use std::{path::Path, sync::Arc};
+use std::{
+ path::Path,
+ sync::{Arc, atomic::AtomicUsize},
+};
+use task::TcpArgumentsTemplate;
use util::path;
#[gpui::test(iterations = 10)]
@@ -688,3 +694,162 @@ async fn test_remote_server_debugger(
shutdown_session.await.unwrap();
}
+
+#[gpui::test]
+async fn test_slow_adapter_startup_retries(
+ cx_a: &mut TestAppContext,
+ server_cx: &mut TestAppContext,
+ executor: BackgroundExecutor,
+) {
+ cx_a.update(|cx| {
+ release_channel::init(SemanticVersion::default(), cx);
+ command_palette_hooks::init(cx);
+ zlog::init_test();
+ dap_adapters::init(cx);
+ });
+ server_cx.update(|cx| {
+ release_channel::init(SemanticVersion::default(), cx);
+ dap_adapters::init(cx);
+ });
+ let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
+ let remote_fs = FakeFs::new(server_cx.executor());
+ remote_fs
+ .insert_tree(
+ path!("/code"),
+ json!({
+ "lib.rs": "fn one() -> usize { 1 }"
+ }),
+ )
+ .await;
+
+ // User A connects to the remote project via SSH.
+ server_cx.update(HeadlessProject::init);
+ let remote_http_client = Arc::new(BlockedHttpClient);
+ let node = NodeRuntime::unavailable();
+ let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
+ let _headless_project = server_cx.new(|cx| {
+ client::init_settings(cx);
+ HeadlessProject::new(
+ HeadlessAppState {
+ session: server_ssh,
+ fs: remote_fs.clone(),
+ http_client: remote_http_client,
+ node_runtime: node,
+ languages,
+ extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
+ },
+ cx,
+ )
+ });
+
+ let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
+ let mut server = TestServer::start(server_cx.executor()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ cx_a.update(|cx| {
+ debugger_ui::init(cx);
+ command_palette_hooks::init(cx);
+ });
+ let (project_a, _) = client_a
+ .build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
+ .await;
+
+ let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
+
+ let debugger_panel = workspace
+ .update_in(cx_a, |_workspace, window, cx| {
+ cx.spawn_in(window, DebugPanel::load)
+ })
+ .await
+ .unwrap();
+
+ workspace.update_in(cx_a, |workspace, window, cx| {
+ workspace.add_panel(debugger_panel, window, cx);
+ });
+
+ cx_a.run_until_parked();
+ let debug_panel = workspace
+ .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
+ .unwrap();
+
+ let workspace_window = cx_a
+ .window_handle()
+ .downcast::<workspace::Workspace>()
+ .unwrap();
+
+ let count = Arc::new(AtomicUsize::new(0));
+ let session = debugger_ui::tests::start_debug_session_with(
+ &workspace_window,
+ cx_a,
+ DebugTaskDefinition {
+ adapter: "fake-adapter".into(),
+ label: "test".into(),
+ config: json!({
+ "request": "launch"
+ }),
+ tcp_connection: Some(TcpArgumentsTemplate {
+ port: None,
+ host: None,
+ timeout: None,
+ }),
+ },
+ move |client| {
+ let count = count.clone();
+ client.on_request_ext::<dap::requests::Initialize, _>(move |_seq, _request| {
+ if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 {
+ return RequestHandling::Exit;
+ }
+ RequestHandling::Respond(Ok(Capabilities::default()))
+ });
+ },
+ )
+ .unwrap();
+ cx_a.run_until_parked();
+
+ let client = session.update(cx_a, |session, _| session.adapter_client().unwrap());
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ cx_a.run_until_parked();
+
+ let active_session = debug_panel
+ .update(cx_a, |this, _| this.active_session())
+ .unwrap();
+
+ let running_state = active_session.update(cx_a, |active_session, _| {
+ active_session.running_state().clone()
+ });
+
+ assert_eq!(
+ client.id(),
+ running_state.read_with(cx_a, |running_state, _| running_state.session_id())
+ );
+ assert_eq!(
+ ThreadId(1),
+ running_state.read_with(cx_a, |running_state, _| running_state
+ .selected_thread_id()
+ .unwrap())
+ );
+
+ let shutdown_session = workspace.update(cx_a, |workspace, cx| {
+ workspace.project().update(cx, |project, cx| {
+ project.dap_store().update(cx, |dap_store, cx| {
+ dap_store.shutdown_session(session.read(cx).session_id(), cx)
+ })
+ })
+ });
+
+ client_ssh.update(cx_a, |a, _| {
+ a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
+ });
+
+ shutdown_session.await.unwrap();
+}
@@ -30,7 +30,13 @@ use workspace::{
};
use workspace::{item::Dedup, notifications::NotificationId};
-actions!(collab, [CopyLink]);
+actions!(
+ collab,
+ [
+ /// Copies a link to the current position in the channel buffer.
+ CopyLink
+ ]
+);
pub fn init(cx: &mut App) {
workspace::FollowableViewRegistry::register::<ChannelView>(cx)
@@ -71,7 +71,13 @@ struct SerializedChatPanel {
width: Option<Pixels>,
}
-actions!(chat_panel, [ToggleFocus]);
+actions!(
+ chat_panel,
+ [
+ /// Toggles focus on the chat panel.
+ ToggleFocus
+ ]
+);
impl ChatPanel {
pub fn new(
@@ -44,15 +44,25 @@ use workspace::{
actions!(
collab_panel,
[
+ /// Toggles focus on the collaboration panel.
ToggleFocus,
+ /// Removes the selected channel or contact.
Remove,
+ /// Opens the context menu for the selected item.
Secondary,
+ /// Collapses the selected channel in the tree view.
CollapseSelectedChannel,
+ /// Expands the selected channel in the tree view.
ExpandSelectedChannel,
+ /// Starts moving a channel to a new location.
StartMoveChannel,
+ /// Moves the selected item to the current location.
MoveSelected,
+ /// Inserts a space character in the filter input.
InsertSpace,
+ /// Moves the selected channel up in the list.
MoveChannelUp,
+ /// Moves the selected channel down in the list.
MoveChannelDown,
]
);
@@ -17,9 +17,13 @@ use workspace::{ModalView, notifications::DetachAndPromptErr};
actions!(
channel_modal,
[
+ /// Selects the next control in the channel modal.
SelectNextControl,
+ /// Toggles between invite members and manage members mode.
ToggleMode,
+ /// Toggles admin status for the selected member.
ToggleMemberAdmin,
+ /// Removes the selected member from the channel.
RemoveMember
]
);
@@ -74,7 +74,13 @@ pub struct NotificationPresenter {
pub can_navigate: bool,
}
-actions!(notification_panel, [ToggleFocus]);
+actions!(
+ notification_panel,
+ [
+ /// Toggles focus on the notification panel.
+ ToggleFocus
+ ]
+);
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _, _| {
@@ -61,7 +61,7 @@ impl RenderOnce for ComponentExample {
12.0,
12.0,
))
- .shadow_sm()
+ .shadow_xs()
.child(self.element),
)
.into_any_element()
@@ -46,11 +46,17 @@ pub use crate::sign_in::{CopilotCodeVerification, initiate_sign_in, reinstall_an
actions!(
copilot,
[
+ /// Requests a code completion suggestion from Copilot.
Suggest,
+ /// Cycles to the next Copilot suggestion.
NextSuggestion,
+ /// Cycles to the previous Copilot suggestion.
PreviousSuggestion,
+ /// Reinstalls the Copilot language server.
Reinstall,
+ /// Signs in to GitHub Copilot.
SignIn,
+ /// Signs out of GitHub Copilot.
SignOut
]
);
@@ -528,6 +528,7 @@ impl CopilotChat {
pub async fn stream_completion(
request: Request,
+ is_user_initiated: bool,
mut cx: AsyncApp,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let this = cx
@@ -562,7 +563,14 @@ impl CopilotChat {
};
let api_url = configuration.api_url_from_endpoint(&token.api_endpoint);
- stream_completion(client.clone(), token.api_key, api_url.into(), request).await
+ stream_completion(
+ client.clone(),
+ token.api_key,
+ api_url.into(),
+ request,
+ is_user_initiated,
+ )
+ .await
}
pub fn set_configuration(
@@ -697,6 +705,7 @@ async fn stream_completion(
api_key: String,
completion_url: Arc<str>,
request: Request,
+ is_user_initiated: bool,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let is_vision_request = request.messages.iter().any(|message| match message {
ChatMessage::User { content }
@@ -707,6 +716,8 @@ async fn stream_completion(
_ => false,
});
+ let request_initiator = if is_user_initiated { "user" } else { "agent" };
+
let mut request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(completion_url.as_ref())
@@ -719,7 +730,8 @@ async fn stream_completion(
)
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
- .header("Copilot-Integration-Id", "vscode-chat");
+ .header("Copilot-Integration-Id", "vscode-chat")
+ .header("X-Initiator", request_initiator);
if is_vision_request {
request_builder =
@@ -378,6 +378,14 @@ pub trait DebugAdapter: 'static + Send + Sync {
fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
None
}
+
+ fn compact_child_session(&self) -> bool {
+ false
+ }
+
+ fn prefer_thread_name(&self) -> bool {
+ false
+ }
}
#[cfg(any(test, feature = "test-support"))]
@@ -442,10 +450,18 @@ impl DebugAdapter for FakeAdapter {
_: Option<Vec<String>>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
+ let connection = task_definition
+ .tcp_connection
+ .as_ref()
+ .map(|connection| TcpArguments {
+ host: connection.host(),
+ port: connection.port.unwrap_or(17),
+ timeout: connection.timeout,
+ });
Ok(DebugAdapterBinary {
command: Some("command".into()),
arguments: vec![],
- connection: None,
+ connection,
envs: HashMap::default(),
cwd: None,
request_args: StartDebuggingRequestArguments {
@@ -108,7 +108,9 @@ impl DebugAdapterClient {
arguments: Some(serialized_arguments),
};
self.transport_delegate
- .add_pending_request(sequence_id, callback_tx);
+ .pending_requests
+ .lock()
+ .insert(sequence_id, callback_tx)?;
log::debug!(
"Client {} send `{}` request with sequence_id: {}",
@@ -166,6 +168,7 @@ impl DebugAdapterClient {
pub fn kill(&self) {
log::debug!("Killing DAP process");
self.transport_delegate.transport.lock().kill();
+ self.transport_delegate.pending_requests.lock().shutdown();
}
pub fn has_adapter_logs(&self) -> bool {
@@ -180,11 +183,34 @@ impl DebugAdapterClient {
}
#[cfg(any(test, feature = "test-support"))]
- pub fn on_request<R: dap_types::requests::Request, F>(&self, handler: F)
+ pub fn on_request<R: dap_types::requests::Request, F>(&self, mut handler: F)
where
F: 'static
+ Send
+ FnMut(u64, R::Arguments) -> Result<R::Response, dap_types::ErrorResponse>,
+ {
+ use crate::transport::RequestHandling;
+
+ self.transport_delegate
+ .transport
+ .lock()
+ .as_fake()
+ .on_request::<R, _>(move |seq, request| {
+ RequestHandling::Respond(handler(seq, request))
+ });
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn on_request_ext<R: dap_types::requests::Request, F>(&self, handler: F)
+ where
+ F: 'static
+ + Send
+ + FnMut(
+ u64,
+ R::Arguments,
+ ) -> crate::transport::RequestHandling<
+ Result<R::Response, dap_types::ErrorResponse>,
+ >,
{
self.transport_delegate
.transport
@@ -49,7 +49,12 @@ pub enum IoKind {
StdErr,
}
-type Requests = Arc<Mutex<HashMap<u64, oneshot::Sender<Result<Response>>>>>;
+#[cfg(any(test, feature = "test-support"))]
+pub enum RequestHandling<T> {
+ Respond(T),
+ Exit,
+}
+
type LogHandlers = Arc<Mutex<SmallVec<[(LogKind, IoHandler); 2]>>>;
pub trait Transport: Send + Sync {
@@ -77,7 +82,11 @@ async fn start(
) -> Result<Box<dyn Transport>> {
#[cfg(any(test, feature = "test-support"))]
if cfg!(any(test, feature = "test-support")) {
- return Ok(Box::new(FakeTransport::start(cx).await?));
+ if let Some(connection) = binary.connection.clone() {
+ return Ok(Box::new(FakeTransport::start_tcp(connection, cx).await?));
+ } else {
+ return Ok(Box::new(FakeTransport::start_stdio(cx).await?));
+ }
}
if binary.connection.is_some() {
@@ -91,20 +100,62 @@ async fn start(
}
}
+pub(crate) struct PendingRequests {
+ inner: Option<HashMap<u64, oneshot::Sender<Result<Response>>>>,
+}
+
+impl PendingRequests {
+ fn new() -> Self {
+ Self {
+ inner: Some(HashMap::default()),
+ }
+ }
+
+ fn flush(&mut self, e: anyhow::Error) {
+ let Some(inner) = self.inner.as_mut() else {
+ return;
+ };
+ for (_, sender) in inner.drain() {
+ sender.send(Err(e.cloned())).ok();
+ }
+ }
+
+ pub(crate) fn insert(
+ &mut self,
+ sequence_id: u64,
+ callback_tx: oneshot::Sender<Result<Response>>,
+ ) -> anyhow::Result<()> {
+ let Some(inner) = self.inner.as_mut() else {
+ bail!("client is closed")
+ };
+ inner.insert(sequence_id, callback_tx);
+ Ok(())
+ }
+
+ pub(crate) fn remove(
+ &mut self,
+ sequence_id: u64,
+ ) -> anyhow::Result<Option<oneshot::Sender<Result<Response>>>> {
+ let Some(inner) = self.inner.as_mut() else {
+ bail!("client is closed");
+ };
+ Ok(inner.remove(&sequence_id))
+ }
+
+ pub(crate) fn shutdown(&mut self) {
+ self.flush(anyhow!("transport shutdown"));
+ self.inner = None;
+ }
+}
+
pub(crate) struct TransportDelegate {
log_handlers: LogHandlers,
- pub(crate) pending_requests: Requests,
+ pub(crate) pending_requests: Arc<Mutex<PendingRequests>>,
pub(crate) transport: Mutex<Box<dyn Transport>>,
pub(crate) server_tx: smol::lock::Mutex<Option<Sender<Message>>>,
tasks: Mutex<Vec<Task<()>>>,
}
-impl Drop for TransportDelegate {
- fn drop(&mut self) {
- self.transport.lock().kill()
- }
-}
-
impl TransportDelegate {
pub(crate) async fn start(binary: &DebugAdapterBinary, cx: &mut AsyncApp) -> Result<Self> {
let log_handlers: LogHandlers = Default::default();
@@ -113,7 +164,7 @@ impl TransportDelegate {
transport: Mutex::new(transport),
log_handlers,
server_tx: Default::default(),
- pending_requests: Default::default(),
+ pending_requests: Arc::new(Mutex::new(PendingRequests::new())),
tasks: Default::default(),
})
}
@@ -154,16 +205,12 @@ impl TransportDelegate {
.await
{
Ok(()) => {
- pending_requests.lock().drain().for_each(|(_, request)| {
- request
- .send(Err(anyhow!("debugger shutdown unexpectedly")))
- .ok();
- });
+ pending_requests
+ .lock()
+ .flush(anyhow!("debugger shutdown unexpectedly"));
}
Err(e) => {
- pending_requests.lock().drain().for_each(|(_, request)| {
- request.send(Err(e.cloned())).ok();
- });
+ pending_requests.lock().flush(e);
}
}
}));
@@ -188,15 +235,6 @@ impl TransportDelegate {
self.transport.lock().tcp_arguments()
}
- pub(crate) fn add_pending_request(
- &self,
- sequence_id: u64,
- request: oneshot::Sender<Result<Response>>,
- ) {
- let mut pending_requests = self.pending_requests.lock();
- pending_requests.insert(sequence_id, request);
- }
-
pub(crate) async fn send_message(&self, message: Message) -> Result<()> {
if let Some(server_tx) = self.server_tx.lock().await.as_ref() {
server_tx.send(message).await.context("sending message")
@@ -290,7 +328,7 @@ impl TransportDelegate {
async fn recv_from_server<Stdout>(
server_stdout: Stdout,
mut message_handler: DapMessageHandler,
- pending_requests: Requests,
+ pending_requests: Arc<Mutex<PendingRequests>>,
log_handlers: Option<LogHandlers>,
) -> Result<()>
where
@@ -300,16 +338,17 @@ impl TransportDelegate {
let mut reader = BufReader::new(server_stdout);
let result = loop {
- match Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref())
- .await
- {
+ let result =
+ Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref())
+ .await;
+ match result {
ConnectionResult::Timeout => anyhow::bail!("Timed out when connecting to debugger"),
ConnectionResult::ConnectionReset => {
log::info!("Debugger closed the connection");
return Ok(());
}
ConnectionResult::Result(Ok(Message::Response(res))) => {
- let tx = pending_requests.lock().remove(&res.request_seq);
+ let tx = pending_requests.lock().remove(res.request_seq)?;
if let Some(tx) = tx {
if let Err(e) = tx.send(Self::process_response(res)) {
log::trace!("Did not send response `{:?}` for a cancelled", e);
@@ -703,8 +742,7 @@ impl Drop for StdioTransport {
}
#[cfg(any(test, feature = "test-support"))]
-type RequestHandler =
- Box<dyn Send + FnMut(u64, serde_json::Value) -> dap_types::messages::Response>;
+type RequestHandler = Box<dyn Send + FnMut(u64, serde_json::Value) -> RequestHandling<Response>>;
#[cfg(any(test, feature = "test-support"))]
type ResponseHandler = Box<dyn Send + Fn(Response)>;
@@ -715,23 +753,38 @@ pub struct FakeTransport {
request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
// for reverse request responses
response_handlers: Arc<Mutex<HashMap<&'static str, ResponseHandler>>>,
-
- stdin_writer: Option<PipeWriter>,
- stdout_reader: Option<PipeReader>,
message_handler: Option<Task<Result<()>>>,
+ kind: FakeTransportKind,
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub enum FakeTransportKind {
+ Stdio {
+ stdin_writer: Option<PipeWriter>,
+ stdout_reader: Option<PipeReader>,
+ },
+ Tcp {
+ connection: TcpArguments,
+ executor: BackgroundExecutor,
+ },
}
#[cfg(any(test, feature = "test-support"))]
impl FakeTransport {
pub fn on_request<R: dap_types::requests::Request, F>(&self, mut handler: F)
where
- F: 'static + Send + FnMut(u64, R::Arguments) -> Result<R::Response, ErrorResponse>,
+ F: 'static
+ + Send
+ + FnMut(u64, R::Arguments) -> RequestHandling<Result<R::Response, ErrorResponse>>,
{
self.request_handlers.lock().insert(
R::COMMAND,
Box::new(move |seq, args| {
let result = handler(seq, serde_json::from_value(args).unwrap());
- let response = match result {
+ let RequestHandling::Respond(response) = result else {
+ return RequestHandling::Exit;
+ };
+ let response = match response {
Ok(response) => Response {
seq: seq + 1,
request_seq: seq,
@@ -749,7 +802,7 @@ impl FakeTransport {
message: None,
},
};
- response
+ RequestHandling::Respond(response)
}),
);
}
@@ -763,86 +816,75 @@ impl FakeTransport {
.insert(R::COMMAND, Box::new(handler));
}
- async fn start(cx: &mut AsyncApp) -> Result<Self> {
- use dap_types::requests::{Request, RunInTerminal, StartDebugging};
- use serde_json::json;
-
- let (stdin_writer, stdin_reader) = async_pipe::pipe();
- let (stdout_writer, stdout_reader) = async_pipe::pipe();
-
- let mut this = Self {
+ async fn start_tcp(connection: TcpArguments, cx: &mut AsyncApp) -> Result<Self> {
+ Ok(Self {
request_handlers: Arc::new(Mutex::new(HashMap::default())),
response_handlers: Arc::new(Mutex::new(HashMap::default())),
- stdin_writer: Some(stdin_writer),
- stdout_reader: Some(stdout_reader),
message_handler: None,
- };
+ kind: FakeTransportKind::Tcp {
+ connection,
+ executor: cx.background_executor().clone(),
+ },
+ })
+ }
- let request_handlers = this.request_handlers.clone();
- let response_handlers = this.response_handlers.clone();
+ async fn handle_messages(
+ request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
+ response_handlers: Arc<Mutex<HashMap<&'static str, ResponseHandler>>>,
+ stdin_reader: PipeReader,
+ stdout_writer: PipeWriter,
+ ) -> Result<()> {
+ use dap_types::requests::{Request, RunInTerminal, StartDebugging};
+ use serde_json::json;
+
+ let mut reader = BufReader::new(stdin_reader);
let stdout_writer = Arc::new(smol::lock::Mutex::new(stdout_writer));
+ let mut buffer = String::new();
- this.message_handler = Some(cx.background_spawn(async move {
- let mut reader = BufReader::new(stdin_reader);
- let mut buffer = String::new();
+ loop {
+ match TransportDelegate::receive_server_message(&mut reader, &mut buffer, None).await {
+ ConnectionResult::Timeout => {
+ anyhow::bail!("Timed out when connecting to debugger");
+ }
+ ConnectionResult::ConnectionReset => {
+ log::info!("Debugger closed the connection");
+ break Ok(());
+ }
+ ConnectionResult::Result(Err(e)) => break Err(e),
+ ConnectionResult::Result(Ok(message)) => {
+ match message {
+ Message::Request(request) => {
+ // redirect reverse requests to stdout writer/reader
+ if request.command == RunInTerminal::COMMAND
+ || request.command == StartDebugging::COMMAND
+ {
+ let message =
+ serde_json::to_string(&Message::Request(request)).unwrap();
- loop {
- match TransportDelegate::receive_server_message(&mut reader, &mut buffer, None)
- .await
- {
- ConnectionResult::Timeout => {
- anyhow::bail!("Timed out when connecting to debugger");
- }
- ConnectionResult::ConnectionReset => {
- log::info!("Debugger closed the connection");
- break Ok(());
- }
- ConnectionResult::Result(Err(e)) => break Err(e),
- ConnectionResult::Result(Ok(message)) => {
- match message {
- Message::Request(request) => {
- // redirect reverse requests to stdout writer/reader
- if request.command == RunInTerminal::COMMAND
- || request.command == StartDebugging::COMMAND
+ let mut writer = stdout_writer.lock().await;
+ writer
+ .write_all(
+ TransportDelegate::build_rpc_message(message).as_bytes(),
+ )
+ .await
+ .unwrap();
+ writer.flush().await.unwrap();
+ } else {
+ let response = if let Some(handle) =
+ request_handlers.lock().get_mut(request.command.as_str())
{
- let message =
- serde_json::to_string(&Message::Request(request)).unwrap();
-
- let mut writer = stdout_writer.lock().await;
- writer
- .write_all(
- TransportDelegate::build_rpc_message(message)
- .as_bytes(),
- )
- .await
- .unwrap();
- writer.flush().await.unwrap();
+ handle(request.seq, request.arguments.unwrap_or(json!({})))
} else {
- let response = if let Some(handle) =
- request_handlers.lock().get_mut(request.command.as_str())
- {
- handle(request.seq, request.arguments.unwrap_or(json!({})))
- } else {
- panic!("No request handler for {}", request.command);
- };
- let message =
- serde_json::to_string(&Message::Response(response))
- .unwrap();
-
- let mut writer = stdout_writer.lock().await;
- writer
- .write_all(
- TransportDelegate::build_rpc_message(message)
- .as_bytes(),
- )
- .await
- .unwrap();
- writer.flush().await.unwrap();
- }
- }
- Message::Event(event) => {
+ panic!("No request handler for {}", request.command);
+ };
+ let response = match response {
+ RequestHandling::Respond(response) => response,
+ RequestHandling::Exit => {
+ break Err(anyhow!("exit in response to request"));
+ }
+ };
let message =
- serde_json::to_string(&Message::Event(event)).unwrap();
+ serde_json::to_string(&Message::Response(response)).unwrap();
let mut writer = stdout_writer.lock().await;
writer
@@ -853,20 +895,56 @@ impl FakeTransport {
.unwrap();
writer.flush().await.unwrap();
}
- Message::Response(response) => {
- if let Some(handle) =
- response_handlers.lock().get(response.command.as_str())
- {
- handle(response);
- } else {
- log::error!("No response handler for {}", response.command);
- }
+ }
+ Message::Event(event) => {
+ let message = serde_json::to_string(&Message::Event(event)).unwrap();
+
+ let mut writer = stdout_writer.lock().await;
+ writer
+ .write_all(TransportDelegate::build_rpc_message(message).as_bytes())
+ .await
+ .unwrap();
+ writer.flush().await.unwrap();
+ }
+ Message::Response(response) => {
+ if let Some(handle) =
+ response_handlers.lock().get(response.command.as_str())
+ {
+ handle(response);
+ } else {
+ log::error!("No response handler for {}", response.command);
}
}
}
}
}
- }));
+ }
+ }
+
+ async fn start_stdio(cx: &mut AsyncApp) -> Result<Self> {
+ let (stdin_writer, stdin_reader) = async_pipe::pipe();
+ let (stdout_writer, stdout_reader) = async_pipe::pipe();
+ let kind = FakeTransportKind::Stdio {
+ stdin_writer: Some(stdin_writer),
+ stdout_reader: Some(stdout_reader),
+ };
+
+ let mut this = Self {
+ request_handlers: Arc::new(Mutex::new(HashMap::default())),
+ response_handlers: Arc::new(Mutex::new(HashMap::default())),
+ message_handler: None,
+ kind,
+ };
+
+ let request_handlers = this.request_handlers.clone();
+ let response_handlers = this.response_handlers.clone();
+
+ this.message_handler = Some(cx.background_spawn(Self::handle_messages(
+ request_handlers,
+ response_handlers,
+ stdin_reader,
+ stdout_writer,
+ )));
Ok(this)
}
@@ -875,7 +953,10 @@ impl FakeTransport {
#[cfg(any(test, feature = "test-support"))]
impl Transport for FakeTransport {
fn tcp_arguments(&self) -> Option<TcpArguments> {
- None
+ match &self.kind {
+ FakeTransportKind::Stdio { .. } => None,
+ FakeTransportKind::Tcp { connection, .. } => Some(connection.clone()),
+ }
}
fn connect(
@@ -886,12 +967,33 @@ impl Transport for FakeTransport {
Box<dyn AsyncRead + Unpin + Send + 'static>,
)>,
> {
- let result = util::maybe!({
- Ok((
- Box::new(self.stdin_writer.take().context("Cannot reconnect")?) as _,
- Box::new(self.stdout_reader.take().context("Cannot reconnect")?) as _,
- ))
- });
+ let result = match &mut self.kind {
+ FakeTransportKind::Stdio {
+ stdin_writer,
+ stdout_reader,
+ } => util::maybe!({
+ Ok((
+ Box::new(stdin_writer.take().context("Cannot reconnect")?) as _,
+ Box::new(stdout_reader.take().context("Cannot reconnect")?) as _,
+ ))
+ }),
+ FakeTransportKind::Tcp { executor, .. } => {
+ let (stdin_writer, stdin_reader) = async_pipe::pipe();
+ let (stdout_writer, stdout_reader) = async_pipe::pipe();
+
+ let request_handlers = self.request_handlers.clone();
+ let response_handlers = self.response_handlers.clone();
+
+ self.message_handler = Some(executor.spawn(Self::handle_messages(
+ request_handlers,
+ response_handlers,
+ stdin_reader,
+ stdout_writer,
+ )));
+
+ Ok((Box::new(stdin_writer) as _, Box::new(stdout_reader) as _))
+ }
+ };
Task::ready(result)
}
@@ -2,7 +2,6 @@ mod codelldb;
mod gdb;
mod go;
mod javascript;
-mod php;
mod python;
use std::sync::Arc;
@@ -22,7 +21,6 @@ use gdb::GdbDebugAdapter;
use go::GoDebugAdapter;
use gpui::{App, BorrowAppContext};
use javascript::JsDebugAdapter;
-use php::PhpDebugAdapter;
use python::PythonDebugAdapter;
use serde_json::json;
use task::{DebugScenario, ZedDebugConfig};
@@ -31,7 +29,6 @@ pub fn init(cx: &mut App) {
cx.update_default_global(|registry: &mut DapRegistry, _cx| {
registry.add_adapter(Arc::from(CodeLldbDebugAdapter::default()));
registry.add_adapter(Arc::from(PythonDebugAdapter::default()));
- registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
registry.add_adapter(Arc::from(GoDebugAdapter::default()));
registry.add_adapter(Arc::from(GdbDebugAdapter));
@@ -547,6 +547,7 @@ async fn handle_envs(
}
};
+ let mut env_vars = HashMap::default();
for path in env_files {
let Some(path) = path
.and_then(|s| PathBuf::from_str(s).ok())
@@ -556,13 +557,33 @@ async fn handle_envs(
};
if let Ok(file) = fs.open_sync(&path).await {
- envs.extend(dotenvy::from_read_iter(file).filter_map(Result::ok))
+ let file_envs: HashMap<String, String> = dotenvy::from_read_iter(file)
+ .filter_map(Result::ok)
+ .collect();
+ envs.extend(file_envs.iter().map(|(k, v)| (k.clone(), v.clone())));
+ env_vars.extend(file_envs);
} else {
warn!("While starting Go debug session: failed to read env file {path:?}");
};
}
+ let mut env_obj: serde_json::Map<String, Value> = serde_json::Map::new();
+
+ for (k, v) in env_vars {
+ env_obj.insert(k, Value::String(v));
+ }
+
+ if let Some(existing_env) = config.get("env").and_then(|v| v.as_object()) {
+ for (k, v) in existing_env {
+ env_obj.insert(k.clone(), v.clone());
+ }
+ }
+
+ if !env_obj.is_empty() {
+ config.insert("env".to_string(), Value::Object(env_obj));
+ }
+
// remove envFile now that it's been handled
- config.remove("entry");
+ config.remove("envFile");
Some(())
}
@@ -1,9 +1,10 @@
use adapters::latest_github_release;
use anyhow::Context as _;
+use collections::HashMap;
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp;
use serde_json::Value;
-use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
+use std::{path::PathBuf, sync::OnceLock};
use task::DebugRequest;
use util::{ResultExt, maybe};
@@ -70,6 +71,8 @@ impl JsDebugAdapter {
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
+ let mut envs = HashMap::default();
+
let mut configuration = task_definition.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
maybe!({
@@ -79,9 +82,9 @@ impl JsDebugAdapter {
let command = configuration.get("command")?.as_str()?.to_owned();
let mut args = shlex::split(&command)?.into_iter();
let program = args.next()?;
- configuration.insert("program".to_owned(), program.into());
+ configuration.insert("runtimeExecutable".to_owned(), program.into());
configuration.insert(
- "args".to_owned(),
+ "runtimeArgs".to_owned(),
args.map(Value::from).collect::<Vec<_>>().into(),
);
configuration.insert("console".to_owned(), "externalTerminal".into());
@@ -110,6 +113,12 @@ impl JsDebugAdapter {
}
}
+ if let Some(env) = configuration.get("env").cloned() {
+ if let Ok(env) = serde_json::from_value(env) {
+ envs = env;
+ }
+ }
+
configuration
.entry("cwd")
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
@@ -158,7 +167,7 @@ impl JsDebugAdapter {
),
arguments,
cwd: Some(delegate.worktree_root_path().to_path_buf()),
- envs: HashMap::default(),
+ envs,
connection: Some(adapters::TcpArguments {
host,
port,
@@ -245,7 +254,7 @@ impl DebugAdapter for JsDebugAdapter {
"properties": {
"type": {
"type": "string",
- "enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge"],
+ "enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge", "node-terminal"],
"description": "The type of debug session",
"default": "pwa-node"
},
@@ -379,10 +388,6 @@ impl DebugAdapter for JsDebugAdapter {
}
}
},
- "oneOf": [
- { "required": ["program"] },
- { "required": ["url"] }
- ]
}
]
},
@@ -529,6 +534,14 @@ impl DebugAdapter for JsDebugAdapter {
.filter(|name| !name.is_empty())?;
Some(label.to_owned())
}
+
+ fn compact_child_session(&self) -> bool {
+ true
+ }
+
+ fn prefer_thread_name(&self) -> bool {
+ true
+ }
}
fn normalize_task_type(task_type: &mut Value) {
@@ -1,368 +0,0 @@
-use adapters::latest_github_release;
-use anyhow::Context as _;
-use anyhow::bail;
-use dap::StartDebuggingRequestArguments;
-use dap::StartDebuggingRequestArgumentsRequest;
-use dap::adapters::{DebugTaskDefinition, TcpArguments};
-use gpui::{AsyncApp, SharedString};
-use language::LanguageName;
-use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
-use util::ResultExt;
-
-use crate::*;
-
-#[derive(Default)]
-pub(crate) struct PhpDebugAdapter {
- checked: OnceLock<()>,
-}
-
-impl PhpDebugAdapter {
- const ADAPTER_NAME: &'static str = "PHP";
- const ADAPTER_PACKAGE_NAME: &'static str = "vscode-php-debug";
- const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js";
-
- async fn fetch_latest_adapter_version(
- &self,
- delegate: &Arc<dyn DapDelegate>,
- ) -> Result<AdapterVersion> {
- let release = latest_github_release(
- &format!("{}/{}", "xdebug", Self::ADAPTER_PACKAGE_NAME),
- true,
- false,
- delegate.http_client(),
- )
- .await?;
-
- let asset_name = format!("php-debug-{}.vsix", release.tag_name.replace("v", ""));
-
- Ok(AdapterVersion {
- tag_name: release.tag_name,
- url: release
- .assets
- .iter()
- .find(|asset| asset.name == asset_name)
- .with_context(|| format!("no asset found matching {asset_name:?}"))?
- .browser_download_url
- .clone(),
- })
- }
-
- async fn get_installed_binary(
- &self,
- delegate: &Arc<dyn DapDelegate>,
- task_definition: &DebugTaskDefinition,
- user_installed_path: Option<PathBuf>,
- user_args: Option<Vec<String>>,
- _: &mut AsyncApp,
- ) -> Result<DebugAdapterBinary> {
- let adapter_path = if let Some(user_installed_path) = user_installed_path {
- user_installed_path
- } else {
- let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
-
- let file_name_prefix = format!("{}_", self.name());
-
- util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
- file_name.starts_with(&file_name_prefix)
- })
- .await
- .context("Couldn't find PHP dap directory")?
- };
-
- let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
- let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
-
- let mut configuration = task_definition.config.clone();
- if let Some(obj) = configuration.as_object_mut() {
- obj.entry("cwd")
- .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
- }
-
- let arguments = if let Some(mut args) = user_args {
- args.insert(
- 0,
- adapter_path
- .join(Self::ADAPTER_PATH)
- .to_string_lossy()
- .to_string(),
- );
- args
- } else {
- vec![
- adapter_path
- .join(Self::ADAPTER_PATH)
- .to_string_lossy()
- .to_string(),
- format!("--server={}", port),
- ]
- };
-
- Ok(DebugAdapterBinary {
- command: Some(
- delegate
- .node_runtime()
- .binary_path()
- .await?
- .to_string_lossy()
- .into_owned(),
- ),
- arguments,
- connection: Some(TcpArguments {
- port,
- host,
- timeout,
- }),
- cwd: Some(delegate.worktree_root_path().to_path_buf()),
- envs: HashMap::default(),
- request_args: StartDebuggingRequestArguments {
- configuration,
- request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)
- .await?,
- },
- })
- }
-}
-
-#[async_trait(?Send)]
-impl DebugAdapter for PhpDebugAdapter {
- fn dap_schema(&self) -> serde_json::Value {
- json!({
- "properties": {
- "request": {
- "type": "string",
- "enum": ["launch"],
- "description": "The request type for the PHP debug adapter, always \"launch\"",
- "default": "launch"
- },
- "hostname": {
- "type": "string",
- "description": "The address to bind to when listening for Xdebug (default: all IPv6 connections if available, else all IPv4 connections) or Unix Domain socket (prefix with unix://) or Windows Pipe (\\\\?\\pipe\\name) - cannot be combined with port"
- },
- "port": {
- "type": "integer",
- "description": "The port on which to listen for Xdebug (default: 9003). If port is set to 0 a random port is chosen by the system and a placeholder ${port} is replaced with the chosen port in env and runtimeArgs.",
- "default": 9003
- },
- "program": {
- "type": "string",
- "description": "The PHP script to debug (typically a path to a file)",
- "default": "${file}"
- },
- "cwd": {
- "type": "string",
- "description": "Working directory for the debugged program"
- },
- "args": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "description": "Command line arguments to pass to the program"
- },
- "env": {
- "type": "object",
- "description": "Environment variables to pass to the program",
- "additionalProperties": {
- "type": "string"
- }
- },
- "stopOnEntry": {
- "type": "boolean",
- "description": "Whether to break at the beginning of the script",
- "default": false
- },
- "pathMappings": {
- "type": "object",
- "description": "A mapping of server paths to local paths.",
- },
- "log": {
- "type": "boolean",
- "description": "Whether to log all communication between editor and the adapter to the debug console",
- "default": false
- },
- "ignore": {
- "type": "array",
- "description": "An array of glob patterns that errors should be ignored from (for example **/vendor/**/*.php)",
- "items": {
- "type": "string"
- }
- },
- "ignoreExceptions": {
- "type": "array",
- "description": "An array of exception class names that should be ignored (for example BaseException, \\NS1\\Exception, \\*\\Exception or \\**\\Exception*)",
- "items": {
- "type": "string"
- }
- },
- "skipFiles": {
- "type": "array",
- "description": "An array of glob patterns to skip when debugging. Star patterns and negations are allowed.",
- "items": {
- "type": "string"
- }
- },
- "skipEntryPaths": {
- "type": "array",
- "description": "An array of glob patterns to immediately detach from and ignore for debugging if the entry script matches",
- "items": {
- "type": "string"
- }
- },
- "maxConnections": {
- "type": "integer",
- "description": "Accept only this number of parallel debugging sessions. Additional connections will be dropped.",
- "default": 1
- },
- "proxy": {
- "type": "object",
- "description": "DBGp Proxy settings",
- "properties": {
- "enable": {
- "type": "boolean",
- "description": "To enable proxy registration",
- "default": false
- },
- "host": {
- "type": "string",
- "description": "The address of the proxy. Supports host name, IP address, or Unix domain socket.",
- "default": "127.0.0.1"
- },
- "port": {
- "type": "integer",
- "description": "The port where the adapter will register with the proxy",
- "default": 9001
- },
- "key": {
- "type": "string",
- "description": "A unique key that allows the proxy to match requests to your editor",
- "default": "vsc"
- },
- "timeout": {
- "type": "integer",
- "description": "The number of milliseconds to wait before giving up on the connection to proxy",
- "default": 3000
- },
- "allowMultipleSessions": {
- "type": "boolean",
- "description": "If the proxy should forward multiple sessions/connections at the same time or not",
- "default": true
- }
- }
- },
- "xdebugSettings": {
- "type": "object",
- "description": "Allows you to override Xdebug's remote debugging settings to fine tune Xdebug to your needs",
- "properties": {
- "max_children": {
- "type": "integer",
- "description": "Max number of array or object children to initially retrieve"
- },
- "max_data": {
- "type": "integer",
- "description": "Max amount of variable data to initially retrieve"
- },
- "max_depth": {
- "type": "integer",
- "description": "Maximum depth that the debugger engine may return when sending arrays, hashes or object structures to the IDE"
- },
- "show_hidden": {
- "type": "integer",
- "description": "Whether to show detailed internal information on properties (e.g. private members of classes). Zero means hidden members are not shown.",
- "enum": [0, 1]
- },
- "breakpoint_include_return_value": {
- "type": "boolean",
- "description": "Determines whether to enable an additional \"return from function\" debugging step, allowing inspection of the return value when a function call returns"
- }
- }
- },
- "xdebugCloudToken": {
- "type": "string",
- "description": "Instead of listening locally, open a connection and register with Xdebug Cloud and accept debugging sessions on that connection"
- },
- "stream": {
- "type": "object",
- "description": "Allows to influence DBGp streams. Xdebug only supports stdout",
- "properties": {
- "stdout": {
- "type": "integer",
- "description": "Redirect stdout stream: 0 (disable), 1 (copy), 2 (redirect)",
- "enum": [0, 1, 2],
- "default": 0
- }
- }
- }
- },
- "required": ["request", "program"]
- })
- }
-
- fn name(&self) -> DebugAdapterName {
- DebugAdapterName(Self::ADAPTER_NAME.into())
- }
-
- fn adapter_language_name(&self) -> Option<LanguageName> {
- Some(SharedString::new_static("PHP").into())
- }
-
- async fn request_kind(
- &self,
- _: &serde_json::Value,
- ) -> Result<StartDebuggingRequestArgumentsRequest> {
- Ok(StartDebuggingRequestArgumentsRequest::Launch)
- }
-
- async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
- let obj = match &zed_scenario.request {
- dap::DebugRequest::Attach(_) => {
- bail!("Php adapter doesn't support attaching")
- }
- dap::DebugRequest::Launch(launch_config) => json!({
- "program": launch_config.program,
- "cwd": launch_config.cwd,
- "args": launch_config.args,
- "env": launch_config.env_json(),
- "stopOnEntry": zed_scenario.stop_on_entry.unwrap_or_default(),
- }),
- };
-
- Ok(DebugScenario {
- adapter: zed_scenario.adapter,
- label: zed_scenario.label,
- build: None,
- config: obj,
- tcp_connection: None,
- })
- }
-
- async fn get_binary(
- &self,
- delegate: &Arc<dyn DapDelegate>,
- task_definition: &DebugTaskDefinition,
- user_installed_path: Option<PathBuf>,
- user_args: Option<Vec<String>>,
- cx: &mut AsyncApp,
- ) -> Result<DebugAdapterBinary> {
- if self.checked.set(()).is_ok() {
- delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
- if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
- adapters::download_adapter_from_github(
- self.name(),
- version,
- adapters::DownloadedFileType::Vsix,
- delegate.as_ref(),
- )
- .await?;
- }
- }
-
- self.get_installed_binary(
- delegate,
- &task_definition,
- user_installed_path,
- user_args,
- cx,
- )
- .await
- }
-}
@@ -660,6 +660,15 @@ impl DebugAdapter for PythonDebugAdapter {
self.get_installed_binary(delegate, &config, None, user_args, toolchain, false)
.await
}
+
+ fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
+ let label = args
+ .configuration
+ .get("name")?
+ .as_str()
+ .filter(|label| !label.is_empty())?;
+ Some(label.to_owned())
+ }
}
async fn fetch_latest_adapter_version_from_github(
@@ -44,7 +44,9 @@ impl DapLocator for ExtensionLocatorAdapter {
.flatten()
}
- async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
- Err(anyhow::anyhow!("Not implemented"))
+ async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
+ self.extension
+ .run_dap_locator(self.locator_name.as_ref().to_owned(), build_config)
+ .await
}
}
@@ -32,12 +32,19 @@ use workspace::{
ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
};
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+enum View {
+ AdapterLogs,
+ RpcMessages,
+ InitializationSequence,
+}
+
struct DapLogView {
editor: Entity<Editor>,
focus_handle: FocusHandle,
log_store: Entity<LogStore>,
editor_subscriptions: Vec<Subscription>,
- current_view: Option<(SessionId, LogKind)>,
+ current_view: Option<(SessionId, View)>,
project: Entity<Project>,
_subscriptions: Vec<Subscription>,
}
@@ -77,6 +84,7 @@ struct DebugAdapterState {
id: SessionId,
log_messages: VecDeque<SharedString>,
rpc_messages: RpcMessages,
+ session_label: SharedString,
adapter_name: DebugAdapterName,
has_adapter_logs: bool,
is_terminated: bool,
@@ -121,12 +129,18 @@ impl MessageKind {
}
impl DebugAdapterState {
- fn new(id: SessionId, adapter_name: DebugAdapterName, has_adapter_logs: bool) -> Self {
+ fn new(
+ id: SessionId,
+ adapter_name: DebugAdapterName,
+ session_label: SharedString,
+ has_adapter_logs: bool,
+ ) -> Self {
Self {
id,
log_messages: VecDeque::new(),
rpc_messages: RpcMessages::new(),
adapter_name,
+ session_label,
has_adapter_logs,
is_terminated: false,
}
@@ -371,18 +385,22 @@ impl LogStore {
return None;
};
- let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
- (
- session.adapter(),
- session
- .adapter_client()
- .map_or(false, |client| client.has_adapter_logs()),
- )
- });
+ let (adapter_name, session_label, has_adapter_logs) =
+ session.read_with(cx, |session, _| {
+ (
+ session.adapter(),
+ session.label(),
+ session
+ .adapter_client()
+ .map_or(false, |client| client.has_adapter_logs()),
+ )
+ });
state.insert(DebugAdapterState::new(
id.session_id,
adapter_name,
+ session_label
+ .unwrap_or_else(|| format!("Session {} (child)", id.session_id.0).into()),
has_adapter_logs,
));
@@ -506,12 +524,13 @@ impl Render for DapLogToolbarItemView {
current_client
.map(|sub_item| {
Cow::Owned(format!(
- "{} ({}) - {}",
+ "{} - {} - {}",
sub_item.adapter_name,
- sub_item.session_id.0,
+ sub_item.session_label,
match sub_item.selected_entry {
- LogKind::Adapter => ADAPTER_LOGS,
- LogKind::Rpc => RPC_MESSAGES,
+ View::AdapterLogs => ADAPTER_LOGS,
+ View::RpcMessages => RPC_MESSAGES,
+ View::InitializationSequence => INITIALIZATION_SEQUENCE,
}
))
})
@@ -529,8 +548,8 @@ impl Render for DapLogToolbarItemView {
.pl_2()
.child(
Label::new(format!(
- "{}. {}",
- row.session_id.0, row.adapter_name,
+ "{} - {}",
+ row.adapter_name, row.session_label
))
.color(workspace::ui::Color::Muted),
)
@@ -669,9 +688,16 @@ impl DapLogView {
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
Event::NewLogEntry { id, entry, kind } => {
- if log_view.current_view == Some((id.session_id, *kind))
- && log_view.project == *id.project
- {
+ let is_current_view = match (log_view.current_view, *kind) {
+ (Some((i, View::AdapterLogs)), LogKind::Adapter)
+ | (Some((i, View::RpcMessages)), LogKind::Rpc)
+ if i == id.session_id =>
+ {
+ log_view.project == *id.project
+ }
+ _ => false,
+ };
+ if is_current_view {
log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
let last_point = editor.buffer().read(cx).len(cx);
@@ -768,10 +794,11 @@ impl DapLogView {
.map(|state| DapMenuItem {
session_id: state.id,
adapter_name: state.adapter_name.clone(),
+ session_label: state.session_label.clone(),
has_adapter_logs: state.has_adapter_logs,
selected_entry: self
.current_view
- .map_or(LogKind::Adapter, |(_, kind)| kind),
+ .map_or(View::AdapterLogs, |(_, kind)| kind),
})
.collect::<Vec<_>>()
})
@@ -789,7 +816,7 @@ impl DapLogView {
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(rpc_log) = rpc_log {
- self.current_view = Some((id.session_id, LogKind::Rpc));
+ self.current_view = Some((id.session_id, View::RpcMessages));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
@@ -830,7 +857,7 @@ impl DapLogView {
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(message_log) = message_log {
- self.current_view = Some((id.session_id, LogKind::Adapter));
+ self.current_view = Some((id.session_id, View::AdapterLogs));
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
editor
.read(cx)
@@ -859,7 +886,7 @@ impl DapLogView {
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(rpc_log) = rpc_log {
- self.current_view = Some((id.session_id, LogKind::Rpc));
+ self.current_view = Some((id.session_id, View::InitializationSequence));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
@@ -899,11 +926,12 @@ fn log_contents(lines: impl Iterator<Item = SharedString>) -> String {
}
#[derive(Clone, PartialEq)]
-pub(crate) struct DapMenuItem {
- pub session_id: SessionId,
- pub adapter_name: DebugAdapterName,
- pub has_adapter_logs: bool,
- pub selected_entry: LogKind,
+struct DapMenuItem {
+ session_id: SessionId,
+ session_label: SharedString,
+ adapter_name: DebugAdapterName,
+ has_adapter_logs: bool,
+ selected_entry: View,
}
const ADAPTER_LOGS: &str = "Adapter Logs";
@@ -918,7 +946,13 @@ impl Render for DapLogView {
}
}
-actions!(dev, [OpenDebugAdapterLogs]);
+actions!(
+ dev,
+ [
+ /// Opens the debug adapter protocol logs viewer.
+ OpenDebugAdapterLogs
+ ]
+);
pub fn init(cx: &mut App) {
let log_store = cx.new(|cx| LogStore::new(cx));
@@ -16,13 +16,13 @@ doctest = false
test-support = [
"dap/test-support",
"dap_adapters/test-support",
+ "debugger_tools/test-support",
"editor/test-support",
"gpui/test-support",
"project/test-support",
"util/test-support",
"workspace/test-support",
"unindent",
- "debugger_tools"
]
[dependencies]
@@ -40,11 +40,15 @@ file_icons.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
+hex.workspace = true
+indoc.workspace = true
itertools.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
+notifications.workspace = true
parking_lot.workspace = true
+parse_int.workspace = true
paths.workspace = true
picker.workspace = true
pretty_assertions.workspace = true
@@ -60,6 +64,7 @@ task.workspace = true
tasks_ui.workspace = true
telemetry.workspace = true
terminal_view.workspace = true
+text.workspace = true
theme.workspace = true
tree-sitter.workspace = true
tree-sitter-json.workspace = true
@@ -67,7 +72,7 @@ ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
-debugger_tools = { workspace = true, optional = true }
+debugger_tools.workspace = true
unindent = { workspace = true, optional = true }
zed_actions.workspace = true
@@ -206,7 +206,7 @@ impl PickerDelegate for AttachModalDelegate {
})
}
- fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let candidate = self
.matches
.get(self.selected_index())
@@ -229,30 +229,44 @@ impl PickerDelegate for AttachModalDelegate {
}
}
+ let workspace = self.workspace.clone();
+ let Some(panel) = workspace
+ .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
+ .ok()
+ .flatten()
+ else {
+ return;
+ };
+
+ if secondary {
+ // let Some(id) = worktree_id else { return };
+ // cx.spawn_in(window, async move |_, cx| {
+ // panel
+ // .update_in(cx, |debug_panel, window, cx| {
+ // debug_panel.save_scenario(&debug_scenario, id, window, cx)
+ // })?
+ // .await?;
+ // anyhow::Ok(())
+ // })
+ // .detach_and_log_err(cx);
+ }
let Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
registry.adapter(&self.definition.adapter)
}) else {
return;
};
- let workspace = self.workspace.clone();
let definition = self.definition.clone();
cx.spawn_in(window, async move |this, cx| {
let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
return;
};
- let panel = workspace
- .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
- .ok()
- .flatten();
- if let Some(panel) = panel {
- panel
- .update_in(cx, |panel, window, cx| {
- panel.start_session(scenario, Default::default(), None, None, window, cx);
- })
- .ok();
- }
+ panel
+ .update_in(cx, |panel, window, cx| {
+ panel.start_session(scenario, Default::default(), None, None, window, cx);
+ })
+ .ok();
this.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
@@ -2,6 +2,7 @@ use crate::persistence::DebuggerPaneItem;
use crate::session::DebugSession;
use crate::session::running::RunningState;
use crate::session::running::breakpoint_list::BreakpointList;
+
use crate::{
ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList,
FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables,
@@ -9,6 +10,7 @@ use crate::{
ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
};
use anyhow::{Context as _, Result, anyhow};
+use collections::IndexMap;
use dap::adapters::DebugAdapterName;
use dap::debugger_settings::DebugPanelDockPosition;
use dap::{
@@ -16,16 +18,18 @@ use dap::{
client::SessionId, debugger_settings::DebuggerSettings,
};
use dap::{DapRegistry, StartDebuggingRequestArguments};
+use editor::Editor;
use gpui::{
Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId,
EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task,
WeakEntity, anchored, deferred,
};
+use text::ToPoint as _;
use itertools::Itertools as _;
use language::Buffer;
-use project::debugger::session::{Session, SessionStateEvent};
-use project::{DebugScenarioContext, Fs, ProjectPath, WorktreeId};
+use project::debugger::session::{Session, SessionQuirks, SessionState, SessionStateEvent};
+use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId};
use project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self};
use settings::Settings;
@@ -33,10 +37,11 @@ use std::sync::{Arc, LazyLock};
use task::{DebugScenario, TaskContext};
use tree_sitter::{Query, StreamingIterator as _};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
-use util::maybe;
+use util::{ResultExt, debug_panic, maybe};
use workspace::SplitDirection;
+use workspace::item::SaveOptions;
use workspace::{
- Pane, Workspace,
+ Item, Pane, Workspace,
dock::{DockPosition, Panel, PanelEvent},
};
use zed_actions::ToggleFocus;
@@ -60,13 +65,14 @@ pub enum DebugPanelEvent {
pub struct DebugPanel {
size: Pixels,
- sessions: Vec<Entity<DebugSession>>,
active_session: Option<Entity<DebugSession>>,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
debug_scenario_scheduled_last: bool,
+ pub(crate) sessions_with_children:
+ IndexMap<Entity<DebugSession>, Vec<WeakEntity<DebugSession>>>,
pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
fs: Arc<dyn Fs>,
@@ -97,7 +103,7 @@ impl DebugPanel {
Self {
size: px(300.),
- sessions: vec![],
+ sessions_with_children: Default::default(),
active_session: None,
focus_handle,
breakpoint_list: BreakpointList::new(
@@ -135,8 +141,9 @@ impl DebugPanel {
});
}
- pub(crate) fn sessions(&self) -> Vec<Entity<DebugSession>> {
- self.sessions.clone()
+ #[cfg(test)]
+ pub(crate) fn sessions(&self) -> impl Iterator<Item = Entity<DebugSession>> {
+ self.sessions_with_children.keys().cloned()
}
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
@@ -182,12 +189,20 @@ impl DebugPanel {
cx: &mut Context<Self>,
) {
let dap_store = self.project.read(cx).dap_store();
+ let Some(adapter) = DapRegistry::global(cx).adapter(&scenario.adapter) else {
+ return;
+ };
+ let quirks = SessionQuirks {
+ compact: adapter.compact_child_session(),
+ prefer_thread_name: adapter.prefer_thread_name(),
+ };
let session = dap_store.update(cx, |dap_store, cx| {
dap_store.new_session(
- scenario.label.clone(),
+ Some(scenario.label.clone()),
DebugAdapterName(scenario.adapter.clone()),
task_context.clone(),
None,
+ quirks,
cx,
)
});
@@ -264,22 +279,34 @@ impl DebugPanel {
}
});
- cx.spawn(async move |_, cx| {
- if let Err(error) = task.await {
- log::error!("{error}");
- session
- .update(cx, |session, cx| {
- session
- .console_output(cx)
- .unbounded_send(format!("error: {}", error))
- .ok();
- session.shutdown(cx)
- })?
- .await;
+ let boot_task = cx.spawn({
+ let session = session.clone();
+
+ async move |_, cx| {
+ if let Err(error) = task.await {
+ log::error!("{error}");
+ session
+ .update(cx, |session, cx| {
+ session
+ .console_output(cx)
+ .unbounded_send(format!("error: {}", error))
+ .ok();
+ session.shutdown(cx)
+ })?
+ .await;
+ }
+ anyhow::Ok(())
}
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
+ });
+
+ session.update(cx, |session, _| match &mut session.mode {
+ SessionState::Building(state_task) => {
+ *state_task = Some(boot_task);
+ }
+ SessionState::Running(_) => {
+ debug_panic!("Session state should be in building because we are just starting it");
+ }
+ });
}
pub(crate) fn rerun_last_session(
@@ -360,17 +387,24 @@ impl DebugPanel {
};
let dap_store_handle = self.project.read(cx).dap_store().clone();
- let label = curr_session.read(cx).label().clone();
+ let label = curr_session.read(cx).label();
+ let quirks = curr_session.read(cx).quirks();
let adapter = curr_session.read(cx).adapter().clone();
let binary = curr_session.read(cx).binary().cloned().unwrap();
- let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
let task_context = curr_session.read(cx).task_context().clone();
+ let curr_session_id = curr_session.read(cx).session_id();
+ self.sessions_with_children
+ .retain(|session, _| session.read(cx).session_id(cx) != curr_session_id);
+ let task = dap_store_handle.update(cx, |dap_store, cx| {
+ dap_store.shutdown_session(curr_session_id, cx)
+ });
+
cx.spawn_in(window, async move |this, cx| {
- task.await;
+ task.await.log_err();
let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
- let session = dap_store.new_session(label, adapter, task_context, None, cx);
+ let session = dap_store.new_session(label, adapter, task_context, None, quirks, cx);
let task = session.update(cx, |session, cx| {
session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
@@ -416,6 +450,7 @@ impl DebugPanel {
let dap_store_handle = self.project.read(cx).dap_store().clone();
let label = self.label_for_child_session(&parent_session, request, cx);
let adapter = parent_session.read(cx).adapter().clone();
+ let quirks = parent_session.read(cx).quirks();
let Some(mut binary) = parent_session.read(cx).binary().cloned() else {
log::error!("Attempted to start a child-session without a binary");
return;
@@ -429,6 +464,7 @@ impl DebugPanel {
adapter,
task_context,
Some(parent_session.clone()),
+ quirks,
cx,
);
@@ -454,8 +490,8 @@ impl DebugPanel {
cx: &mut Context<Self>,
) {
let Some(session) = self
- .sessions
- .iter()
+ .sessions_with_children
+ .keys()
.find(|other| entity_id == other.entity_id())
.cloned()
else {
@@ -489,15 +525,14 @@ impl DebugPanel {
}
session.update(cx, |session, cx| session.shutdown(cx)).ok();
this.update(cx, |this, cx| {
- this.sessions.retain(|other| entity_id != other.entity_id());
-
+ this.retain_sessions(|other| entity_id != other.entity_id());
if let Some(active_session_id) = this
.active_session
.as_ref()
.map(|session| session.entity_id())
{
if active_session_id == entity_id {
- this.active_session = this.sessions.first().cloned();
+ this.active_session = this.sessions_with_children.keys().next().cloned();
}
}
cx.notify()
@@ -613,6 +648,14 @@ impl DebugPanel {
.on_click(move |_, _, cx| cx.open_url("https://zed.dev/docs/debugger"))
.tooltip(Tooltip::text("Open Documentation"))
};
+ let logs_button = || {
+ IconButton::new("debug-open-logs", IconName::ScrollText)
+ .icon_size(IconSize::Small)
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(debugger_tools::OpenDebugAdapterLogs.boxed_clone(), cx)
+ })
+ .tooltip(Tooltip::text("Open Debug Adapter Logs"))
+ };
Some(
div.border_b_1()
@@ -796,13 +839,24 @@ impl DebugPanel {
.on_click(window.listener_for(
&running_state,
|this, _, _window, cx| {
- this.stop_thread(cx);
+ if this.session().read(cx).is_building() {
+ this.session().update(cx, |session, cx| {
+ session.shutdown(cx).detach()
+ });
+ } else {
+ this.stop_thread(cx);
+ }
+ },
+ ))
+ .disabled(active_session.as_ref().is_none_or(
+ |session| {
+ session
+ .read(cx)
+ .session(cx)
+ .read(cx)
+ .is_terminated()
},
))
- .disabled(
- thread_status != ThreadStatus::Stopped
- && thread_status != ThreadStatus::Running,
- )
.tooltip({
let focus_handle = focus_handle.clone();
let label = if capabilities
@@ -864,6 +918,7 @@ impl DebugPanel {
.justify_around()
.when(is_side, |this| {
this.child(new_session_button())
+ .child(logs_button())
.child(documentation_button())
}),
)
@@ -913,6 +968,7 @@ impl DebugPanel {
))
.when(!is_side, |this| {
this.child(new_session_button())
+ .child(logs_button())
.child(documentation_button())
}),
),
@@ -957,8 +1013,8 @@ impl DebugPanel {
cx: &mut Context<Self>,
) {
if let Some(session) = self
- .sessions
- .iter()
+ .sessions_with_children
+ .keys()
.find(|session| session.read(cx).session_id(cx) == session_id)
{
self.activate_session(session.clone(), window, cx);
@@ -971,7 +1027,7 @@ impl DebugPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
- debug_assert!(self.sessions.contains(&session_item));
+ debug_assert!(self.sessions_with_children.contains_key(&session_item));
session_item.focus_handle(cx).focus(window);
session_item.update(cx, |this, cx| {
this.running_state().update(cx, |this, cx| {
@@ -982,13 +1038,90 @@ impl DebugPanel {
cx.notify();
}
+ pub(crate) fn go_to_scenario_definition(
+ &self,
+ kind: TaskSourceKind,
+ scenario: DebugScenario,
+ worktree_id: WorktreeId,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ let Some(workspace) = self.workspace.upgrade() else {
+ return Task::ready(Ok(()));
+ };
+ let project_path = match kind {
+ TaskSourceKind::AbsPath { abs_path, .. } => {
+ let Some(project_path) = workspace
+ .read(cx)
+ .project()
+ .read(cx)
+ .project_path_for_absolute_path(&abs_path, cx)
+ else {
+ return Task::ready(Err(anyhow!("no abs path")));
+ };
+
+ project_path
+ }
+ TaskSourceKind::Worktree {
+ id,
+ directory_in_worktree: dir,
+ ..
+ } => {
+ let relative_path = if dir.ends_with(".vscode") {
+ dir.join("launch.json")
+ } else {
+ dir.join("debug.json")
+ };
+ ProjectPath {
+ worktree_id: id,
+ path: Arc::from(relative_path),
+ }
+ }
+ _ => return self.save_scenario(scenario, worktree_id, window, cx),
+ };
+
+ let editor = workspace.update(cx, |workspace, cx| {
+ workspace.open_path(project_path, None, true, window, cx)
+ });
+ cx.spawn_in(window, async move |_, cx| {
+ let editor = editor.await?;
+ let editor = cx
+ .update(|_, cx| editor.act_as::<Editor>(cx))?
+ .context("expected editor")?;
+
+ // unfortunately debug tasks don't have an easy way to globally
+ // identify them. to jump to the one that you just created or an
+ // old one that you're choosing to edit we use a heuristic of searching for a line with `label: <your label>` from the end rather than the start so we bias towards more renctly
+ editor.update_in(cx, |editor, window, cx| {
+ let row = editor.text(cx).lines().enumerate().find_map(|(row, text)| {
+ if text.contains(scenario.label.as_ref()) && text.contains("\"label\": ") {
+ Some(row)
+ } else {
+ None
+ }
+ });
+ if let Some(row) = row {
+ editor.go_to_singleton_buffer_point(
+ text::Point::new(row as u32, 4),
+ window,
+ cx,
+ );
+ }
+ })?;
+
+ Ok(())
+ })
+ }
+
pub(crate) fn save_scenario(
&self,
- scenario: &DebugScenario,
+ scenario: DebugScenario,
worktree_id: WorktreeId,
window: &mut Window,
- cx: &mut App,
- ) -> Task<Result<ProjectPath>> {
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ let this = cx.weak_entity();
+ let project = self.project.clone();
self.workspace
.update(cx, |workspace, cx| {
let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
@@ -1021,47 +1154,7 @@ impl DebugPanel {
)
.await?;
}
-
- let mut content = fs.load(path).await?;
- let new_scenario = serde_json_lenient::to_string_pretty(&serialized_scenario)?
- .lines()
- .map(|l| format!(" {l}"))
- .join("\n");
-
- static ARRAY_QUERY: LazyLock<Query> = LazyLock::new(|| {
- Query::new(
- &tree_sitter_json::LANGUAGE.into(),
- "(document (array (object) @object))", // TODO: use "." anchor to only match last object
- )
- .expect("Failed to create ARRAY_QUERY")
- });
-
- let mut parser = tree_sitter::Parser::new();
- parser
- .set_language(&tree_sitter_json::LANGUAGE.into())
- .unwrap();
- let mut cursor = tree_sitter::QueryCursor::new();
- let syntax_tree = parser.parse(&content, None).unwrap();
- let mut matches =
- cursor.matches(&ARRAY_QUERY, syntax_tree.root_node(), content.as_bytes());
-
- // we don't have `.last()` since it's a lending iterator, so loop over
- // the whole thing to find the last one
- let mut last_offset = None;
- while let Some(mat) = matches.next() {
- if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end) {
- last_offset = Some(pos)
- }
- }
-
- if let Some(pos) = last_offset {
- content.insert_str(pos, &new_scenario);
- content.insert_str(pos, ",\n");
- }
-
- fs.write(path, content.as_bytes()).await?;
-
- workspace.update(cx, |workspace, cx| {
+ let project_path = workspace.update(cx, |workspace, cx| {
workspace
.project()
.read(cx)
@@ -1069,12 +1162,113 @@ impl DebugPanel {
.context(
"Couldn't get project path for .zed/debug.json in active worktree",
)
- })?
+ })??;
+
+ let editor = this
+ .update_in(cx, |this, window, cx| {
+ this.workspace.update(cx, |workspace, cx| {
+ workspace.open_path(project_path, None, true, window, cx)
+ })
+ })??
+ .await?;
+ let editor = cx
+ .update(|_, cx| editor.act_as::<Editor>(cx))?
+ .context("expected editor")?;
+
+ let new_scenario = serde_json_lenient::to_string_pretty(&serialized_scenario)?
+ .lines()
+ .map(|l| format!(" {l}"))
+ .join("\n");
+
+ editor
+ .update_in(cx, |editor, window, cx| {
+ Self::insert_task_into_editor(editor, new_scenario, project, window, cx)
+ })??
+ .await
})
})
.unwrap_or_else(|err| Task::ready(Err(err)))
}
+ pub fn insert_task_into_editor(
+ editor: &mut Editor,
+ new_scenario: String,
+ project: Entity<Project>,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) -> Result<Task<Result<()>>> {
+ static LAST_ITEM_QUERY: LazyLock<Query> = LazyLock::new(|| {
+ Query::new(
+ &tree_sitter_json::LANGUAGE.into(),
+ "(document (array (object) @object))", // TODO: use "." anchor to only match last object
+ )
+ .expect("Failed to create LAST_ITEM_QUERY")
+ });
+ static EMPTY_ARRAY_QUERY: LazyLock<Query> = LazyLock::new(|| {
+ Query::new(
+ &tree_sitter_json::LANGUAGE.into(),
+ "(document (array) @array)",
+ )
+ .expect("Failed to create EMPTY_ARRAY_QUERY")
+ });
+
+ let content = editor.text(cx);
+ let mut parser = tree_sitter::Parser::new();
+ parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
+ let mut cursor = tree_sitter::QueryCursor::new();
+ let syntax_tree = parser
+ .parse(&content, None)
+ .context("could not parse debug.json")?;
+ let mut matches = cursor.matches(
+ &LAST_ITEM_QUERY,
+ syntax_tree.root_node(),
+ content.as_bytes(),
+ );
+
+ let mut last_offset = None;
+ while let Some(mat) = matches.next() {
+ if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end) {
+ last_offset = Some(pos)
+ }
+ }
+ let mut edits = Vec::new();
+ let mut cursor_position = 0;
+
+ if let Some(pos) = last_offset {
+ edits.push((pos..pos, format!(",\n{new_scenario}")));
+ cursor_position = pos + ",\n ".len();
+ } else {
+ let mut matches = cursor.matches(
+ &EMPTY_ARRAY_QUERY,
+ syntax_tree.root_node(),
+ content.as_bytes(),
+ );
+
+ if let Some(mat) = matches.next() {
+ if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end - 1) {
+ edits.push((pos..pos, format!("\n{new_scenario}\n")));
+ cursor_position = pos + "\n ".len();
+ }
+ } else {
+ edits.push((0..0, format!("[\n{}\n]", new_scenario)));
+ cursor_position = "[\n ".len();
+ }
+ }
+ editor.transact(window, cx, |editor, window, cx| {
+ editor.edit(edits, cx);
+ let snapshot = editor
+ .buffer()
+ .read(cx)
+ .as_singleton()
+ .unwrap()
+ .read(cx)
+ .snapshot();
+ let point = cursor_position.to_point(&snapshot);
+ editor.go_to_singleton_buffer_point(point, window, cx);
+ });
+ Ok(editor.save(SaveOptions::default(), project, window, cx))
+ }
+
pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.thread_picker_menu_handle.toggle(window, cx);
}
@@ -1104,18 +1298,27 @@ impl DebugPanel {
parent_session: &Entity<Session>,
request: &StartDebuggingRequestArguments,
cx: &mut Context<'_, Self>,
- ) -> SharedString {
+ ) -> Option<SharedString> {
let adapter = parent_session.read(cx).adapter();
if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
if let Some(label) = adapter.label_for_child_session(request) {
- return label.into();
+ return Some(label.into());
}
}
- let mut label = parent_session.read(cx).label().clone();
- if !label.ends_with("(child)") {
- label = format!("{label} (child)").into();
+ None
+ }
+
+ fn retain_sessions(&mut self, keep: impl Fn(&Entity<DebugSession>) -> bool) {
+ self.sessions_with_children
+ .retain(|session, _| keep(session));
+ for children in self.sessions_with_children.values_mut() {
+ children.retain(|child| {
+ let Some(child) = child.upgrade() else {
+ return false;
+ };
+ keep(&child)
+ });
}
- label
}
}
@@ -1145,11 +1348,11 @@ async fn register_session_inner(
let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
let debug_session = this.update_in(cx, |this, window, cx| {
let parent_session = this
- .sessions
- .iter()
+ .sessions_with_children
+ .keys()
.find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx))
.cloned();
- this.sessions.retain(|session| {
+ this.retain_sessions(|session| {
!session
.read(cx)
.running_state()
@@ -1180,13 +1383,23 @@ async fn register_session_inner(
)
.detach();
let insert_position = this
- .sessions
- .iter()
+ .sessions_with_children
+ .keys()
.position(|session| Some(session) == parent_session.as_ref())
.map(|position| position + 1)
- .unwrap_or(this.sessions.len());
+ .unwrap_or(this.sessions_with_children.len());
// Maintain topological sort order of sessions
- this.sessions.insert(insert_position, debug_session.clone());
+ let (_, old) = this.sessions_with_children.insert_before(
+ insert_position,
+ debug_session.clone(),
+ Default::default(),
+ );
+ debug_assert!(old.is_none());
+ if let Some(parent_session) = parent_session {
+ this.sessions_with_children
+ .entry(parent_session)
+ .and_modify(|children| children.push(debug_session.downgrade()));
+ }
debug_session
})?;
@@ -1226,7 +1439,7 @@ impl Panel for DebugPanel {
cx: &mut Context<Self>,
) {
if position.axis() != self.position(window, cx).axis() {
- self.sessions.iter().for_each(|session_item| {
+ self.sessions_with_children.keys().for_each(|session_item| {
session_item.update(cx, |item, cx| {
item.running_state()
.update(cx, |state, _| state.invert_axies())
@@ -1298,9 +1511,7 @@ impl Panel for DebugPanel {
impl Render for DebugPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let has_sessions = self.sessions.len() > 0;
let this = cx.weak_entity();
- debug_assert_eq!(has_sessions, self.active_session.is_some());
if self
.active_session
@@ -1487,8 +1698,8 @@ impl Render for DebugPanel {
}))
})
.map(|this| {
- if has_sessions {
- this.children(self.active_session.clone())
+ if let Some(active_session) = self.active_session.clone() {
+ this.child(active_session)
} else {
let docked_to_bottom = self.position(window, cx) == DockPosition::Bottom;
let welcome_experience = v_flex()
@@ -1594,6 +1805,7 @@ impl Render for DebugPanel {
.child(breakpoint_list)
.child(Divider::vertical())
.child(welcome_experience)
+ .child(Divider::vertical())
} else {
this.items_end()
.child(welcome_experience)
@@ -32,36 +32,69 @@ pub mod tests;
actions!(
debugger,
[
+ /// Starts a new debugging session.
Start,
+ /// Continues execution until the next breakpoint.
Continue,
+ /// Detaches the debugger from the running process.
Detach,
+ /// Pauses the currently running program.
Pause,
+ /// Restarts the current debugging session.
Restart,
+ /// Reruns the current debugging session with the same configuration.
RerunSession,
+ /// Steps into the next function call.
StepInto,
+ /// Steps over the current line.
StepOver,
+ /// Steps out of the current function.
StepOut,
+ /// Steps back to the previous statement.
StepBack,
+ /// Stops the debugging session.
Stop,
+ /// Toggles whether to ignore all breakpoints.
ToggleIgnoreBreakpoints,
+ /// Clears all breakpoints in the project.
ClearAllBreakpoints,
+ /// Focuses on the debugger console panel.
FocusConsole,
+ /// Focuses on the variables panel.
FocusVariables,
+ /// Focuses on the breakpoint list panel.
FocusBreakpointList,
+ /// Focuses on the call stack frames panel.
FocusFrames,
+ /// Focuses on the loaded modules panel.
FocusModules,
+ /// Focuses on the loaded sources panel.
FocusLoadedSources,
+ /// Focuses on the terminal panel.
FocusTerminal,
+ /// Shows the stack trace for the current thread.
ShowStackTrace,
+ /// Toggles the thread picker dropdown.
ToggleThreadPicker,
+ /// Toggles the session picker dropdown.
ToggleSessionPicker,
+ /// Reruns the last debugging session.
#[action(deprecated_aliases = ["debugger::RerunLastSession"])]
Rerun,
+ /// Toggles expansion of the selected item in the debugger UI.
ToggleExpandItem,
+ /// Set a data breakpoint on the selected variable or memory region.
+ ToggleDataBreakpoint,
]
);
-actions!(dev, [CopyDebugAdapterArguments]);
+actions!(
+ dev,
+ [
+ /// Copies debug adapter launch arguments to clipboard.
+ CopyDebugAdapterArguments
+ ]
+);
pub fn init(cx: &mut App) {
DebuggerSettings::register(cx);
@@ -1,16 +1,82 @@
-use std::time::Duration;
+use std::{rc::Rc, time::Duration};
use collections::HashMap;
-use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};
+use gpui::{Animation, AnimationExt as _, Entity, Transformation, WeakEntity, percentage};
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
-use util::truncate_and_trailoff;
+use util::{maybe, truncate_and_trailoff};
use crate::{
debugger_panel::DebugPanel,
session::{DebugSession, running::RunningState},
};
+struct SessionListEntry {
+ ancestors: Vec<Entity<DebugSession>>,
+ leaf: Entity<DebugSession>,
+}
+
+impl SessionListEntry {
+ pub(crate) fn label_element(&self, depth: usize, cx: &mut App) -> AnyElement {
+ const MAX_LABEL_CHARS: usize = 150;
+
+ let mut label = String::new();
+ for ancestor in &self.ancestors {
+ label.push_str(&ancestor.update(cx, |ancestor, cx| {
+ ancestor.label(cx).unwrap_or("(child)".into())
+ }));
+ label.push_str(" » ");
+ }
+ label.push_str(
+ &self
+ .leaf
+ .update(cx, |leaf, cx| leaf.label(cx).unwrap_or("(child)".into())),
+ );
+ let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS);
+
+ let is_terminated = self
+ .leaf
+ .read(cx)
+ .running_state
+ .read(cx)
+ .session()
+ .read(cx)
+ .is_terminated();
+ let icon = {
+ if is_terminated {
+ Some(Indicator::dot().color(Color::Error))
+ } else {
+ match self
+ .leaf
+ .read(cx)
+ .running_state
+ .read(cx)
+ .thread_status(cx)
+ .unwrap_or_default()
+ {
+ project::debugger::session::ThreadStatus::Stopped => {
+ Some(Indicator::dot().color(Color::Conflict))
+ }
+ _ => Some(Indicator::dot().color(Color::Success)),
+ }
+ }
+ };
+
+ h_flex()
+ .id("session-label")
+ .ml(depth * px(16.0))
+ .gap_2()
+ .when_some(icon, |this, indicator| this.child(indicator))
+ .justify_between()
+ .child(
+ Label::new(label)
+ .size(LabelSize::Small)
+ .when(is_terminated, |this| this.strikethrough()),
+ )
+ .into_any_element()
+ }
+}
+
impl DebugPanel {
fn dropdown_label(label: impl Into<SharedString>) -> Label {
const MAX_LABEL_CHARS: usize = 50;
@@ -25,145 +91,205 @@ impl DebugPanel {
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
- if let Some(running_state) = running_state {
- let sessions = self.sessions().clone();
- let weak = cx.weak_entity();
- let running_state = running_state.read(cx);
- let label = if let Some(active_session) = active_session.clone() {
- active_session.read(cx).session(cx).read(cx).label()
- } else {
- SharedString::new_static("Unknown Session")
- };
+ let running_state = running_state?;
+
+ let mut session_entries = Vec::with_capacity(self.sessions_with_children.len() * 3);
+ let mut sessions_with_children = self.sessions_with_children.iter().peekable();
- let is_terminated = running_state.session().read(cx).is_terminated();
- let is_started = active_session
- .is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
-
- let session_state_indicator = if is_terminated {
- Indicator::dot().color(Color::Error).into_any_element()
- } else if !is_started {
- Icon::new(IconName::ArrowCircle)
- .size(IconSize::Small)
- .color(Color::Muted)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
- )
- .into_any_element()
+ while let Some((root, children)) = sessions_with_children.next() {
+ let root_entry = if let Ok([single_child]) = <&[_; 1]>::try_from(children.as_slice())
+ && let Some(single_child) = single_child.upgrade()
+ && single_child.read(cx).quirks.compact
+ {
+ sessions_with_children.next();
+ SessionListEntry {
+ leaf: single_child.clone(),
+ ancestors: vec![root.clone()],
+ }
} else {
- match running_state.thread_status(cx).unwrap_or_default() {
- ThreadStatus::Stopped => {
- Indicator::dot().color(Color::Conflict).into_any_element()
- }
- _ => Indicator::dot().color(Color::Success).into_any_element(),
+ SessionListEntry {
+ leaf: root.clone(),
+ ancestors: Vec::new(),
}
};
+ session_entries.push(root_entry);
+
+ session_entries.extend(
+ sessions_with_children
+ .by_ref()
+ .take_while(|(session, _)| {
+ session
+ .read(cx)
+ .session(cx)
+ .read(cx)
+ .parent_id(cx)
+ .is_some()
+ })
+ .map(|(session, _)| SessionListEntry {
+ leaf: session.clone(),
+ ancestors: vec![],
+ }),
+ );
+ }
- let trigger = h_flex()
- .gap_2()
- .child(session_state_indicator)
- .justify_between()
- .child(
- DebugPanel::dropdown_label(label)
- .when(is_terminated, |this| this.strikethrough()),
+ let weak = cx.weak_entity();
+ let trigger_label = if let Some(active_session) = active_session.clone() {
+ active_session.update(cx, |active_session, cx| {
+ active_session.label(cx).unwrap_or("(child)".into())
+ })
+ } else {
+ SharedString::new_static("Unknown Session")
+ };
+ let running_state = running_state.read(cx);
+
+ let is_terminated = running_state.session().read(cx).is_terminated();
+ let is_started = active_session
+ .is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
+
+ let session_state_indicator = if is_terminated {
+ Indicator::dot().color(Color::Error).into_any_element()
+ } else if !is_started {
+ Icon::new(IconName::ArrowCircle)
+ .size(IconSize::Small)
+ .color(Color::Muted)
+ .with_animation(
+ "arrow-circle",
+ Animation::new(Duration::from_secs(2)).repeat(),
+ |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
- .into_any_element();
+ .into_any_element()
+ } else {
+ match running_state.thread_status(cx).unwrap_or_default() {
+ ThreadStatus::Stopped => Indicator::dot().color(Color::Conflict).into_any_element(),
+ _ => Indicator::dot().color(Color::Success).into_any_element(),
+ }
+ };
- Some(
- DropdownMenu::new_with_element(
- "debugger-session-list",
- trigger,
- ContextMenu::build(window, cx, move |mut this, _, cx| {
- let context_menu = cx.weak_entity();
- let mut session_depths = HashMap::default();
- for session in sessions.into_iter() {
- let weak_session = session.downgrade();
- let weak_session_id = weak_session.entity_id();
- let session_id = session.read(cx).session_id(cx);
- let parent_depth = session
- .read(cx)
- .session(cx)
- .read(cx)
- .parent_id(cx)
- .and_then(|parent_id| session_depths.get(&parent_id).cloned());
- let self_depth =
- *session_depths.entry(session_id).or_insert_with(|| {
- parent_depth.map(|depth| depth + 1).unwrap_or(0usize)
- });
- this = this.custom_entry(
- {
- let weak = weak.clone();
- let context_menu = context_menu.clone();
- move |_, cx| {
- weak_session
- .read_with(cx, |session, cx| {
- let context_menu = context_menu.clone();
-
- let id: SharedString =
- format!("debug-session-{}", session_id.0)
- .into();
-
- h_flex()
- .w_full()
- .group(id.clone())
- .justify_between()
- .child(session.label_element(self_depth, cx))
- .child(
- IconButton::new(
- "close-debug-session",
- IconName::Close,
- )
- .visible_on_hover(id.clone())
- .icon_size(IconSize::Small)
- .on_click({
- let weak = weak.clone();
- move |_, window, cx| {
- weak.update(cx, |panel, cx| {
- panel.close_session(
- weak_session_id,
- window,
- cx,
- );
- })
- .ok();
- context_menu
- .update(cx, |this, cx| {
- this.cancel(
- &Default::default(),
- window,
- cx,
- );
- })
- .ok();
- }
- }),
- )
- .into_any_element()
- })
- .unwrap_or_else(|_| div().into_any_element())
- }
- },
- {
- let weak = weak.clone();
- move |window, cx| {
- weak.update(cx, |panel, cx| {
- panel.activate_session(session.clone(), window, cx);
- })
- .ok();
- }
- },
- );
+ let trigger = h_flex()
+ .gap_2()
+ .child(session_state_indicator)
+ .justify_between()
+ .child(
+ DebugPanel::dropdown_label(trigger_label)
+ .when(is_terminated, |this| this.strikethrough()),
+ )
+ .into_any_element();
+
+ let menu = DropdownMenu::new_with_element(
+ "debugger-session-list",
+ trigger,
+ ContextMenu::build(window, cx, move |mut this, _, cx| {
+ let context_menu = cx.weak_entity();
+ let mut session_depths = HashMap::default();
+ for session_entry in session_entries {
+ let session_id = session_entry.leaf.read(cx).session_id(cx);
+ let parent_depth = session_entry
+ .ancestors
+ .first()
+ .unwrap_or(&session_entry.leaf)
+ .read(cx)
+ .session(cx)
+ .read(cx)
+ .parent_id(cx)
+ .and_then(|parent_id| session_depths.get(&parent_id).cloned());
+ let self_depth = *session_depths
+ .entry(session_id)
+ .or_insert_with(|| parent_depth.map(|depth| depth + 1).unwrap_or(0usize));
+ this = this.custom_entry(
+ {
+ let weak = weak.clone();
+ let context_menu = context_menu.clone();
+ let ancestors: Rc<[_]> = session_entry
+ .ancestors
+ .iter()
+ .map(|session| session.downgrade())
+ .collect();
+ let leaf = session_entry.leaf.downgrade();
+ move |window, cx| {
+ Self::render_session_menu_entry(
+ weak.clone(),
+ context_menu.clone(),
+ ancestors.clone(),
+ leaf.clone(),
+ self_depth,
+ window,
+ cx,
+ )
+ }
+ },
+ {
+ let weak = weak.clone();
+ let leaf = session_entry.leaf.clone();
+ move |window, cx| {
+ weak.update(cx, |panel, cx| {
+ panel.activate_session(leaf.clone(), window, cx);
+ })
+ .ok();
+ }
+ },
+ );
+ }
+ this
+ }),
+ )
+ .style(DropdownStyle::Ghost)
+ .handle(self.session_picker_menu_handle.clone());
+
+ Some(menu)
+ }
+
+ fn render_session_menu_entry(
+ weak: WeakEntity<DebugPanel>,
+ context_menu: WeakEntity<ContextMenu>,
+ ancestors: Rc<[WeakEntity<DebugSession>]>,
+ leaf: WeakEntity<DebugSession>,
+ self_depth: usize,
+ _window: &mut Window,
+ cx: &mut App,
+ ) -> AnyElement {
+ let Some(session_entry) = maybe!({
+ let ancestors = ancestors
+ .iter()
+ .map(|ancestor| ancestor.upgrade())
+ .collect::<Option<Vec<_>>>()?;
+ let leaf = leaf.upgrade()?;
+ Some(SessionListEntry { ancestors, leaf })
+ }) else {
+ return div().into_any_element();
+ };
+
+ let id: SharedString = format!(
+ "debug-session-{}",
+ session_entry.leaf.read(cx).session_id(cx).0
+ )
+ .into();
+ let session_entity_id = session_entry.leaf.entity_id();
+
+ h_flex()
+ .w_full()
+ .group(id.clone())
+ .justify_between()
+ .child(session_entry.label_element(self_depth, cx))
+ .child(
+ IconButton::new("close-debug-session", IconName::Close)
+ .visible_on_hover(id.clone())
+ .icon_size(IconSize::Small)
+ .on_click({
+ let weak = weak.clone();
+ move |_, window, cx| {
+ weak.update(cx, |panel, cx| {
+ panel.close_session(session_entity_id, window, cx);
+ })
+ .ok();
+ context_menu
+ .update(cx, |this, cx| {
+ this.cancel(&Default::default(), window, cx);
+ })
+ .ok();
}
- this
}),
- )
- .style(DropdownStyle::Ghost)
- .handle(self.session_picker_menu_handle.clone()),
)
- } else {
- None
- }
+ .into_any_element()
}
pub(crate) fn render_thread_dropdown(
@@ -1,12 +1,10 @@
-use anyhow::bail;
+use anyhow::{Context as _, bail};
use collections::{FxHashMap, HashMap};
use language::LanguageRegistry;
-use paths::local_debug_file_relative_path;
use std::{
borrow::Cow,
path::{Path, PathBuf},
sync::Arc,
- time::Duration,
usize,
};
use tasks_ui::{TaskOverrides, TasksModal};
@@ -18,35 +16,27 @@ use editor::{Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
- HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText,
- Subscription, Task, TextStyle, UnderlineStyle, WeakEntity,
+ KeyContext, Render, Subscription, Task, TextStyle, WeakEntity,
};
use itertools::Itertools as _;
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
-use project::{
- DebugScenarioContext, ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore,
-};
-use settings::{Settings, initial_local_debug_tasks_content};
+use project::{DebugScenarioContext, TaskContexts, TaskSourceKind, task_store::TaskStore};
+use settings::Settings;
use task::{DebugScenario, RevealTarget, ZedDebugConfig};
use theme::ThemeSettings;
use ui::{
- ActiveTheme, CheckboxWithLabel, Clickable, Context, ContextMenu, Disableable, DropdownMenu,
- FluentBuilder, IconWithIndicator, Indicator, IntoElement, KeyBinding, ListItem,
- ListItemSpacing, ParentElement, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip,
- Window, div, prelude::*, px, relative, rems,
+ ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
+ ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
+ IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label,
+ LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce,
+ SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div,
+ h_flex, relative, rems, v_flex,
};
use util::ResultExt;
-use workspace::{ModalView, Workspace, pane};
+use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane};
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
-#[allow(unused)]
-enum SaveScenarioState {
- Saving,
- Saved((ProjectPath, SharedString)),
- Failed(SharedString),
-}
-
pub(super) struct NewProcessModal {
workspace: WeakEntity<Workspace>,
debug_panel: WeakEntity<DebugPanel>,
@@ -56,7 +46,6 @@ pub(super) struct NewProcessModal {
configure_mode: Entity<ConfigureMode>,
task_mode: TaskMode,
debugger: Option<DebugAdapterName>,
- save_scenario_state: Option<SaveScenarioState>,
_subscriptions: [Subscription; 3],
}
@@ -268,7 +257,6 @@ impl NewProcessModal {
mode,
debug_panel: debug_panel.downgrade(),
workspace: workspace_handle,
- save_scenario_state: None,
_subscriptions,
}
});
@@ -420,63 +408,29 @@ impl NewProcessModal {
self.debug_picker.read(cx).delegate.task_contexts.clone()
}
- fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- let task_contents = self.task_contexts(cx);
+ pub fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ let task_contexts = self.task_contexts(cx);
let Some(adapter) = self.debugger.as_ref() else {
return;
};
let scenario = self.debug_scenario(&adapter, cx);
-
- self.save_scenario_state = Some(SaveScenarioState::Saving);
-
cx.spawn_in(window, async move |this, cx| {
- let Some((scenario, worktree_id)) = scenario
- .await
- .zip(task_contents.and_then(|tcx| tcx.worktree()))
- else {
- this.update(cx, |this, _| {
- this.save_scenario_state = Some(SaveScenarioState::Failed(
- "Couldn't get scenario or task contents".into(),
- ))
- })
- .ok();
- return;
- };
-
- let Some(save_scenario) = this
- .update_in(cx, |this, window, cx| {
- this.debug_panel
- .update(cx, |panel, cx| {
- panel.save_scenario(&scenario, worktree_id, window, cx)
- })
- .ok()
+ let scenario = scenario.await.context("no scenario to save")?;
+ let worktree_id = task_contexts
+ .context("no task contexts")?
+ .worktree()
+ .context("no active worktree")?;
+ this.update_in(cx, |this, window, cx| {
+ this.debug_panel.update(cx, |panel, cx| {
+ panel.save_scenario(scenario, worktree_id, window, cx)
})
- .ok()
- .flatten()
- else {
- return;
- };
- let res = save_scenario.await;
-
- this.update(cx, |this, _| match res {
- Ok(saved_file) => {
- this.save_scenario_state = Some(SaveScenarioState::Saved((
- saved_file,
- scenario.label.clone(),
- )))
- }
- Err(error) => {
- this.save_scenario_state =
- Some(SaveScenarioState::Failed(error.to_string().into()))
- }
+ })??
+ .await?;
+ this.update_in(cx, |_, _, cx| {
+ cx.emit(DismissEvent);
})
- .ok();
-
- cx.background_executor().timer(Duration::from_secs(3)).await;
- this.update(cx, |this, _| this.save_scenario_state.take())
- .ok();
})
- .detach();
+ .detach_and_prompt_err("Failed to edit debug.json", window, cx, |_, _, _| None);
}
fn adapter_drop_down_menu(
@@ -544,70 +498,6 @@ impl NewProcessModal {
}),
)
}
-
- fn open_debug_json(&self, window: &mut Window, cx: &mut Context<NewProcessModal>) {
- let this = cx.entity();
- window
- .spawn(cx, async move |cx| {
- let worktree_id = this.update(cx, |this, cx| {
- let tcx = this.task_contexts(cx);
- tcx?.worktree()
- })?;
-
- let Some(worktree_id) = worktree_id else {
- let _ = cx.prompt(
- PromptLevel::Critical,
- "Cannot open debug.json",
- Some("You must have at least one project open"),
- &[PromptButton::ok("Ok")],
- );
- return Ok(());
- };
-
- let editor = this
- .update_in(cx, |this, window, cx| {
- this.workspace.update(cx, |workspace, cx| {
- workspace.open_path(
- ProjectPath {
- worktree_id,
- path: local_debug_file_relative_path().into(),
- },
- None,
- true,
- window,
- cx,
- )
- })
- })??
- .await?;
-
- cx.update(|_window, cx| {
- if let Some(editor) = editor.act_as::<Editor>(cx) {
- editor.update(cx, |editor, cx| {
- editor.buffer().update(cx, |buffer, cx| {
- if let Some(singleton) = buffer.as_singleton() {
- singleton.update(cx, |buffer, cx| {
- if buffer.is_empty() {
- buffer.edit(
- [(0..0, initial_local_debug_tasks_content())],
- None,
- cx,
- );
- }
- })
- }
- })
- });
- }
- })
- .ok();
-
- this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
-
- anyhow::Ok(())
- })
- .detach();
- }
}
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
@@ -812,39 +702,21 @@ impl Render for NewProcessModal {
NewProcessMode::Launch => el.child(
container
.child(
- h_flex()
- .text_ui_sm(cx)
- .text_color(Color::Muted.color(cx))
- .child(
- InteractiveText::new(
- "open-debug-json",
- StyledText::new(
- "Open .zed/debug.json for advanced configuration.",
- )
- .with_highlights([(
- 5..20,
- HighlightStyle {
- underline: Some(UnderlineStyle {
- thickness: px(1.0),
- color: None,
- wavy: false,
- }),
- ..Default::default()
- },
- )]),
- )
- .on_click(
- vec![5..20],
- {
- let this = cx.entity();
- move |_, window, cx| {
- this.update(cx, |this, cx| {
- this.open_debug_json(window, cx);
- })
- }
- },
+ h_flex().child(
+ Button::new("edit-custom-debug", "Edit in debug.json")
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.save_debug_scenario(window, cx);
+ }))
+ .disabled(
+ self.debugger.is_none()
+ || self
+ .configure_mode
+ .read(cx)
+ .program
+ .read(cx)
+ .is_empty(cx),
),
- ),
+ ),
)
.child(
Button::new("debugger-spawn", "Start")
@@ -862,29 +734,48 @@ impl Render for NewProcessModal {
),
),
),
- NewProcessMode::Attach => el.child(
+ NewProcessMode::Attach => el.child({
+ let disabled = self.debugger.is_none()
+ || self
+ .attach_mode
+ .read(cx)
+ .attach_picker
+ .read(cx)
+ .picker
+ .read(cx)
+ .delegate
+ .match_count()
+ == 0;
+ let secondary_action = menu::SecondaryConfirm.boxed_clone();
container
- .child(div().child(self.adapter_drop_down_menu(window, cx)))
+ .child(div().children(
+ KeyBinding::for_action(&*secondary_action, window, cx).map(
+ |keybind| {
+ Button::new("edit-attach-task", "Edit in debug.json")
+ .label_size(LabelSize::Small)
+ .key_binding(keybind)
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(
+ secondary_action.boxed_clone(),
+ cx,
+ )
+ })
+ .disabled(disabled)
+ },
+ ),
+ ))
.child(
- Button::new("debugger-spawn", "Start")
- .on_click(cx.listener(|this, _, window, cx| {
- this.start_new_session(window, cx)
- }))
- .disabled(
- self.debugger.is_none()
- || self
- .attach_mode
- .read(cx)
- .attach_picker
- .read(cx)
- .picker
- .read(cx)
- .delegate
- .match_count()
- == 0,
+ h_flex()
+ .child(div().child(self.adapter_drop_down_menu(window, cx)))
+ .child(
+ Button::new("debugger-spawn", "Start")
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.start_new_session(window, cx)
+ }))
+ .disabled(disabled),
),
- ),
- ),
+ )
+ }),
NewProcessMode::Debug => el,
NewProcessMode::Task => el,
}
@@ -1048,25 +939,6 @@ impl ConfigureMode {
)
.checkbox_position(ui::IconPosition::End),
)
- .child(
- CheckboxWithLabel::new(
- "debugger-save-to-debug-json",
- Label::new("Save to debug.json")
- .size(LabelSize::Small)
- .color(Color::Muted),
- self.save_to_debug_json,
- {
- let this = cx.weak_entity();
- move |state, _, cx| {
- this.update(cx, |this, _| {
- this.save_to_debug_json = *state;
- })
- .ok();
- }
- },
- )
- .checkbox_position(ui::IconPosition::End),
- )
}
}
@@ -1329,12 +1201,7 @@ impl PickerDelegate for DebugDelegate {
}
}
- fn confirm_input(
- &mut self,
- _secondary: bool,
- window: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) {
+ fn confirm_input(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let text = self.prompt.clone();
let (task_context, worktree_id) = self
.task_contexts
@@ -1364,7 +1231,7 @@ impl PickerDelegate for DebugDelegate {
let args = args.collect::<Vec<_>>();
let task = task::TaskTemplate {
- label: "one-off".to_owned(),
+ label: "one-off".to_owned(), // TODO: rename using command as label
env,
command: program,
args,
@@ -1405,7 +1272,11 @@ impl PickerDelegate for DebugDelegate {
.background_spawn(async move {
for locator in locators {
if let Some(scenario) =
- locator.1.create_scenario(&task, "one-off", &adapter).await
+ // TODO: use a more informative label than "one-off"
+ locator
+ .1
+ .create_scenario(&task, &task.label, &adapter)
+ .await
{
return Some(scenario);
}
@@ -1439,13 +1310,18 @@ impl PickerDelegate for DebugDelegate {
.detach();
}
- fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
+ fn confirm(
+ &mut self,
+ secondary: bool,
+ window: &mut Window,
+ cx: &mut Context<picker::Picker<Self>>,
+ ) {
let debug_scenario = self
.matches
.get(self.selected_index())
.and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
- let Some((_, debug_scenario, context)) = debug_scenario else {
+ let Some((kind, debug_scenario, context)) = debug_scenario else {
return;
};
@@ -1463,24 +1339,38 @@ impl PickerDelegate for DebugDelegate {
});
let DebugScenarioContext {
task_context,
- active_buffer,
+ active_buffer: _,
worktree_id,
} = context;
- let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade());
-
- send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
- self.debug_panel
- .update(cx, |panel, cx| {
- panel.start_session(
- debug_scenario,
- task_context,
- active_buffer,
- worktree_id,
- window,
- cx,
- );
+
+ if secondary {
+ let Some(kind) = kind else { return };
+ let Some(id) = worktree_id else { return };
+ let debug_panel = self.debug_panel.clone();
+ cx.spawn_in(window, async move |_, cx| {
+ debug_panel
+ .update_in(cx, |debug_panel, window, cx| {
+ debug_panel.go_to_scenario_definition(kind, debug_scenario, id, window, cx)
+ })?
+ .await?;
+ anyhow::Ok(())
})
- .ok();
+ .detach();
+ } else {
+ send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
+ self.debug_panel
+ .update(cx, |panel, cx| {
+ panel.start_session(
+ debug_scenario,
+ task_context,
+ None,
+ worktree_id,
+ window,
+ cx,
+ );
+ })
+ .ok();
+ }
cx.emit(DismissEvent);
}
@@ -1498,19 +1388,23 @@ impl PickerDelegate for DebugDelegate {
let footer = h_flex()
.w_full()
.p_1p5()
- .justify_end()
+ .justify_between()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
- // .child(
- // // TODO: add button to open selected task in debug.json
- // h_flex().into_any_element(),
- // )
+ .children({
+ let action = menu::SecondaryConfirm.boxed_clone();
+ KeyBinding::for_action(&*action, window, cx).map(|keybind| {
+ Button::new("edit-debug-task", "Edit in debug.json")
+ .label_size(LabelSize::Small)
+ .key_binding(keybind)
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(action.boxed_clone(), cx)
+ })
+ })
+ })
.map(|this| {
if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
- let action = picker::ConfirmInput {
- secondary: current_modifiers.secondary(),
- }
- .boxed_clone();
+ let action = picker::ConfirmInput { secondary: false }.boxed_clone();
this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
Button::new("launch-custom", "Launch Custom")
.key_binding(keybind)
@@ -1607,3 +1501,35 @@ pub(crate) fn resolve_path(path: &mut String) {
);
};
}
+
+#[cfg(test)]
+impl NewProcessModal {
+ pub(crate) fn set_configure(
+ &mut self,
+ program: impl AsRef<str>,
+ cwd: impl AsRef<str>,
+ stop_on_entry: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.mode = NewProcessMode::Launch;
+ self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into()));
+
+ self.configure_mode.update(cx, |configure, cx| {
+ configure.program.update(cx, |editor, cx| {
+ editor.clear(window, cx);
+ editor.set_text(program.as_ref(), window, cx);
+ });
+
+ configure.cwd.update(cx, |editor, cx| {
+ editor.clear(window, cx);
+ editor.set_text(cwd.as_ref(), window, cx);
+ });
+
+ configure.stop_on_entry = match stop_on_entry {
+ true => ToggleState::Selected,
+ _ => ToggleState::Unselected,
+ }
+ })
+ }
+}
@@ -11,7 +11,7 @@ use workspace::{Member, Pane, PaneAxis, Workspace};
use crate::session::running::{
self, DebugTerminal, RunningState, SubView, breakpoint_list::BreakpointList, console::Console,
- loaded_source_list::LoadedSourceList, module_list::ModuleList,
+ loaded_source_list::LoadedSourceList, memory_view::MemoryView, module_list::ModuleList,
stack_frame_list::StackFrameList, variable_list::VariableList,
};
@@ -24,6 +24,7 @@ pub(crate) enum DebuggerPaneItem {
Modules,
LoadedSources,
Terminal,
+ MemoryView,
}
impl DebuggerPaneItem {
@@ -36,6 +37,7 @@ impl DebuggerPaneItem {
DebuggerPaneItem::Modules,
DebuggerPaneItem::LoadedSources,
DebuggerPaneItem::Terminal,
+ DebuggerPaneItem::MemoryView,
];
VARIANTS
}
@@ -43,6 +45,9 @@ impl DebuggerPaneItem {
pub(crate) fn is_supported(&self, capabilities: &Capabilities) -> bool {
match self {
DebuggerPaneItem::Modules => capabilities.supports_modules_request.unwrap_or_default(),
+ DebuggerPaneItem::MemoryView => capabilities
+ .supports_read_memory_request
+ .unwrap_or_default(),
DebuggerPaneItem::LoadedSources => capabilities
.supports_loaded_sources_request
.unwrap_or_default(),
@@ -59,6 +64,7 @@ impl DebuggerPaneItem {
DebuggerPaneItem::Modules => SharedString::new_static("Modules"),
DebuggerPaneItem::LoadedSources => SharedString::new_static("Sources"),
DebuggerPaneItem::Terminal => SharedString::new_static("Terminal"),
+ DebuggerPaneItem::MemoryView => SharedString::new_static("Memory View"),
}
}
pub(crate) fn tab_tooltip(self) -> SharedString {
@@ -80,6 +86,7 @@ impl DebuggerPaneItem {
DebuggerPaneItem::Terminal => {
"Provides an interactive terminal session within the debugging environment."
}
+ DebuggerPaneItem::MemoryView => "Allows inspection of memory contents.",
};
SharedString::new_static(tooltip)
}
@@ -204,6 +211,7 @@ pub(crate) fn deserialize_pane_layout(
breakpoint_list: &Entity<BreakpointList>,
loaded_sources: &Entity<LoadedSourceList>,
terminal: &Entity<DebugTerminal>,
+ memory_view: &Entity<MemoryView>,
subscriptions: &mut HashMap<EntityId, Subscription>,
window: &mut Window,
cx: &mut Context<RunningState>,
@@ -228,6 +236,7 @@ pub(crate) fn deserialize_pane_layout(
breakpoint_list,
loaded_sources,
terminal,
+ memory_view,
subscriptions,
window,
cx,
@@ -298,6 +307,12 @@ pub(crate) fn deserialize_pane_layout(
DebuggerPaneItem::Terminal,
cx,
)),
+ DebuggerPaneItem::MemoryView => Box::new(SubView::new(
+ memory_view.focus_handle(cx),
+ memory_view.clone().into(),
+ DebuggerPaneItem::MemoryView,
+ cx,
+ )),
})
.collect();
@@ -5,14 +5,13 @@ use dap::client::SessionId;
use gpui::{
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
};
-use project::Project;
use project::debugger::session::Session;
use project::worktree_store::WorktreeStore;
+use project::{Project, debugger::session::SessionQuirks};
use rpc::proto;
use running::RunningState;
-use std::{cell::OnceCell, sync::OnceLock};
-use ui::{Indicator, Tooltip, prelude::*};
-use util::truncate_and_trailoff;
+use std::cell::OnceCell;
+use ui::prelude::*;
use workspace::{
CollaboratorId, FollowableItem, ViewId, Workspace,
item::{self, Item},
@@ -20,8 +19,8 @@ use workspace::{
pub struct DebugSession {
remote_id: Option<workspace::ViewId>,
- running_state: Entity<RunningState>,
- label: OnceLock<SharedString>,
+ pub(crate) running_state: Entity<RunningState>,
+ pub(crate) quirks: SessionQuirks,
stack_trace_view: OnceCell<Entity<StackTraceView>>,
_worktree_store: WeakEntity<WorktreeStore>,
workspace: WeakEntity<Workspace>,
@@ -57,6 +56,7 @@ impl DebugSession {
cx,
)
});
+ let quirks = session.read(cx).quirks();
cx.new(|cx| Self {
_subscriptions: [cx.subscribe(&running_state, |_, _, _, cx| {
@@ -64,7 +64,7 @@ impl DebugSession {
})],
remote_id: None,
running_state,
- label: OnceLock::new(),
+ quirks,
stack_trace_view: OnceCell::new(),
_worktree_store: project.read(cx).worktree_store().downgrade(),
workspace,
@@ -110,65 +110,28 @@ impl DebugSession {
.update(cx, |state, cx| state.shutdown(cx));
}
- pub(crate) fn label(&self, cx: &App) -> SharedString {
- if let Some(label) = self.label.get() {
- return label.clone();
- }
-
- let session = self.running_state.read(cx).session();
-
- self.label
- .get_or_init(|| session.read(cx).label())
- .to_owned()
- }
-
- pub(crate) fn running_state(&self) -> &Entity<RunningState> {
- &self.running_state
- }
-
- pub(crate) fn label_element(&self, depth: usize, cx: &App) -> AnyElement {
- const MAX_LABEL_CHARS: usize = 150;
-
- let label = self.label(cx);
- let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS);
-
- let is_terminated = self
- .running_state
- .read(cx)
- .session()
- .read(cx)
- .is_terminated();
- let icon = {
- if is_terminated {
- Some(Indicator::dot().color(Color::Error))
- } else {
- match self
- .running_state
- .read(cx)
- .thread_status(cx)
- .unwrap_or_default()
- {
- project::debugger::session::ThreadStatus::Stopped => {
- Some(Indicator::dot().color(Color::Conflict))
- }
- _ => Some(Indicator::dot().color(Color::Success)),
+ pub(crate) fn label(&self, cx: &mut App) -> Option<SharedString> {
+ let session = self.running_state.read(cx).session().clone();
+ session.update(cx, |session, cx| {
+ let session_label = session.label();
+ let quirks = session.quirks();
+ let mut single_thread_name = || {
+ let threads = session.threads(cx);
+ match threads.as_slice() {
+ [(thread, _)] => Some(SharedString::from(&thread.name)),
+ _ => None,
}
+ };
+ if quirks.prefer_thread_name {
+ single_thread_name().or(session_label)
+ } else {
+ session_label.or_else(single_thread_name)
}
- };
+ })
+ }
- h_flex()
- .id("session-label")
- .tooltip(Tooltip::text(format!("Session {}", self.session_id(cx).0,)))
- .ml(depth * px(16.0))
- .gap_2()
- .when_some(icon, |this, indicator| this.child(indicator))
- .justify_between()
- .child(
- Label::new(label)
- .size(LabelSize::Small)
- .when(is_terminated, |this| this.strikethrough()),
- )
- .into_any_element()
+ pub fn running_state(&self) -> &Entity<RunningState> {
+ &self.running_state
}
}
@@ -1,16 +1,17 @@
pub(crate) mod breakpoint_list;
pub(crate) mod console;
pub(crate) mod loaded_source_list;
+pub(crate) mod memory_view;
pub(crate) mod module_list;
pub mod stack_frame_list;
pub mod variable_list;
-
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
use crate::{
ToggleExpandItem,
new_process_modal::resolve_path,
persistence::{self, DebuggerPaneItem, SerializedLayout},
+ session::running::memory_view::MemoryView,
};
use super::DebugPanelItemEvent;
@@ -34,7 +35,7 @@ use loaded_source_list::LoadedSourceList;
use module_list::ModuleList;
use project::{
DebugScenarioContext, Project, WorktreeId,
- debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
+ debugger::session::{self, Session, SessionEvent, SessionStateEvent, ThreadId, ThreadStatus},
terminals::TerminalKind,
};
use rpc::proto::ViewId;
@@ -81,6 +82,7 @@ pub struct RunningState {
_schedule_serialize: Option<Task<()>>,
pub(crate) scenario: Option<DebugScenario>,
pub(crate) scenario_context: Option<DebugScenarioContext>,
+ memory_view: Entity<MemoryView>,
}
impl RunningState {
@@ -676,14 +678,36 @@ impl RunningState {
let session_id = session.read(cx).session_id();
let weak_state = cx.weak_entity();
let stack_frame_list = cx.new(|cx| {
- StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
+ StackFrameList::new(
+ workspace.clone(),
+ session.clone(),
+ weak_state.clone(),
+ window,
+ cx,
+ )
});
let debug_terminal =
parent_terminal.unwrap_or_else(|| cx.new(|cx| DebugTerminal::empty(window, cx)));
-
- let variable_list =
- cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
+ let memory_view = cx.new(|cx| {
+ MemoryView::new(
+ session.clone(),
+ workspace.clone(),
+ stack_frame_list.downgrade(),
+ window,
+ cx,
+ )
+ });
+ let variable_list = cx.new(|cx| {
+ VariableList::new(
+ session.clone(),
+ stack_frame_list.clone(),
+ memory_view.clone(),
+ weak_state.clone(),
+ window,
+ cx,
+ )
+ });
let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
@@ -770,6 +794,15 @@ impl RunningState {
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
this.serialize_layout(window, cx);
}),
+ cx.subscribe(
+ &session,
+ |this, session, event: &SessionStateEvent, cx| match event {
+ SessionStateEvent::Shutdown if session.read(cx).is_building() => {
+ this.shutdown(cx);
+ }
+ _ => {}
+ },
+ ),
];
let mut pane_close_subscriptions = HashMap::default();
@@ -786,6 +819,7 @@ impl RunningState {
&breakpoint_list,
&loaded_source_list,
&debug_terminal,
+ &memory_view,
&mut pane_close_subscriptions,
window,
cx,
@@ -814,6 +848,7 @@ impl RunningState {
let active_pane = panes.first_pane();
Self {
+ memory_view,
session,
workspace,
focus_handle,
@@ -884,6 +919,7 @@ impl RunningState {
let weak_project = project.downgrade();
let weak_workspace = workspace.downgrade();
let is_local = project.read(cx).is_local();
+
cx.spawn_in(window, async move |this, cx| {
let DebugScenario {
adapter,
@@ -973,7 +1009,7 @@ impl RunningState {
let task_with_shell = SpawnInTerminal {
command_label,
- command,
+ command: Some(command),
args,
..task.resolved.clone()
};
@@ -1085,19 +1121,6 @@ impl RunningState {
.map(PathBuf::from)
.or_else(|| session.binary().unwrap().cwd.clone());
- let mut args = request.args.clone();
-
- // Handle special case for NodeJS debug adapter
- // If only the Node binary path is provided, we set the command to None
- // This prevents the NodeJS REPL from appearing, which is not the desired behavior
- // The expected usage is for users to provide their own Node command, e.g., `node test.js`
- // This allows the NodeJS debug client to attach correctly
- let command = if args.len() > 1 {
- Some(args.remove(0))
- } else {
- None
- };
-
let mut envs: HashMap<String, String> =
self.session.read(cx).task_context().project_env.clone();
if let Some(Value::Object(env)) = &request.env {
@@ -1111,32 +1134,58 @@ impl RunningState {
}
}
- let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
- let kind = if let Some(command) = command {
- let title = request.title.clone().unwrap_or(command.clone());
- TerminalKind::Task(task::SpawnInTerminal {
- id: task::TaskId("debug".to_string()),
- full_label: title.clone(),
- label: title.clone(),
- command: command.clone(),
- args,
- command_label: title.clone(),
- cwd,
- env: envs,
- use_new_terminal: true,
- allow_concurrent_runs: true,
- reveal: task::RevealStrategy::NoFocus,
- reveal_target: task::RevealTarget::Dock,
- hide: task::HideStrategy::Never,
- shell,
- show_summary: false,
- show_command: false,
- show_rerun: false,
- })
+ let mut args = request.args.clone();
+ let command = if envs.contains_key("VSCODE_INSPECTOR_OPTIONS") {
+ // Handle special case for NodeJS debug adapter
+ // If the Node binary path is provided (possibly with arguments like --experimental-network-inspection),
+ // we set the command to None
+ // This prevents the NodeJS REPL from appearing, which is not the desired behavior
+ // The expected usage is for users to provide their own Node command, e.g., `node test.js`
+ // This allows the NodeJS debug client to attach correctly
+ if args
+ .iter()
+ .filter(|arg| !arg.starts_with("--"))
+ .collect::<Vec<_>>()
+ .len()
+ > 1
+ {
+ Some(args.remove(0))
+ } else {
+ None
+ }
+ } else if args.len() > 0 {
+ Some(args.remove(0))
} else {
- TerminalKind::Shell(cwd.map(|c| c.to_path_buf()))
+ None
};
+ let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
+ let title = request
+ .title
+ .clone()
+ .filter(|title| !title.is_empty())
+ .or_else(|| command.clone())
+ .unwrap_or_else(|| "Debug terminal".to_string());
+ let kind = TerminalKind::Task(task::SpawnInTerminal {
+ id: task::TaskId("debug".to_string()),
+ full_label: title.clone(),
+ label: title.clone(),
+ command: command.clone(),
+ args,
+ command_label: title.clone(),
+ cwd,
+ env: envs,
+ use_new_terminal: true,
+ allow_concurrent_runs: true,
+ reveal: task::RevealStrategy::NoFocus,
+ reveal_target: task::RevealTarget::Dock,
+ hide: task::HideStrategy::Never,
+ shell,
+ show_summary: false,
+ show_command: false,
+ show_rerun: false,
+ });
+
let workspace = self.workspace.clone();
let weak_project = project.downgrade();
@@ -1211,6 +1260,12 @@ impl RunningState {
item_kind,
cx,
)),
+ DebuggerPaneItem::MemoryView => Box::new(SubView::new(
+ self.memory_view.focus_handle(cx),
+ self.memory_view.clone().into(),
+ item_kind,
+ cx,
+ )),
}
}
@@ -1395,7 +1450,14 @@ impl RunningState {
&self.module_list
}
- pub(crate) fn activate_item(&self, item: DebuggerPaneItem, window: &mut Window, cx: &mut App) {
+ pub(crate) fn activate_item(
+ &mut self,
+ item: DebuggerPaneItem,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.ensure_pane_item(item, window, cx);
+
let (variable_list_position, pane) = self
.panes
.panes()
@@ -1407,9 +1469,10 @@ impl RunningState {
.map(|view| (view, pane))
})
.unwrap();
+
pane.update(cx, |this, cx| {
this.activate_item(variable_list_position, true, true, window, cx);
- })
+ });
}
#[cfg(test)]
@@ -1446,7 +1509,7 @@ impl RunningState {
}
}
- pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
+ pub fn selected_thread_id(&self) -> Option<ThreadId> {
self.thread_id
}
@@ -1586,9 +1649,21 @@ impl RunningState {
})
.log_err();
- self.session.update(cx, |session, cx| {
+ let is_building = self.session.update(cx, |session, cx| {
session.shutdown(cx).detach();
- })
+ matches!(session.mode, session::SessionState::Building(_))
+ });
+
+ if is_building {
+ self.debug_terminal.update(cx, |terminal, cx| {
+ if let Some(view) = terminal.terminal.as_ref() {
+ view.update(cx, |view, cx| {
+ view.terminal()
+ .update(cx, |terminal, _| terminal.kill_active_task())
+ })
+ }
+ })
+ }
}
pub fn stop_thread(&self, cx: &mut Context<Self>) {
@@ -5,7 +5,8 @@ use std::{
time::Duration,
};
-use dap::{Capabilities, ExceptionBreakpointsFilter};
+use dap::{Capabilities, ExceptionBreakpointsFilter, adapters::DebugAdapterName};
+use db::kvp::KEY_VALUE_STORE;
use editor::Editor;
use gpui::{
Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
@@ -16,16 +17,17 @@ use project::{
Project,
debugger::{
breakpoint_store::{BreakpointEditAction, BreakpointStore, SourceBreakpoint},
+ dap_store::{DapStore, PersistedAdapterOptions},
session::Session,
},
worktree_store::WorktreeStore,
};
use ui::{
ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div,
- Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, Indicator,
- InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement,
- Render, RenderOnce, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
- Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
+ Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, InteractiveElement,
+ IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
+ Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Toggleable,
+ Tooltip, Window, div, h_flex, px, v_flex,
};
use util::ResultExt;
use workspace::Workspace;
@@ -33,16 +35,23 @@ use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
actions!(
debugger,
- [PreviousBreakpointProperty, NextBreakpointProperty]
+ [
+ /// Navigates to the previous breakpoint property in the list.
+ PreviousBreakpointProperty,
+ /// Navigates to the next breakpoint property in the list.
+ NextBreakpointProperty
+ ]
);
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum SelectedBreakpointKind {
Source,
Exception,
+ Data,
}
pub(crate) struct BreakpointList {
workspace: WeakEntity<Workspace>,
breakpoint_store: Entity<BreakpointStore>,
+ dap_store: Entity<DapStore>,
worktree_store: Entity<WorktreeStore>,
scrollbar_state: ScrollbarState,
breakpoints: Vec<BreakpointEntry>,
@@ -54,6 +63,7 @@ pub(crate) struct BreakpointList {
selected_ix: Option<usize>,
input: Entity<Editor>,
strip_mode: Option<ActiveBreakpointStripMode>,
+ serialize_exception_breakpoints_task: Option<Task<anyhow::Result<()>>>,
}
impl Focusable for BreakpointList {
@@ -80,24 +90,34 @@ impl BreakpointList {
let project = project.read(cx);
let breakpoint_store = project.breakpoint_store();
let worktree_store = project.worktree_store();
+ let dap_store = project.dap_store();
let focus_handle = cx.focus_handle();
let scroll_handle = UniformListScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
- cx.new(|cx| Self {
- breakpoint_store,
- worktree_store,
- scrollbar_state,
- breakpoints: Default::default(),
- hide_scrollbar_task: None,
- show_scrollbar: false,
- workspace,
- session,
- focus_handle,
- scroll_handle,
- selected_ix: None,
- input: cx.new(|cx| Editor::single_line(window, cx)),
- strip_mode: None,
+ let adapter_name = session.as_ref().map(|session| session.read(cx).adapter());
+ cx.new(|cx| {
+ let this = Self {
+ breakpoint_store,
+ dap_store,
+ worktree_store,
+ scrollbar_state,
+ breakpoints: Default::default(),
+ hide_scrollbar_task: None,
+ show_scrollbar: false,
+ workspace,
+ session,
+ focus_handle,
+ scroll_handle,
+ selected_ix: None,
+ input: cx.new(|cx| Editor::single_line(window, cx)),
+ strip_mode: None,
+ serialize_exception_breakpoints_task: None,
+ };
+ if let Some(name) = adapter_name {
+ _ = this.deserialize_exception_breakpoints(name, cx);
+ }
+ this
})
}
@@ -169,6 +189,9 @@ impl BreakpointList {
BreakpointEntryKind::ExceptionBreakpoint(bp) => {
(SelectedBreakpointKind::Exception, bp.is_enabled)
}
+ BreakpointEntryKind::DataBreakpoint(bp) => {
+ (SelectedBreakpointKind::Data, bp.0.is_enabled)
+ }
})
})
}
@@ -372,7 +395,8 @@ impl BreakpointList {
let row = line_breakpoint.breakpoint.row;
self.go_to_line_breakpoint(path, row, window, cx);
}
- BreakpointEntryKind::ExceptionBreakpoint(_) => {}
+ BreakpointEntryKind::DataBreakpoint(_)
+ | BreakpointEntryKind::ExceptionBreakpoint(_) => {}
}
}
@@ -399,12 +423,12 @@ impl BreakpointList {
self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
}
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
- if let Some(session) = &self.session {
- let id = exception_breakpoint.id.clone();
- session.update(cx, |session, cx| {
- session.toggle_exception_breakpoint(&id, cx);
- });
- }
+ let id = exception_breakpoint.id.clone();
+ self.toggle_exception_breakpoint(&id, cx);
+ }
+ BreakpointEntryKind::DataBreakpoint(data_breakpoint) => {
+ let id = data_breakpoint.0.dap.data_id.clone();
+ self.toggle_data_breakpoint(&id, cx);
}
}
cx.notify();
@@ -426,7 +450,7 @@ impl BreakpointList {
let row = line_breakpoint.breakpoint.row;
self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
}
- BreakpointEntryKind::ExceptionBreakpoint(_) => {}
+ _ => {}
}
cx.notify();
}
@@ -475,6 +499,72 @@ impl BreakpointList {
cx.notify();
}
+ fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
+ if let Some(session) = &self.session {
+ session.update(cx, |this, cx| {
+ this.toggle_data_breakpoint(&id, cx);
+ });
+ }
+ }
+
+ fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
+ if let Some(session) = &self.session {
+ session.update(cx, |this, cx| {
+ this.toggle_exception_breakpoint(&id, cx);
+ });
+ cx.notify();
+ const EXCEPTION_SERIALIZATION_INTERVAL: Duration = Duration::from_secs(1);
+ self.serialize_exception_breakpoints_task = Some(cx.spawn(async move |this, cx| {
+ cx.background_executor()
+ .timer(EXCEPTION_SERIALIZATION_INTERVAL)
+ .await;
+ this.update(cx, |this, cx| this.serialize_exception_breakpoints(cx))?
+ .await?;
+ Ok(())
+ }));
+ }
+ }
+
+ fn kvp_key(adapter_name: &str) -> String {
+ format!("debug_adapter_`{adapter_name}`_persistence")
+ }
+ fn serialize_exception_breakpoints(
+ &mut self,
+ cx: &mut Context<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ if let Some(session) = self.session.as_ref() {
+ let key = {
+ let session = session.read(cx);
+ let name = session.adapter().0;
+ Self::kvp_key(&name)
+ };
+ let settings = self.dap_store.update(cx, |this, cx| {
+ this.sync_adapter_options(session, cx);
+ });
+ let value = serde_json::to_string(&settings);
+
+ cx.background_executor()
+ .spawn(async move { KEY_VALUE_STORE.write_kvp(key, value?).await })
+ } else {
+ return Task::ready(Result::Ok(()));
+ }
+ }
+
+ fn deserialize_exception_breakpoints(
+ &self,
+ adapter_name: DebugAdapterName,
+ cx: &mut Context<Self>,
+ ) -> anyhow::Result<()> {
+ let Some(val) = KEY_VALUE_STORE.read_kvp(&Self::kvp_key(&adapter_name))? else {
+ return Ok(());
+ };
+ let value: PersistedAdapterOptions = serde_json::from_str(&val)?;
+ self.dap_store
+ .update(cx, |this, _| this.set_adapter_options(adapter_name, value));
+
+ Ok(())
+ }
+
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
@@ -569,6 +659,7 @@ impl BreakpointList {
SelectedBreakpointKind::Exception => {
"Exception Breakpoints cannot be removed from the breakpoint list"
}
+ SelectedBreakpointKind::Data => "Remove data breakpoint from a breakpoint list",
});
let toggle_label = selection_kind.map(|(_, is_enabled)| {
if is_enabled {
@@ -710,8 +801,20 @@ impl Render for BreakpointList {
weak: weak.clone(),
})
});
- self.breakpoints
- .extend(breakpoints.chain(exception_breakpoints));
+ let data_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
+ session
+ .read(cx)
+ .data_breakpoints()
+ .map(|state| BreakpointEntry {
+ kind: BreakpointEntryKind::DataBreakpoint(DataBreakpoint(state.clone())),
+ weak: weak.clone(),
+ })
+ });
+ self.breakpoints.extend(
+ breakpoints
+ .chain(data_breakpoints)
+ .chain(exception_breakpoints),
+ );
v_flex()
.id("breakpoint-list")
.key_context("BreakpointList")
@@ -832,7 +935,11 @@ impl LineBreakpoint {
.ok();
}
})
- .child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
+ .child(
+ Icon::new(icon_name)
+ .color(Color::Debugger)
+ .size(IconSize::XSmall),
+ )
.on_mouse_down(MouseButton::Left, move |_, _, _| {});
ListItem::new(SharedString::from(format!(
@@ -923,6 +1030,103 @@ struct ExceptionBreakpoint {
data: ExceptionBreakpointsFilter,
is_enabled: bool,
}
+#[derive(Clone, Debug)]
+struct DataBreakpoint(project::debugger::session::DataBreakpointState);
+
+impl DataBreakpoint {
+ fn render(
+ &self,
+ props: SupportedBreakpointProperties,
+ strip_mode: Option<ActiveBreakpointStripMode>,
+ ix: usize,
+ is_selected: bool,
+ focus_handle: FocusHandle,
+ list: WeakEntity<BreakpointList>,
+ ) -> ListItem {
+ let color = if self.0.is_enabled {
+ Color::Debugger
+ } else {
+ Color::Muted
+ };
+ let is_enabled = self.0.is_enabled;
+ let id = self.0.dap.data_id.clone();
+ ListItem::new(SharedString::from(format!(
+ "data-breakpoint-ui-item-{}",
+ self.0.dap.data_id
+ )))
+ .rounded()
+ .start_slot(
+ div()
+ .id(SharedString::from(format!(
+ "data-breakpoint-ui-item-{}-click-handler",
+ self.0.dap.data_id
+ )))
+ .tooltip({
+ let focus_handle = focus_handle.clone();
+ move |window, cx| {
+ Tooltip::for_action_in(
+ if is_enabled {
+ "Disable Data Breakpoint"
+ } else {
+ "Enable Data Breakpoint"
+ },
+ &ToggleEnableBreakpoint,
+ &focus_handle,
+ window,
+ cx,
+ )
+ }
+ })
+ .on_click({
+ let list = list.clone();
+ move |_, _, cx| {
+ list.update(cx, |this, cx| {
+ this.toggle_data_breakpoint(&id, cx);
+ })
+ .ok();
+ }
+ })
+ .cursor_pointer()
+ .child(
+ Icon::new(IconName::Binary)
+ .color(color)
+ .size(IconSize::Small),
+ ),
+ )
+ .child(
+ h_flex()
+ .w_full()
+ .mr_4()
+ .py_0p5()
+ .justify_between()
+ .child(
+ v_flex()
+ .py_1()
+ .gap_1()
+ .min_h(px(26.))
+ .justify_center()
+ .id(("data-breakpoint-label", ix))
+ .child(
+ Label::new(self.0.context.human_readable_label())
+ .size(LabelSize::Small)
+ .line_height_style(ui::LineHeightStyle::UiLabel),
+ ),
+ )
+ .child(BreakpointOptionsStrip {
+ props,
+ breakpoint: BreakpointEntry {
+ kind: BreakpointEntryKind::DataBreakpoint(self.clone()),
+ weak: list,
+ },
+ is_selected,
+ focus_handle,
+ strip_mode,
+ index: ix,
+ }),
+ )
+ .toggle_state(is_selected)
+ }
+}
impl ExceptionBreakpoint {
fn render(
@@ -983,18 +1187,17 @@ impl ExceptionBreakpoint {
let list = list.clone();
move |_, _, cx| {
list.update(cx, |this, cx| {
- if let Some(session) = &this.session {
- session.update(cx, |this, cx| {
- this.toggle_exception_breakpoint(&id, cx);
- });
- cx.notify();
- }
+ this.toggle_exception_breakpoint(&id, cx);
})
.ok();
}
})
.cursor_pointer()
- .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
+ .child(
+ Icon::new(IconName::Flame)
+ .color(color)
+ .size(IconSize::Small),
+ ),
)
.child(
h_flex()
@@ -1037,6 +1240,7 @@ impl ExceptionBreakpoint {
enum BreakpointEntryKind {
LineBreakpoint(LineBreakpoint),
ExceptionBreakpoint(ExceptionBreakpoint),
+ DataBreakpoint(DataBreakpoint),
}
#[derive(Clone, Debug)]
@@ -1072,6 +1276,14 @@ impl BreakpointEntry {
focus_handle,
self.weak.clone(),
),
+ BreakpointEntryKind::DataBreakpoint(data_breakpoint) => data_breakpoint.render(
+ props.for_data_breakpoints(),
+ strip_mode,
+ ix,
+ is_selected,
+ focus_handle,
+ self.weak.clone(),
+ ),
}
}
@@ -1087,6 +1299,11 @@ impl BreakpointEntry {
exception_breakpoint.id
)
.into(),
+ BreakpointEntryKind::DataBreakpoint(data_breakpoint) => format!(
+ "data-breakpoint-control-strip--{}",
+ data_breakpoint.0.dap.data_id
+ )
+ .into(),
}
}
@@ -1104,8 +1321,8 @@ impl BreakpointEntry {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
line_breakpoint.breakpoint.condition.is_some()
}
- // We don't support conditions on exception breakpoints
- BreakpointEntryKind::ExceptionBreakpoint(_) => false,
+ // We don't support conditions on exception/data breakpoints
+ _ => false,
}
}
@@ -1157,6 +1374,10 @@ impl SupportedBreakpointProperties {
// TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
Self::empty()
}
+ fn for_data_breakpoints(self) -> Self {
+ // TODO: we don't yet support conditions for data breakpoints at the data layer, hence all props are disabled here.
+ Self::empty()
+ }
}
#[derive(IntoElement)]
struct BreakpointOptionsStrip {
@@ -5,25 +5,35 @@ use super::{
use alacritty_terminal::vte::ansi;
use anyhow::Result;
use collections::HashMap;
-use dap::OutputEvent;
+use dap::{CompletionItem, CompletionItemType, OutputEvent};
use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
use fuzzy::StringMatchCandidate;
use gpui::{
Action as _, AppContext, Context, Corner, Entity, FocusHandle, Focusable, HighlightStyle, Hsla,
Render, Subscription, Task, TextStyle, WeakEntity, actions,
};
-use language::{Buffer, CodeLabel, ToOffset};
-use menu::Confirm;
+use language::{Anchor, Buffer, CodeLabel, TextBufferSnapshot, ToOffset};
+use menu::{Confirm, SelectNext, SelectPrevious};
use project::{
Completion, CompletionResponse,
- debugger::session::{CompletionsQuery, OutputToken, Session, SessionEvent},
+ debugger::session::{CompletionsQuery, OutputToken, Session},
+ lsp_store::CompletionDocumentation,
+ search_history::{SearchHistory, SearchHistoryCursor},
};
use settings::Settings;
+use std::fmt::Write;
use std::{cell::RefCell, ops::Range, rc::Rc, usize};
use theme::{Theme, ThemeSettings};
use ui::{ContextMenu, Divider, PopoverMenu, SplitButton, Tooltip, prelude::*};
+use util::ResultExt;
-actions!(console, [WatchExpression]);
+actions!(
+ console,
+ [
+ /// Adds an expression to the watch list.
+ WatchExpression
+ ]
+);
pub struct Console {
console: Entity<Editor>,
@@ -33,8 +43,10 @@ pub struct Console {
variable_list: Entity<VariableList>,
stack_frame_list: Entity<StackFrameList>,
last_token: OutputToken,
- update_output_task: Task<()>,
+ update_output_task: Option<Task<()>>,
focus_handle: FocusHandle,
+ history: SearchHistory,
+ cursor: SearchHistoryCursor,
}
impl Console {
@@ -83,11 +95,6 @@ impl Console {
let _subscriptions = vec![
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
- cx.subscribe_in(&session, window, |this, _, event, window, cx| {
- if let SessionEvent::ConsoleOutput = event {
- this.update_output(window, cx)
- }
- }),
cx.on_focus(&focus_handle, window, |console, window, cx| {
if console.is_running(cx) {
console.query_bar.focus_handle(cx).focus(window);
@@ -102,9 +109,14 @@ impl Console {
variable_list,
_subscriptions,
stack_frame_list,
- update_output_task: Task::ready(()),
+ update_output_task: None,
last_token: OutputToken(0),
focus_handle,
+ history: SearchHistory::new(
+ None,
+ project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
+ ),
+ cursor: Default::default(),
}
}
@@ -133,202 +145,116 @@ impl Console {
self.session.read(cx).has_new_output(self.last_token)
}
- pub fn add_messages<'a>(
+ fn add_messages(
&mut self,
- events: impl Iterator<Item = &'a OutputEvent>,
+ events: Vec<OutputEvent>,
window: &mut Window,
cx: &mut App,
- ) {
- self.console.update(cx, |console, cx| {
- console.set_read_only(false);
-
- for event in events {
- let to_insert = format!("{}\n", event.output.trim_end());
-
- let mut ansi_handler = ConsoleHandler::default();
- let mut ansi_processor = ansi::Processor::<ansi::StdSyncHandler>::default();
-
- let len = console.buffer().read(cx).len(cx);
- ansi_processor.advance(&mut ansi_handler, to_insert.as_bytes());
- let output = std::mem::take(&mut ansi_handler.output);
- let mut spans = std::mem::take(&mut ansi_handler.spans);
- let mut background_spans = std::mem::take(&mut ansi_handler.background_spans);
- if ansi_handler.current_range_start < output.len() {
- spans.push((
- ansi_handler.current_range_start..output.len(),
- ansi_handler.current_color,
- ));
- }
- if ansi_handler.current_background_range_start < output.len() {
- background_spans.push((
- ansi_handler.current_background_range_start..output.len(),
- ansi_handler.current_background_color,
- ));
- }
- console.move_to_end(&editor::actions::MoveToEnd, window, cx);
- console.insert(&output, window, cx);
- let buffer = console.buffer().read(cx).snapshot(cx);
-
- struct ConsoleAnsiHighlight;
-
- for (range, color) in spans {
- let Some(color) = color else { continue };
- let start_offset = len + range.start;
- let range = start_offset..len + range.end;
- let range = buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
- let style = HighlightStyle {
- color: Some(terminal_view::terminal_element::convert_color(
- &color,
- cx.theme(),
- )),
- ..Default::default()
- };
- console.highlight_text_key::<ConsoleAnsiHighlight>(
- start_offset,
- vec![range],
- style,
- cx,
- );
- }
-
- for (range, color) in background_spans {
- let Some(color) = color else { continue };
- let start_offset = len + range.start;
- let range = start_offset..len + range.end;
- let range = buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
-
- let color_fetcher: fn(&Theme) -> Hsla = match color {
- // Named and theme defined colors
- ansi::Color::Named(n) => match n {
- ansi::NamedColor::Black => |theme| theme.colors().terminal_ansi_black,
- ansi::NamedColor::Red => |theme| theme.colors().terminal_ansi_red,
- ansi::NamedColor::Green => |theme| theme.colors().terminal_ansi_green,
- ansi::NamedColor::Yellow => |theme| theme.colors().terminal_ansi_yellow,
- ansi::NamedColor::Blue => |theme| theme.colors().terminal_ansi_blue,
- ansi::NamedColor::Magenta => {
- |theme| theme.colors().terminal_ansi_magenta
- }
- ansi::NamedColor::Cyan => |theme| theme.colors().terminal_ansi_cyan,
- ansi::NamedColor::White => |theme| theme.colors().terminal_ansi_white,
- ansi::NamedColor::BrightBlack => {
- |theme| theme.colors().terminal_ansi_bright_black
- }
- ansi::NamedColor::BrightRed => {
- |theme| theme.colors().terminal_ansi_bright_red
- }
- ansi::NamedColor::BrightGreen => {
- |theme| theme.colors().terminal_ansi_bright_green
- }
- ansi::NamedColor::BrightYellow => {
- |theme| theme.colors().terminal_ansi_bright_yellow
- }
- ansi::NamedColor::BrightBlue => {
- |theme| theme.colors().terminal_ansi_bright_blue
- }
- ansi::NamedColor::BrightMagenta => {
- |theme| theme.colors().terminal_ansi_bright_magenta
+ ) -> Task<Result<()>> {
+ self.console.update(cx, |_, cx| {
+ cx.spawn_in(window, async move |console, cx| {
+ let mut len = console.update(cx, |this, cx| this.buffer().read(cx).len(cx))?;
+ let (output, spans, background_spans) = cx
+ .background_spawn(async move {
+ let mut all_spans = Vec::new();
+ let mut all_background_spans = Vec::new();
+ let mut to_insert = String::new();
+ let mut scratch = String::new();
+
+ for event in &events {
+ scratch.clear();
+ let mut ansi_handler = ConsoleHandler::default();
+ let mut ansi_processor =
+ ansi::Processor::<ansi::StdSyncHandler>::default();
+
+ let trimmed_output = event.output.trim_end();
+ let _ = writeln!(&mut scratch, "{trimmed_output}");
+ ansi_processor.advance(&mut ansi_handler, scratch.as_bytes());
+ let output = std::mem::take(&mut ansi_handler.output);
+ to_insert.extend(output.chars());
+ let mut spans = std::mem::take(&mut ansi_handler.spans);
+ let mut background_spans =
+ std::mem::take(&mut ansi_handler.background_spans);
+ if ansi_handler.current_range_start < output.len() {
+ spans.push((
+ ansi_handler.current_range_start..output.len(),
+ ansi_handler.current_color,
+ ));
}
- ansi::NamedColor::BrightCyan => {
- |theme| theme.colors().terminal_ansi_bright_cyan
+ if ansi_handler.current_background_range_start < output.len() {
+ background_spans.push((
+ ansi_handler.current_background_range_start..output.len(),
+ ansi_handler.current_background_color,
+ ));
}
- ansi::NamedColor::BrightWhite => {
- |theme| theme.colors().terminal_ansi_bright_white
- }
- ansi::NamedColor::Foreground => {
- |theme| theme.colors().terminal_foreground
- }
- ansi::NamedColor::Background => {
- |theme| theme.colors().terminal_background
- }
- ansi::NamedColor::Cursor => |theme| theme.players().local().cursor,
- ansi::NamedColor::DimBlack => {
- |theme| theme.colors().terminal_ansi_dim_black
- }
- ansi::NamedColor::DimRed => {
- |theme| theme.colors().terminal_ansi_dim_red
- }
- ansi::NamedColor::DimGreen => {
- |theme| theme.colors().terminal_ansi_dim_green
- }
- ansi::NamedColor::DimYellow => {
- |theme| theme.colors().terminal_ansi_dim_yellow
- }
- ansi::NamedColor::DimBlue => {
- |theme| theme.colors().terminal_ansi_dim_blue
- }
- ansi::NamedColor::DimMagenta => {
- |theme| theme.colors().terminal_ansi_dim_magenta
- }
- ansi::NamedColor::DimCyan => {
- |theme| theme.colors().terminal_ansi_dim_cyan
- }
- ansi::NamedColor::DimWhite => {
- |theme| theme.colors().terminal_ansi_dim_white
- }
- ansi::NamedColor::BrightForeground => {
- |theme| theme.colors().terminal_bright_foreground
- }
- ansi::NamedColor::DimForeground => {
- |theme| theme.colors().terminal_dim_foreground
+
+ for (range, _) in spans.iter_mut() {
+ let start_offset = len + range.start;
+ *range = start_offset..len + range.end;
}
- },
- // 'True' colors
- ansi::Color::Spec(_) => |theme| theme.colors().editor_background,
- // 8 bit, indexed colors
- ansi::Color::Indexed(i) => {
- match i {
- // 0-15 are the same as the named colors above
- 0 => |theme| theme.colors().terminal_ansi_black,
- 1 => |theme| theme.colors().terminal_ansi_red,
- 2 => |theme| theme.colors().terminal_ansi_green,
- 3 => |theme| theme.colors().terminal_ansi_yellow,
- 4 => |theme| theme.colors().terminal_ansi_blue,
- 5 => |theme| theme.colors().terminal_ansi_magenta,
- 6 => |theme| theme.colors().terminal_ansi_cyan,
- 7 => |theme| theme.colors().terminal_ansi_white,
- 8 => |theme| theme.colors().terminal_ansi_bright_black,
- 9 => |theme| theme.colors().terminal_ansi_bright_red,
- 10 => |theme| theme.colors().terminal_ansi_bright_green,
- 11 => |theme| theme.colors().terminal_ansi_bright_yellow,
- 12 => |theme| theme.colors().terminal_ansi_bright_blue,
- 13 => |theme| theme.colors().terminal_ansi_bright_magenta,
- 14 => |theme| theme.colors().terminal_ansi_bright_cyan,
- 15 => |theme| theme.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
- _ => |_| gpui::black(),
+
+ for (range, _) in background_spans.iter_mut() {
+ let start_offset = len + range.start;
+ *range = start_offset..len + range.end;
}
+
+ len += output.len();
+
+ all_spans.extend(spans);
+ all_background_spans.extend(background_spans);
}
- };
-
- console.highlight_background_key::<ConsoleAnsiHighlight>(
- start_offset,
- &[range],
- color_fetcher,
- cx,
- );
- }
- }
+ (to_insert, all_spans, all_background_spans)
+ })
+ .await;
+ console.update_in(cx, |console, window, cx| {
+ console.set_read_only(false);
+ console.move_to_end(&editor::actions::MoveToEnd, window, cx);
+ console.insert(&output, window, cx);
+ console.set_read_only(true);
+
+ struct ConsoleAnsiHighlight;
+
+ let buffer = console.buffer().read(cx).snapshot(cx);
+
+ for (range, color) in spans {
+ let Some(color) = color else { continue };
+ let start_offset = range.start;
+ let range =
+ buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
+ let style = HighlightStyle {
+ color: Some(terminal_view::terminal_element::convert_color(
+ &color,
+ cx.theme(),
+ )),
+ ..Default::default()
+ };
+ console.highlight_text_key::<ConsoleAnsiHighlight>(
+ start_offset,
+ vec![range],
+ style,
+ cx,
+ );
+ }
- console.set_read_only(true);
- cx.notify();
- });
+ for (range, color) in background_spans {
+ let Some(color) = color else { continue };
+ let start_offset = range.start;
+ let range =
+ buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
+ console.highlight_background_key::<ConsoleAnsiHighlight>(
+ start_offset,
+ &[range],
+ color_fetcher(color),
+ cx,
+ );
+ }
+
+ cx.notify();
+ })?;
+
+ Ok(())
+ })
+ })
}
pub fn watch_expression(
@@ -345,7 +271,8 @@ impl Console {
expression
});
-
+ self.history.add(&mut self.cursor, expression.clone());
+ self.cursor.reset();
self.session.update(cx, |session, cx| {
session
.evaluate(
@@ -365,7 +292,28 @@ impl Console {
});
}
- pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
+ fn previous_query(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
+ let prev = self.history.previous(&mut self.cursor);
+ if let Some(prev) = prev {
+ self.query_bar.update(cx, |editor, cx| {
+ editor.set_text(prev, window, cx);
+ });
+ }
+ }
+
+ fn next_query(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
+ let next = self.history.next(&mut self.cursor);
+ let query = next.unwrap_or_else(|| {
+ self.cursor.reset();
+ ""
+ });
+
+ self.query_bar.update(cx, |editor, cx| {
+ editor.set_text(query, window, cx);
+ });
+ }
+
+ fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
let expression = self.query_bar.update(cx, |editor, cx| {
let expression = editor.text(cx);
cx.defer_in(window, |editor, window, cx| {
@@ -375,6 +323,8 @@ impl Console {
expression
});
+ self.history.add(&mut self.cursor, expression.clone());
+ self.cursor.reset();
self.session.update(cx, |session, cx| {
session
.evaluate(
@@ -458,31 +408,50 @@ impl Console {
EditorElement::new(&self.query_bar, Self::editor_style(&self.query_bar, cx))
}
- fn update_output(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ pub(crate) fn update_output(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ if self.update_output_task.is_some() {
+ return;
+ }
let session = self.session.clone();
let token = self.last_token;
-
- self.update_output_task = cx.spawn_in(window, async move |this, cx| {
- _ = session.update_in(cx, move |session, window, cx| {
- let (output, last_processed_token) = session.output(token);
-
- _ = this.update(cx, |this, cx| {
- if last_processed_token == this.last_token {
- return;
- }
- this.add_messages(output, window, cx);
-
- this.last_token = last_processed_token;
+ self.update_output_task = Some(cx.spawn_in(window, async move |this, cx| {
+ let Some((last_processed_token, task)) = session
+ .update_in(cx, |session, window, cx| {
+ let (output, last_processed_token) = session.output(token);
+
+ this.update(cx, |this, cx| {
+ if last_processed_token == this.last_token {
+ return None;
+ }
+ Some((
+ last_processed_token,
+ this.add_messages(output.cloned().collect(), window, cx),
+ ))
+ })
+ .ok()
+ .flatten()
+ })
+ .ok()
+ .flatten()
+ else {
+ _ = this.update(cx, |this, _| {
+ this.update_output_task.take();
});
+ return;
+ };
+ _ = task.await.log_err();
+ _ = this.update(cx, |this, _| {
+ this.last_token = last_processed_token;
+ this.update_output_task.take();
});
- });
+ }));
}
}
impl Render for Console {
- fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let query_focus_handle = self.query_bar.focus_handle(cx);
-
+ self.update_output(window, cx);
v_flex()
.track_focus(&self.focus_handle)
.key_context("DebugConsole")
@@ -493,6 +462,8 @@ impl Render for Console {
.when(self.is_running(cx), |this| {
this.child(Divider::horizontal()).child(
h_flex()
+ .on_action(cx.listener(Self::previous_query))
+ .on_action(cx.listener(Self::next_query))
.gap_1()
.bg(cx.theme().colors().editor_background)
.child(self.render_query_bar(cx))
@@ -585,15 +556,27 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
buffer: &Entity<Buffer>,
position: language::Anchor,
text: &str,
- _trigger_in_words: bool,
+ trigger_in_words: bool,
menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
+ let mut chars = text.chars();
+ let char = if let Some(char) = chars.next() {
+ char
+ } else {
+ return false;
+ };
+
let snapshot = buffer.read(cx).snapshot();
if !menu_is_open && !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;
+ }
+
self.0
.read_with(cx, |console, cx| {
console
@@ -626,48 +609,41 @@ impl ConsoleQueryBarCompletionProvider {
variable_list.completion_variables(cx)
}) {
if let Some(evaluate_name) = &variable.evaluate_name {
- variables.insert(evaluate_name.clone(), variable.value.clone());
+ if variables
+ .insert(evaluate_name.clone(), variable.value.clone())
+ .is_none()
+ {
+ string_matches.push(StringMatchCandidate {
+ id: 0,
+ string: evaluate_name.clone(),
+ char_bag: evaluate_name.chars().collect(),
+ });
+ }
+ }
+
+ if variables
+ .insert(variable.name.clone(), variable.value.clone())
+ .is_none()
+ {
string_matches.push(StringMatchCandidate {
id: 0,
- string: evaluate_name.clone(),
- char_bag: evaluate_name.chars().collect(),
+ string: variable.name.clone(),
+ char_bag: variable.name.chars().collect(),
});
}
-
- variables.insert(variable.name.clone(), variable.value.clone());
-
- string_matches.push(StringMatchCandidate {
- id: 0,
- string: variable.name.clone(),
- char_bag: variable.name.chars().collect(),
- });
}
(variables, string_matches)
});
let snapshot = buffer.read(cx).text_snapshot();
- let query = snapshot.text();
- let replace_range = {
- let buffer_offset = buffer_position.to_offset(&snapshot);
- let reversed_chars = snapshot.reversed_chars_for_range(0..buffer_offset);
- let mut word_len = 0;
- for ch in reversed_chars {
- if ch.is_alphanumeric() || ch == '_' {
- word_len += 1;
- } else {
- break;
- }
- }
- let word_start_offset = buffer_offset - word_len;
- let start_anchor = snapshot.anchor_at(word_start_offset, Bias::Left);
- start_anchor..buffer_position
- };
+ let buffer_text = snapshot.text();
+
cx.spawn(async move |_, cx| {
const LIMIT: usize = 10;
let matches = fuzzy::match_strings(
&string_matches,
- &query,
+ &buffer_text,
true,
true,
LIMIT,
@@ -682,15 +658,22 @@ impl ConsoleQueryBarCompletionProvider {
let variable_value = variables.get(&string_match.string)?;
Some(project::Completion {
- replace_range: replace_range.clone(),
+ replace_range: Self::replace_range_for_completion(
+ &buffer_text,
+ buffer_position,
+ string_match.string.as_bytes(),
+ &snapshot,
+ ),
new_text: string_match.string.clone(),
label: CodeLabel {
filter_range: 0..string_match.string.len(),
- text: format!("{} {}", string_match.string, variable_value),
+ text: string_match.string.clone(),
runs: Vec::new(),
},
icon_path: None,
- documentation: None,
+ documentation: Some(CompletionDocumentation::MultiLineMarkdown(
+ variable_value.into(),
+ )),
confirm: None,
source: project::CompletionSource::Custom,
insert_text_mode: None,
@@ -705,6 +688,54 @@ impl ConsoleQueryBarCompletionProvider {
})
}
+ fn replace_range_for_completion(
+ buffer_text: &String,
+ buffer_position: Anchor,
+ new_bytes: &[u8],
+ snapshot: &TextBufferSnapshot,
+ ) -> Range<Anchor> {
+ let buffer_offset = buffer_position.to_offset(&snapshot);
+ let buffer_bytes = &buffer_text.as_bytes()[0..buffer_offset];
+
+ let mut prefix_len = 0;
+ for i in (0..new_bytes.len()).rev() {
+ if buffer_bytes.ends_with(&new_bytes[0..i]) {
+ prefix_len = i;
+ break;
+ }
+ }
+
+ let start = snapshot.clip_offset(buffer_offset - prefix_len, Bias::Left);
+
+ snapshot.anchor_before(start)..buffer_position
+ }
+
+ const fn completion_type_score(completion_type: CompletionItemType) -> usize {
+ match completion_type {
+ CompletionItemType::Field | CompletionItemType::Property => 0,
+ CompletionItemType::Variable | CompletionItemType::Value => 1,
+ CompletionItemType::Method
+ | CompletionItemType::Function
+ | CompletionItemType::Constructor => 2,
+ CompletionItemType::Class
+ | CompletionItemType::Interface
+ | CompletionItemType::Module => 3,
+ _ => 4,
+ }
+ }
+
+ fn completion_item_sort_text(completion_item: &CompletionItem) -> String {
+ completion_item.sort_text.clone().unwrap_or_else(|| {
+ format!(
+ "{:03}_{}",
+ Self::completion_type_score(
+ completion_item.type_.unwrap_or(CompletionItemType::Text)
+ ),
+ completion_item.label.to_ascii_lowercase()
+ )
+ })
+ }
+
fn client_completions(
&self,
console: &Entity<Console>,
@@ -726,34 +757,25 @@ impl ConsoleQueryBarCompletionProvider {
cx.background_executor().spawn(async move {
let completions = completion_task.await?;
+ let buffer_text = snapshot.text();
+
let completions = completions
.into_iter()
.map(|completion| {
+ let sort_text = Self::completion_item_sort_text(&completion);
let new_text = completion
.text
.as_ref()
.unwrap_or(&completion.label)
.to_owned();
- let buffer_text = snapshot.text();
- let buffer_bytes = buffer_text.as_bytes();
- let new_bytes = new_text.as_bytes();
-
- let mut prefix_len = 0;
- for i in (0..new_bytes.len()).rev() {
- if buffer_bytes.ends_with(&new_bytes[0..i]) {
- prefix_len = i;
- break;
- }
- }
-
- let buffer_offset = buffer_position.to_offset(&snapshot);
- let start = buffer_offset - prefix_len;
- let start = snapshot.clip_offset(start, Bias::Left);
- let start = snapshot.anchor_before(start);
- let replace_range = start..buffer_position;
project::Completion {
- replace_range,
+ replace_range: Self::replace_range_for_completion(
+ &buffer_text,
+ buffer_position,
+ new_text.as_bytes(),
+ &snapshot,
+ ),
new_text,
label: CodeLabel {
filter_range: 0..completion.label.len(),
@@ -761,12 +783,11 @@ impl ConsoleQueryBarCompletionProvider {
runs: Vec::new(),
},
icon_path: None,
- documentation: None,
+ documentation: completion.detail.map(|detail| {
+ CompletionDocumentation::MultiLineMarkdown(detail.into())
+ }),
confirm: None,
- source: project::CompletionSource::BufferWord {
- word_range: buffer_position..language::Anchor::MAX,
- resolved: false,
- },
+ source: project::CompletionSource::Dap { sort_text },
insert_text_mode: None,
}
})
@@ -845,3 +866,145 @@ impl ansi::Handler for ConsoleHandler {
}
}
}
+
+fn color_fetcher(color: ansi::Color) -> fn(&Theme) -> Hsla {
+ let color_fetcher: fn(&Theme) -> Hsla = match color {
+ // Named and theme defined colors
+ ansi::Color::Named(n) => match n {
+ ansi::NamedColor::Black => |theme| theme.colors().terminal_ansi_black,
+ ansi::NamedColor::Red => |theme| theme.colors().terminal_ansi_red,
+ ansi::NamedColor::Green => |theme| theme.colors().terminal_ansi_green,
+ ansi::NamedColor::Yellow => |theme| theme.colors().terminal_ansi_yellow,
+ ansi::NamedColor::Blue => |theme| theme.colors().terminal_ansi_blue,
+ ansi::NamedColor::Magenta => |theme| theme.colors().terminal_ansi_magenta,
+ ansi::NamedColor::Cyan => |theme| theme.colors().terminal_ansi_cyan,
+ ansi::NamedColor::White => |theme| theme.colors().terminal_ansi_white,
+ ansi::NamedColor::BrightBlack => |theme| theme.colors().terminal_ansi_bright_black,
+ ansi::NamedColor::BrightRed => |theme| theme.colors().terminal_ansi_bright_red,
+ ansi::NamedColor::BrightGreen => |theme| theme.colors().terminal_ansi_bright_green,
+ ansi::NamedColor::BrightYellow => |theme| theme.colors().terminal_ansi_bright_yellow,
+ ansi::NamedColor::BrightBlue => |theme| theme.colors().terminal_ansi_bright_blue,
+ ansi::NamedColor::BrightMagenta => |theme| theme.colors().terminal_ansi_bright_magenta,
+ ansi::NamedColor::BrightCyan => |theme| theme.colors().terminal_ansi_bright_cyan,
+ ansi::NamedColor::BrightWhite => |theme| theme.colors().terminal_ansi_bright_white,
+ ansi::NamedColor::Foreground => |theme| theme.colors().terminal_foreground,
+ ansi::NamedColor::Background => |theme| theme.colors().terminal_background,
+ ansi::NamedColor::Cursor => |theme| theme.players().local().cursor,
+ ansi::NamedColor::DimBlack => |theme| theme.colors().terminal_ansi_dim_black,
+ ansi::NamedColor::DimRed => |theme| theme.colors().terminal_ansi_dim_red,
+ ansi::NamedColor::DimGreen => |theme| theme.colors().terminal_ansi_dim_green,
+ ansi::NamedColor::DimYellow => |theme| theme.colors().terminal_ansi_dim_yellow,
+ ansi::NamedColor::DimBlue => |theme| theme.colors().terminal_ansi_dim_blue,
+ ansi::NamedColor::DimMagenta => |theme| theme.colors().terminal_ansi_dim_magenta,
+ ansi::NamedColor::DimCyan => |theme| theme.colors().terminal_ansi_dim_cyan,
+ ansi::NamedColor::DimWhite => |theme| theme.colors().terminal_ansi_dim_white,
+ ansi::NamedColor::BrightForeground => |theme| theme.colors().terminal_bright_foreground,
+ ansi::NamedColor::DimForeground => |theme| theme.colors().terminal_dim_foreground,
+ },
+ // 'True' colors
+ ansi::Color::Spec(_) => |theme| theme.colors().editor_background,
+ // 8 bit, indexed colors
+ ansi::Color::Indexed(i) => {
+ match i {
+ // 0-15 are the same as the named colors above
+ 0 => |theme| theme.colors().terminal_ansi_black,
+ 1 => |theme| theme.colors().terminal_ansi_red,
+ 2 => |theme| theme.colors().terminal_ansi_green,
+ 3 => |theme| theme.colors().terminal_ansi_yellow,
+ 4 => |theme| theme.colors().terminal_ansi_blue,
+ 5 => |theme| theme.colors().terminal_ansi_magenta,
+ 6 => |theme| theme.colors().terminal_ansi_cyan,
+ 7 => |theme| theme.colors().terminal_ansi_white,
+ 8 => |theme| theme.colors().terminal_ansi_bright_black,
+ 9 => |theme| theme.colors().terminal_ansi_bright_red,
+ 10 => |theme| theme.colors().terminal_ansi_bright_green,
+ 11 => |theme| theme.colors().terminal_ansi_bright_yellow,
+ 12 => |theme| theme.colors().terminal_ansi_bright_blue,
+ 13 => |theme| theme.colors().terminal_ansi_bright_magenta,
+ 14 => |theme| theme.colors().terminal_ansi_bright_cyan,
+ 15 => |theme| theme.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
+ _ => |_| gpui::black(),
+ }
+ }
+ };
+ color_fetcher
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::tests::init_test;
+ use editor::test::editor_test_context::EditorTestContext;
+ use gpui::TestAppContext;
+ use language::Point;
+
+ #[track_caller]
+ fn assert_completion_range(
+ input: &str,
+ expect: &str,
+ replacement: &str,
+ cx: &mut EditorTestContext,
+ ) {
+ cx.set_state(input);
+
+ let buffer_position =
+ cx.editor(|editor, _, cx| editor.selections.newest::<Point>(cx).start);
+
+ let snapshot = &cx.buffer_snapshot();
+
+ let replace_range = ConsoleQueryBarCompletionProvider::replace_range_for_completion(
+ &cx.buffer_text(),
+ snapshot.anchor_before(buffer_position),
+ replacement.as_bytes(),
+ &snapshot,
+ );
+
+ cx.update_editor(|editor, _, cx| {
+ editor.edit(
+ vec![(
+ snapshot.offset_for_anchor(&replace_range.start)
+ ..snapshot.offset_for_anchor(&replace_range.end),
+ replacement,
+ )],
+ cx,
+ );
+ });
+
+ pretty_assertions::assert_eq!(expect, cx.display_text());
+ }
+
+ #[gpui::test]
+ async fn test_determine_completion_replace_range(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ assert_completion_range("resˇ", "result", "result", &mut cx);
+ assert_completion_range("print(resˇ)", "print(result)", "result", &mut cx);
+ assert_completion_range("$author->nˇ", "$author->name", "$author->name", &mut cx);
+ assert_completion_range(
+ "$author->books[ˇ",
+ "$author->books[0]",
+ "$author->books[0]",
+ &mut cx,
+ );
+ }
+}
@@ -0,0 +1,963 @@
+use std::{
+ cell::LazyCell,
+ fmt::Write,
+ ops::RangeInclusive,
+ sync::{Arc, LazyLock},
+ time::Duration,
+};
+
+use editor::{Editor, EditorElement, EditorStyle};
+use gpui::{
+ Action, AppContext, DismissEvent, Empty, Entity, FocusHandle, Focusable, MouseButton,
+ MouseMoveEvent, Point, ScrollStrategy, ScrollWheelEvent, Stateful, Subscription, Task,
+ TextStyle, UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, bounds,
+ deferred, point, size, uniform_list,
+};
+use notifications::status_toast::{StatusToast, ToastIcon};
+use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session::Session};
+use settings::Settings;
+use theme::ThemeSettings;
+use ui::{
+ ActiveTheme, AnyElement, App, Color, Context, ContextMenu, Div, Divider, DropdownMenu, Element,
+ FluentBuilder, Icon, IconName, InteractiveElement, IntoElement, Label, LabelCommon,
+ ParentElement, Pixels, PopoverMenuHandle, Render, Scrollbar, ScrollbarState, SharedString,
+ StatefulInteractiveElement, Styled, TextSize, Tooltip, Window, div, h_flex, px, v_flex,
+};
+use util::ResultExt;
+use workspace::Workspace;
+
+use crate::{ToggleDataBreakpoint, session::running::stack_frame_list::StackFrameList};
+
+actions!(debugger, [GoToSelectedAddress]);
+
+pub(crate) struct MemoryView {
+ workspace: WeakEntity<Workspace>,
+ scroll_handle: UniformListScrollHandle,
+ scroll_state: ScrollbarState,
+ show_scrollbar: bool,
+ stack_frame_list: WeakEntity<StackFrameList>,
+ hide_scrollbar_task: Option<Task<()>>,
+ focus_handle: FocusHandle,
+ view_state: ViewState,
+ query_editor: Entity<Editor>,
+ session: Entity<Session>,
+ width_picker_handle: PopoverMenuHandle<ContextMenu>,
+ is_writing_memory: bool,
+ open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
+}
+
+impl Focusable for MemoryView {
+ fn focus_handle(&self, _: &ui::App) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+#[derive(Clone, Debug)]
+struct Drag {
+ start_address: u64,
+ end_address: u64,
+}
+
+impl Drag {
+ fn contains(&self, address: u64) -> bool {
+ let range = self.memory_range();
+ range.contains(&address)
+ }
+
+ fn memory_range(&self) -> RangeInclusive<u64> {
+ if self.start_address < self.end_address {
+ self.start_address..=self.end_address
+ } else {
+ self.end_address..=self.start_address
+ }
+ }
+}
+#[derive(Clone, Debug)]
+enum SelectedMemoryRange {
+ DragUnderway(Drag),
+ DragComplete(Drag),
+}
+
+impl SelectedMemoryRange {
+ fn contains(&self, address: u64) -> bool {
+ match self {
+ SelectedMemoryRange::DragUnderway(drag) => drag.contains(address),
+ SelectedMemoryRange::DragComplete(drag) => drag.contains(address),
+ }
+ }
+ fn is_dragging(&self) -> bool {
+ matches!(self, SelectedMemoryRange::DragUnderway(_))
+ }
+ fn drag(&self) -> &Drag {
+ match self {
+ SelectedMemoryRange::DragUnderway(drag) => drag,
+ SelectedMemoryRange::DragComplete(drag) => drag,
+ }
+ }
+}
+
+#[derive(Clone)]
+struct ViewState {
+ /// Uppermost row index
+ base_row: u64,
+ /// How many cells per row do we have?
+ line_width: ViewWidth,
+ selection: Option<SelectedMemoryRange>,
+}
+
+impl ViewState {
+ fn new(base_row: u64, line_width: ViewWidth) -> Self {
+ Self {
+ base_row,
+ line_width,
+ selection: None,
+ }
+ }
+ fn row_count(&self) -> u64 {
+ // This was picked fully arbitrarily. There's no incentive for us to care about page sizes other than the fact that it seems to be a good
+ // middle ground for data size.
+ const PAGE_SIZE: u64 = 4096;
+ PAGE_SIZE / self.line_width.width as u64
+ }
+ fn schedule_scroll_down(&mut self) {
+ self.base_row = self.base_row.saturating_add(1)
+ }
+ fn schedule_scroll_up(&mut self) {
+ self.base_row = self.base_row.saturating_sub(1);
+ }
+}
+
+static HEX_BYTES_MEMOIZED: LazyLock<[SharedString; 256]> =
+ LazyLock::new(|| std::array::from_fn(|byte| SharedString::from(format!("{byte:02X}"))));
+static UNKNOWN_BYTE: SharedString = SharedString::new_static("??");
+impl MemoryView {
+ pub(crate) fn new(
+ session: Entity<Session>,
+ workspace: WeakEntity<Workspace>,
+ stack_frame_list: WeakEntity<StackFrameList>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let view_state = ViewState::new(0, WIDTHS[4].clone());
+ let scroll_handle = UniformListScrollHandle::default();
+
+ let query_editor = cx.new(|cx| Editor::single_line(window, cx));
+
+ let scroll_state = ScrollbarState::new(scroll_handle.clone());
+ let mut this = Self {
+ workspace,
+ scroll_state,
+ scroll_handle,
+ stack_frame_list,
+ show_scrollbar: false,
+ hide_scrollbar_task: None,
+ focus_handle: cx.focus_handle(),
+ view_state,
+ query_editor,
+ session,
+ width_picker_handle: Default::default(),
+ is_writing_memory: true,
+ open_context_menu: None,
+ };
+ this.change_query_bar_mode(false, window, cx);
+ this
+ }
+ fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
+ self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
+ cx.background_executor()
+ .timer(SCROLLBAR_SHOW_INTERVAL)
+ .await;
+ panel
+ .update(cx, |panel, cx| {
+ panel.show_scrollbar = false;
+ cx.notify();
+ })
+ .log_err();
+ }))
+ }
+
+ fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
+ if !(self.show_scrollbar || self.scroll_state.is_dragging()) {
+ return None;
+ }
+ Some(
+ div()
+ .occlude()
+ .id("memory-view-vertical-scrollbar")
+ .on_mouse_move(cx.listener(|this, evt, _, cx| {
+ this.handle_drag(evt);
+ cx.notify();
+ cx.stop_propagation()
+ }))
+ .on_hover(|_, _, cx| {
+ cx.stop_propagation();
+ })
+ .on_any_mouse_down(|_, _, cx| {
+ cx.stop_propagation();
+ })
+ .on_mouse_up(
+ MouseButton::Left,
+ cx.listener(|_, _, _, cx| {
+ cx.stop_propagation();
+ }),
+ )
+ .on_scroll_wheel(cx.listener(|_, _, _, cx| {
+ cx.notify();
+ }))
+ .h_full()
+ .absolute()
+ .right_1()
+ .top_1()
+ .bottom_0()
+ .w(px(12.))
+ .cursor_default()
+ .children(Scrollbar::vertical(self.scroll_state.clone())),
+ )
+ }
+
+ fn render_memory(&self, cx: &mut Context<Self>) -> UniformList {
+ let weak = cx.weak_entity();
+ let session = self.session.clone();
+ let view_state = self.view_state.clone();
+ uniform_list(
+ "debugger-memory-view",
+ self.view_state.row_count() as usize,
+ move |range, _, cx| {
+ let mut line_buffer = Vec::with_capacity(view_state.line_width.width as usize);
+ let memory_start =
+ (view_state.base_row + range.start as u64) * view_state.line_width.width as u64;
+ let memory_end = (view_state.base_row + range.end as u64)
+ * view_state.line_width.width as u64
+ - 1;
+ let mut memory = session.update(cx, |this, cx| {
+ this.read_memory(memory_start..=memory_end, cx)
+ });
+ let mut rows = Vec::with_capacity(range.end - range.start);
+ for ix in range {
+ line_buffer.extend((&mut memory).take(view_state.line_width.width as usize));
+ rows.push(render_single_memory_view_line(
+ &line_buffer,
+ ix as u64,
+ weak.clone(),
+ cx,
+ ));
+ line_buffer.clear();
+ }
+ rows
+ },
+ )
+ .track_scroll(self.scroll_handle.clone())
+ .on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| {
+ let delta = evt.delta.pixel_delta(window.line_height());
+ let scroll_handle = this.scroll_state.scroll_handle();
+ let size = scroll_handle.content_size();
+ let viewport = scroll_handle.viewport();
+ let current_offset = scroll_handle.offset();
+ let first_entry_offset_boundary = size.height / this.view_state.row_count() as f32;
+ let last_entry_offset_boundary = size.height - first_entry_offset_boundary;
+ if first_entry_offset_boundary + viewport.size.height > current_offset.y.abs() {
+ // The topmost entry is visible, hence if we're scrolling up, we need to load extra lines.
+ this.view_state.schedule_scroll_up();
+ } else if last_entry_offset_boundary < current_offset.y.abs() + viewport.size.height {
+ this.view_state.schedule_scroll_down();
+ }
+ scroll_handle.set_offset(current_offset + point(px(0.), delta.y));
+ }))
+ }
+ fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
+ EditorElement::new(
+ &self.query_editor,
+ Self::editor_style(&self.query_editor, cx),
+ )
+ }
+ pub(super) fn go_to_memory_reference(
+ &mut self,
+ memory_reference: &str,
+ evaluate_name: Option<&str>,
+ stack_frame_id: Option<u64>,
+ cx: &mut Context<Self>,
+ ) {
+ use parse_int::parse;
+ let Ok(as_address) = parse::<u64>(&memory_reference) else {
+ return;
+ };
+ let access_size = evaluate_name
+ .map(|typ| {
+ self.session.update(cx, |this, cx| {
+ this.data_access_size(stack_frame_id, typ, cx)
+ })
+ })
+ .unwrap_or_else(|| Task::ready(None));
+ cx.spawn(async move |this, cx| {
+ let access_size = access_size.await.unwrap_or(1);
+ this.update(cx, |this, cx| {
+ this.view_state.selection = Some(SelectedMemoryRange::DragComplete(Drag {
+ start_address: as_address,
+ end_address: as_address + access_size - 1,
+ }));
+ this.jump_to_address(as_address, cx);
+ })
+ .ok();
+ })
+ .detach();
+ }
+
+ fn handle_drag(&mut self, evt: &MouseMoveEvent) {
+ if !evt.dragging() {
+ return;
+ }
+ if !self.scroll_state.is_dragging()
+ && !self
+ .view_state
+ .selection
+ .as_ref()
+ .is_some_and(|selection| selection.is_dragging())
+ {
+ return;
+ }
+ let row_count = self.view_state.row_count();
+ debug_assert!(row_count > 1);
+ let scroll_handle = self.scroll_state.scroll_handle();
+ let viewport = scroll_handle.viewport();
+ let (top_area, bottom_area) = {
+ let size = size(viewport.size.width, viewport.size.height / 10.);
+ (
+ bounds(viewport.origin, size),
+ bounds(
+ point(viewport.origin.x, viewport.origin.y + size.height * 2.),
+ size,
+ ),
+ )
+ };
+
+ if bottom_area.contains(&evt.position) {
+ //ix == row_count - 1 {
+ self.view_state.schedule_scroll_down();
+ } else if top_area.contains(&evt.position) {
+ self.view_state.schedule_scroll_up();
+ }
+ }
+
+ fn editor_style(editor: &Entity<Editor>, cx: &Context<Self>) -> EditorStyle {
+ let is_read_only = editor.read(cx).read_only(cx);
+ let settings = ThemeSettings::get_global(cx);
+ let theme = cx.theme();
+ let text_style = TextStyle {
+ color: if is_read_only {
+ theme.colors().text_muted
+ } else {
+ theme.colors().text
+ },
+ font_family: settings.buffer_font.family.clone(),
+ font_features: settings.buffer_font.features.clone(),
+ font_size: TextSize::Small.rems(cx).into(),
+ font_weight: settings.buffer_font.weight,
+
+ ..Default::default()
+ };
+ EditorStyle {
+ background: theme.colors().editor_background,
+ local_player: theme.players().local(),
+ text: text_style,
+ ..Default::default()
+ }
+ }
+
+ fn render_width_picker(&self, window: &mut Window, cx: &mut Context<Self>) -> DropdownMenu {
+ let weak = cx.weak_entity();
+ let selected_width = self.view_state.line_width.clone();
+ DropdownMenu::new(
+ "memory-view-width-picker",
+ selected_width.label.clone(),
+ ContextMenu::build(window, cx, |mut this, window, cx| {
+ for width in &WIDTHS {
+ let weak = weak.clone();
+ let width = width.clone();
+ this = this.entry(width.label.clone(), None, move |_, cx| {
+ _ = weak.update(cx, |this, _| {
+ // Convert base ix between 2 line widths to keep the shown memory address roughly the same.
+ // All widths are powers of 2, so the conversion should be lossless.
+ match this.view_state.line_width.width.cmp(&width.width) {
+ std::cmp::Ordering::Less => {
+ // We're converting up.
+ let shift = width.width.trailing_zeros()
+ - this.view_state.line_width.width.trailing_zeros();
+ this.view_state.base_row >>= shift;
+ }
+ std::cmp::Ordering::Greater => {
+ // We're converting down.
+ let shift = this.view_state.line_width.width.trailing_zeros()
+ - width.width.trailing_zeros();
+ this.view_state.base_row <<= shift;
+ }
+ _ => {}
+ }
+ this.view_state.line_width = width.clone();
+ });
+ });
+ }
+ if let Some(ix) = WIDTHS
+ .iter()
+ .position(|width| width.width == selected_width.width)
+ {
+ for _ in 0..=ix {
+ this.select_next(&Default::default(), window, cx);
+ }
+ }
+ this
+ }),
+ )
+ .handle(self.width_picker_handle.clone())
+ }
+
+ fn page_down(&mut self, _: &menu::SelectLast, _: &mut Window, cx: &mut Context<Self>) {
+ self.view_state.base_row = self
+ .view_state
+ .base_row
+ .overflowing_add(self.view_state.row_count())
+ .0;
+ cx.notify();
+ }
+ fn page_up(&mut self, _: &menu::SelectFirst, _: &mut Window, cx: &mut Context<Self>) {
+ self.view_state.base_row = self
+ .view_state
+ .base_row
+ .overflowing_sub(self.view_state.row_count())
+ .0;
+ cx.notify();
+ }
+
+ fn change_query_bar_mode(
+ &mut self,
+ is_writing_memory: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if is_writing_memory == self.is_writing_memory {
+ return;
+ }
+ if !self.is_writing_memory {
+ self.query_editor.update(cx, |this, cx| {
+ this.clear(window, cx);
+ this.set_placeholder_text("Write to Selected Memory Range", cx);
+ });
+ self.is_writing_memory = true;
+ self.query_editor.focus_handle(cx).focus(window);
+ } else {
+ self.query_editor.update(cx, |this, cx| {
+ this.clear(window, cx);
+ this.set_placeholder_text("Go to Memory Address / Expression", cx);
+ });
+ self.is_writing_memory = false;
+ }
+ }
+
+ fn toggle_data_breakpoint(
+ &mut self,
+ _: &crate::ToggleDataBreakpoint,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(SelectedMemoryRange::DragComplete(selection)) = self.view_state.selection.clone()
+ else {
+ return;
+ };
+ let range = selection.memory_range();
+ let context = Arc::new(DataBreakpointContext::Address {
+ address: range.start().to_string(),
+ bytes: Some(*range.end() - *range.start()),
+ });
+
+ self.session.update(cx, |this, cx| {
+ let data_breakpoint_info = this.data_breakpoint_info(context.clone(), None, cx);
+ cx.spawn(async move |this, cx| {
+ if let Some(info) = data_breakpoint_info.await {
+ let Some(data_id) = info.data_id.clone() else {
+ return;
+ };
+ _ = this.update(cx, |this, cx| {
+ this.create_data_breakpoint(
+ context,
+ data_id.clone(),
+ dap::DataBreakpoint {
+ data_id,
+ access_type: None,
+ condition: None,
+ hit_condition: None,
+ },
+ cx,
+ );
+ });
+ }
+ })
+ .detach();
+ })
+ }
+
+ fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
+ if let Some(SelectedMemoryRange::DragComplete(drag)) = &self.view_state.selection {
+ // Go into memory writing mode.
+ if !self.is_writing_memory {
+ let should_return = self.session.update(cx, |session, cx| {
+ if !session
+ .capabilities()
+ .supports_write_memory_request
+ .unwrap_or_default()
+ {
+ let adapter_name = session.adapter();
+ // We cannot write memory with this adapter.
+ _ = self.workspace.update(cx, |this, cx| {
+ this.toggle_status_toast(
+ StatusToast::new(format!(
+ "Debug Adapter `{adapter_name}` does not support writing to memory"
+ ), cx, |this, cx| {
+ cx.spawn(async move |this, cx| {
+ cx.background_executor().timer(Duration::from_secs(2)).await;
+ _ = this.update(cx, |_, cx| {
+ cx.emit(DismissEvent)
+ });
+ }).detach();
+ this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
+ }),
+ cx,
+ );
+ });
+ true
+ } else {
+ false
+ }
+ });
+ if should_return {
+ return;
+ }
+
+ self.change_query_bar_mode(true, window, cx);
+ } else if self.query_editor.focus_handle(cx).is_focused(window) {
+ let mut text = self.query_editor.read(cx).text(cx);
+ if text.chars().any(|c| !c.is_ascii_hexdigit()) {
+ // Interpret this text as a string and oh-so-conveniently convert it.
+ text = text.bytes().map(|byte| format!("{:02x}", byte)).collect();
+ }
+ self.session.update(cx, |this, cx| {
+ let range = drag.memory_range();
+
+ if let Ok(as_hex) = hex::decode(text) {
+ this.write_memory(*range.start(), &as_hex, cx);
+ }
+ });
+ self.change_query_bar_mode(false, window, cx);
+ }
+
+ cx.notify();
+ return;
+ }
+ // Just change the currently viewed address.
+ if !self.query_editor.focus_handle(cx).is_focused(window) {
+ return;
+ }
+ self.jump_to_query_bar_address(cx);
+ }
+
+ fn jump_to_query_bar_address(&mut self, cx: &mut Context<Self>) {
+ use parse_int::parse;
+ let text = self.query_editor.read(cx).text(cx);
+
+ let Ok(as_address) = parse::<u64>(&text) else {
+ return self.jump_to_expression(text, cx);
+ };
+ self.jump_to_address(as_address, cx);
+ }
+
+ fn jump_to_address(&mut self, address: u64, cx: &mut Context<Self>) {
+ self.view_state.base_row = (address & !0xfff) / self.view_state.line_width.width as u64;
+ let line_ix = (address & 0xfff) / self.view_state.line_width.width as u64;
+ self.scroll_handle
+ .scroll_to_item(line_ix as usize, ScrollStrategy::Center);
+ cx.notify();
+ }
+
+ fn jump_to_expression(&mut self, expr: String, cx: &mut Context<Self>) {
+ let Ok(selected_frame) = self
+ .stack_frame_list
+ .update(cx, |this, _| this.opened_stack_frame_id())
+ else {
+ return;
+ };
+ let reference = self.session.update(cx, |this, cx| {
+ this.memory_reference_of_expr(selected_frame, expr, cx)
+ });
+ cx.spawn(async move |this, cx| {
+ if let Some(reference) = reference.await {
+ _ = this.update(cx, |this, cx| {
+ let Ok(address) = parse_int::parse::<u64>(&reference) else {
+ return;
+ };
+ this.jump_to_address(address, cx);
+ });
+ }
+ })
+ .detach();
+ }
+
+ fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
+ self.view_state.selection = None;
+ cx.notify();
+ }
+
+ /// Jump to memory pointed to by selected memory range.
+ fn go_to_address(
+ &mut self,
+ _: &GoToSelectedAddress,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(SelectedMemoryRange::DragComplete(drag)) = self.view_state.selection.clone()
+ else {
+ return;
+ };
+ let range = drag.memory_range();
+ let Some(memory): Option<Vec<u8>> = self.session.update(cx, |this, cx| {
+ this.read_memory(range, cx).map(|cell| cell.0).collect()
+ }) else {
+ return;
+ };
+ if memory.len() > 8 {
+ return;
+ }
+ let zeros_to_write = 8 - memory.len();
+ let mut acc = String::from("0x");
+ acc.extend(std::iter::repeat("00").take(zeros_to_write));
+ let as_query = memory.into_iter().rev().fold(acc, |mut acc, byte| {
+ _ = write!(&mut acc, "{:02x}", byte);
+ acc
+ });
+ self.query_editor.update(cx, |this, cx| {
+ this.set_text(as_query, window, cx);
+ });
+ self.jump_to_query_bar_address(cx);
+ }
+
+ fn deploy_memory_context_menu(
+ &mut self,
+ range: RangeInclusive<u64>,
+ position: Point<Pixels>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let session = self.session.clone();
+ let context_menu = ContextMenu::build(window, cx, |menu, _, cx| {
+ let range_too_large = range.end() - range.start() > std::mem::size_of::<u64>() as u64;
+ let caps = session.read(cx).capabilities();
+ let supports_data_breakpoints = caps.supports_data_breakpoints.unwrap_or_default()
+ && caps.supports_data_breakpoint_bytes.unwrap_or_default();
+ let memory_unreadable = LazyCell::new(|| {
+ session.update(cx, |this, cx| {
+ this.read_memory(range.clone(), cx)
+ .any(|cell| cell.0.is_none())
+ })
+ });
+
+ let mut menu = menu.action_disabled_when(
+ range_too_large || *memory_unreadable,
+ "Go To Selected Address",
+ GoToSelectedAddress.boxed_clone(),
+ );
+
+ if supports_data_breakpoints {
+ menu = menu.action_disabled_when(
+ *memory_unreadable,
+ "Set Data Breakpoint",
+ ToggleDataBreakpoint.boxed_clone(),
+ );
+ }
+ menu.context(self.focus_handle.clone())
+ });
+
+ cx.focus_view(&context_menu, window);
+ let subscription = cx.subscribe_in(
+ &context_menu,
+ window,
+ |this, _, _: &DismissEvent, window, cx| {
+ if this.open_context_menu.as_ref().is_some_and(|context_menu| {
+ context_menu.0.focus_handle(cx).contains_focused(window, cx)
+ }) {
+ cx.focus_self(window);
+ }
+ this.open_context_menu.take();
+ cx.notify();
+ },
+ );
+
+ self.open_context_menu = Some((context_menu, position, subscription));
+ }
+}
+
+#[derive(Clone)]
+struct ViewWidth {
+ width: u8,
+ label: SharedString,
+}
+
+impl ViewWidth {
+ const fn new(width: u8, label: &'static str) -> Self {
+ Self {
+ width,
+ label: SharedString::new_static(label),
+ }
+ }
+}
+
+static WIDTHS: [ViewWidth; 7] = [
+ ViewWidth::new(1, "1 byte"),
+ ViewWidth::new(2, "2 bytes"),
+ ViewWidth::new(4, "4 bytes"),
+ ViewWidth::new(8, "8 bytes"),
+ ViewWidth::new(16, "16 bytes"),
+ ViewWidth::new(32, "32 bytes"),
+ ViewWidth::new(64, "64 bytes"),
+];
+
+fn render_single_memory_view_line(
+ memory: &[MemoryCell],
+ ix: u64,
+ weak: gpui::WeakEntity<MemoryView>,
+ cx: &mut App,
+) -> AnyElement {
+ let Ok(view_state) = weak.update(cx, |this, _| this.view_state.clone()) else {
+ return div().into_any();
+ };
+ let base_address = (view_state.base_row + ix) * view_state.line_width.width as u64;
+
+ h_flex()
+ .id((
+ "memory-view-row-full",
+ ix * view_state.line_width.width as u64,
+ ))
+ .size_full()
+ .gap_x_2()
+ .child(
+ div()
+ .child(
+ Label::new(format!("{:016X}", base_address))
+ .buffer_font(cx)
+ .size(ui::LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .px_1()
+ .border_r_1()
+ .border_color(Color::Muted.color(cx)),
+ )
+ .child(
+ h_flex()
+ .id((
+ "memory-view-row-raw-memory",
+ ix * view_state.line_width.width as u64,
+ ))
+ .px_1()
+ .children(memory.iter().enumerate().map(|(cell_ix, cell)| {
+ let weak = weak.clone();
+ div()
+ .id(("memory-view-row-raw-memory-cell", cell_ix as u64))
+ .px_0p5()
+ .when_some(view_state.selection.as_ref(), |this, selection| {
+ this.when(selection.contains(base_address + cell_ix as u64), |this| {
+ let weak = weak.clone();
+
+ this.bg(Color::Accent.color(cx)).when(
+ !selection.is_dragging(),
+ |this| {
+ let selection = selection.drag().memory_range();
+ this.on_mouse_down(
+ MouseButton::Right,
+ move |click, window, cx| {
+ _ = weak.update(cx, |this, cx| {
+ this.deploy_memory_context_menu(
+ selection.clone(),
+ click.position,
+ window,
+ cx,
+ )
+ });
+ cx.stop_propagation();
+ },
+ )
+ },
+ )
+ })
+ })
+ .child(
+ Label::new(
+ cell.0
+ .map(|val| HEX_BYTES_MEMOIZED[val as usize].clone())
+ .unwrap_or_else(|| UNKNOWN_BYTE.clone()),
+ )
+ .buffer_font(cx)
+ .when(cell.0.is_none(), |this| this.color(Color::Muted))
+ .size(ui::LabelSize::Small),
+ )
+ .on_drag(
+ Drag {
+ start_address: base_address + cell_ix as u64,
+ end_address: base_address + cell_ix as u64,
+ },
+ {
+ let weak = weak.clone();
+ move |drag, _, _, cx| {
+ _ = weak.update(cx, |this, _| {
+ this.view_state.selection =
+ Some(SelectedMemoryRange::DragUnderway(drag.clone()));
+ });
+
+ cx.new(|_| Empty)
+ }
+ },
+ )
+ .on_drop({
+ let weak = weak.clone();
+ move |drag: &Drag, _, cx| {
+ _ = weak.update(cx, |this, _| {
+ this.view_state.selection =
+ Some(SelectedMemoryRange::DragComplete(Drag {
+ start_address: drag.start_address,
+ end_address: base_address + cell_ix as u64,
+ }));
+ });
+ }
+ })
+ .drag_over(move |style, drag: &Drag, _, cx| {
+ _ = weak.update(cx, |this, _| {
+ this.view_state.selection =
+ Some(SelectedMemoryRange::DragUnderway(Drag {
+ start_address: drag.start_address,
+ end_address: base_address + cell_ix as u64,
+ }));
+ });
+
+ style
+ })
+ })),
+ )
+ .child(
+ h_flex()
+ .id((
+ "memory-view-row-ascii-memory",
+ ix * view_state.line_width.width as u64,
+ ))
+ .h_full()
+ .px_1()
+ .mr_4()
+ // .gap_x_1p5()
+ .border_x_1()
+ .border_color(Color::Muted.color(cx))
+ .children(memory.iter().enumerate().map(|(ix, cell)| {
+ let as_character = char::from(cell.0.unwrap_or(0));
+ let as_visible = if as_character.is_ascii_graphic() {
+ as_character
+ } else {
+ '·'
+ };
+ div()
+ .px_0p5()
+ .when_some(view_state.selection.as_ref(), |this, selection| {
+ this.when(selection.contains(base_address + ix as u64), |this| {
+ this.bg(Color::Accent.color(cx))
+ })
+ })
+ .child(
+ Label::new(format!("{as_visible}"))
+ .buffer_font(cx)
+ .when(cell.0.is_none(), |this| this.color(Color::Muted))
+ .size(ui::LabelSize::Small),
+ )
+ })),
+ )
+ .into_any()
+}
+
+impl Render for MemoryView {
+ fn render(
+ &mut self,
+ window: &mut ui::Window,
+ cx: &mut ui::Context<Self>,
+ ) -> impl ui::IntoElement {
+ let (icon, tooltip_text) = if self.is_writing_memory {
+ (IconName::Pencil, "Edit memory at a selected address")
+ } else {
+ (
+ IconName::LocationEdit,
+ "Change address of currently viewed memory",
+ )
+ };
+ v_flex()
+ .id("Memory-view")
+ .on_action(cx.listener(Self::cancel))
+ .on_action(cx.listener(Self::go_to_address))
+ .p_1()
+ .on_action(cx.listener(Self::confirm))
+ .on_action(cx.listener(Self::toggle_data_breakpoint))
+ .on_action(cx.listener(Self::page_down))
+ .on_action(cx.listener(Self::page_up))
+ .size_full()
+ .track_focus(&self.focus_handle)
+ .on_hover(cx.listener(|this, hovered, window, cx| {
+ if *hovered {
+ this.show_scrollbar = true;
+ this.hide_scrollbar_task.take();
+ cx.notify();
+ } else if !this.focus_handle.contains_focused(window, cx) {
+ this.hide_scrollbar(window, cx);
+ }
+ }))
+ .child(
+ h_flex()
+ .w_full()
+ .mb_0p5()
+ .gap_1()
+ .child(
+ h_flex()
+ .w_full()
+ .rounded_md()
+ .border_1()
+ .gap_x_2()
+ .px_2()
+ .py_0p5()
+ .mb_0p5()
+ .bg(cx.theme().colors().editor_background)
+ .when_else(
+ self.query_editor
+ .focus_handle(cx)
+ .contains_focused(window, cx),
+ |this| this.border_color(cx.theme().colors().border_focused),
+ |this| this.border_color(cx.theme().colors().border_transparent),
+ )
+ .child(
+ div()
+ .id("memory-view-editor-icon")
+ .child(Icon::new(icon).size(ui::IconSize::XSmall))
+ .tooltip(Tooltip::text(tooltip_text)),
+ )
+ .child(self.render_query_bar(cx)),
+ )
+ .child(self.render_width_picker(window, cx)),
+ )
+ .child(Divider::horizontal())
+ .child(
+ v_flex()
+ .size_full()
+ .on_mouse_move(cx.listener(|this, evt: &MouseMoveEvent, _, _| {
+ this.handle_drag(evt);
+ }))
+ .child(self.render_memory(cx).size_full())
+ .children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
+ deferred(
+ anchored()
+ .position(*position)
+ .anchor(gpui::Corner::TopLeft)
+ .child(menu.clone()),
+ )
+ .with_priority(1)
+ }))
+ .children(self.render_vertical_scrollbar(cx)),
+ )
+ }
+}
@@ -1,3 +1,5 @@
+use crate::session::running::{RunningState, memory_view::MemoryView};
+
use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
use dap::{
ScopePresentationHint, StackFrameId, VariablePresentationHint, VariablePresentationHintKind,
@@ -7,24 +9,37 @@ use editor::Editor;
use gpui::{
Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity,
FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription,
- TextStyleRefinement, UniformListScrollHandle, actions, anchored, deferred, uniform_list,
+ TextStyleRefinement, UniformListScrollHandle, WeakEntity, actions, anchored, deferred,
+ uniform_list,
};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
-use project::debugger::session::{Session, SessionEvent, Watcher};
+use project::debugger::{
+ dap_command::DataBreakpointContext,
+ session::{Session, SessionEvent, Watcher},
+};
use std::{collections::HashMap, ops::Range, sync::Arc};
use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*};
-use util::debug_panic;
+use util::{debug_panic, maybe};
actions!(
variable_list,
[
+ /// Expands the selected variable entry to show its children.
ExpandSelectedEntry,
+ /// Collapses the selected variable entry to hide its children.
CollapseSelectedEntry,
+ /// Copies the variable name to the clipboard.
CopyVariableName,
+ /// Copies the variable value to the clipboard.
CopyVariableValue,
+ /// Edits the value of the selected variable.
EditVariable,
+ /// Adds the selected variable to the watch list.
AddWatch,
+ /// Removes the selected variable from the watch list.
RemoveWatch,
+ /// Jump to variable's memory location.
+ GoToMemory,
]
);
@@ -79,30 +94,30 @@ impl EntryPath {
}
#[derive(Debug, Clone, PartialEq)]
-enum EntryKind {
+enum DapEntry {
Watcher(Watcher),
Variable(dap::Variable),
Scope(dap::Scope),
}
-impl EntryKind {
+impl DapEntry {
fn as_watcher(&self) -> Option<&Watcher> {
match self {
- EntryKind::Watcher(watcher) => Some(watcher),
+ DapEntry::Watcher(watcher) => Some(watcher),
_ => None,
}
}
fn as_variable(&self) -> Option<&dap::Variable> {
match self {
- EntryKind::Variable(dap) => Some(dap),
+ DapEntry::Variable(dap) => Some(dap),
_ => None,
}
}
fn as_scope(&self) -> Option<&dap::Scope> {
match self {
- EntryKind::Scope(dap) => Some(dap),
+ DapEntry::Scope(dap) => Some(dap),
_ => None,
}
}
@@ -110,38 +125,38 @@ impl EntryKind {
#[cfg(test)]
fn name(&self) -> &str {
match self {
- EntryKind::Watcher(watcher) => &watcher.expression,
- EntryKind::Variable(dap) => &dap.name,
- EntryKind::Scope(dap) => &dap.name,
+ DapEntry::Watcher(watcher) => &watcher.expression,
+ DapEntry::Variable(dap) => &dap.name,
+ DapEntry::Scope(dap) => &dap.name,
}
}
}
#[derive(Debug, Clone, PartialEq)]
struct ListEntry {
- dap_kind: EntryKind,
+ entry: DapEntry,
path: EntryPath,
}
impl ListEntry {
fn as_watcher(&self) -> Option<&Watcher> {
- self.dap_kind.as_watcher()
+ self.entry.as_watcher()
}
fn as_variable(&self) -> Option<&dap::Variable> {
- self.dap_kind.as_variable()
+ self.entry.as_variable()
}
fn as_scope(&self) -> Option<&dap::Scope> {
- self.dap_kind.as_scope()
+ self.entry.as_scope()
}
fn item_id(&self) -> ElementId {
use std::fmt::Write;
- let mut id = match &self.dap_kind {
- EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
- EntryKind::Variable(dap) => format!("variable-{}", dap.name),
- EntryKind::Scope(dap) => format!("scope-{}", dap.name),
+ let mut id = match &self.entry {
+ DapEntry::Watcher(watcher) => format!("watcher-{}", watcher.expression),
+ DapEntry::Variable(dap) => format!("variable-{}", dap.name),
+ DapEntry::Scope(dap) => format!("scope-{}", dap.name),
};
for name in self.path.indices.iter() {
_ = write!(id, "-{}", name);
@@ -151,10 +166,10 @@ impl ListEntry {
fn item_value_id(&self) -> ElementId {
use std::fmt::Write;
- let mut id = match &self.dap_kind {
- EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
- EntryKind::Variable(dap) => format!("variable-{}", dap.name),
- EntryKind::Scope(dap) => format!("scope-{}", dap.name),
+ let mut id = match &self.entry {
+ DapEntry::Watcher(watcher) => format!("watcher-{}", watcher.expression),
+ DapEntry::Variable(dap) => format!("variable-{}", dap.name),
+ DapEntry::Scope(dap) => format!("scope-{}", dap.name),
};
for name in self.path.indices.iter() {
_ = write!(id, "-{}", name);
@@ -181,13 +196,17 @@ pub struct VariableList {
focus_handle: FocusHandle,
edited_path: Option<(EntryPath, Entity<Editor>)>,
disabled: bool,
+ memory_view: Entity<MemoryView>,
+ weak_running: WeakEntity<RunningState>,
_subscriptions: Vec<Subscription>,
}
impl VariableList {
- pub fn new(
+ pub(crate) fn new(
session: Entity<Session>,
stack_frame_list: Entity<StackFrameList>,
+ memory_view: Entity<MemoryView>,
+ weak_running: WeakEntity<RunningState>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -204,6 +223,7 @@ impl VariableList {
SessionEvent::Variables | SessionEvent::Watchers => {
this.build_entries(cx);
}
+
_ => {}
}),
cx.on_focus_out(&focus_handle, window, |this, _, _, cx| {
@@ -227,6 +247,8 @@ impl VariableList {
edited_path: None,
entries: Default::default(),
entry_states: Default::default(),
+ weak_running,
+ memory_view,
}
}
@@ -277,7 +299,7 @@ impl VariableList {
scope.variables_reference,
scope.variables_reference,
EntryPath::for_scope(&scope.name),
- EntryKind::Scope(scope),
+ DapEntry::Scope(scope),
)
})
.collect::<Vec<_>>();
@@ -291,7 +313,7 @@ impl VariableList {
watcher.variables_reference,
watcher.variables_reference,
EntryPath::for_watcher(watcher.expression.clone()),
- EntryKind::Watcher(watcher.clone()),
+ DapEntry::Watcher(watcher.clone()),
)
})
.collect::<Vec<_>>(),
@@ -302,9 +324,9 @@ impl VariableList {
while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop()
{
match &dap_kind {
- EntryKind::Watcher(watcher) => path = path.with_child(watcher.expression.clone()),
- EntryKind::Variable(dap) => path = path.with_name(dap.name.clone().into()),
- EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()),
+ DapEntry::Watcher(watcher) => path = path.with_child(watcher.expression.clone()),
+ DapEntry::Variable(dap) => path = path.with_name(dap.name.clone().into()),
+ DapEntry::Scope(dap) => path = path.with_child(dap.name.clone().into()),
}
let var_state = self
@@ -329,7 +351,7 @@ impl VariableList {
});
entries.push(ListEntry {
- dap_kind,
+ entry: dap_kind,
path: path.clone(),
});
@@ -342,7 +364,7 @@ impl VariableList {
variables_reference,
child.variables_reference,
path.with_child(child.name.clone().into()),
- EntryKind::Variable(child),
+ DapEntry::Variable(child),
)
}));
}
@@ -373,9 +395,9 @@ impl VariableList {
pub fn completion_variables(&self, _cx: &mut Context<Self>) -> Vec<dap::Variable> {
self.entries
.iter()
- .filter_map(|entry| match &entry.dap_kind {
- EntryKind::Variable(dap) => Some(dap.clone()),
- EntryKind::Scope(_) | EntryKind::Watcher { .. } => None,
+ .filter_map(|entry| match &entry.entry {
+ DapEntry::Variable(dap) => Some(dap.clone()),
+ DapEntry::Scope(_) | DapEntry::Watcher { .. } => None,
})
.collect()
}
@@ -393,12 +415,12 @@ impl VariableList {
.get(ix)
.and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?;
- match &entry.dap_kind {
- EntryKind::Watcher { .. } => {
+ match &entry.entry {
+ DapEntry::Watcher { .. } => {
Some(self.render_watcher(entry, *state, window, cx))
}
- EntryKind::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
- EntryKind::Scope(_) => Some(self.render_scope(entry, *state, cx)),
+ DapEntry::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
+ DapEntry::Scope(_) => Some(self.render_scope(entry, *state, cx)),
}
})
.collect()
@@ -555,6 +577,51 @@ impl VariableList {
}
}
+ fn jump_to_variable_memory(
+ &mut self,
+ _: &GoToMemory,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ _ = maybe!({
+ let selection = self.selection.as_ref()?;
+ let entry = self.entries.iter().find(|entry| &entry.path == selection)?;
+ let var = entry.entry.as_variable()?;
+ let memory_reference = var.memory_reference.as_deref()?;
+
+ let sizeof_expr = if var.type_.as_ref().is_some_and(|t| {
+ t.chars()
+ .all(|c| c.is_whitespace() || c.is_alphabetic() || c == '*')
+ }) {
+ var.type_.as_deref()
+ } else {
+ var.evaluate_name
+ .as_deref()
+ .map(|name| name.strip_prefix("/nat ").unwrap_or_else(|| name))
+ };
+ self.memory_view.update(cx, |this, cx| {
+ this.go_to_memory_reference(
+ memory_reference,
+ sizeof_expr,
+ self.selected_stack_frame_id,
+ cx,
+ );
+ });
+ let weak_panel = self.weak_running.clone();
+
+ window.defer(cx, move |window, cx| {
+ _ = weak_panel.update(cx, |this, cx| {
+ this.activate_item(
+ crate::persistence::DebuggerPaneItem::MemoryView,
+ window,
+ cx,
+ );
+ });
+ });
+ Some(())
+ });
+ }
+
fn deploy_list_entry_context_menu(
&mut self,
entry: ListEntry,
@@ -562,49 +629,156 @@ impl VariableList {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let supports_set_variable = self
- .session
- .read(cx)
- .capabilities()
- .supports_set_variable
- .unwrap_or_default();
-
- let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
- menu.when(entry.as_variable().is_some(), |menu| {
- menu.action("Copy Name", CopyVariableName.boxed_clone())
- .action("Copy Value", CopyVariableValue.boxed_clone())
- .when(supports_set_variable, |menu| {
- menu.action("Edit Value", EditVariable.boxed_clone())
+ let (supports_set_variable, supports_data_breakpoints, supports_go_to_memory) =
+ self.session.read_with(cx, |session, _| {
+ (
+ session
+ .capabilities()
+ .supports_set_variable
+ .unwrap_or_default(),
+ session
+ .capabilities()
+ .supports_data_breakpoints
+ .unwrap_or_default(),
+ session
+ .capabilities()
+ .supports_read_memory_request
+ .unwrap_or_default(),
+ )
+ });
+ let can_toggle_data_breakpoint = entry
+ .as_variable()
+ .filter(|_| supports_data_breakpoints)
+ .and_then(|variable| {
+ let variables_reference = self
+ .entry_states
+ .get(&entry.path)
+ .map(|state| state.parent_reference)?;
+ Some(self.session.update(cx, |session, cx| {
+ session.data_breakpoint_info(
+ Arc::new(DataBreakpointContext::Variable {
+ variables_reference,
+ name: variable.name.clone(),
+ bytes: None,
+ }),
+ None,
+ cx,
+ )
+ }))
+ });
+
+ let focus_handle = self.focus_handle.clone();
+ cx.spawn_in(window, async move |this, cx| {
+ let can_toggle_data_breakpoint = if let Some(task) = can_toggle_data_breakpoint {
+ task.await.is_some()
+ } else {
+ true
+ };
+ cx.update(|window, cx| {
+ let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
+ menu.when_some(entry.as_variable(), |menu, _| {
+ menu.action("Copy Name", CopyVariableName.boxed_clone())
+ .action("Copy Value", CopyVariableValue.boxed_clone())
+ .when(supports_set_variable, |menu| {
+ menu.action("Edit Value", EditVariable.boxed_clone())
+ })
+ .when(supports_go_to_memory, |menu| {
+ menu.action("Go To Memory", GoToMemory.boxed_clone())
+ })
+ .action("Watch Variable", AddWatch.boxed_clone())
+ .when(can_toggle_data_breakpoint, |menu| {
+ menu.action(
+ "Toggle Data Breakpoint",
+ crate::ToggleDataBreakpoint.boxed_clone(),
+ )
+ })
})
- .action("Watch Variable", AddWatch.boxed_clone())
- })
- .when(entry.as_watcher().is_some(), |menu| {
- menu.action("Copy Name", CopyVariableName.boxed_clone())
- .action("Copy Value", CopyVariableValue.boxed_clone())
- .when(supports_set_variable, |menu| {
- menu.action("Edit Value", EditVariable.boxed_clone())
+ .when(entry.as_watcher().is_some(), |menu| {
+ menu.action("Copy Name", CopyVariableName.boxed_clone())
+ .action("Copy Value", CopyVariableValue.boxed_clone())
+ .when(supports_set_variable, |menu| {
+ menu.action("Edit Value", EditVariable.boxed_clone())
+ })
+ .action("Remove Watch", RemoveWatch.boxed_clone())
})
- .action("Remove Watch", RemoveWatch.boxed_clone())
+ .context(focus_handle.clone())
+ });
+
+ _ = this.update(cx, |this, cx| {
+ cx.focus_view(&context_menu, window);
+ let subscription = cx.subscribe_in(
+ &context_menu,
+ window,
+ |this, _, _: &DismissEvent, window, cx| {
+ if this.open_context_menu.as_ref().is_some_and(|context_menu| {
+ context_menu.0.focus_handle(cx).contains_focused(window, cx)
+ }) {
+ cx.focus_self(window);
+ }
+ this.open_context_menu.take();
+ cx.notify();
+ },
+ );
+
+ this.open_context_menu = Some((context_menu, position, subscription));
+ });
})
- .context(self.focus_handle.clone())
+ })
+ .detach();
+ }
+
+ fn toggle_data_breakpoint(
+ &mut self,
+ _: &crate::ToggleDataBreakpoint,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(entry) = self
+ .selection
+ .as_ref()
+ .and_then(|selection| self.entries.iter().find(|entry| &entry.path == selection))
+ else {
+ return;
+ };
+
+ let Some((name, var_ref)) = entry.as_variable().map(|var| &var.name).zip(
+ self.entry_states
+ .get(&entry.path)
+ .map(|state| state.parent_reference),
+ ) else {
+ return;
+ };
+
+ let context = Arc::new(DataBreakpointContext::Variable {
+ variables_reference: var_ref,
+ name: name.clone(),
+ bytes: None,
+ });
+ let data_breakpoint = self.session.update(cx, |session, cx| {
+ session.data_breakpoint_info(context.clone(), None, cx)
});
- cx.focus_view(&context_menu, window);
- let subscription = cx.subscribe_in(
- &context_menu,
- window,
- |this, _, _: &DismissEvent, window, cx| {
- if this.open_context_menu.as_ref().is_some_and(|context_menu| {
- context_menu.0.focus_handle(cx).contains_focused(window, cx)
- }) {
- cx.focus_self(window);
- }
- this.open_context_menu.take();
+ let session = self.session.downgrade();
+ cx.spawn(async move |_, cx| {
+ let Some(data_id) = data_breakpoint.await.and_then(|info| info.data_id) else {
+ return;
+ };
+ _ = session.update(cx, |session, cx| {
+ session.create_data_breakpoint(
+ context,
+ data_id.clone(),
+ dap::DataBreakpoint {
+ data_id,
+ access_type: None,
+ condition: None,
+ hit_condition: None,
+ },
+ cx,
+ );
cx.notify();
- },
- );
-
- self.open_context_menu = Some((context_menu, position, subscription));
+ });
+ })
+ .detach();
}
fn copy_variable_name(
@@ -621,10 +795,10 @@ impl VariableList {
return;
};
- let variable_name = match &entry.dap_kind {
- EntryKind::Variable(dap) => dap.name.clone(),
- EntryKind::Watcher(watcher) => watcher.expression.to_string(),
- EntryKind::Scope(_) => return,
+ let variable_name = match &entry.entry {
+ DapEntry::Variable(dap) => dap.name.clone(),
+ DapEntry::Watcher(watcher) => watcher.expression.to_string(),
+ DapEntry::Scope(_) => return,
};
cx.write_to_clipboard(ClipboardItem::new_string(variable_name));
@@ -644,10 +818,10 @@ impl VariableList {
return;
};
- let variable_value = match &entry.dap_kind {
- EntryKind::Variable(dap) => dap.value.clone(),
- EntryKind::Watcher(watcher) => watcher.value.to_string(),
- EntryKind::Scope(_) => return,
+ let variable_value = match &entry.entry {
+ DapEntry::Variable(dap) => dap.value.clone(),
+ DapEntry::Watcher(watcher) => watcher.value.to_string(),
+ DapEntry::Scope(_) => return,
};
cx.write_to_clipboard(ClipboardItem::new_string(variable_value));
@@ -662,10 +836,10 @@ impl VariableList {
return;
};
- let variable_value = match &entry.dap_kind {
- EntryKind::Watcher(watcher) => watcher.value.to_string(),
- EntryKind::Variable(variable) => variable.value.clone(),
- EntryKind::Scope(_) => return,
+ let variable_value = match &entry.entry {
+ DapEntry::Watcher(watcher) => watcher.value.to_string(),
+ DapEntry::Variable(variable) => variable.value.clone(),
+ DapEntry::Scope(_) => return,
};
let editor = Self::create_variable_editor(&variable_value, window, cx);
@@ -746,7 +920,7 @@ impl VariableList {
"{}{} {}{}",
INDENT.repeat(state.depth - 1),
if state.is_expanded { "v" } else { ">" },
- entry.dap_kind.name(),
+ entry.entry.name(),
if self.selection.as_ref() == Some(&entry.path) {
" <=== selected"
} else {
@@ -763,8 +937,8 @@ impl VariableList {
pub(crate) fn scopes(&self) -> Vec<dap::Scope> {
self.entries
.iter()
- .filter_map(|entry| match &entry.dap_kind {
- EntryKind::Scope(scope) => Some(scope),
+ .filter_map(|entry| match &entry.entry {
+ DapEntry::Scope(scope) => Some(scope),
_ => None,
})
.cloned()
@@ -778,10 +952,10 @@ impl VariableList {
let mut idx = 0;
for entry in self.entries.iter() {
- match &entry.dap_kind {
- EntryKind::Watcher { .. } => continue,
- EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()),
- EntryKind::Scope(scope) => {
+ match &entry.entry {
+ DapEntry::Watcher { .. } => continue,
+ DapEntry::Variable(dap) => scopes[idx].1.push(dap.clone()),
+ DapEntry::Scope(scope) => {
if scopes.len() > 0 {
idx += 1;
}
@@ -799,8 +973,8 @@ impl VariableList {
pub(crate) fn variables(&self) -> Vec<dap::Variable> {
self.entries
.iter()
- .filter_map(|entry| match &entry.dap_kind {
- EntryKind::Variable(variable) => Some(variable),
+ .filter_map(|entry| match &entry.entry {
+ DapEntry::Variable(variable) => Some(variable),
_ => None,
})
.cloned()
@@ -1351,6 +1525,8 @@ impl Render for VariableList {
.on_action(cx.listener(Self::edit_variable))
.on_action(cx.listener(Self::add_watcher))
.on_action(cx.listener(Self::remove_watcher))
+ .on_action(cx.listener(Self::toggle_data_breakpoint))
+ .on_action(cx.listener(Self::jump_to_variable_memory))
.child(
uniform_list(
"variable-list",
@@ -27,7 +27,7 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
- let session = start_debug_session_with(
+ let _session = start_debug_session_with(
&workspace,
cx,
DebugTaskDefinition {
@@ -59,14 +59,6 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
assert!(workspace.active_modal::<AttachModal>(cx).is_none());
})
.unwrap();
-
- let shutdown_session = project.update(cx, |project, cx| {
- project.dap_store().update(cx, |dap_store, cx| {
- dap_store.shutdown_session(session.read(cx).session_id(), cx)
- })
- });
-
- shutdown_session.await.unwrap();
}
#[gpui::test]
@@ -232,7 +232,6 @@ async fn test_escape_code_processing(executor: BackgroundExecutor, cx: &mut Test
location_reference: None,
}))
.await;
- // [crates/debugger_ui/src/session/running/console.rs:147:9] &to_insert = "Could not read source map for file:///Users/cole/roles-at/node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/typescript.js: ENOENT: no such file or directory, open '/Users/cole/roles-at/node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/typescript.js.map'\n"
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: None,
@@ -260,7 +259,6 @@ async fn test_escape_code_processing(executor: BackgroundExecutor, cx: &mut Test
}))
.await;
- // introduce some background highlight
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: None,
@@ -274,7 +272,6 @@ async fn test_escape_code_processing(executor: BackgroundExecutor, cx: &mut Test
location_reference: None,
}))
.await;
- // another random line
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: None,
@@ -294,6 +291,11 @@ async fn test_escape_code_processing(executor: BackgroundExecutor, cx: &mut Test
let _running_state =
active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
cx.focus_self(window);
+ item.running_state().update(cx, |this, cx| {
+ this.console()
+ .update(cx, |this, cx| this.update_output(window, cx));
+ });
+
item.running_state().clone()
});
@@ -427,7 +427,7 @@ async fn test_handle_start_debugging_request(
let sessions = workspace
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
- debug_panel.read(cx).sessions()
+ debug_panel.read(cx).sessions().collect::<Vec<_>>()
})
.unwrap();
assert_eq!(sessions.len(), 1);
@@ -451,7 +451,7 @@ async fn test_handle_start_debugging_request(
.unwrap()
.read(cx)
.session(cx);
- let current_sessions = debug_panel.read(cx).sessions();
+ let current_sessions = debug_panel.read(cx).sessions().collect::<Vec<_>>();
assert_eq!(active_session, current_sessions[1].read(cx).session(cx));
assert_eq!(
active_session.read(cx).parent_session(),
@@ -1796,7 +1796,7 @@ async fn test_debug_adapters_shutdown_on_app_quit(
let panel = workspace.panel::<DebugPanel>(cx).unwrap();
panel.read_with(cx, |panel, _| {
assert!(
- !panel.sessions().is_empty(),
+ panel.sessions().next().is_some(),
"Debug session should be active"
);
});
@@ -2241,3 +2241,34 @@ func main() {
)
.await;
}
+
+#[gpui::test]
+async fn test_trim_multi_line_inline_value(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+ let variables = [("y", "hello\n world")];
+
+ let before = r#"
+fn main() {
+ let y = "hello\n world";
+}
+"#
+ .unindent();
+
+ let after = r#"
+fn main() {
+ let y: hello… = "hello\n world";
+}
+"#
+ .unindent();
+
+ test_inline_values_util(
+ &variables,
+ &[],
+ &before,
+ &after,
+ None,
+ rust_lang(),
+ executor,
+ cx,
+ )
+ .await;
+}
@@ -111,7 +111,6 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
});
running_state.update_in(cx, |this, window, cx| {
- this.ensure_pane_item(DebuggerPaneItem::Modules, window, cx);
this.activate_item(DebuggerPaneItem::Modules, window, cx);
cx.refresh_windows();
});
@@ -1,13 +1,15 @@
use dap::DapRegistry;
+use editor::Editor;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
-use project::{FakeFs, Project};
+use project::{FakeFs, Fs as _, Project};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
+use text::Point;
use util::path;
-// use crate::new_process_modal::NewProcessMode;
+use crate::NewProcessMode;
use crate::tests::{init_test, init_test_workspace};
#[gpui::test]
@@ -159,111 +161,127 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
}
}
-// #[gpui::test]
-// async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
-// init_test(cx);
-
-// let fs = FakeFs::new(executor.clone());
-// fs.insert_tree(
-// path!("/project"),
-// json!({
-// "main.rs": "fn main() {}"
-// }),
-// )
-// .await;
-
-// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
-// let workspace = init_test_workspace(&project, cx).await;
-// let cx = &mut VisualTestContext::from_window(*workspace, cx);
-
-// workspace
-// .update(cx, |workspace, window, cx| {
-// crate::new_process_modal::NewProcessModal::show(
-// workspace,
-// window,
-// NewProcessMode::Debug,
-// None,
-// cx,
-// );
-// })
-// .unwrap();
-
-// cx.run_until_parked();
-
-// let modal = workspace
-// .update(cx, |workspace, _, cx| {
-// workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx)
-// })
-// .unwrap()
-// .expect("Modal should be active");
-
-// modal.update_in(cx, |modal, window, cx| {
-// modal.set_configure("/project/main", "/project", false, window, cx);
-// modal.save_scenario(window, cx);
-// });
-
-// cx.executor().run_until_parked();
-
-// let debug_json_content = fs
-// .load(path!("/project/.zed/debug.json").as_ref())
-// .await
-// .expect("debug.json should exist");
-
-// let expected_content = vec![
-// "[",
-// " {",
-// r#" "adapter": "fake-adapter","#,
-// r#" "label": "main (fake-adapter)","#,
-// r#" "request": "launch","#,
-// r#" "program": "/project/main","#,
-// r#" "cwd": "/project","#,
-// r#" "args": [],"#,
-// r#" "env": {}"#,
-// " }",
-// "]",
-// ];
-
-// let actual_lines: Vec<&str> = debug_json_content.lines().collect();
-// pretty_assertions::assert_eq!(expected_content, actual_lines);
-
-// modal.update_in(cx, |modal, window, cx| {
-// modal.set_configure("/project/other", "/project", true, window, cx);
-// modal.save_scenario(window, cx);
-// });
-
-// cx.executor().run_until_parked();
-
-// let debug_json_content = fs
-// .load(path!("/project/.zed/debug.json").as_ref())
-// .await
-// .expect("debug.json should exist after second save");
-
-// let expected_content = vec![
-// "[",
-// " {",
-// r#" "adapter": "fake-adapter","#,
-// r#" "label": "main (fake-adapter)","#,
-// r#" "request": "launch","#,
-// r#" "program": "/project/main","#,
-// r#" "cwd": "/project","#,
-// r#" "args": [],"#,
-// r#" "env": {}"#,
-// " },",
-// " {",
-// r#" "adapter": "fake-adapter","#,
-// r#" "label": "other (fake-adapter)","#,
-// r#" "request": "launch","#,
-// r#" "program": "/project/other","#,
-// r#" "cwd": "/project","#,
-// r#" "args": [],"#,
-// r#" "env": {}"#,
-// " }",
-// "]",
-// ];
-
-// let actual_lines: Vec<&str> = debug_json_content.lines().collect();
-// pretty_assertions::assert_eq!(expected_content, actual_lines);
-// }
+#[gpui::test]
+async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(executor.clone());
+ fs.insert_tree(
+ path!("/project"),
+ json!({
+ "main.rs": "fn main() {}"
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
+ let workspace = init_test_workspace(&project, cx).await;
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+ workspace
+ .update(cx, |workspace, window, cx| {
+ crate::new_process_modal::NewProcessModal::show(
+ workspace,
+ window,
+ NewProcessMode::Debug,
+ None,
+ cx,
+ );
+ })
+ .unwrap();
+
+ cx.run_until_parked();
+
+ let modal = workspace
+ .update(cx, |workspace, _, cx| {
+ workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx)
+ })
+ .unwrap()
+ .expect("Modal should be active");
+
+ modal.update_in(cx, |modal, window, cx| {
+ modal.set_configure("/project/main", "/project", false, window, cx);
+ modal.save_debug_scenario(window, cx);
+ });
+
+ cx.executor().run_until_parked();
+
+ let editor = workspace
+ .update(cx, |workspace, _window, cx| {
+ workspace.active_item_as::<Editor>(cx).unwrap()
+ })
+ .unwrap();
+
+ let debug_json_content = fs
+ .load(path!("/project/.zed/debug.json").as_ref())
+ .await
+ .expect("debug.json should exist")
+ .lines()
+ .filter(|line| !line.starts_with("//"))
+ .collect::<Vec<_>>()
+ .join("\n");
+
+ let expected_content = indoc::indoc! {r#"
+ [
+ {
+ "adapter": "fake-adapter",
+ "label": "main (fake-adapter)",
+ "request": "launch",
+ "program": "/project/main",
+ "cwd": "/project",
+ "args": [],
+ "env": {}
+ }
+ ]"#};
+
+ pretty_assertions::assert_eq!(expected_content, debug_json_content);
+
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ editor.selections.newest::<Point>(cx).head(),
+ Point::new(5, 2)
+ )
+ });
+
+ modal.update_in(cx, |modal, window, cx| {
+ modal.set_configure("/project/other", "/project", true, window, cx);
+ modal.save_debug_scenario(window, cx);
+ });
+
+ cx.executor().run_until_parked();
+
+ let expected_content = indoc::indoc! {r#"
+ [
+ {
+ "adapter": "fake-adapter",
+ "label": "main (fake-adapter)",
+ "request": "launch",
+ "program": "/project/main",
+ "cwd": "/project",
+ "args": [],
+ "env": {}
+ },
+ {
+ "adapter": "fake-adapter",
+ "label": "other (fake-adapter)",
+ "request": "launch",
+ "program": "/project/other",
+ "cwd": "/project",
+ "args": [],
+ "env": {}
+ }
+ ]"#};
+
+ let debug_json_content = fs
+ .load(path!("/project/.zed/debug.json").as_ref())
+ .await
+ .expect("debug.json should exist")
+ .lines()
+ .filter(|line| !line.starts_with("//"))
+ .collect::<Vec<_>>()
+ .join("\n");
+ pretty_assertions::assert_eq!(expected_content, debug_json_content);
+}
#[gpui::test]
async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
@@ -272,7 +290,6 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte
let mut expected_adapters = vec![
"CodeLLDB",
"Debugpy",
- "PHP",
"JavaScript",
"Delve",
"GDB",
@@ -144,7 +144,6 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
style: BlockStyle::Flex,
render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)),
priority: 1,
- render_in_minimap: false,
}
})
.collect()
@@ -48,7 +48,14 @@ use workspace::{
actions!(
diagnostics,
- [Deploy, ToggleWarnings, ToggleDiagnosticsRefresh]
+ [
+ /// Opens the project diagnostics view.
+ Deploy,
+ /// Toggles the display of warning-level diagnostics.
+ ToggleWarnings,
+ /// Toggles automatic refresh of diagnostics.
+ ToggleDiagnosticsRefresh
+ ]
);
#[derive(Default)]
@@ -649,7 +656,6 @@ impl ProjectDiagnosticsEditor {
block.render_block(editor.clone(), bcx)
}),
priority: 1,
- render_in_minimap: false,
}
});
let block_ids = this.editor.update(cx, |editor, cx| {
@@ -14,7 +14,10 @@ use indoc::indoc;
use language::{DiagnosticSourceKind, Rope};
use lsp::LanguageServerId;
use pretty_assertions::assert_eq;
-use project::FakeFs;
+use project::{
+ FakeFs,
+ project_settings::{GoToDiagnosticSeverity, GoToDiagnosticSeverityFilter},
+};
use rand::{Rng, rngs::StdRng, seq::IteratorRandom as _};
use serde_json::json;
use settings::SettingsStore;
@@ -1005,7 +1008,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
- editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
+ editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
assert_eq!(
editor
.active_diagnostic_group()
@@ -1047,7 +1050,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
"});
cx.update_editor(|editor, window, cx| {
- editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
+ editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
assert_eq!(editor.active_diagnostic_group(), None);
});
cx.assert_editor_state(indoc! {"
@@ -1126,7 +1129,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Fourth diagnostic
cx.update_editor(|editor, window, cx| {
- editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
+ editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc def: i32) -> ˇu32 {
@@ -1135,7 +1138,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Third diagnostic
cx.update_editor(|editor, window, cx| {
- editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
+ editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc ˇdef: i32) -> u32 {
@@ -1144,7 +1147,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Second diagnostic, same place
cx.update_editor(|editor, window, cx| {
- editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
+ editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc ˇdef: i32) -> u32 {
@@ -1153,7 +1156,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// First diagnostic
cx.update_editor(|editor, window, cx| {
- editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
+ editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
@@ -1162,7 +1165,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Wrapped over, fourth diagnostic
cx.update_editor(|editor, window, cx| {
- editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
+ editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc def: i32) -> ˇu32 {
@@ -1181,7 +1184,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// First diagnostic
cx.update_editor(|editor, window, cx| {
- editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
+ editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
@@ -1190,7 +1193,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Second diagnostic
cx.update_editor(|editor, window, cx| {
- editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
+ editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc ˇdef: i32) -> u32 {
@@ -1199,7 +1202,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Third diagnostic, same place
cx.update_editor(|editor, window, cx| {
- editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
+ editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc ˇdef: i32) -> u32 {
@@ -1208,7 +1211,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Fourth diagnostic
cx.update_editor(|editor, window, cx| {
- editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
+ editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abc def: i32) -> ˇu32 {
@@ -1217,7 +1220,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
// Wrapped around, first diagnostic
cx.update_editor(|editor, window, cx| {
- editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
+ editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
@@ -1441,6 +1444,128 @@ async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
);
}
+#[gpui::test]
+async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let lsp_store =
+ cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
+
+ cx.set_state(indoc! {"error warning info hiˇnt"});
+
+ cx.update(|_, cx| {
+ lsp_store.update(cx, |lsp_store, cx| {
+ lsp_store
+ .update_diagnostics(
+ LanguageServerId(0),
+ lsp::PublishDiagnosticsParams {
+ uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
+ version: None,
+ diagnostics: vec![
+ lsp::Diagnostic {
+ range: lsp::Range::new(
+ lsp::Position::new(0, 0),
+ lsp::Position::new(0, 5),
+ ),
+ severity: Some(lsp::DiagnosticSeverity::ERROR),
+ ..Default::default()
+ },
+ lsp::Diagnostic {
+ range: lsp::Range::new(
+ lsp::Position::new(0, 6),
+ lsp::Position::new(0, 13),
+ ),
+ severity: Some(lsp::DiagnosticSeverity::WARNING),
+ ..Default::default()
+ },
+ lsp::Diagnostic {
+ range: lsp::Range::new(
+ lsp::Position::new(0, 14),
+ lsp::Position::new(0, 18),
+ ),
+ severity: Some(lsp::DiagnosticSeverity::INFORMATION),
+ ..Default::default()
+ },
+ lsp::Diagnostic {
+ range: lsp::Range::new(
+ lsp::Position::new(0, 19),
+ lsp::Position::new(0, 23),
+ ),
+ severity: Some(lsp::DiagnosticSeverity::HINT),
+ ..Default::default()
+ },
+ ],
+ },
+ None,
+ DiagnosticSourceKind::Pushed,
+ &[],
+ cx,
+ )
+ .unwrap()
+ });
+ });
+ cx.run_until_parked();
+
+ macro_rules! go {
+ ($severity:expr) => {
+ cx.update_editor(|editor, window, cx| {
+ editor.go_to_diagnostic(
+ &GoToDiagnostic {
+ severity: $severity,
+ },
+ window,
+ cx,
+ );
+ });
+ };
+ }
+
+ // Default, should cycle through all diagnostics
+ go!(GoToDiagnosticSeverityFilter::default());
+ cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
+ go!(GoToDiagnosticSeverityFilter::default());
+ cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
+ go!(GoToDiagnosticSeverityFilter::default());
+ cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
+ go!(GoToDiagnosticSeverityFilter::default());
+ cx.assert_editor_state(indoc! {"error warning info ˇhint"});
+ go!(GoToDiagnosticSeverityFilter::default());
+ cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
+
+ let only_info = GoToDiagnosticSeverityFilter::Only(GoToDiagnosticSeverity::Information);
+ go!(only_info);
+ cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
+ go!(only_info);
+ cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
+
+ let no_hints = GoToDiagnosticSeverityFilter::Range {
+ min: GoToDiagnosticSeverity::Information,
+ max: GoToDiagnosticSeverity::Error,
+ };
+
+ go!(no_hints);
+ cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
+ go!(no_hints);
+ cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
+ go!(no_hints);
+ cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
+ go!(no_hints);
+ cx.assert_editor_state(indoc! {"ˇerror warning info hint"});
+
+ let warning_info = GoToDiagnosticSeverityFilter::Range {
+ min: GoToDiagnosticSeverity::Information,
+ max: GoToDiagnosticSeverity::Warning,
+ };
+
+ go!(warning_info);
+ cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
+ go!(warning_info);
+ cx.assert_editor_state(indoc! {"error warning ˇinfo hint"});
+ go!(warning_info);
+ cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
+}
+
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
zlog::init_test();
@@ -6,7 +6,7 @@ use gpui::{
WeakEntity, Window,
};
use language::Diagnostic;
-use project::project_settings::ProjectSettings;
+use project::project_settings::{GoToDiagnosticSeverityFilter, ProjectSettings};
use settings::Settings;
use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
@@ -77,7 +77,7 @@ impl Render for DiagnosticIndicator {
.tooltip(|window, cx| {
Tooltip::for_action(
"Next Diagnostic",
- &editor::actions::GoToDiagnostic,
+ &editor::actions::GoToDiagnostic::default(),
window,
cx,
)
@@ -156,7 +156,12 @@ impl DiagnosticIndicator {
fn go_to_next_diagnostic(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
editor.update(cx, |editor, cx| {
- editor.go_to_diagnostic_impl(editor::Direction::Next, window, cx);
+ editor.go_to_diagnostic_impl(
+ editor::Direction::Next,
+ GoToDiagnosticSeverityFilter::default(),
+ window,
+ cx,
+ );
})
}
}
@@ -243,7 +243,6 @@ struct ActionDef {
fn dump_all_gpui_actions() -> Vec<ActionDef> {
let mut actions = gpui::generate_list_of_all_registered_actions()
- .into_iter()
.map(|action| ActionDef {
name: action.name,
human_name: command_palette::humanize_action_name(action.name),
@@ -109,6 +109,7 @@ theme = { workspace = true, features = ["test-support"] }
tree-sitter-html.workspace = true
tree-sitter-rust.workspace = true
tree-sitter-typescript.workspace = true
+tree-sitter-yaml.workspace = true
unindent.workspace = true
util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
@@ -1,9 +1,11 @@
//! This module contains all actions supported by [`Editor`].
use super::*;
use gpui::{Action, actions};
+use project::project_settings::GoToDiagnosticSeverityFilter;
use schemars::JsonSchema;
use util::serde::default_true;
+/// Selects the next occurrence of the current selection.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -12,6 +14,7 @@ pub struct SelectNext {
pub replace_newest: bool,
}
+/// Selects the previous occurrence of the current selection.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -20,6 +23,7 @@ pub struct SelectPrevious {
pub replace_newest: bool,
}
+/// Moves the cursor to the beginning of the current line.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -30,6 +34,7 @@ pub struct MoveToBeginningOfLine {
pub stop_at_indent: bool,
}
+/// Selects from the cursor to the beginning of the current line.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -40,6 +45,7 @@ pub struct SelectToBeginningOfLine {
pub stop_at_indent: bool,
}
+/// Deletes from the cursor to the beginning of the current line.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -48,6 +54,7 @@ pub struct DeleteToBeginningOfLine {
pub(super) stop_at_indent: bool,
}
+/// Moves the cursor up by one page.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -56,6 +63,7 @@ pub struct MovePageUp {
pub(super) center_cursor: bool,
}
+/// Moves the cursor down by one page.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -64,6 +72,7 @@ pub struct MovePageDown {
pub(super) center_cursor: bool,
}
+/// Moves the cursor to the end of the current line.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -72,6 +81,7 @@ pub struct MoveToEndOfLine {
pub stop_at_soft_wraps: bool,
}
+/// Selects from the cursor to the end of the current line.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -80,6 +90,7 @@ pub struct SelectToEndOfLine {
pub(super) stop_at_soft_wraps: bool,
}
+/// Toggles the display of available code actions at the cursor position.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -101,6 +112,7 @@ pub enum CodeActionSource {
QuickActionBar,
}
+/// Confirms and accepts the currently selected completion suggestion.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -109,6 +121,7 @@ pub struct ConfirmCompletion {
pub item_ix: Option<usize>,
}
+/// Composes multiple completion suggestions into a single completion.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -117,6 +130,7 @@ pub struct ComposeCompletion {
pub item_ix: Option<usize>,
}
+/// Confirms and applies the currently selected code action.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -125,6 +139,7 @@ pub struct ConfirmCodeAction {
pub item_ix: Option<usize>,
}
+/// Toggles comment markers for the selected lines.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -135,6 +150,7 @@ pub struct ToggleComments {
pub ignore_indent: bool,
}
+/// Moves the cursor up by a specified number of lines.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -143,6 +159,7 @@ pub struct MoveUpByLines {
pub(super) lines: u32,
}
+/// Moves the cursor down by a specified number of lines.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -151,6 +168,7 @@ pub struct MoveDownByLines {
pub(super) lines: u32,
}
+/// Extends selection up by a specified number of lines.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -159,6 +177,7 @@ pub struct SelectUpByLines {
pub(super) lines: u32,
}
+/// Extends selection down by a specified number of lines.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -167,6 +186,7 @@ pub struct SelectDownByLines {
pub(super) lines: u32,
}
+/// Expands all excerpts in the editor.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -175,6 +195,7 @@ pub struct ExpandExcerpts {
pub(super) lines: u32,
}
+/// Expands excerpts above the current position.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -183,6 +204,7 @@ pub struct ExpandExcerptsUp {
pub(super) lines: u32,
}
+/// Expands excerpts below the current position.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -191,6 +213,7 @@ pub struct ExpandExcerptsDown {
pub(super) lines: u32,
}
+/// Shows code completion suggestions at the cursor position.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -199,10 +222,12 @@ pub struct ShowCompletions {
pub(super) trigger: Option<String>,
}
+/// Handles text input in the editor.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
pub struct HandleInput(pub String);
+/// Deletes from the cursor to the end of the next word.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -211,6 +236,7 @@ pub struct DeleteToNextWordEnd {
pub ignore_newlines: bool,
}
+/// Deletes from the cursor to the start of the previous word.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -219,10 +245,12 @@ pub struct DeleteToPreviousWordStart {
pub ignore_newlines: bool,
}
+/// Folds all code blocks at the specified indentation level.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
pub struct FoldAtLevel(pub u32);
+/// Spawns the nearest available task from the current cursor position.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
@@ -238,11 +266,38 @@ pub enum UuidVersion {
V7,
}
-actions!(debugger, [RunToCursor, EvaluateSelectedText]);
+/// Goes to the next diagnostic in the file.
+#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
+#[action(namespace = editor)]
+#[serde(deny_unknown_fields)]
+pub struct GoToDiagnostic {
+ #[serde(default)]
+ pub severity: GoToDiagnosticSeverityFilter,
+}
+
+/// Goes to the previous diagnostic in the file.
+#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
+#[action(namespace = editor)]
+#[serde(deny_unknown_fields)]
+pub struct GoToPreviousDiagnostic {
+ #[serde(default)]
+ pub severity: GoToDiagnosticSeverityFilter,
+}
+
+actions!(
+ debugger,
+ [
+ /// Runs program execution to the current cursor position.
+ RunToCursor,
+ /// Evaluates the selected text in the debugger context.
+ EvaluateSelectedText
+ ]
+);
actions!(
go_to_line,
[
+ /// Toggles the go to line dialog.
#[action(name = "Toggle")]
ToggleGoToLine
]
@@ -251,219 +306,430 @@ actions!(
actions!(
editor,
[
+ /// Accepts the full edit prediction.
AcceptEditPrediction,
+ /// Accepts a partial Copilot suggestion.
AcceptPartialCopilotSuggestion,
+ /// Accepts a partial edit prediction.
AcceptPartialEditPrediction,
+ /// Adds a cursor above the current selection.
AddSelectionAbove,
+ /// Adds a cursor below the current selection.
AddSelectionBelow,
+ /// Applies all diff hunks in the editor.
ApplyAllDiffHunks,
+ /// Applies the diff hunk at the current position.
ApplyDiffHunk,
+ /// Deletes the character before the cursor.
Backspace,
+ /// Cancels the current operation.
Cancel,
+ /// Cancels the running flycheck operation.
CancelFlycheck,
+ /// Cancels pending language server work.
CancelLanguageServerWork,
+ /// Clears flycheck results.
ClearFlycheck,
+ /// Confirms the rename operation.
ConfirmRename,
+ /// Confirms completion by inserting at cursor.
ConfirmCompletionInsert,
+ /// Confirms completion by replacing existing text.
ConfirmCompletionReplace,
+ /// Navigates to the first item in the context menu.
ContextMenuFirst,
+ /// Navigates to the last item in the context menu.
ContextMenuLast,
+ /// Navigates to the next item in the context menu.
ContextMenuNext,
+ /// Navigates to the previous item in the context menu.
ContextMenuPrevious,
+ /// Converts indentation from tabs to spaces.
ConvertIndentationToSpaces,
+ /// Converts indentation from spaces to tabs.
ConvertIndentationToTabs,
+ /// Converts selected text to kebab-case.
ConvertToKebabCase,
+ /// Converts selected text to lowerCamelCase.
ConvertToLowerCamelCase,
+ /// Converts selected text to lowercase.
ConvertToLowerCase,
+ /// Toggles the case of selected text.
ConvertToOppositeCase,
+ /// Converts selected text to snake_case.
ConvertToSnakeCase,
+ /// Converts selected text to Title Case.
ConvertToTitleCase,
+ /// Converts selected text to UpperCamelCase.
ConvertToUpperCamelCase,
+ /// Converts selected text to UPPERCASE.
ConvertToUpperCase,
+ /// Applies ROT13 cipher to selected text.
ConvertToRot13,
+ /// Applies ROT47 cipher to selected text.
ConvertToRot47,
+ /// Copies selected text to the clipboard.
Copy,
+ /// Copies selected text to the clipboard with leading/trailing whitespace trimmed.
CopyAndTrim,
+ /// Copies the current file location to the clipboard.
CopyFileLocation,
+ /// Copies the highlighted text as JSON.
CopyHighlightJson,
+ /// Copies the current file name to the clipboard.
CopyFileName,
+ /// Copies the file name without extension to the clipboard.
CopyFileNameWithoutExtension,
+ /// Copies a permalink to the current line.
CopyPermalinkToLine,
+ /// Cuts selected text to the clipboard.
Cut,
+ /// Cuts from cursor to end of line.
CutToEndOfLine,
+ /// Deletes the character after the cursor.
Delete,
+ /// Deletes the current line.
DeleteLine,
+ /// Deletes from cursor to end of line.
DeleteToEndOfLine,
+ /// Deletes to the end of the next subword.
DeleteToNextSubwordEnd,
+ /// Deletes to the start of the previous subword.
DeleteToPreviousSubwordStart,
+ /// Displays names of all active cursors.
DisplayCursorNames,
+ /// Duplicates the current line below.
DuplicateLineDown,
+ /// Duplicates the current line above.
DuplicateLineUp,
+ /// Duplicates the current selection.
DuplicateSelection,
+ /// Expands all diff hunks in the editor.
#[action(deprecated_aliases = ["editor::ExpandAllHunkDiffs"])]
ExpandAllDiffHunks,
+ /// Expands macros recursively at cursor position.
ExpandMacroRecursively,
+ /// Finds all references to the symbol at cursor.
FindAllReferences,
+ /// Finds the next match in the search.
FindNextMatch,
+ /// Finds the previous match in the search.
FindPreviousMatch,
+ /// Folds the current code block.
Fold,
+ /// Folds all foldable regions in the editor.
FoldAll,
+ /// Folds all function bodies in the editor.
FoldFunctionBodies,
+ /// Folds the current code block and all its children.
FoldRecursive,
+ /// Folds the selected ranges.
FoldSelectedRanges,
+ /// Toggles folding at the current position.
ToggleFold,
+ /// Toggles recursive folding at the current position.
ToggleFoldRecursive,
+ /// Toggles all folds in a buffer or all excerpts in multibuffer.
+ ToggleFoldAll,
+ /// Formats the entire document.
Format,
+ /// Formats only the selected text.
FormatSelections,
+ /// Goes to the declaration of the symbol at cursor.
GoToDeclaration,
+ /// Goes to declaration in a split pane.
GoToDeclarationSplit,
+ /// Goes to the definition of the symbol at cursor.
GoToDefinition,
+ /// Goes to definition in a split pane.
GoToDefinitionSplit,
- GoToDiagnostic,
+ /// Goes to the next diff hunk.
GoToHunk,
+ /// Goes to the previous diff hunk.
GoToPreviousHunk,
+ /// Goes to the implementation of the symbol at cursor.
GoToImplementation,
+ /// Goes to implementation in a split pane.
GoToImplementationSplit,
+ /// Goes to the next change in the file.
GoToNextChange,
+ /// Goes to the parent module of the current file.
GoToParentModule,
+ /// Goes to the previous change in the file.
GoToPreviousChange,
- GoToPreviousDiagnostic,
+ /// Goes to the type definition of the symbol at cursor.
GoToTypeDefinition,
+ /// Goes to type definition in a split pane.
GoToTypeDefinitionSplit,
+ /// Scrolls down by half a page.
HalfPageDown,
+ /// Scrolls up by half a page.
HalfPageUp,
+ /// Shows hover information for the symbol at cursor.
Hover,
+ /// Increases indentation of selected lines.
Indent,
+ /// Inserts a UUID v4 at cursor position.
InsertUuidV4,
+ /// Inserts a UUID v7 at cursor position.
InsertUuidV7,
+ /// Joins the current line with the next line.
JoinLines,
+ /// Cuts to kill ring (Emacs-style).
KillRingCut,
+ /// Yanks from kill ring (Emacs-style).
KillRingYank,
+ /// Moves cursor down one line.
LineDown,
+ /// Moves cursor up one line.
LineUp,
+ /// Moves cursor down.
MoveDown,
+ /// Moves cursor left.
MoveLeft,
+ /// Moves the current line down.
MoveLineDown,
+ /// Moves the current line up.
MoveLineUp,
+ /// Moves cursor right.
MoveRight,
+ /// Moves cursor to the beginning of the document.
MoveToBeginning,
+ /// Moves cursor to the enclosing bracket.
MoveToEnclosingBracket,
+ /// Moves cursor to the end of the document.
MoveToEnd,
+ /// Moves cursor to the end of the paragraph.
MoveToEndOfParagraph,
+ /// Moves cursor to the end of the next subword.
MoveToNextSubwordEnd,
+ /// Moves cursor to the end of the next word.
MoveToNextWordEnd,
+ /// Moves cursor to the start of the previous subword.
MoveToPreviousSubwordStart,
+ /// Moves cursor to the start of the previous word.
MoveToPreviousWordStart,
+ /// Moves cursor to the start of the paragraph.
MoveToStartOfParagraph,
+ /// Moves cursor to the start of the current excerpt.
MoveToStartOfExcerpt,
+ /// Moves cursor to the start of the next excerpt.
MoveToStartOfNextExcerpt,
+ /// Moves cursor to the end of the current excerpt.
MoveToEndOfExcerpt,
+ /// Moves cursor to the end of the previous excerpt.
MoveToEndOfPreviousExcerpt,
+ /// Moves cursor up.
MoveUp,
+ /// Inserts a new line and moves cursor to it.
Newline,
+ /// Inserts a new line above the current line.
NewlineAbove,
+ /// Inserts a new line below the current line.
NewlineBelow,
+ /// Navigates to the next edit prediction.
NextEditPrediction,
+ /// Scrolls to the next screen.
NextScreen,
+ /// Opens the context menu at cursor position.
OpenContextMenu,
+ /// Opens excerpts from the current file.
OpenExcerpts,
+ /// Opens excerpts in a split pane.
OpenExcerptsSplit,
+ /// Opens the proposed changes editor.
OpenProposedChangesEditor,
+ /// Opens documentation for the symbol at cursor.
OpenDocs,
+ /// Opens a permalink to the current line.
OpenPermalinkToLine,
+ /// Opens the file whose name is selected in the editor.
#[action(deprecated_aliases = ["editor::OpenFile"])]
OpenSelectedFilename,
+ /// Opens all selections in a multibuffer.
OpenSelectionsInMultibuffer,
+ /// Opens the URL at cursor position.
OpenUrl,
+ /// Organizes import statements.
OrganizeImports,
+ /// Decreases indentation of selected lines.
Outdent,
+ /// Automatically adjusts indentation based on context.
AutoIndent,
+ /// Scrolls down by one page.
PageDown,
+ /// Scrolls up by one page.
PageUp,
+ /// Pastes from clipboard.
Paste,
+ /// Navigates to the previous edit prediction.
PreviousEditPrediction,
+ /// Redoes the last undone edit.
Redo,
+ /// Redoes the last selection change.
RedoSelection,
+ /// Renames the symbol at cursor.
Rename,
+ /// Restarts the language server for the current file.
RestartLanguageServer,
+ /// Reveals the current file in the system file manager.
RevealInFileManager,
+ /// Reverses the order of selected lines.
ReverseLines,
+ /// Reloads the file from disk.
ReloadFile,
+ /// Rewraps text to fit within the preferred line length.
Rewrap,
+ /// Runs flycheck diagnostics.
RunFlycheck,
+ /// Scrolls the cursor to the bottom of the viewport.
ScrollCursorBottom,
+ /// Scrolls the cursor to the center of the viewport.
ScrollCursorCenter,
+ /// Cycles cursor position between center, top, and bottom.
ScrollCursorCenterTopBottom,
+ /// Scrolls the cursor to the top of the viewport.
ScrollCursorTop,
+ /// Selects all text in the editor.
SelectAll,
+ /// Selects all matches of the current selection.
SelectAllMatches,
+ /// Selects to the start of the current excerpt.
SelectToStartOfExcerpt,
+ /// Selects to the start of the next excerpt.
SelectToStartOfNextExcerpt,
+ /// Selects to the end of the current excerpt.
SelectToEndOfExcerpt,
+ /// Selects to the end of the previous excerpt.
SelectToEndOfPreviousExcerpt,
+ /// Extends selection down.
SelectDown,
+ /// Selects the enclosing symbol.
SelectEnclosingSymbol,
+ /// Selects the next larger syntax node.
SelectLargerSyntaxNode,
+ /// Extends selection left.
SelectLeft,
+ /// Selects the current line.
SelectLine,
+ /// Extends selection down by one page.
SelectPageDown,
+ /// Extends selection up by one page.
SelectPageUp,
+ /// Extends selection right.
SelectRight,
+ /// Selects the next smaller syntax node.
SelectSmallerSyntaxNode,
+ /// Selects to the beginning of the document.
SelectToBeginning,
+ /// Selects to the end of the document.
SelectToEnd,
+ /// Selects to the end of the paragraph.
SelectToEndOfParagraph,
+ /// Selects to the end of the next subword.
SelectToNextSubwordEnd,
+ /// Selects to the end of the next word.
SelectToNextWordEnd,
+ /// Selects to the start of the previous subword.
SelectToPreviousSubwordStart,
+ /// Selects to the start of the previous word.
SelectToPreviousWordStart,
+ /// Selects to the start of the paragraph.
SelectToStartOfParagraph,
+ /// Extends selection up.
SelectUp,
+ /// Shows the system character palette.
ShowCharacterPalette,
+ /// Shows edit prediction at cursor.
ShowEditPrediction,
+ /// Shows signature help for the current function.
ShowSignatureHelp,
+ /// Shows word completions.
ShowWordCompletions,
+ /// Randomly shuffles selected lines.
ShuffleLines,
+ /// Navigates to the next signature in the signature help popup.
SignatureHelpNext,
+ /// Navigates to the previous signature in the signature help popup.
SignatureHelpPrevious,
+ /// Sorts selected lines by length.
+ SortLinesByLength,
+ /// Sorts selected lines case-insensitively.
SortLinesCaseInsensitive,
+ /// Sorts selected lines case-sensitively.
SortLinesCaseSensitive,
+ /// Splits selection into individual lines.
SplitSelectionIntoLines,
+ /// Stops the language server for the current file.
StopLanguageServer,
+ /// Switches between source and header files.
SwitchSourceHeader,
+ /// Inserts a tab character or indents.
Tab,
+ /// Removes a tab character or outdents.
Backtab,
+ /// Toggles a breakpoint at the current line.
ToggleBreakpoint,
+ /// Toggles the case of selected text.
ToggleCase,
+ /// Disables the breakpoint at the current line.
DisableBreakpoint,
+ /// Enables the breakpoint at the current line.
EnableBreakpoint,
+ /// Edits the log message for a breakpoint.
EditLogBreakpoint,
+ /// Toggles automatic signature help.
ToggleAutoSignatureHelp,
+ /// Toggles inline git blame display.
ToggleGitBlameInline,
+ /// Opens the git commit for the blame at cursor.
OpenGitBlameCommit,
+ /// Toggles the diagnostics panel.
ToggleDiagnostics,
+ /// Toggles indent guides display.
ToggleIndentGuides,
+ /// Toggles inlay hints display.
ToggleInlayHints,
+ /// Toggles inline values display.
ToggleInlineValues,
+ /// Toggles inline diagnostics display.
ToggleInlineDiagnostics,
+ /// Toggles edit prediction feature.
ToggleEditPrediction,
+ /// Toggles line numbers display.
ToggleLineNumbers,
+ /// Toggles the minimap display.
ToggleMinimap,
+ /// Swaps the start and end of the current selection.
SwapSelectionEnds,
+ /// Sets a mark at the current position.
SetMark,
+ /// Toggles relative line numbers display.
ToggleRelativeLineNumbers,
+ /// Toggles diff display for selected hunks.
#[action(deprecated_aliases = ["editor::ToggleHunkDiff"])]
ToggleSelectedDiffHunks,
+ /// Toggles the selection menu.
ToggleSelectionMenu,
+ /// Toggles soft wrap mode.
ToggleSoftWrap,
+ /// Toggles the tab bar display.
ToggleTabBar,
+ /// Transposes characters around cursor.
Transpose,
+ /// Undoes the last edit.
Undo,
+ /// Undoes the last selection change.
UndoSelection,
+ /// Unfolds all folded regions.
UnfoldAll,
+ /// Unfolds lines at cursor.
UnfoldLines,
+ /// Unfolds recursively at cursor.
UnfoldRecursive,
+ /// Removes duplicate lines (case-insensitive).
UniqueLinesCaseInsensitive,
+ /// Removes duplicate lines (case-sensitive).
UniqueLinesCaseSensitive,
]
);
@@ -1083,11 +1083,10 @@ impl CompletionsMenu {
if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
);
- let sort_text = if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source
- {
- lsp_completion.sort_text.as_deref()
- } else {
- None
+ let sort_text = match &completion.source {
+ CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(),
+ CompletionSource::Dap { sort_text } => Some(sort_text.as_str()),
+ _ => None,
};
let (sort_kind, sort_label) = completion.sort_key();
@@ -271,7 +271,6 @@ impl DisplayMap {
height: Some(height),
style,
priority,
- render_in_minimap: true,
}
}),
);
@@ -1066,7 +1065,7 @@ impl DisplaySnapshot {
}
let font_size = editor_style.text.font_size.to_pixels(*rem_size);
- text_system.layout_line(&line, font_size, &runs)
+ text_system.layout_line(&line, font_size, &runs, None)
}
pub fn x_for_display_point(
@@ -1663,7 +1662,6 @@ pub mod tests {
height: Some(height),
render: Arc::new(|_| div().into_any()),
priority,
- render_in_minimap: true,
}
})
.collect::<Vec<_>>();
@@ -2029,7 +2027,6 @@ pub mod tests {
style: BlockStyle::Sticky,
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
}],
cx,
);
@@ -2227,7 +2224,6 @@ pub mod tests {
style: BlockStyle::Sticky,
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
BlockProperties {
placement: BlockPlacement::Below(
@@ -2237,7 +2233,6 @@ pub mod tests {
style: BlockStyle::Sticky,
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
],
cx,
@@ -2344,7 +2339,6 @@ pub mod tests {
style: BlockStyle::Sticky,
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
}],
cx,
)
@@ -2420,7 +2414,6 @@ pub mod tests {
style: BlockStyle::Fixed,
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
}],
cx,
);
@@ -193,7 +193,6 @@ pub struct CustomBlock {
style: BlockStyle,
render: Arc<Mutex<RenderBlock>>,
priority: usize,
- pub(crate) render_in_minimap: bool,
}
#[derive(Clone)]
@@ -205,7 +204,6 @@ pub struct BlockProperties<P> {
pub style: BlockStyle,
pub render: RenderBlock,
pub priority: usize,
- pub render_in_minimap: bool,
}
impl<P: Debug> Debug for BlockProperties<P> {
@@ -1044,7 +1042,6 @@ impl BlockMapWriter<'_> {
render: Arc::new(Mutex::new(block.render)),
style: block.style,
priority: block.priority,
- render_in_minimap: block.render_in_minimap,
});
self.0.custom_blocks.insert(block_ix, new_block.clone());
self.0.custom_blocks_by_id.insert(id, new_block);
@@ -1079,7 +1076,6 @@ impl BlockMapWriter<'_> {
style: block.style,
render: block.render.clone(),
priority: block.priority,
- render_in_minimap: block.render_in_minimap,
};
let new_block = Arc::new(new_block);
*block = new_block.clone();
@@ -1976,7 +1972,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -1984,7 +1979,6 @@ mod tests {
height: Some(2),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -1992,7 +1986,6 @@ mod tests {
height: Some(3),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
]);
@@ -2217,7 +2210,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2225,7 +2217,6 @@ mod tests {
height: Some(2),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2233,7 +2224,6 @@ mod tests {
height: Some(3),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
]);
@@ -2322,7 +2312,6 @@ mod tests {
render: Arc::new(|_| div().into_any()),
height: Some(1),
priority: 0,
- render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2330,7 +2319,6 @@ mod tests {
render: Arc::new(|_| div().into_any()),
height: Some(1),
priority: 0,
- render_in_minimap: true,
},
]);
@@ -2370,7 +2358,6 @@ mod tests {
height: Some(4),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
}])[0];
let blocks_snapshot = block_map.read(wraps_snapshot, Default::default());
@@ -2424,7 +2411,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2432,7 +2418,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2440,7 +2425,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
]);
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
@@ -2455,7 +2439,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2463,7 +2446,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2471,7 +2453,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
]);
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
@@ -2571,7 +2552,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2579,7 +2559,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2587,7 +2566,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
]);
let excerpt_blocks_3 = writer.insert(vec![
@@ -2597,7 +2575,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -2605,7 +2582,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
},
]);
@@ -2653,7 +2629,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
}]);
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
let blocks = blocks_snapshot
@@ -3011,7 +2986,6 @@ mod tests {
height: Some(height),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
}
})
.collect::<Vec<_>>();
@@ -3032,7 +3006,6 @@ mod tests {
style: props.style,
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
}));
for (block_properties, block_id) in block_properties.iter().zip(block_ids) {
@@ -3557,7 +3530,6 @@ mod tests {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
}])[0];
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
@@ -296,12 +296,25 @@ impl<'a> Iterator for InlayChunks<'a> {
*chunk = self.buffer_chunks.next().unwrap();
}
- let (prefix, suffix) = chunk.text.split_at(
- chunk
- .text
- .len()
- .min(self.transforms.end(&()).0.0 - self.output_offset.0),
- );
+ let desired_bytes = self.transforms.end(&()).0.0 - self.output_offset.0;
+
+ // If we're already at the transform boundary, skip to the next transform
+ if desired_bytes == 0 {
+ self.inlay_chunks = None;
+ self.transforms.next(&());
+ return self.next();
+ }
+
+ // Determine split index handling edge cases
+ let split_index = if desired_bytes >= chunk.text.len() {
+ chunk.text.len()
+ } else if chunk.text.is_char_boundary(desired_bytes) {
+ desired_bytes
+ } else {
+ find_next_utf8_boundary(chunk.text, desired_bytes)
+ };
+
+ let (prefix, suffix) = chunk.text.split_at(split_index);
chunk.text = suffix;
self.output_offset.0 += prefix.len();
@@ -391,8 +404,24 @@ impl<'a> Iterator for InlayChunks<'a> {
let inlay_chunk = self
.inlay_chunk
.get_or_insert_with(|| inlay_chunks.next().unwrap());
- let (chunk, remainder) =
- inlay_chunk.split_at(inlay_chunk.len().min(next_inlay_highlight_endpoint));
+
+ // Determine split index handling edge cases
+ let split_index = if next_inlay_highlight_endpoint >= inlay_chunk.len() {
+ inlay_chunk.len()
+ } else if next_inlay_highlight_endpoint == 0 {
+ // Need to take at least one character to make progress
+ inlay_chunk
+ .chars()
+ .next()
+ .map(|c| c.len_utf8())
+ .unwrap_or(1)
+ } else if inlay_chunk.is_char_boundary(next_inlay_highlight_endpoint) {
+ next_inlay_highlight_endpoint
+ } else {
+ find_next_utf8_boundary(inlay_chunk, next_inlay_highlight_endpoint)
+ };
+
+ let (chunk, remainder) = inlay_chunk.split_at(split_index);
*inlay_chunk = remainder;
if inlay_chunk.is_empty() {
self.inlay_chunk = None;
@@ -412,7 +441,7 @@ impl<'a> Iterator for InlayChunks<'a> {
}
};
- if self.output_offset == self.transforms.end(&()).0 {
+ if self.output_offset >= self.transforms.end(&()).0 {
self.inlay_chunks = None;
self.transforms.next(&());
}
@@ -1143,6 +1172,31 @@ fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
}
}
+/// Given a byte index that is NOT a UTF-8 boundary, find the next one.
+/// Assumes: 0 < byte_index < text.len() and !text.is_char_boundary(byte_index)
+#[inline(always)]
+fn find_next_utf8_boundary(text: &str, byte_index: usize) -> usize {
+ let bytes = text.as_bytes();
+ let mut idx = byte_index + 1;
+
+ // Scan forward until we find a boundary
+ while idx < text.len() {
+ if is_utf8_char_boundary(bytes[idx]) {
+ return idx;
+ }
+ idx += 1;
+ }
+
+ // Hit the end, return the full length
+ text.len()
+}
+
+// Private helper function taken from Rust's core::num module (which is both Apache2 and MIT licensed)
+const fn is_utf8_char_boundary(byte: u8) -> bool {
+ // This is bit magic equivalent to: b < 128 || b >= 192
+ (byte as i8) >= -0x40
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -1882,4 +1936,210 @@ mod tests {
cx.set_global(store);
theme::init(theme::LoadThemes::JustBase, cx);
}
+
+ /// Helper to create test highlights for an inlay
+ fn create_inlay_highlights(
+ inlay_id: InlayId,
+ highlight_range: Range<usize>,
+ position: Anchor,
+ ) -> TreeMap<TypeId, TreeMap<InlayId, (HighlightStyle, InlayHighlight)>> {
+ let mut inlay_highlights = TreeMap::default();
+ let mut type_highlights = TreeMap::default();
+ type_highlights.insert(
+ inlay_id,
+ (
+ HighlightStyle::default(),
+ InlayHighlight {
+ inlay: inlay_id,
+ range: highlight_range,
+ inlay_position: position,
+ },
+ ),
+ );
+ inlay_highlights.insert(TypeId::of::<()>(), type_highlights);
+ inlay_highlights
+ }
+
+ #[gpui::test]
+ fn test_inlay_utf8_boundary_panic_fix(cx: &mut App) {
+ init_test(cx);
+
+ // This test verifies that we handle UTF-8 character boundaries correctly
+ // when splitting inlay text for highlighting. Previously, this would panic
+ // when trying to split at byte 13, which is in the middle of the '…' character.
+ //
+ // See https://github.com/zed-industries/zed/issues/33641
+ let buffer = MultiBuffer::build_simple("fn main() {}\n", cx);
+ let (mut inlay_map, _) = InlayMap::new(buffer.read(cx).snapshot(cx));
+
+ // Create an inlay with text that contains a multi-byte character
+ // The string "SortingDirec…" contains an ellipsis character '…' which is 3 bytes (E2 80 A6)
+ let inlay_text = "SortingDirec…";
+ let position = buffer.read(cx).snapshot(cx).anchor_before(Point::new(0, 5));
+
+ let inlay = Inlay {
+ id: InlayId::Hint(0),
+ position,
+ text: text::Rope::from(inlay_text),
+ color: None,
+ };
+
+ let (inlay_snapshot, _) = inlay_map.splice(&[], vec![inlay]);
+
+ // Create highlights that request a split at byte 13, which is in the middle
+ // of the '…' character (bytes 12..15). We include the full character.
+ let inlay_highlights = create_inlay_highlights(InlayId::Hint(0), 0..13, position);
+
+ let highlights = crate::display_map::Highlights {
+ text_highlights: None,
+ inlay_highlights: Some(&inlay_highlights),
+ styles: crate::display_map::HighlightStyles::default(),
+ };
+
+ // Collect chunks - this previously would panic
+ let chunks: Vec<_> = inlay_snapshot
+ .chunks(
+ InlayOffset(0)..InlayOffset(inlay_snapshot.len().0),
+ false,
+ highlights,
+ )
+ .collect();
+
+ // Verify the chunks are correct
+ let full_text: String = chunks.iter().map(|c| c.chunk.text).collect();
+ assert_eq!(full_text, "fn maSortingDirec…in() {}\n");
+
+ // Verify the highlighted portion includes the complete ellipsis character
+ let highlighted_chunks: Vec<_> = chunks
+ .iter()
+ .filter(|c| c.chunk.highlight_style.is_some() && c.chunk.is_inlay)
+ .collect();
+
+ assert_eq!(highlighted_chunks.len(), 1);
+ assert_eq!(highlighted_chunks[0].chunk.text, "SortingDirec…");
+ }
+
+ #[gpui::test]
+ fn test_inlay_utf8_boundaries(cx: &mut App) {
+ init_test(cx);
+
+ struct TestCase {
+ inlay_text: &'static str,
+ highlight_range: Range<usize>,
+ expected_highlighted: &'static str,
+ description: &'static str,
+ }
+
+ let test_cases = vec![
+ TestCase {
+ inlay_text: "Hello👋World",
+ highlight_range: 0..7,
+ expected_highlighted: "Hello👋",
+ description: "Emoji boundary - rounds up to include full emoji",
+ },
+ TestCase {
+ inlay_text: "Test→End",
+ highlight_range: 0..5,
+ expected_highlighted: "Test→",
+ description: "Arrow boundary - rounds up to include full arrow",
+ },
+ TestCase {
+ inlay_text: "café",
+ highlight_range: 0..4,
+ expected_highlighted: "café",
+ description: "Accented char boundary - rounds up to include full é",
+ },
+ TestCase {
+ inlay_text: "🎨🎭🎪",
+ highlight_range: 0..5,
+ expected_highlighted: "🎨🎭",
+ description: "Multiple emojis - partial highlight",
+ },
+ TestCase {
+ inlay_text: "普通话",
+ highlight_range: 0..4,
+ expected_highlighted: "普通",
+ description: "Chinese characters - partial highlight",
+ },
+ TestCase {
+ inlay_text: "Hello",
+ highlight_range: 0..2,
+ expected_highlighted: "He",
+ description: "ASCII only - no adjustment needed",
+ },
+ TestCase {
+ inlay_text: "👋",
+ highlight_range: 0..1,
+ expected_highlighted: "👋",
+ description: "Single emoji - partial byte range includes whole char",
+ },
+ TestCase {
+ inlay_text: "Test",
+ highlight_range: 0..0,
+ expected_highlighted: "",
+ description: "Empty range",
+ },
+ TestCase {
+ inlay_text: "🎨ABC",
+ highlight_range: 2..5,
+ expected_highlighted: "A",
+ description: "Range starting mid-emoji skips the emoji",
+ },
+ ];
+
+ for test_case in test_cases {
+ let buffer = MultiBuffer::build_simple("test", cx);
+ let (mut inlay_map, _) = InlayMap::new(buffer.read(cx).snapshot(cx));
+ let position = buffer.read(cx).snapshot(cx).anchor_before(Point::new(0, 2));
+
+ let inlay = Inlay {
+ id: InlayId::Hint(0),
+ position,
+ text: text::Rope::from(test_case.inlay_text),
+ color: None,
+ };
+
+ let (inlay_snapshot, _) = inlay_map.splice(&[], vec![inlay]);
+ let inlay_highlights = create_inlay_highlights(
+ InlayId::Hint(0),
+ test_case.highlight_range.clone(),
+ position,
+ );
+
+ let highlights = crate::display_map::Highlights {
+ text_highlights: None,
+ inlay_highlights: Some(&inlay_highlights),
+ styles: crate::display_map::HighlightStyles::default(),
+ };
+
+ let chunks: Vec<_> = inlay_snapshot
+ .chunks(
+ InlayOffset(0)..InlayOffset(inlay_snapshot.len().0),
+ false,
+ highlights,
+ )
+ .collect();
+
+ // Verify we got chunks and they total to the expected text
+ let full_text: String = chunks.iter().map(|c| c.chunk.text).collect();
+ assert_eq!(
+ full_text,
+ format!("te{}st", test_case.inlay_text),
+ "Full text mismatch for case: {}",
+ test_case.description
+ );
+
+ // Verify that the highlighted portion matches expectations
+ let highlighted_text: String = chunks
+ .iter()
+ .filter(|c| c.chunk.highlight_style.is_some() && c.chunk.is_inlay)
+ .map(|c| c.chunk.text)
+ .collect();
+ assert_eq!(
+ highlighted_text, test_case.expected_highlighted,
+ "Highlighted text mismatch for case: {} (text: '{}', range: {:?})",
+ test_case.description, test_case.inlay_text, test_case.highlight_range
+ );
+ }
+ }
}
@@ -134,7 +134,7 @@ use project::{
session::{Session, SessionEvent},
},
git_store::{GitStoreEvent, RepositoryEvent},
- project_settings::DiagnosticSeverity,
+ project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter},
};
pub use git::blame::BlameRenderer;
@@ -865,9 +865,19 @@ pub trait Addon: 'static {
}
}
+struct ChangeLocation {
+ current: Option<Vec<Anchor>>,
+ original: Vec<Anchor>,
+}
+impl ChangeLocation {
+ fn locations(&self) -> &[Anchor] {
+ self.current.as_ref().unwrap_or(&self.original)
+ }
+}
+
/// A set of caret positions, registered when the editor was edited.
pub struct ChangeList {
- changes: Vec<Vec<Anchor>>,
+ changes: Vec<ChangeLocation>,
/// Currently "selected" change.
position: Option<usize>,
}
@@ -894,20 +904,38 @@ impl ChangeList {
(prev + count).min(self.changes.len() - 1)
};
self.position = Some(next);
- self.changes.get(next).map(|anchors| anchors.as_slice())
+ self.changes.get(next).map(|change| change.locations())
}
/// 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<Anchor>) {
+ pub fn push_to_change_list(&mut self, group: bool, new_positions: Vec<Anchor>) {
self.position.take();
- if pop_state {
- self.changes.pop();
+ if let Some(last) = self.changes.last_mut()
+ && group
+ {
+ last.current = Some(new_positions)
+ } else {
+ self.changes.push(ChangeLocation {
+ original: new_positions,
+ current: None,
+ });
}
- self.changes.push(new_positions.clone());
}
pub fn last(&self) -> Option<&[Anchor]> {
- self.changes.last().map(|anchors| anchors.as_slice())
+ self.changes.last().map(|change| change.locations())
+ }
+
+ pub fn last_before_grouping(&self) -> Option<&[Anchor]> {
+ self.changes.last().map(|change| change.original.as_slice())
+ }
+
+ pub fn invert_last_group(&mut self) {
+ if let Some(last) = self.changes.last_mut() {
+ if let Some(current) = last.current.as_mut() {
+ mem::swap(&mut last.original, current);
+ }
+ }
}
}
@@ -1142,7 +1170,6 @@ pub struct Editor {
pub change_list: ChangeList,
inline_value_cache: InlineValueCache,
selection_drag_state: SelectionDragState,
- drag_and_drop_selection_enabled: bool,
next_color_inlay_id: usize,
colors: Option<LspColorData>,
folding_newlines: Task<()>,
@@ -1768,6 +1795,7 @@ impl Editor {
);
let full_mode = mode.is_full();
+ let is_minimap = mode.is_minimap();
let diagnostics_max_severity = if full_mode {
EditorSettings::get_global(cx)
.diagnostics_max_severity
@@ -1828,13 +1856,19 @@ impl Editor {
let selections = SelectionsCollection::new(display_map.clone(), buffer.clone());
- let blink_manager = cx.new(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
+ let blink_manager = cx.new(|cx| {
+ let mut blink_manager = BlinkManager::new(CURSOR_BLINK_INTERVAL, cx);
+ if is_minimap {
+ blink_manager.disable(cx);
+ }
+ blink_manager
+ });
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 full_mode {
if let Some(project) = project.as_ref() {
project_subscriptions.push(cx.subscribe_in(
project,
@@ -1945,18 +1979,23 @@ impl Editor {
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();
- cx.observe_pending_input(window, Self::observe_pending_input)
- .detach();
-
- let show_indent_guides = if matches!(mode, EditorMode::SingleLine { .. }) {
+ if !is_minimap {
+ 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();
+ cx.observe_pending_input(window, Self::observe_pending_input)
+ .detach();
+ }
+
+ let show_indent_guides = if matches!(
+ mode,
+ EditorMode::SingleLine { .. } | EditorMode::Minimap { .. }
+ ) {
Some(false)
} else {
None
@@ -2022,10 +2061,10 @@ impl Editor {
minimap_visibility: MinimapVisibility::for_mode(&mode, cx),
offset_content: !matches!(mode, EditorMode::SingleLine { .. }),
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
- show_gutter: mode.is_full(),
- show_line_numbers: None,
+ show_gutter: full_mode,
+ show_line_numbers: (!full_mode).then_some(false),
use_relative_line_numbers: None,
- disable_expand_excerpt_buttons: false,
+ disable_expand_excerpt_buttons: !full_mode,
show_git_diff_gutter: None,
show_code_actions: None,
show_runnables: None,
@@ -2059,7 +2098,7 @@ impl Editor {
document_highlights_task: None,
linked_editing_range_task: None,
pending_rename: None,
- searchable: true,
+ searchable: !is_minimap,
cursor_shape: EditorSettings::get_global(cx)
.cursor_shape
.unwrap_or_default(),
@@ -2067,9 +2106,9 @@ impl Editor {
autoindent_mode: Some(AutoindentMode::EachLine),
collapse_matches: false,
workspace: None,
- input_enabled: true,
- use_modal_editing: mode.is_full(),
- read_only: mode.is_minimap(),
+ input_enabled: !is_minimap,
+ use_modal_editing: full_mode,
+ read_only: is_minimap,
use_autoclose: true,
use_auto_surround: true,
auto_replace_emoji_shortcode: false,
@@ -2085,11 +2124,10 @@ impl Editor {
edit_prediction_preview: EditPredictionPreview::Inactive {
released_too_fast: false,
},
- inline_diagnostics_enabled: mode.is_full(),
- diagnostics_enabled: mode.is_full(),
+ inline_diagnostics_enabled: full_mode,
+ diagnostics_enabled: full_mode,
inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints),
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
-
gutter_hovered: false,
pixel_position_of_newest_cursor: None,
last_bounds: None,
@@ -2112,9 +2150,10 @@ impl Editor {
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(),
+ git_blame_inline_enabled: full_mode
+ && ProjectSettings::get_global(cx).git.inline_blame_enabled(),
render_diff_hunk_controls: Arc::new(render_diff_hunk_controls),
- serialize_dirty_buffers: !mode.is_minimap()
+ serialize_dirty_buffers: !is_minimap
&& ProjectSettings::get_global(cx)
.session
.restore_unsaved_buffers,
@@ -2125,27 +2164,31 @@ impl Editor {
breakpoint_store,
gutter_breakpoint_indicator: (None, None),
hovered_diff_hunk_row: 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::<SettingsStore>(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 {
- editor.show_mouse_cursor(cx);
- }
- }),
- ],
+ _subscriptions: (!is_minimap)
+ .then(|| {
+ 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::<SettingsStore>(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 {
+ editor.show_mouse_cursor(cx);
+ }
+ }),
+ ]
+ })
+ .unwrap_or_default(),
tasks_update_task: None,
pull_diagnostics_task: Task::ready(()),
colors: None,
@@ -2174,9 +2217,13 @@ impl Editor {
change_list: ChangeList::new(),
mode,
selection_drag_state: SelectionDragState::None,
- drag_and_drop_selection_enabled: EditorSettings::get_global(cx).drag_and_drop_selection,
folding_newlines: Task::ready(()),
};
+
+ if is_minimap {
+ return editor;
+ }
+
if let Some(breakpoints) = editor.breakpoint_store.as_ref() {
editor
._subscriptions
@@ -2296,7 +2343,10 @@ impl Editor {
editor.update_lsp_data(false, None, window, cx);
}
- editor.report_editor_event("Editor Opened", None, cx);
+ if editor.mode.is_full() {
+ editor.report_editor_event("Editor Opened", None, cx);
+ }
+
editor
}
@@ -4355,7 +4405,7 @@ impl Editor {
.take_while(|c| c.is_whitespace())
.count();
let comment_candidate = snapshot
- .chars_for_range(range)
+ .chars_for_range(range.clone())
.skip(num_of_whitespaces)
.take(max_len_of_delimiter)
.collect::<String>();
@@ -4371,6 +4421,22 @@ impl Editor {
})
.max_by_key(|(_, len)| *len)?;
+ if let Some((block_start, _)) = language.block_comment_delimiters()
+ {
+ let block_start_trimmed = block_start.trim_end();
+ if block_start_trimmed.starts_with(delimiter.trim_end()) {
+ let line_content = snapshot
+ .chars_for_range(range)
+ .skip(num_of_whitespaces)
+ .take(block_start_trimmed.len())
+ .collect::<String>();
+
+ if line_content.starts_with(block_start_trimmed) {
+ return None;
+ }
+ }
+ }
+
let cursor_is_placed_after_comment_marker =
num_of_whitespaces + trimmed_len <= start_point.column as usize;
if cursor_is_placed_after_comment_marker {
@@ -8715,7 +8781,7 @@ impl Editor {
h_flex()
.bg(cx.theme().colors().editor_background)
.border(BORDER_WIDTH)
- .shadow_sm()
+ .shadow_xs()
.border_color(cx.theme().colors().border)
.rounded_l_lg()
.when(line_count > 1, |el| el.rounded_br_lg())
@@ -8915,7 +8981,7 @@ impl Editor {
.border_1()
.bg(Self::edit_prediction_line_popover_bg_color(cx))
.border_color(Self::edit_prediction_callout_popover_border_color(cx))
- .shadow_sm()
+ .shadow_xs()
.when(!has_keybind, |el| {
let status_colors = cx.theme().status();
@@ -10204,6 +10270,17 @@ impl Editor {
self.manipulate_immutable_lines(window, cx, |lines| lines.sort())
}
+ pub fn sort_lines_by_length(
+ &mut self,
+ _: &SortLinesByLength,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.manipulate_immutable_lines(window, cx, |lines| {
+ lines.sort_by_key(|&line| line.chars().count())
+ })
+ }
+
pub fn sort_lines_case_insensitive(
&mut self,
_: &SortLinesCaseInsensitive,
@@ -10389,7 +10466,6 @@ impl Editor {
cloned_prompt.clone().into_any_element()
}),
priority: 0,
- render_in_minimap: true,
}];
let focus_handle = bp_prompt.focus_handle(cx);
@@ -15010,7 +15086,7 @@ impl Editor {
pub fn go_to_diagnostic(
&mut self,
- _: &GoToDiagnostic,
+ action: &GoToDiagnostic,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -15018,12 +15094,12 @@ impl Editor {
return;
}
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.go_to_diagnostic_impl(Direction::Next, window, cx)
+ self.go_to_diagnostic_impl(Direction::Next, action.severity, window, cx)
}
pub fn go_to_prev_diagnostic(
&mut self,
- _: &GoToPreviousDiagnostic,
+ action: &GoToPreviousDiagnostic,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -15031,12 +15107,13 @@ impl Editor {
return;
}
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.go_to_diagnostic_impl(Direction::Prev, window, cx)
+ self.go_to_diagnostic_impl(Direction::Prev, action.severity, window, cx)
}
pub fn go_to_diagnostic_impl(
&mut self,
direction: Direction,
+ severity: GoToDiagnosticSeverityFilter,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -15052,9 +15129,11 @@ impl Editor {
fn filtered(
snapshot: EditorSnapshot,
+ severity: GoToDiagnosticSeverityFilter,
diagnostics: impl Iterator<Item = DiagnosticEntry<usize>>,
) -> impl Iterator<Item = DiagnosticEntry<usize>> {
diagnostics
+ .filter(move |entry| severity.matches(entry.diagnostic.severity))
.filter(|entry| entry.range.start != entry.range.end)
.filter(|entry| !entry.diagnostic.is_unnecessary)
.filter(move |entry| !snapshot.intersects_fold(entry.range.start))
@@ -15063,12 +15142,14 @@ impl Editor {
let snapshot = self.snapshot(window, cx);
let before = filtered(
snapshot.clone(),
+ severity,
buffer
.diagnostics_in_range(0..selection.start)
.filter(|entry| entry.range.start <= selection.start),
);
let after = filtered(
snapshot,
+ severity,
buffer
.diagnostics_in_range(selection.start..buffer.len())
.filter(|entry| entry.range.start >= selection.start),
@@ -16085,7 +16166,6 @@ impl Editor {
}
}),
priority: 0,
- render_in_minimap: true,
}],
Some(Autoscroll::fit()),
cx,
@@ -17019,6 +17099,46 @@ impl Editor {
}
}
+ pub fn toggle_fold_all(
+ &mut self,
+ _: &actions::ToggleFoldAll,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self.buffer.read(cx).is_singleton() {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let has_folds = display_map
+ .folds_in_range(0..display_map.buffer_snapshot.len())
+ .next()
+ .is_some();
+
+ if has_folds {
+ self.unfold_all(&actions::UnfoldAll, window, cx);
+ } else {
+ self.fold_all(&actions::FoldAll, window, cx);
+ }
+ } else {
+ let buffer_ids = self.buffer.read(cx).excerpt_buffer_ids();
+ let should_unfold = buffer_ids
+ .iter()
+ .any(|buffer_id| self.is_buffer_folded(*buffer_id, cx));
+
+ self.toggle_fold_multiple_buffers = cx.spawn_in(window, async move |editor, cx| {
+ editor
+ .update_in(cx, |editor, _, cx| {
+ for buffer_id in buffer_ids {
+ if should_unfold {
+ editor.unfold_buffer(buffer_id, cx);
+ } else {
+ editor.fold_buffer(buffer_id, cx);
+ }
+ }
+ })
+ .ok();
+ });
+ }
+ }
+
fn fold_at_level(
&mut self,
fold_at: &FoldAtLevel,
@@ -17948,7 +18068,7 @@ impl Editor {
parent: cx.weak_entity(),
},
self.buffer.clone(),
- self.project.clone(),
+ None,
Some(self.display_map.clone()),
window,
cx,
@@ -19602,8 +19722,9 @@ impl Editor {
Anchor::in_buffer(excerpt_id, buffer_id, hint.position),
hint.text(),
);
-
- new_inlays.push(inlay);
+ if !inlay.text.chars().contains(&'\n') {
+ new_inlays.push(inlay);
+ }
});
}
@@ -19831,17 +19952,16 @@ impl Editor {
}
fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- let new_severity = if self.diagnostics_enabled() {
- EditorSettings::get_global(cx)
+ if self.diagnostics_enabled() {
+ let new_severity = EditorSettings::get_global(cx)
.diagnostics_max_severity
- .unwrap_or(DiagnosticSeverity::Hint)
- } else {
- DiagnosticSeverity::Off
- };
- self.set_max_diagnostics_severity(new_severity, cx);
+ .unwrap_or(DiagnosticSeverity::Hint);
+ self.set_max_diagnostics_severity(new_severity, cx);
+ }
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_inline_values(cx);
self.refresh_inlay_hints(
InlayHintRefreshReason::SettingsChange(inlay_hint_settings(
self.selections.newest_anchor().head(),
@@ -19859,7 +19979,6 @@ impl Editor {
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();
- self.drag_and_drop_selection_enabled = editor_settings.drag_and_drop_selection;
}
if old_cursor_shape != self.cursor_shape {
@@ -52,7 +52,7 @@ pub struct EditorSettings {
#[serde(default)]
pub diagnostics_max_severity: Option<DiagnosticSeverity>,
pub inline_code_actions: bool,
- pub drag_and_drop_selection: bool,
+ pub drag_and_drop_selection: DragAndDropSelection,
pub lsp_document_colors: DocumentColorsRenderMode,
}
@@ -275,6 +275,26 @@ pub struct ScrollbarAxes {
pub vertical: bool,
}
+/// Whether to allow drag and drop text selection in buffer.
+#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct DragAndDropSelection {
+ /// When true, enables drag and drop text selection in buffer.
+ ///
+ /// Default: true
+ #[serde(default = "default_true")]
+ pub enabled: bool,
+
+ /// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created.
+ ///
+ /// Default: 300
+ #[serde(default = "default_drag_and_drop_selection_delay_ms")]
+ pub delay: u64,
+}
+
+fn default_drag_and_drop_selection_delay_ms() -> u64 {
+ 300
+}
+
/// Which diagnostic indicators to show in the scrollbar.
///
/// Default: all
@@ -536,10 +556,8 @@ pub struct EditorSettingsContent {
/// Default: true
pub inline_code_actions: Option<bool>,
- /// Whether to allow drag and drop text selection in buffer.
- ///
- /// Default: true
- pub drag_and_drop_selection: Option<bool>,
+ /// Drag and drop related settings
+ pub drag_and_drop_selection: Option<DragAndDropSelection>,
/// How to render LSP `textDocument/documentColor` colors in the editor.
///
@@ -3080,6 +3080,45 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
"});
}
+#[gpui::test]
+async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.tab_size = NonZeroU32::new(4)
+ });
+
+ let lua_language = Arc::new(Language::new(
+ LanguageConfig {
+ line_comments: vec!["--".into()],
+ block_comment: Some(("--[[".into(), "]]".into())),
+ ..LanguageConfig::default()
+ },
+ None,
+ ));
+
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
+
+ // Line with line comment should extend
+ cx.set_state(indoc! {"
+ --ˇ
+ "});
+ cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
+ cx.assert_editor_state(indoc! {"
+ --
+ --ˇ
+ "});
+
+ // Line with block comment that matches line comment should not extend
+ cx.set_state(indoc! {"
+ --[[ˇ
+ "});
+ cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
+ cx.assert_editor_state(indoc! {"
+ --[[
+ ˇ
+ "});
+}
+
#[gpui::test]
fn test_insert_with_old_selections(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -3468,6 +3507,70 @@ async fn test_indent_outdent(cx: &mut TestAppContext) {
"});
}
+#[gpui::test]
+async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
+ // This is a regression test for issue #33761
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
+
+ cx.set_state(
+ r#"ˇ# ingress:
+ˇ# api:
+ˇ# enabled: false
+ˇ# pathType: Prefix
+ˇ# console:
+ˇ# enabled: false
+ˇ# pathType: Prefix
+"#,
+ );
+
+ // Press tab to indent all lines
+ cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
+
+ cx.assert_editor_state(
+ r#" ˇ# ingress:
+ ˇ# api:
+ ˇ# enabled: false
+ ˇ# pathType: Prefix
+ ˇ# console:
+ ˇ# enabled: false
+ ˇ# pathType: Prefix
+"#,
+ );
+}
+
+#[gpui::test]
+async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
+ // This is a test to make sure our fix for issue #33761 didn't break anything
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
+
+ cx.set_state(
+ r#"ˇingress:
+ˇ api:
+ˇ enabled: false
+ˇ pathType: Prefix
+"#,
+ );
+
+ // Press tab to indent all lines
+ cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
+
+ cx.assert_editor_state(
+ r#"ˇingress:
+ ˇapi:
+ ˇenabled: false
+ ˇpathType: Prefix
+"#,
+ );
+}
+
#[gpui::test]
async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
init_test(cx, |settings| {
@@ -4011,6 +4114,29 @@ async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppC
Zˇ»
"});
+ // Test sort_lines_by_length()
+ //
+ // Demonstrates:
+ // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
+ // - sort is stable
+ cx.set_state(indoc! {"
+ «123
+ æ
+ 12
+ ∞
+ 1
+ æˇ»
+ "});
+ cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
+ cx.assert_editor_state(indoc! {"
+ «æ
+ ∞
+ 1
+ æ
+ 12
+ 123ˇ»
+ "});
+
// Test reverse_lines()
cx.set_state(indoc! {"
«5
@@ -4955,7 +5081,6 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
- render_in_minimap: true,
}],
Some(Autoscroll::fit()),
cx,
@@ -4998,7 +5123,6 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
style: BlockStyle::Sticky,
render: Arc::new(|_| gpui::div().into_any_element()),
priority: 0,
- render_in_minimap: true,
}],
None,
cx,
@@ -14610,7 +14734,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
- editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
+ editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
@@ -14619,7 +14743,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
"});
cx.update_editor(|editor, window, cx| {
- editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
+ editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
@@ -14628,7 +14752,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
"});
cx.update_editor(|editor, window, cx| {
- editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
+ editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
@@ -14637,7 +14761,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
"});
cx.update_editor(|editor, window, cx| {
- editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
+ editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
});
cx.assert_editor_state(indoc! {"
@@ -21339,7 +21463,7 @@ println!("5");
.unwrap();
pane_1
.update_in(cx, |pane, window, cx| {
- pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
+ pane.close_inactive_items(&CloseInactiveItems::default(), None, window, cx)
})
.await
.unwrap();
@@ -21375,7 +21499,7 @@ println!("5");
.unwrap();
pane_2
.update_in(cx, |pane, window, cx| {
- pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
+ pane.close_inactive_items(&CloseInactiveItems::default(), None, window, cx)
})
.await
.unwrap();
@@ -22261,6 +22385,19 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
def f() -> list[str]:
aˇ
"});
+
+ // test does not outdent on typing : after case keyword
+ cx.set_state(indoc! {"
+ match 1:
+ caseˇ
+ "});
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input(":", window, cx);
+ });
+ cx.assert_editor_state(indoc! {"
+ match 1:
+ case:ˇ
+ "});
}
#[gpui::test]
@@ -9,7 +9,7 @@ use crate::{
LineUp, MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase,
SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint,
- ToggleFold,
+ ToggleFold, ToggleFoldAll,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
@@ -87,7 +87,6 @@ use util::{RangeExt, ResultExt, debug_panic};
use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt};
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
-const SELECTION_DRAG_DELAY: Duration = Duration::from_millis(300);
/// Determines what kinds of highlights should be applied to a lines background.
#[derive(Clone, Copy, Default)]
@@ -225,6 +224,7 @@ impl EditorElement {
register_action(editor, window, Editor::autoindent);
register_action(editor, window, Editor::delete_line);
register_action(editor, window, Editor::join_lines);
+ register_action(editor, window, Editor::sort_lines_by_length);
register_action(editor, window, Editor::sort_lines_case_sensitive);
register_action(editor, window, Editor::sort_lines_case_insensitive);
register_action(editor, window, Editor::reverse_lines);
@@ -416,6 +416,7 @@ impl EditorElement {
register_action(editor, window, Editor::fold_recursive);
register_action(editor, window, Editor::toggle_fold);
register_action(editor, window, Editor::toggle_fold_recursive);
+ register_action(editor, window, Editor::toggle_fold_all);
register_action(editor, window, Editor::unfold_lines);
register_action(editor, window, Editor::unfold_recursive);
register_action(editor, window, Editor::unfold_all);
@@ -643,7 +644,11 @@ impl EditorElement {
return;
}
- if editor.drag_and_drop_selection_enabled && click_count == 1 {
+ if EditorSettings::get_global(cx)
+ .drag_and_drop_selection
+ .enabled
+ && click_count == 1
+ {
let newest_anchor = editor.selections.newest_anchor();
let snapshot = editor.snapshot(window, cx);
let selection = newest_anchor.map(|anchor| anchor.to_display_point(&snapshot));
@@ -1021,7 +1026,10 @@ impl EditorElement {
ref click_position,
ref mouse_down_time,
} => {
- if mouse_down_time.elapsed() >= SELECTION_DRAG_DELAY {
+ let drag_and_drop_delay = Duration::from_millis(
+ EditorSettings::get_global(cx).drag_and_drop_selection.delay,
+ );
+ if mouse_down_time.elapsed() >= drag_and_drop_delay {
let drop_cursor = Selection {
id: post_inc(&mut editor.selections.next_selection_id),
start: drop_anchor,
@@ -1610,6 +1618,7 @@ impl EditorElement {
strikethrough: None,
underline: None,
}],
+ None,
)
})
} else {
@@ -1875,7 +1884,7 @@ impl EditorElement {
let mut minimap = div()
.size_full()
- .shadow_sm()
+ .shadow_xs()
.px(PADDING_OFFSET)
.child(minimap_editor)
.into_any_element();
@@ -2085,16 +2094,19 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> HashMap<DisplayRow, AnyElement> {
- if self.editor.read(cx).mode().is_minimap() {
- return HashMap::default();
- }
-
- let max_severity = match ProjectSettings::get_global(cx)
- .diagnostics
- .inline
- .max_severity
- .unwrap_or_else(|| self.editor.read(cx).diagnostics_max_severity)
- .into_lsp()
+ let max_severity = match self
+ .editor
+ .read(cx)
+ .inline_diagnostics_enabled()
+ .then(|| {
+ ProjectSettings::get_global(cx)
+ .diagnostics
+ .inline
+ .max_severity
+ .unwrap_or_else(|| self.editor.read(cx).diagnostics_max_severity)
+ .into_lsp()
+ })
+ .flatten()
{
Some(max_severity) => max_severity,
None => return HashMap::default(),
@@ -2444,7 +2456,7 @@ impl EditorElement {
.git
.inline_blame
.and_then(|settings| settings.min_column)
- .map(|col| self.column_pixels(col as usize, window, cx))
+ .map(|col| self.column_pixels(col as usize, window))
.unwrap_or(px(0.));
let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
@@ -2610,9 +2622,6 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Option<Vec<IndentGuideLayout>> {
- if self.editor.read(cx).mode().is_minimap() {
- return None;
- }
let indent_guides = self.editor.update(cx, |editor, cx| {
editor.indent_guides(visible_buffer_range, snapshot, cx)
})?;
@@ -2629,7 +2638,7 @@ impl EditorElement {
.enumerate()
.filter_map(|(i, indent_guide)| {
let single_indent_width =
- self.column_pixels(indent_guide.tab_size as usize, window, cx);
+ self.column_pixels(indent_guide.tab_size as usize, window);
let total_width = single_indent_width * indent_guide.depth as f32;
let start_x = content_origin.x + total_width - scroll_pixel_position.x;
if start_x >= text_origin.x {
@@ -2657,6 +2666,39 @@ impl EditorElement {
)
}
+ fn layout_wrap_guides(
+ &self,
+ em_advance: Pixels,
+ scroll_position: gpui::Point<f32>,
+ content_origin: gpui::Point<Pixels>,
+ scrollbar_layout: Option<&EditorScrollbars>,
+ vertical_scrollbar_width: Pixels,
+ hitbox: &Hitbox,
+ window: &Window,
+ cx: &App,
+ ) -> SmallVec<[(Pixels, bool); 2]> {
+ let scroll_left = scroll_position.x * em_advance;
+ let content_origin = content_origin.x;
+ let horizontal_offset = content_origin - scroll_left;
+ let vertical_scrollbar_width = scrollbar_layout
+ .and_then(|layout| layout.visible.then_some(vertical_scrollbar_width))
+ .unwrap_or_default();
+
+ self.editor
+ .read(cx)
+ .wrap_guides(cx)
+ .into_iter()
+ .flat_map(|(guide, active)| {
+ let wrap_position = self.column_pixels(guide, window);
+ let wrap_guide_x = wrap_position + horizontal_offset;
+ let display_wrap_guide = wrap_guide_x >= content_origin
+ && wrap_guide_x <= hitbox.bounds.right() - vertical_scrollbar_width;
+
+ display_wrap_guide.then_some((wrap_guide_x, active))
+ })
+ .collect()
+ }
+
fn calculate_indent_guide_bounds(
row_range: Range<MultiBufferRow>,
line_height: Pixels,
@@ -2795,6 +2837,7 @@ impl EditorElement {
) -> Vec<AnyElement> {
self.editor.update(cx, |editor, cx| {
let active_task_indicator_row =
+ // TODO: add edit button on the right side of each row in the context menu
if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
deployed_from,
actions,
@@ -3042,9 +3085,9 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Arc<HashMap<MultiBufferRow, LineNumberLayout>> {
- let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| {
- EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode.is_full()
- });
+ let include_line_numbers = snapshot
+ .show_line_numbers
+ .unwrap_or_else(|| EditorSettings::get_global(cx).gutter.line_numbers);
if !include_line_numbers {
return Arc::default();
}
@@ -3228,10 +3271,12 @@ impl EditorElement {
underline: None,
strikethrough: None,
};
- let line =
- window
- .text_system()
- .shape_line(line.to_string().into(), font_size, &[run]);
+ let line = window.text_system().shape_line(
+ line.to_string().into(),
+ font_size,
+ &[run],
+ None,
+ );
LineWithInvisibles {
width: line.width,
len: line.len,
@@ -3355,22 +3400,18 @@ impl EditorElement {
div()
.size_full()
- .children(
- (!snapshot.mode.is_minimap() || custom.render_in_minimap).then(|| {
- custom.render(&mut BlockContext {
- window,
- app: cx,
- anchor_x,
- margins: editor_margins,
- line_height,
- em_width,
- block_id,
- selected,
- max_width: text_hitbox.size.width.max(*scroll_width),
- editor_style: &self.style,
- })
- }),
- )
+ .child(custom.render(&mut BlockContext {
+ window,
+ app: cx,
+ anchor_x,
+ margins: editor_margins,
+ line_height,
+ em_width,
+ block_id,
+ selected,
+ max_width: text_hitbox.size.width.max(*scroll_width),
+ editor_style: &self.style,
+ }))
.into_any()
}
@@ -3576,24 +3617,37 @@ impl EditorElement {
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
- Tooltip::for_action_in(
+ Tooltip::with_meta_in(
"Toggle Excerpt Fold",
- &ToggleFold,
+ Some(&ToggleFold),
+ "Alt+click to toggle all",
&focus_handle,
window,
cx,
)
}
})
- .on_click(move |_, _, cx| {
- if is_folded {
+ .on_click(move |event, window, cx| {
+ if event.modifiers().alt {
+ // Alt+click toggles all buffers
editor.update(cx, |editor, cx| {
- editor.unfold_buffer(buffer_id, cx);
+ editor.toggle_fold_all(
+ &ToggleFoldAll,
+ window,
+ cx,
+ );
});
} else {
- editor.update(cx, |editor, cx| {
- editor.fold_buffer(buffer_id, cx);
- });
+ // Regular click toggles single buffer
+ if is_folded {
+ editor.update(cx, |editor, cx| {
+ editor.unfold_buffer(buffer_id, cx);
+ });
+ } else {
+ editor.update(cx, |editor, cx| {
+ editor.fold_buffer(buffer_id, cx);
+ });
+ }
}
}),
),
@@ -5240,26 +5294,7 @@ impl EditorElement {
paint_highlight(range.start, range.end, color, edges);
}
- let scroll_left =
- layout.position_map.snapshot.scroll_position().x * layout.position_map.em_width;
-
- for (wrap_position, active) in layout.wrap_guides.iter() {
- let x = (layout.position_map.text_hitbox.origin.x
- + *wrap_position
- + layout.position_map.em_width / 2.)
- - scroll_left;
-
- let show_scrollbars = layout
- .scrollbars_layout
- .as_ref()
- .map_or(false, |layout| layout.visible);
-
- if x < layout.position_map.text_hitbox.origin.x
- || (show_scrollbars && x > self.scrollbar_left(&layout.hitbox.bounds))
- {
- continue;
- }
-
+ for (guide_x, active) in layout.wrap_guides.iter() {
let color = if *active {
cx.theme().colors().editor_active_wrap_guide
} else {
@@ -5267,7 +5302,7 @@ impl EditorElement {
};
window.paint_quad(fill(
Bounds {
- origin: point(x, layout.position_map.text_hitbox.origin.y),
+ origin: point(*guide_x, layout.position_map.text_hitbox.origin.y),
size: size(px(1.), layout.position_map.text_hitbox.size.height),
},
color,
@@ -5691,6 +5726,19 @@ impl EditorElement {
let editor = self.editor.read(cx);
if editor.mouse_cursor_hidden {
window.set_window_cursor_style(CursorStyle::None);
+ } else if let SelectionDragState::ReadyToDrag {
+ mouse_down_time, ..
+ } = &editor.selection_drag_state
+ {
+ let drag_and_drop_delay = Duration::from_millis(
+ EditorSettings::get_global(cx).drag_and_drop_selection.delay,
+ );
+ if mouse_down_time.elapsed() >= drag_and_drop_delay {
+ window.set_cursor_style(
+ CursorStyle::DragCopy,
+ &layout.position_map.text_hitbox,
+ );
+ }
} else if matches!(
editor.selection_drag_state,
SelectionDragState::Dragging { .. }
@@ -6678,7 +6726,7 @@ impl EditorElement {
let position_map: &PositionMap = &position_map;
let line_height = position_map.line_height;
- let max_glyph_width = position_map.em_width;
+ let max_glyph_advance = position_map.em_advance;
let (delta, axis) = match delta {
gpui::ScrollDelta::Pixels(mut pixels) => {
//Trackpad
@@ -6689,15 +6737,15 @@ impl EditorElement {
gpui::ScrollDelta::Lines(lines) => {
//Not trackpad
let pixels =
- point(lines.x * max_glyph_width, lines.y * line_height);
+ point(lines.x * max_glyph_advance, lines.y * line_height);
(pixels, None)
}
};
let current_scroll_position = position_map.snapshot.scroll_position();
- let x = (current_scroll_position.x * max_glyph_width
+ let x = (current_scroll_position.x * max_glyph_advance
- (delta.x * scroll_sensitivity))
- / max_glyph_width;
+ / max_glyph_advance;
let y = (current_scroll_position.y * line_height
- (delta.y * scroll_sensitivity))
/ line_height;
@@ -6724,7 +6772,7 @@ impl EditorElement {
}
fn paint_mouse_listeners(&mut self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
- if self.editor.read(cx).mode.is_minimap() {
+ if layout.mode.is_minimap() {
return;
}
@@ -6858,11 +6906,7 @@ impl EditorElement {
});
}
- fn scrollbar_left(&self, bounds: &Bounds<Pixels>) -> Pixels {
- bounds.top_right().x - self.style.scrollbar_width
- }
-
- fn column_pixels(&self, column: usize, window: &mut Window, _: &mut App) -> Pixels {
+ fn column_pixels(&self, column: usize, window: &Window) -> Pixels {
let style = &self.style;
let font_size = style.text.font_size.to_pixels(window.rem_size());
let layout = window.text_system().shape_line(
@@ -6876,19 +6920,15 @@ impl EditorElement {
underline: None,
strikethrough: None,
}],
+ None,
);
layout.width
}
- fn max_line_number_width(
- &self,
- snapshot: &EditorSnapshot,
- window: &mut Window,
- cx: &mut App,
- ) -> Pixels {
+ fn max_line_number_width(&self, snapshot: &EditorSnapshot, window: &mut Window) -> Pixels {
let digit_count = snapshot.widest_line_number().ilog10() + 1;
- self.column_pixels(digit_count as usize, window, cx)
+ self.column_pixels(digit_count as usize, window)
}
fn shape_line_number(
@@ -6909,6 +6949,7 @@ impl EditorElement {
text,
self.style.text.font_size.to_pixels(window.rem_size()),
&[run],
+ None,
)
}
@@ -7177,10 +7218,12 @@ impl LineWithInvisibles {
}]) {
if let Some(replacement) = highlighted_chunk.replacement {
if !line.is_empty() {
- let shaped_line =
- window
- .text_system()
- .shape_line(line.clone().into(), font_size, &styles);
+ let shaped_line = window.text_system().shape_line(
+ line.clone().into(),
+ font_size,
+ &styles,
+ None,
+ );
width += shaped_line.width;
len += shaped_line.len;
fragments.push(LineFragment::Text(shaped_line));
@@ -7200,6 +7243,7 @@ impl LineWithInvisibles {
chunk,
font_size,
&[text_style.to_run(highlighted_chunk.text.len())],
+ None,
);
AvailableSpace::Definite(shaped_line.width)
} else {
@@ -7244,7 +7288,7 @@ impl LineWithInvisibles {
};
let line_layout = window
.text_system()
- .shape_line(x, font_size, &[run])
+ .shape_line(x, font_size, &[run], None)
.with_len(highlighted_chunk.text.len());
width += line_layout.width;
@@ -7259,6 +7303,7 @@ impl LineWithInvisibles {
line.clone().into(),
font_size,
&styles,
+ None,
);
width += shaped_line.width;
len += shaped_line.len;
@@ -7789,7 +7834,7 @@ impl Element for EditorElement {
} => {
let editor_handle = cx.entity().clone();
let max_line_number_width =
- self.max_line_number_width(&editor.snapshot(window, cx), window, cx);
+ self.max_line_number_width(&editor.snapshot(window, cx), window);
window.request_measured_layout(
Style::default(),
move |known_dimensions, available_space, window, cx| {
@@ -7854,9 +7899,14 @@ impl Element for EditorElement {
line_height: Some(self.style.text.line_height),
..Default::default()
};
- let focus_handle = self.editor.focus_handle(cx);
- window.set_view_id(self.editor.entity_id());
- window.set_focus_handle(&focus_handle, cx);
+
+ let is_minimap = self.editor.read(cx).mode.is_minimap();
+
+ if !is_minimap {
+ let focus_handle = self.editor.focus_handle(cx);
+ window.set_view_id(self.editor.entity_id());
+ window.set_focus_handle(&focus_handle, cx);
+ }
let rem_size = self.rem_size(cx);
window.with_rem_size(rem_size, |window| {
@@ -7879,7 +7929,7 @@ impl Element for EditorElement {
.gutter_dimensions(
font_id,
font_size,
- self.max_line_number_width(&snapshot, window, cx),
+ self.max_line_number_width(&snapshot, window),
cx,
)
.or_else(|| {
@@ -7928,6 +7978,7 @@ impl Element for EditorElement {
editor.last_bounds = Some(bounds);
editor.gutter_dimensions = gutter_dimensions;
editor.set_visible_line_count(bounds.size.height / line_height, window, cx);
+ editor.set_visible_column_count(editor_content_width / em_advance);
if matches!(
editor.mode,
@@ -7954,14 +8005,6 @@ impl Element for EditorElement {
}
});
- let wrap_guides = self
- .editor
- .read(cx)
- .wrap_guides(cx)
- .iter()
- .map(|(guide, active)| (self.column_pixels(*guide, window, cx), *active))
- .collect::<SmallVec<[_; 2]>>();
-
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
let gutter_hitbox = window.insert_hitbox(
gutter_bounds(bounds, gutter_dimensions),
@@ -8007,23 +8050,25 @@ impl Element for EditorElement {
}
};
- // TODO: Autoscrolling for both axes
- let mut autoscroll_request = None;
- let mut autoscroll_containing_element = false;
- let mut autoscroll_horizontally = false;
- self.editor.update(cx, |editor, cx| {
- autoscroll_request = editor.autoscroll_request();
- autoscroll_containing_element =
+ let (
+ autoscroll_request,
+ autoscroll_containing_element,
+ needs_horizontal_autoscroll,
+ ) = self.editor.update(cx, |editor, cx| {
+ let autoscroll_request = editor.autoscroll_request();
+ let autoscroll_containing_element =
autoscroll_request.is_some() || editor.has_pending_selection();
- // TODO: Is this horizontal or vertical?!
- autoscroll_horizontally = editor.autoscroll_vertically(
- bounds,
- line_height,
- max_scroll_top,
- window,
- cx,
- );
- snapshot = editor.snapshot(window, cx);
+
+ let (needs_horizontal_autoscroll, was_scrolled) = editor
+ .autoscroll_vertically(bounds, line_height, max_scroll_top, window, cx);
+ if was_scrolled.0 {
+ snapshot = editor.snapshot(window, cx);
+ }
+ (
+ autoscroll_request,
+ autoscroll_containing_element,
+ needs_horizontal_autoscroll,
+ )
});
let mut scroll_position = snapshot.scroll_position();
@@ -8299,18 +8344,22 @@ impl Element for EditorElement {
window,
cx,
);
- let new_renrerer_widths = line_layouts
- .iter()
- .flat_map(|layout| &layout.fragments)
- .filter_map(|fragment| {
- if let LineFragment::Element { id, size, .. } = fragment {
- Some((*id, size.width))
- } else {
- None
- }
- });
- if self.editor.update(cx, |editor, cx| {
- editor.update_renderer_widths(new_renrerer_widths, cx)
+ let new_renderer_widths = (!is_minimap).then(|| {
+ line_layouts
+ .iter()
+ .flat_map(|layout| &layout.fragments)
+ .filter_map(|fragment| {
+ if let LineFragment::Element { id, size, .. } = fragment {
+ Some((*id, size.width))
+ } else {
+ None
+ }
+ })
+ });
+ if new_renderer_widths.is_some_and(|new_renderer_widths| {
+ self.editor.update(cx, |editor, cx| {
+ editor.update_renderer_widths(new_renderer_widths, cx)
+ })
}) {
// If the fold widths have changed, we need to prepaint
// the element again to account for any changes in
@@ -8373,27 +8422,31 @@ impl Element for EditorElement {
let sticky_header_excerpt_id =
sticky_header_excerpt.as_ref().map(|top| top.excerpt.id);
- let blocks = window.with_element_namespace("blocks", |window| {
- self.render_blocks(
- start_row..end_row,
- &snapshot,
- &hitbox,
- &text_hitbox,
- editor_width,
- &mut scroll_width,
- &editor_margins,
- em_width,
- gutter_dimensions.full_width(),
- line_height,
- &mut line_layouts,
- &local_selections,
- &selected_buffer_ids,
- is_row_soft_wrapped,
- sticky_header_excerpt_id,
- window,
- cx,
- )
- });
+ let blocks = (!is_minimap)
+ .then(|| {
+ window.with_element_namespace("blocks", |window| {
+ self.render_blocks(
+ start_row..end_row,
+ &snapshot,
+ &hitbox,
+ &text_hitbox,
+ editor_width,
+ &mut scroll_width,
+ &editor_margins,
+ em_width,
+ gutter_dimensions.full_width(),
+ line_height,
+ &mut line_layouts,
+ &local_selections,
+ &selected_buffer_ids,
+ is_row_soft_wrapped,
+ sticky_header_excerpt_id,
+ window,
+ cx,
+ )
+ })
+ })
+ .unwrap_or_else(|| Ok((Vec::default(), HashMap::default())));
let (mut blocks, row_block_types) = match blocks {
Ok(blocks) => blocks,
Err(resized_blocks) => {
@@ -8432,24 +8485,22 @@ impl Element for EditorElement {
);
self.editor.update(cx, |editor, cx| {
- let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
+ if editor.scroll_manager.clamp_scroll_left(scroll_max.x) {
+ scroll_position.x = scroll_position.x.min(scroll_max.x);
+ }
- let autoscrolled = if autoscroll_horizontally {
- editor.autoscroll_horizontally(
+ if needs_horizontal_autoscroll.0
+ && let Some(new_scroll_position) = editor.autoscroll_horizontally(
start_row,
editor_content_width,
scroll_width,
em_advance,
&line_layouts,
+ window,
cx,
)
- } else {
- false
- };
-
- if clamped || autoscrolled {
- snapshot = editor.snapshot(window, cx);
- scroll_position = snapshot.scroll_position();
+ {
+ scroll_position = new_scroll_position;
}
});
@@ -8564,7 +8615,9 @@ impl Element for EditorElement {
}
} else {
log::error!(
- "bug: line_ix {} is out of bounds - row_infos.len(): {}, line_layouts.len(): {}, crease_trailers.len(): {}",
+ "bug: line_ix {} is out of bounds - row_infos.len(): {}, \
+ line_layouts.len(): {}, \
+ crease_trailers.len(): {}",
line_ix,
row_infos.len(),
line_layouts.len(),
@@ -8585,28 +8638,6 @@ impl Element for EditorElement {
cx,
);
- self.editor.update(cx, |editor, cx| {
- let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
-
- let autoscrolled = if autoscroll_horizontally {
- editor.autoscroll_horizontally(
- start_row,
- editor_content_width,
- scroll_width,
- em_width,
- &line_layouts,
- cx,
- )
- } else {
- false
- };
-
- if clamped || autoscrolled {
- snapshot = editor.snapshot(window, cx);
- scroll_position = snapshot.scroll_position();
- }
- });
-
let line_elements = self.prepaint_lines(
start_row,
&mut line_layouts,
@@ -8797,6 +8828,17 @@ impl Element for EditorElement {
self.prepaint_expand_toggles(&mut expand_toggles, window, cx)
});
+ let wrap_guides = self.layout_wrap_guides(
+ em_advance,
+ scroll_position,
+ content_origin,
+ scrollbars_layout.as_ref(),
+ vertical_scrollbar_width,
+ &hitbox,
+ window,
+ cx,
+ );
+
let minimap = window.with_element_namespace("minimap", |window| {
self.layout_minimap(
&snapshot,
@@ -8821,6 +8863,7 @@ impl Element for EditorElement {
underline: None,
strikethrough: None,
}],
+ None,
);
let space_invisible = window.text_system().shape_line(
"•".into(),
@@ -8833,6 +8876,7 @@ impl Element for EditorElement {
underline: None,
strikethrough: None,
}],
+ None,
);
let mode = snapshot.mode.clone();
@@ -8934,19 +8978,21 @@ impl Element for EditorElement {
window: &mut Window,
cx: &mut App,
) {
- let focus_handle = self.editor.focus_handle(cx);
- let key_context = self
- .editor
- .update(cx, |editor, cx| editor.key_context(window, cx));
-
- window.set_key_context(key_context);
- window.handle_input(
- &focus_handle,
- ElementInputHandler::new(bounds, self.editor.clone()),
- cx,
- );
- self.register_actions(window, cx);
- self.register_key_listeners(window, cx, layout);
+ if !layout.mode.is_minimap() {
+ let focus_handle = self.editor.focus_handle(cx);
+ let key_context = self
+ .editor
+ .update(cx, |editor, cx| editor.key_context(window, cx));
+
+ window.set_key_context(key_context);
+ window.handle_input(
+ &focus_handle,
+ ElementInputHandler::new(bounds, self.editor.clone()),
+ cx,
+ );
+ self.register_actions(window, cx);
+ self.register_key_listeners(window, cx, layout);
+ }
let text_style = TextStyleRefinement {
font_size: Some(self.style.text.font_size),
@@ -10255,7 +10301,6 @@ mod tests {
height: Some(3),
render: Arc::new(|cx| div().h(3. * cx.window.line_height()).into_any()),
priority: 0,
- render_in_minimap: true,
}],
None,
cx,
@@ -381,10 +381,14 @@ fn show_hover(
.anchor_after(local_diagnostic.range.end),
};
+ let scroll_handle = ScrollHandle::new();
+
Some(DiagnosticPopover {
local_diagnostic,
markdown,
border_color,
+ scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
+ scroll_handle,
background_color,
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
anchor,
@@ -955,6 +959,8 @@ pub struct DiagnosticPopover {
pub keyboard_grace: Rc<RefCell<bool>>,
pub anchor: Anchor,
_subscription: Subscription,
+ pub scroll_handle: ScrollHandle,
+ pub scrollbar_state: ScrollbarState,
}
impl DiagnosticPopover {
@@ -968,10 +974,7 @@ impl DiagnosticPopover {
let this = cx.entity().downgrade();
div()
.id("diagnostic")
- .block()
- .max_h(max_size.height)
- .overflow_y_scroll()
- .max_w(max_size.width)
+ .occlude()
.elevation_2_borderless(cx)
// Don't draw the background color if the theme
// allows transparent surfaces.
@@ -992,27 +995,72 @@ impl DiagnosticPopover {
div()
.py_1()
.px_2()
- .child(
- MarkdownElement::new(
- self.markdown.clone(),
- diagnostics_markdown_style(window, cx),
- )
- .on_url_click(move |link, window, cx| {
- if let Some(renderer) = GlobalDiagnosticRenderer::global(cx) {
- this.update(cx, |this, cx| {
- renderer.as_ref().open_link(this, link, window, cx);
- })
- .ok();
- }
- }),
- )
.bg(self.background_color)
.border_1()
.border_color(self.border_color)
- .rounded_lg(),
+ .rounded_lg()
+ .child(
+ div()
+ .id("diagnostic-content-container")
+ .overflow_y_scroll()
+ .max_w(max_size.width)
+ .max_h(max_size.height)
+ .track_scroll(&self.scroll_handle)
+ .child(
+ MarkdownElement::new(
+ self.markdown.clone(),
+ diagnostics_markdown_style(window, cx),
+ )
+ .on_url_click(
+ move |link, window, cx| {
+ if let Some(renderer) = GlobalDiagnosticRenderer::global(cx)
+ {
+ this.update(cx, |this, cx| {
+ renderer.as_ref().open_link(this, link, window, cx);
+ })
+ .ok();
+ }
+ },
+ ),
+ ),
+ )
+ .child(self.render_vertical_scrollbar(cx)),
)
.into_any_element()
}
+
+ fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
+ div()
+ .occlude()
+ .id("diagnostic-popover-vertical-scroll")
+ .on_mouse_move(cx.listener(|_, _, _, cx| {
+ cx.notify();
+ cx.stop_propagation()
+ }))
+ .on_hover(|_, _, cx| {
+ cx.stop_propagation();
+ })
+ .on_any_mouse_down(|_, _, cx| {
+ cx.stop_propagation();
+ })
+ .on_mouse_up(
+ MouseButton::Left,
+ cx.listener(|_, _, _, cx| {
+ cx.stop_propagation();
+ }),
+ )
+ .on_scroll_wheel(cx.listener(|_, _, _, cx| {
+ cx.notify();
+ }))
+ .h_full()
+ .absolute()
+ .right_1()
+ .top_1()
+ .bottom_0()
+ .w(px(12.))
+ .cursor_default()
+ .children(Scrollbar::vertical(self.scrollbar_state.clone()))
+ }
}
#[cfg(test)]
@@ -813,7 +813,13 @@ impl Item for Editor {
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
- self.report_editor_event("Editor Saved", None, cx);
+ // Add meta data tracking # of auto saves
+ if options.autosave {
+ self.report_editor_event("Editor Autosaved", None, cx);
+ } else {
+ self.report_editor_event("Editor Saved", None, cx);
+ }
+
let buffers = self.buffer().clone().read(cx).all_buffers();
let buffers = buffers
.into_iter()
@@ -1220,7 +1226,20 @@ impl SerializableItem for Editor {
abs_path: None,
contents: None,
..
- } => Task::ready(Err(anyhow!("No path or contents found for buffer"))),
+ } => window.spawn(cx, async move |cx| {
+ let buffer = project
+ .update(cx, |project, cx| project.create_buffer(cx))?
+ .await?;
+
+ cx.update(|window, cx| {
+ cx.new(|cx| {
+ let mut editor = Editor::for_buffer(buffer, Some(project), window, cx);
+
+ editor.read_metadata_from_db(item_id, workspace_id, window, cx);
+ editor
+ })
+ })
+ }),
}
}
@@ -1607,24 +1626,10 @@ impl SearchableItem for Editor {
let text = self.buffer.read(cx);
let text = text.snapshot(cx);
let mut edits = vec![];
- let mut last_point: Option<Point> = None;
for m in matches {
- let point = m.start.to_point(&text);
let text = text.text_for_range(m.clone()).collect::<Vec<_>>();
- // Check if the row for the current match is different from the last
- // match. If that's not the case and we're still replacing matches
- // in the same row/line, skip this match if the `one_match_per_line`
- // option is enabled.
- if last_point.is_none() {
- last_point = Some(point);
- } else if last_point.is_some() && point.row != last_point.unwrap().row {
- last_point = Some(point);
- } else if query.one_match_per_line().is_some_and(|enabled| enabled) {
- continue;
- }
-
let text: Cow<_> = if text.len() == 1 {
text.first().cloned().unwrap().into()
} else {
@@ -2106,5 +2111,38 @@ mod tests {
assert!(editor.has_conflict(cx)); // The editor should have a conflict
});
}
+
+ // Test case 5: Deserialize with no path, no content, no language, and no old mtime (new, empty, unsaved buffer)
+ {
+ let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
+ let (workspace, cx) =
+ cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+
+ let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
+
+ let item_id = 10000 as ItemId;
+ let serialized_editor = SerializedEditor {
+ abs_path: None,
+ contents: None,
+ language: None,
+ mtime: None,
+ };
+
+ DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
+ .await
+ .unwrap();
+
+ let deserialized =
+ deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
+
+ deserialized.update(cx, |editor, cx| {
+ assert_eq!(editor.text(cx), "");
+ assert!(!editor.is_dirty(cx));
+ assert!(!editor.has_conflict(cx));
+
+ let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
+ assert!(buffer.file().is_none());
+ });
+ }
}
}
@@ -13,6 +13,7 @@ use crate::{
pub use autoscroll::{Autoscroll, AutoscrollStrategy};
use core::fmt::Debug;
use gpui::{App, Axis, Context, Global, Pixels, Task, Window, point, px};
+use language::language_settings::{AllLanguageSettings, SoftWrap};
use language::{Bias, Point};
pub use scroll_amount::ScrollAmount;
use settings::Settings;
@@ -26,6 +27,8 @@ use workspace::{ItemId, WorkspaceId};
pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
+pub struct WasScrolled(pub(crate) bool);
+
#[derive(Default)]
pub struct ScrollbarAutoHide(pub bool);
@@ -151,12 +154,16 @@ pub struct ScrollManager {
pub(crate) vertical_scroll_margin: f32,
anchor: ScrollAnchor,
ongoing: OngoingScroll,
+ /// The second element indicates whether the autoscroll request is local
+ /// (true) or remote (false). Local requests are initiated by user actions,
+ /// while remote requests come from external sources.
autoscroll_request: Option<(Autoscroll, bool)>,
last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
show_scrollbars: bool,
hide_scrollbar_task: Option<Task<()>>,
active_scrollbar: Option<ActiveScrollbarState>,
visible_line_count: Option<f32>,
+ visible_column_count: Option<f32>,
forbid_vertical_scroll: bool,
minimap_thumb_state: Option<ScrollbarThumbState>,
}
@@ -173,6 +180,7 @@ impl ScrollManager {
active_scrollbar: None,
last_autoscroll: None,
visible_line_count: None,
+ visible_column_count: None,
forbid_vertical_scroll: false,
minimap_thumb_state: None,
}
@@ -209,66 +217,56 @@ impl ScrollManager {
workspace_id: Option<WorkspaceId>,
window: &mut Window,
cx: &mut Context<Editor>,
- ) {
- let (new_anchor, top_row) = if scroll_position.y <= 0. {
- (
- ScrollAnchor {
- anchor: Anchor::min(),
- offset: scroll_position.max(&gpui::Point::default()),
- },
- 0,
- )
- } else {
- let scroll_top = scroll_position.y;
- let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line {
- ScrollBeyondLastLine::OnePage => scroll_top,
- ScrollBeyondLastLine::Off => {
- if let Some(height_in_lines) = self.visible_line_count {
- let max_row = map.max_point().row().0 as f32;
- scroll_top.min(max_row - height_in_lines + 1.).max(0.)
- } else {
- scroll_top
- }
+ ) -> WasScrolled {
+ let scroll_top = scroll_position.y.max(0.);
+ let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line {
+ ScrollBeyondLastLine::OnePage => scroll_top,
+ ScrollBeyondLastLine::Off => {
+ if let Some(height_in_lines) = self.visible_line_count {
+ let max_row = map.max_point().row().0 as f32;
+ scroll_top.min(max_row - height_in_lines + 1.).max(0.)
+ } else {
+ scroll_top
}
- ScrollBeyondLastLine::VerticalScrollMargin => {
- if let Some(height_in_lines) = self.visible_line_count {
- let max_row = map.max_point().row().0 as f32;
- scroll_top
- .min(max_row - height_in_lines + 1. + self.vertical_scroll_margin)
- .max(0.)
- } else {
- scroll_top
- }
+ }
+ ScrollBeyondLastLine::VerticalScrollMargin => {
+ if let Some(height_in_lines) = self.visible_line_count {
+ let max_row = map.max_point().row().0 as f32;
+ scroll_top
+ .min(max_row - height_in_lines + 1. + self.vertical_scroll_margin)
+ .max(0.)
+ } else {
+ scroll_top
}
- };
+ }
+ };
- let scroll_top_buffer_point =
- DisplayPoint::new(DisplayRow(scroll_top as u32), 0).to_point(map);
- let top_anchor = map
- .buffer_snapshot
- .anchor_at(scroll_top_buffer_point, Bias::Right);
-
- (
- ScrollAnchor {
- anchor: top_anchor,
- offset: point(
- scroll_position.x.max(0.),
- scroll_top - top_anchor.to_display_point(map).row().as_f32(),
- ),
- },
- scroll_top_buffer_point.row,
+ let scroll_top_row = DisplayRow(scroll_top as u32);
+ let scroll_top_buffer_point = map
+ .clip_point(
+ DisplayPoint::new(scroll_top_row, scroll_position.x as u32),
+ Bias::Left,
)
- };
+ .to_point(map);
+ let top_anchor = map
+ .buffer_snapshot
+ .anchor_at(scroll_top_buffer_point, Bias::Right);
self.set_anchor(
- new_anchor,
- top_row,
+ ScrollAnchor {
+ anchor: top_anchor,
+ offset: point(
+ scroll_position.x.max(0.),
+ scroll_top - top_anchor.to_display_point(map).row().as_f32(),
+ ),
+ },
+ scroll_top_buffer_point.row,
local,
autoscroll,
workspace_id,
window,
cx,
- );
+ )
}
fn set_anchor(
@@ -280,7 +278,7 @@ impl ScrollManager {
workspace_id: Option<WorkspaceId>,
window: &mut Window,
cx: &mut Context<Editor>,
- ) {
+ ) -> WasScrolled {
let adjusted_anchor = if self.forbid_vertical_scroll {
ScrollAnchor {
offset: gpui::Point::new(anchor.offset.x, self.anchor.offset.y),
@@ -290,10 +288,14 @@ impl ScrollManager {
anchor
};
+ self.autoscroll_request.take();
+ if self.anchor == adjusted_anchor {
+ return WasScrolled(false);
+ }
+
self.anchor = adjusted_anchor;
cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
self.show_scrollbars(window, cx);
- self.autoscroll_request.take();
if let Some(workspace_id) = workspace_id {
let item_id = cx.entity().entity_id().as_u64() as ItemId;
@@ -315,6 +317,8 @@ impl ScrollManager {
.detach()
}
cx.notify();
+
+ WasScrolled(true)
}
pub fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
@@ -476,6 +480,10 @@ impl Editor {
.map(|line_count| line_count as u32 - 1)
}
+ pub fn visible_column_count(&self) -> Option<f32> {
+ self.scroll_manager.visible_column_count
+ }
+
pub(crate) fn set_visible_line_count(
&mut self,
lines: f32,
@@ -497,6 +505,10 @@ impl Editor {
}
}
+ pub(crate) fn set_visible_column_count(&mut self, columns: f32) {
+ self.scroll_manager.visible_column_count = Some(columns);
+ }
+
pub fn apply_scroll_delta(
&mut self,
scroll_delta: gpui::Point<f32>,
@@ -517,13 +529,13 @@ impl Editor {
scroll_position: gpui::Point<f32>,
window: &mut Window,
cx: &mut Context<Self>,
- ) {
+ ) -> WasScrolled {
let mut position = scroll_position;
if self.scroll_manager.forbid_vertical_scroll {
let current_position = self.scroll_position(cx);
position.y = current_position.y;
}
- self.set_scroll_position_internal(position, true, false, window, cx);
+ self.set_scroll_position_internal(position, true, false, window, cx)
}
/// Scrolls so that `row` is at the top of the editor view.
@@ -555,7 +567,7 @@ impl Editor {
autoscroll: bool,
window: &mut Window,
cx: &mut Context<Self>,
- ) {
+ ) -> WasScrolled {
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
self.set_scroll_position_taking_display_map(
scroll_position,
@@ -564,7 +576,7 @@ impl Editor {
map,
window,
cx,
- );
+ )
}
fn set_scroll_position_taking_display_map(
@@ -575,7 +587,7 @@ impl Editor {
display_map: DisplaySnapshot,
window: &mut Window,
cx: &mut Context<Self>,
- ) {
+ ) -> WasScrolled {
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
@@ -589,7 +601,7 @@ impl Editor {
scroll_position
};
- self.scroll_manager.set_scroll_position(
+ let editor_was_scrolled = self.scroll_manager.set_scroll_position(
adjusted_position,
&display_map,
local,
@@ -601,6 +613,7 @@ impl Editor {
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
self.refresh_colors(false, None, window, cx);
+ editor_was_scrolled
}
pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<f32> {
@@ -675,25 +688,48 @@ impl Editor {
let Some(visible_line_count) = self.visible_line_count() else {
return;
};
+ let Some(mut visible_column_count) = self.visible_column_count() else {
+ return;
+ };
+
+ // If the user has a preferred line length, and has the editor
+ // configured to wrap at the preferred line length, or bounded to it,
+ // use that value over the visible column count. This was mostly done so
+ // that tests could actually be written for vim's `z l`, `z h`, `z
+ // shift-l` and `z shift-h` commands, as there wasn't a good way to
+ // configure the editor to only display a certain number of columns. If
+ // that ever happens, this could probably be removed.
+ let settings = AllLanguageSettings::get_global(cx);
+ if matches!(
+ settings.defaults.soft_wrap,
+ SoftWrap::PreferredLineLength | SoftWrap::Bounded
+ ) {
+ if (settings.defaults.preferred_line_length as f32) < visible_column_count {
+ visible_column_count = settings.defaults.preferred_line_length as f32;
+ }
+ }
// If the scroll position is currently at the left edge of the document
// (x == 0.0) and the intent is to scroll right, the gutter's margin
// should first be added to the current position, otherwise the cursor
// will end at the column position minus the margin, which looks off.
- if current_position.x == 0.0 && amount.columns() > 0. {
+ if current_position.x == 0.0 && amount.columns(visible_column_count) > 0. {
if let Some(last_position_map) = &self.last_position_map {
current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance;
}
}
- let new_position =
- current_position + point(amount.columns(), amount.lines(visible_line_count));
+ let new_position = current_position
+ + point(
+ amount.columns(visible_column_count),
+ amount.lines(visible_line_count),
+ );
self.set_scroll_position(new_position, window, cx);
}
/// Returns an ordering. The newest selection is:
/// Ordering::Equal => on screen
- /// Ordering::Less => above the screen
- /// Ordering::Greater => below the screen
+ /// Ordering::Less => above or to the left of the screen
+ /// Ordering::Greater => below or to the right of the screen
pub fn newest_selection_on_screen(&self, cx: &mut App) -> Ordering {
let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let newest_head = self
@@ -711,8 +747,12 @@ impl Editor {
return Ordering::Less;
}
- if let Some(visible_lines) = self.visible_line_count() {
- if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) {
+ if let (Some(visible_lines), Some(visible_columns)) =
+ (self.visible_line_count(), self.visible_column_count())
+ {
+ if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32)
+ && newest_head.column() <= screen_top.column() + visible_columns as u32
+ {
return Ordering::Equal;
}
}
@@ -1,6 +1,6 @@
use crate::{
DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, SelectionEffects,
- display_map::ToDisplayPoint,
+ display_map::ToDisplayPoint, scroll::WasScrolled,
};
use gpui::{Bounds, Context, Pixels, Window, px};
use language::Point;
@@ -99,19 +99,21 @@ impl AutoscrollStrategy {
}
}
+pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool);
+
impl Editor {
pub fn autoscroll_request(&self) -> Option<Autoscroll> {
self.scroll_manager.autoscroll_request()
}
- pub fn autoscroll_vertically(
+ pub(crate) fn autoscroll_vertically(
&mut self,
bounds: Bounds<Pixels>,
line_height: Pixels,
max_scroll_top: f32,
window: &mut Window,
cx: &mut Context<Editor>,
- ) -> bool {
+ ) -> (NeedsHorizontalAutoscroll, WasScrolled) {
let viewport_height = bounds.size.height;
let visible_lines = viewport_height / line_height;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -129,12 +131,14 @@ impl Editor {
scroll_position.y = max_scroll_top;
}
- if original_y != scroll_position.y {
- self.set_scroll_position(scroll_position, window, cx);
- }
+ let editor_was_scrolled = if original_y != scroll_position.y {
+ self.set_scroll_position(scroll_position, window, cx)
+ } else {
+ WasScrolled(false)
+ };
let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else {
- return false;
+ return (NeedsHorizontalAutoscroll(false), editor_was_scrolled);
};
let mut target_top;
@@ -212,7 +216,7 @@ impl Editor {
target_bottom = target_top + 1.;
}
- match strategy {
+ let was_autoscrolled = match strategy {
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
let target_top = (target_top - margin).max(0.0);
@@ -225,39 +229,42 @@ impl Editor {
if needs_scroll_up && !needs_scroll_down {
scroll_position.y = target_top;
- self.set_scroll_position_internal(scroll_position, local, true, window, cx);
- }
- if !needs_scroll_up && needs_scroll_down {
+ } else if !needs_scroll_up && needs_scroll_down {
scroll_position.y = target_bottom - visible_lines;
- self.set_scroll_position_internal(scroll_position, local, true, window, cx);
+ }
+
+ if needs_scroll_up ^ needs_scroll_down {
+ self.set_scroll_position_internal(scroll_position, local, true, window, cx)
+ } else {
+ WasScrolled(false)
}
}
AutoscrollStrategy::Center => {
scroll_position.y = (target_top - margin).max(0.0);
- self.set_scroll_position_internal(scroll_position, local, true, window, cx);
+ self.set_scroll_position_internal(scroll_position, local, true, window, cx)
}
AutoscrollStrategy::Focused => {
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
scroll_position.y = (target_top - margin).max(0.0);
- self.set_scroll_position_internal(scroll_position, local, true, window, cx);
+ self.set_scroll_position_internal(scroll_position, local, true, window, cx)
}
AutoscrollStrategy::Top => {
scroll_position.y = (target_top).max(0.0);
- self.set_scroll_position_internal(scroll_position, local, true, window, cx);
+ self.set_scroll_position_internal(scroll_position, local, true, window, cx)
}
AutoscrollStrategy::Bottom => {
scroll_position.y = (target_bottom - visible_lines).max(0.0);
- self.set_scroll_position_internal(scroll_position, local, true, window, cx);
+ self.set_scroll_position_internal(scroll_position, local, true, window, cx)
}
AutoscrollStrategy::TopRelative(lines) => {
scroll_position.y = target_top - lines as f32;
- self.set_scroll_position_internal(scroll_position, local, true, window, cx);
+ self.set_scroll_position_internal(scroll_position, local, true, window, cx)
}
AutoscrollStrategy::BottomRelative(lines) => {
scroll_position.y = target_bottom + lines as f32;
- self.set_scroll_position_internal(scroll_position, local, true, window, cx);
+ self.set_scroll_position_internal(scroll_position, local, true, window, cx)
}
- }
+ };
self.scroll_manager.last_autoscroll = Some((
self.scroll_manager.anchor.offset,
@@ -266,7 +273,8 @@ impl Editor {
strategy,
));
- true
+ let was_scrolled = WasScrolled(editor_was_scrolled.0 || was_autoscrolled.0);
+ (NeedsHorizontalAutoscroll(true), was_scrolled)
}
pub(crate) fn autoscroll_horizontally(
@@ -274,12 +282,14 @@ impl Editor {
start_row: DisplayRow,
viewport_width: Pixels,
scroll_width: Pixels,
- max_glyph_width: Pixels,
+ em_advance: Pixels,
layouts: &[LineWithInvisibles],
+ window: &mut Window,
cx: &mut Context<Self>,
- ) -> bool {
+ ) -> Option<gpui::Point<f32>> {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all::<Point>(cx);
+ let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
let mut target_left;
let mut target_right;
@@ -295,16 +305,17 @@ impl Editor {
if head.row() >= start_row
&& head.row() < DisplayRow(start_row.0 + layouts.len() as u32)
{
- let start_column = head.column().saturating_sub(3);
- let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
+ let start_column = head.column();
+ let end_column = cmp::min(display_map.line_len(head.row()), head.column());
target_left = target_left.min(
layouts[head.row().minus(start_row) as usize]
- .x_for_index(start_column as usize),
+ .x_for_index(start_column as usize)
+ + self.gutter_dimensions.margin,
);
target_right = target_right.max(
layouts[head.row().minus(start_row) as usize]
.x_for_index(end_column as usize)
- + max_glyph_width,
+ + em_advance,
);
}
}
@@ -316,20 +327,26 @@ impl Editor {
target_right = target_right.min(scroll_width);
if target_right - target_left > viewport_width {
- return false;
+ return None;
}
- let scroll_left = self.scroll_manager.anchor.offset.x * max_glyph_width;
+ let scroll_left = self.scroll_manager.anchor.offset.x * em_advance;
let scroll_right = scroll_left + viewport_width;
- if target_left < scroll_left {
- self.scroll_manager.anchor.offset.x = target_left / max_glyph_width;
- true
+ let was_scrolled = if target_left < scroll_left {
+ scroll_position.x = target_left / em_advance;
+ self.set_scroll_position_internal(scroll_position, true, true, window, cx)
} else if target_right > scroll_right {
- self.scroll_manager.anchor.offset.x = (target_right - viewport_width) / max_glyph_width;
- true
+ scroll_position.x = (target_right - viewport_width) / em_advance;
+ self.set_scroll_position_internal(scroll_position, true, true, window, cx)
+ } else {
+ WasScrolled(false)
+ };
+
+ if was_scrolled.0 {
+ Some(scroll_position)
} else {
- false
+ None
}
}
@@ -23,6 +23,8 @@ pub enum ScrollAmount {
Page(f32),
// Scroll N columns (positive is towards the right of the document)
Column(f32),
+ // Scroll N page width (positive is towards the right of the document)
+ PageWidth(f32),
}
impl ScrollAmount {
@@ -37,14 +39,16 @@ impl ScrollAmount {
(visible_line_count * count).trunc()
}
Self::Column(_count) => 0.0,
+ Self::PageWidth(_count) => 0.0,
}
}
- pub fn columns(&self) -> f32 {
+ pub fn columns(&self, visible_column_count: f32) -> f32 {
match self {
Self::Line(_count) => 0.0,
Self::Page(_count) => 0.0,
Self::Column(count) => *count,
+ Self::PageWidth(count) => (visible_column_count * count).trunc(),
}
}
@@ -58,6 +62,7 @@ impl ScrollAmount {
// so I'm leaving this at 0.0 for now to try and make it clear that
// this should not have an impact on that?
ScrollAmount::Column(_) => px(0.0),
+ ScrollAmount::PageWidth(_) => px(0.0),
}
}
@@ -8,6 +8,7 @@ mod tool_metrics;
use assertions::{AssertionsReport, display_error_row};
use instance::{ExampleInstance, JudgeOutput, RunOutput, run_git};
+use language_extension::LspAccess;
pub(crate) use tool_metrics::*;
use ::fs::RealFs;
@@ -415,7 +416,11 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
language::init(cx);
debug_adapter_extension::init(extension_host_proxy.clone(), cx);
- language_extension::init(extension_host_proxy.clone(), languages.clone());
+ language_extension::init(
+ LspAccess::Noop,
+ extension_host_proxy.clone(),
+ languages.clone(),
+ );
language_model::init(client.clone(), cx);
language_models::init(user_store.clone(), client.clone(), cx);
languages::init(languages.clone(), node_runtime.clone(), cx);
@@ -14,7 +14,7 @@ impl Example for FileChangeNotificationExample {
url: "https://github.com/octocat/hello-world".to_string(),
revision: "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d".to_string(),
language_server: None,
- max_assertions: Some(1),
+ max_assertions: None,
profile_id: AgentProfileId::default(),
existing_thread_json: None,
max_turns: Some(3),
@@ -324,20 +324,8 @@
<body>
<h1 id="current-filename">Thread Explorer</h1>
<div class="view-switcher">
- <button
- id="full-view"
- class="view-button active"
- onclick="switchView('full')"
- >
- Full View
- </button>
- <button
- id="compact-view"
- class="view-button"
- onclick="switchView('compact')"
- >
- Compact View
- </button>
+ <button id="full-view" class="view-button active" onclick="switchView('full')">Full View</button>
+ <button id="compact-view" class="view-button" onclick="switchView('compact')">Compact View</button>
<button
id="export-button"
class="view-button"
@@ -347,11 +335,7 @@
Export
</button>
<div class="theme-switcher">
- <button
- id="theme-toggle"
- class="theme-button"
- onclick="toggleTheme()"
- >
+ <button id="theme-toggle" class="theme-button" onclick="toggleTheme()">
<span id="theme-icon" class="theme-icon">☀️</span>
<span id="theme-text">Light</span>
</button>
@@ -368,8 +352,7 @@
← Previous
</button>
<div class="thread-indicator">
- Thread <span id="current-thread-index">1</span> of
- <span id="total-threads">1</span>:
+ Thread <span id="current-thread-index">1</span> of <span id="total-threads">1</span>:
<span id="thread-id">Default Thread</span>
</div>
<button
@@ -423,9 +406,7 @@
function toggleTheme() {
// If currently system or light, switch to dark
if (themeMode === "system") {
- const systemDark = window.matchMedia(
- "(prefers-color-scheme: dark)",
- ).matches;
+ const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
themeMode = systemDark ? "light" : "dark";
} else {
themeMode = themeMode === "light" ? "dark" : "light";
@@ -442,19 +423,15 @@
function initTheme() {
if (themeMode === "system") {
// Use system preference
- const systemDark = window.matchMedia(
- "(prefers-color-scheme: dark)",
- ).matches;
+ const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
applyTheme(systemDark ? "dark" : "light");
// Listen for system theme changes
- window
- .matchMedia("(prefers-color-scheme: dark)")
- .addEventListener("change", (e) => {
- if (themeMode === "system") {
- applyTheme(e.matches ? "dark" : "light");
- }
- });
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
+ if (themeMode === "system") {
+ applyTheme(e.matches ? "dark" : "light");
+ }
+ });
} else {
// Use saved preference
applyTheme(themeMode);
@@ -466,49 +443,38 @@
viewMode = mode;
// Update button states
- document
- .getElementById("full-view")
- .classList.toggle("active", mode === "full");
- document
- .getElementById("compact-view")
- .classList.toggle("active", mode === "compact");
+ document.getElementById("full-view").classList.toggle("active", mode === "full");
+ document.getElementById("compact-view").classList.toggle("active", mode === "compact");
// Add or remove compact-mode class on the body
- document.body.classList.toggle(
- "compact-mode",
- mode === "compact",
- );
+ document.body.classList.toggle("compact-mode", mode === "compact");
// Re-render the thread with the new view mode
renderThread();
}
-
+
// Function to export the current thread as a JSON file
function exportThreadAsJson() {
// Clone the thread to avoid modifying the original
const threadToExport = JSON.parse(JSON.stringify(thread));
-
+
// Create a Blob with the JSON data
- const blob = new Blob(
- [JSON.stringify(threadToExport, null, 2)],
- { type: "application/json" }
- );
-
+ const blob = new Blob([JSON.stringify(threadToExport, null, 2)], { type: "application/json" });
+
// Create a download link
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
-
+
// Generate filename based on thread ID or index
- const filename = threadToExport.thread_id ||
- threadToExport.filename ||
- `thread-${currentThreadIndex + 1}.json`;
+ const filename =
+ threadToExport.thread_id || threadToExport.filename || `thread-${currentThreadIndex + 1}.json`;
a.download = filename.endsWith(".json") ? filename : `${filename}.json`;
-
+
// Trigger the download
document.body.appendChild(a);
a.click();
-
+
// Clean up
setTimeout(() => {
document.body.removeChild(a);
@@ -524,9 +490,7 @@
},
{
role: "user",
- content: [
- { Text: "Fix the bug: kwargs not passed..." },
- ],
+ content: [{ Text: "Fix the bug: kwargs not passed..." }],
},
{
role: "assistant",
@@ -593,12 +557,9 @@
name: "edit_file",
input: {
path: "fastmcp/core.py",
- old_string:
- "def start_server(app):\n anyio.run(app)",
- new_string:
- "def start_server(app, **kwargs):\n anyio.run(app, **kwargs)",
- display_description:
- "Fix kwargs passing to anyio.run",
+ old_string: "def start_server(app):\n anyio.run(app)",
+ new_string: "def start_server(app, **kwargs):\n anyio.run(app, **kwargs)",
+ display_description: "Fix kwargs passing to anyio.run",
},
is_input_complete: true,
},
@@ -681,14 +642,10 @@
// Function to update the navigation buttons state
function updateNavigationButtons() {
- document.getElementById("prev-thread").disabled =
- currentThreadIndex <= 0;
- document.getElementById("next-thread").disabled =
- currentThreadIndex >= threads.length - 1;
- document.getElementById("current-thread-index").textContent =
- currentThreadIndex + 1;
- document.getElementById("total-threads").textContent =
- threads.length;
+ document.getElementById("prev-thread").disabled = currentThreadIndex <= 0;
+ document.getElementById("next-thread").disabled = currentThreadIndex >= threads.length - 1;
+ document.getElementById("current-thread-index").textContent = currentThreadIndex + 1;
+ document.getElementById("total-threads").textContent = threads.length;
}
function renderThread() {
@@ -696,20 +653,15 @@
tbody.innerHTML = ""; // Clear existing content
// Set thread name if available
- const threadId =
- thread.thread_id || `Thread ${currentThreadIndex + 1}`;
+ const threadId = thread.thread_id || `Thread ${currentThreadIndex + 1}`;
document.getElementById("thread-id").textContent = threadId;
// Set filename in the header if available
- const filename =
- thread.filename || `Thread ${currentThreadIndex + 1}`;
- document.getElementById("current-filename").textContent =
- filename;
+ const filename = thread.filename || `Thread ${currentThreadIndex + 1}`;
+ document.getElementById("current-filename").textContent = filename;
// Skip system message
- const nonSystemMessages = thread.messages.filter(
- (msg) => msg.role !== "system",
- );
+ const nonSystemMessages = thread.messages.filter((msg) => msg.role !== "system");
let turnNumber = 0;
processMessages(nonSystemMessages, tbody, turnNumber);
@@ -737,9 +689,7 @@
for (const content of msg.content) {
if (content.hasOwnProperty("Text")) {
if (assistantText) {
- assistantText +=
- "<br><br>" +
- formatContent(content.Text);
+ assistantText += "<br><br>" + formatContent(content.Text);
} else {
assistantText = formatContent(content.Text);
}
@@ -763,49 +713,33 @@
tbody.appendChild(row);
// Add all tool calls to the tools cell
- const toolsCell = document.getElementById(
- `tools-${turnNumber}`,
- );
- const resultsCell = document.getElementById(
- `results-${turnNumber}`,
- );
+ const toolsCell = document.getElementById(`tools-${turnNumber}`);
+ const resultsCell = document.getElementById(`results-${turnNumber}`);
// Process all tools and their results
for (let j = 0; j < toolUses.length; j++) {
const toolUse = toolUses[j];
- const toolCall = formatToolCall(
- toolUse.name,
- toolUse.input,
- );
+ const toolCall = formatToolCall(toolUse.name, toolUse.input);
// Add the tool call to the tools cell
if (j > 0) toolsCell.innerHTML += "<hr>";
toolsCell.innerHTML += toolCall;
// Look for corresponding tool result
- if (
- hasMatchingToolResult(messages, i, toolUse.name)
- ) {
+ if (hasMatchingToolResult(messages, i, toolUse.name)) {
const resultMsg = messages[i + 1];
- const toolResult = findToolResult(
- resultMsg,
- toolUse.name,
- );
+ const toolResult = findToolResult(resultMsg, toolUse.name);
if (toolResult) {
// Add the result to the results cell
if (j > 0) resultsCell.innerHTML += "<hr>";
// Create a container for the result
- const resultDiv =
- document.createElement("div");
+ const resultDiv = document.createElement("div");
resultDiv.className = "tool-result";
// Format and display the tool result
- formatToolResultInline(
- toolResult.content,
- resultDiv,
- );
+ formatToolResultInline(toolResult.content.Text, resultDiv);
resultsCell.appendChild(resultDiv);
// Skip the result message in the next iteration
@@ -815,10 +749,7 @@
}
}
}
- } else if (
- msg.role === "user" &&
- msg.content.some((c) => c.hasOwnProperty("ToolResult"))
- ) {
+ } else if (msg.role === "user" && msg.content.some((c) => c.hasOwnProperty("ToolResult"))) {
// Skip tool result messages as they are handled with their corresponding tool use
continue;
}
@@ -826,10 +757,7 @@
}
function isUserQuery(message) {
- return (
- message.role === "user" &&
- !message.content.some((c) => c.hasOwnProperty("ToolResult"))
- );
+ return message.role === "user" && !message.content.some((c) => c.hasOwnProperty("ToolResult"));
}
function renderUserMessage(message, turnNumber, tbody) {
@@ -848,18 +776,14 @@
currentIndex + 1 < messages.length &&
messages[currentIndex + 1].role === "user" &&
messages[currentIndex + 1].content.some(
- (c) =>
- c.hasOwnProperty("ToolResult") &&
- c.ToolResult.tool_name === toolName,
+ (c) => c.hasOwnProperty("ToolResult") && c.ToolResult.tool_name === toolName,
)
);
}
function findToolResult(resultMessage, toolName) {
const toolResultContent = resultMessage.content.find(
- (c) =>
- c.hasOwnProperty("ToolResult") &&
- c.ToolResult.tool_name === toolName,
+ (c) => c.hasOwnProperty("ToolResult") && c.ToolResult.tool_name === toolName,
);
return toolResultContent ? toolResultContent.ToolResult : null;
@@ -874,18 +798,12 @@
for (const [key, value] of Object.entries(input)) {
if (value !== null && value !== undefined) {
// Store full parameter for expanded view
- let fullValue =
- typeof value === "string"
- ? `"${value}"`
- : value;
+ let fullValue = typeof value === "string" ? `"${value}"` : value;
fullParams.push([key, fullValue]);
// Abbreviated value for compact view
let displayValue = fullValue;
- if (
- typeof value === "string" &&
- value.length > 30
- ) {
+ if (typeof value === "string" && value.length > 30) {
displayValue = `"${value.substring(0, 30)}..."`;
}
params.push(`${key}=${displayValue}`);
@@ -903,10 +821,7 @@
// For the full view, use the original untruncated values
let result = `<span class="tool-name">${name}</span>(`;
const formattedParams = fullParams
- .map(
- (p) =>
- ` ${p[0]}=${p[1]}`,
- )
+ .map((p) => ` ${p[0]}=${p[1]}`)
.join(",<br/>");
const fullView = `${result}<br/>${formattedParams}<br/>)`;
@@ -925,8 +840,7 @@
for (const [key, value] of Object.entries(input)) {
if (value !== null && value !== undefined) {
// Format different types of values
- let formattedValue =
- typeof value === "string" ? `"${value}"` : value;
+ let formattedValue = typeof value === "string" ? `"${value}"` : value;
params.push([key, formattedValue]);
}
}
@@ -938,9 +852,7 @@
return `${result}${params[0][1]})`;
} else {
// Format parameters
- const formattedParams = params
- .map((p) => ` ${p[0]}=${p[1]}`)
- .join(",<br/>");
+ const formattedParams = params.map((p) => ` ${p[0]}=${p[1]}`).join(",<br/>");
return `${result}<br/>${formattedParams}<br/>)`;
}
}
@@ -1013,21 +925,13 @@
// Keyboard navigation handler
document.addEventListener("keydown", function (event) {
// previous thread
- if (
- (event.ctrlKey && event.key === "ArrowLeft") ||
- event.key === "h" ||
- event.key === "k"
- ) {
+ if ((event.ctrlKey && event.key === "ArrowLeft") || event.key === "h" || event.key === "k") {
if (!document.getElementById("prev-thread").disabled) {
previousThread();
}
}
// next thread
- else if (
- (event.ctrlKey && event.key === "ArrowRight") ||
- event.key === "j" ||
- event.key === "l"
- ) {
+ else if ((event.ctrlKey && event.key === "ArrowRight") || event.key === "j" || event.key === "l") {
if (!document.getElementById("next-thread").disabled) {
nextThread();
}
@@ -594,6 +594,7 @@ impl ExampleInstance {
tools: Vec::new(),
tool_choice: None,
stop: Vec::new(),
+ thinking_allowed: true,
};
let model = model.clone();
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Package
+ xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
+ xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
+ xmlns:uap2="http://schemas.microsoft.com/appx/manifest/uap/windows10/2"
+ xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
+ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
+ xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
+ xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
+ xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5"
+ xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
+ xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
+ xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
+ IgnorableNamespaces="uap uap2 uap3 rescap desktop desktop4 desktop5 desktop6 uap10 com">
+ <!-- TODO: Use Zed's signature here. -->
+ <Identity
+ Name="ZedIndustries.Zed.Nightly"
+ Publisher="CN=Zed Industries Inc, O=Zed Industries Inc, L=Denver, S=Colorado, C=US"
+ Version="1.0.0.0" />
+ <Properties>
+ <DisplayName>Zed Nightly</DisplayName>
+ <PublisherDisplayName>Zed Industries</PublisherDisplayName>
+ <!-- TODO: Use actual icon here. -->
+ <Logo>resources\logo_150x150.png</Logo>
+ <uap10:AllowExternalContent>true</uap10:AllowExternalContent>
+ <desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization>
+ <desktop6:FileSystemWriteVirtualization>disabled</desktop6:FileSystemWriteVirtualization>
+ </Properties>
+ <Resources>
+ <Resource Language="en-us" />
+ <Resource Language="zh-cn" />
+ </Resources>
+ <Dependencies>
+ <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19000.0" MaxVersionTested="10.0.22000.0" />
+ </Dependencies>
+ <Capabilities>
+ <rescap:Capability Name="runFullTrust" />
+ <rescap:Capability Name="unvirtualizedResources"/>
+ </Capabilities>
+ <Applications>
+ <Application Id="ZedNightly"
+ Executable="Zed.exe"
+ uap10:TrustLevel="mediumIL"
+ uap10:RuntimeBehavior="win32App">
+ <!-- TODO: Use actual icon here. -->
+ <uap:VisualElements
+ AppListEntry="none"
+ DisplayName="Zed Nightly"
+ Description="Zed Nightly explorer command injector"
+ BackgroundColor="transparent"
+ Square150x150Logo="resources\logo_150x150.png"
+ Square44x44Logo="resources\logo_70x70.png">
+ </uap:VisualElements>
+ <Extensions>
+ <desktop4:Extension Category="windows.fileExplorerContextMenus">
+ <desktop4:FileExplorerContextMenus>
+ <desktop5:ItemType Type="Directory">
+ <desktop5:Verb Id="OpenWithZedNightly" Clsid="266f2cfe-1653-42af-b55c-fe3590c83871" />
+ </desktop5:ItemType>
+ <desktop5:ItemType Type="Directory\Background">
+ <desktop5:Verb Id="OpenWithZedNightly" Clsid="266f2cfe-1653-42af-b55c-fe3590c83871" />
+ </desktop5:ItemType>
+ <desktop5:ItemType Type="*">
+ <desktop5:Verb Id="OpenWithZedNightly" Clsid="266f2cfe-1653-42af-b55c-fe3590c83871" />
+ </desktop5:ItemType>
+ </desktop4:FileExplorerContextMenus>
+ </desktop4:Extension>
+ <com:Extension Category="windows.comServer">
+ <com:ComServer>
+ <com:SurrogateServer DisplayName="Zed Nightly">
+ <com:Class Id="266f2cfe-1653-42af-b55c-fe3590c83871" Path="zed_explorer_command_injector.dll" ThreadingModel="STA"/>
+ </com:SurrogateServer>
+ </com:ComServer>
+ </com:Extension>
+ </Extensions>
+ </Application>
+ </Applications>
+</Package>
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Package
+ xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
+ xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
+ xmlns:uap2="http://schemas.microsoft.com/appx/manifest/uap/windows10/2"
+ xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
+ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
+ xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
+ xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
+ xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5"
+ xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
+ xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
+ xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
+ IgnorableNamespaces="uap uap2 uap3 rescap desktop desktop4 desktop5 desktop6 uap10 com">
+ <!-- TODO: Use Zed's signature here. -->
+ <Identity
+ Name="ZedIndustries.Zed.Preview"
+ Publisher="CN=Zed Industries Inc, O=Zed Industries Inc, L=Denver, S=Colorado, C=US"
+ Version="1.0.0.0" />
+ <Properties>
+ <DisplayName>Zed Preview</DisplayName>
+ <PublisherDisplayName>Zed Industries</PublisherDisplayName>
+ <!-- TODO: Use actual icon here. -->
+ <Logo>resources\logo_150x150.png</Logo>
+ <uap10:AllowExternalContent>true</uap10:AllowExternalContent>
+ <desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization>
+ <desktop6:FileSystemWriteVirtualization>disabled</desktop6:FileSystemWriteVirtualization>
+ </Properties>
+ <Resources>
+ <Resource Language="en-us" />
+ <Resource Language="zh-cn" />
+ </Resources>
+ <Dependencies>
+ <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19000.0" MaxVersionTested="10.0.22000.0" />
+ </Dependencies>
+ <Capabilities>
+ <rescap:Capability Name="runFullTrust" />
+ <rescap:Capability Name="unvirtualizedResources"/>
+ </Capabilities>
+ <Applications>
+ <Application Id="ZedPreview"
+ Executable="Zed.exe"
+ uap10:TrustLevel="mediumIL"
+ uap10:RuntimeBehavior="win32App">
+ <!-- TODO: Use actual icon here. -->
+ <uap:VisualElements
+ AppListEntry="none"
+ DisplayName="Zed Preview"
+ Description="Zed Preview explorer command injector"
+ BackgroundColor="transparent"
+ Square150x150Logo="resources\logo_150x150.png"
+ Square44x44Logo="resources\logo_70x70.png">
+ </uap:VisualElements>
+ <Extensions>
+ <desktop4:Extension Category="windows.fileExplorerContextMenus">
+ <desktop4:FileExplorerContextMenus>
+ <desktop5:ItemType Type="Directory">
+ <desktop5:Verb Id="OpenWithZedPreview" Clsid="af8e85ea-fb20-4db2-93cf-56513c1ec697" />
+ </desktop5:ItemType>
+ <desktop5:ItemType Type="Directory\Background">
+ <desktop5:Verb Id="OpenWithZedPreview" Clsid="af8e85ea-fb20-4db2-93cf-56513c1ec697" />
+ </desktop5:ItemType>
+ <desktop5:ItemType Type="*">
+ <desktop5:Verb Id="OpenWithZedPreview" Clsid="af8e85ea-fb20-4db2-93cf-56513c1ec697" />
+ </desktop5:ItemType>
+ </desktop4:FileExplorerContextMenus>
+ </desktop4:Extension>
+ <com:Extension Category="windows.comServer">
+ <com:ComServer>
+ <com:SurrogateServer DisplayName="Zed Preview">
+ <com:Class Id="af8e85ea-fb20-4db2-93cf-56513c1ec697" Path="zed_explorer_command_injector.dll" ThreadingModel="STA"/>
+ </com:SurrogateServer>
+ </com:ComServer>
+ </com:Extension>
+ </Extensions>
+ </Application>
+ </Applications>
+</Package>
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Package
+ xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
+ xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
+ xmlns:uap2="http://schemas.microsoft.com/appx/manifest/uap/windows10/2"
+ xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
+ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
+ xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
+ xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
+ xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5"
+ xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
+ xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
+ xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
+ IgnorableNamespaces="uap uap2 uap3 rescap desktop desktop4 desktop5 desktop6 uap10 com">
+ <!-- TODO: Use Zed's signature here. -->
+ <Identity
+ Name="ZedIndustries.Zed"
+ Publisher="CN=Zed Industries Inc, O=Zed Industries Inc, L=Denver, S=Colorado, C=US"
+ Version="1.0.0.0" />
+ <Properties>
+ <DisplayName>Zed</DisplayName>
+
+ <PublisherDisplayName>Zed Industries</PublisherDisplayName>
+ <!-- TODO: Use actual icon here. -->
+ <Logo>resources\logo_150x150.png</Logo>
+ <uap10:AllowExternalContent>true</uap10:AllowExternalContent>
+ <desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization>
+ <desktop6:FileSystemWriteVirtualization>disabled</desktop6:FileSystemWriteVirtualization>
+ </Properties>
+ <Resources>
+ <Resource Language="en-us" />
+ <Resource Language="zh-cn" />
+ </Resources>
+ <Dependencies>
+ <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19000.0" MaxVersionTested="10.0.22000.0" />
+ </Dependencies>
+ <Capabilities>
+ <rescap:Capability Name="runFullTrust" />
+ <rescap:Capability Name="unvirtualizedResources"/>
+ </Capabilities>
+ <Applications>
+ <Application Id="Zed"
+ Executable="Zed.exe"
+ uap10:TrustLevel="mediumIL"
+ uap10:RuntimeBehavior="win32App">
+ <!-- TODO: Use actual icon here. -->
+ <uap:VisualElements
+ AppListEntry="none"
+ DisplayName="Zed"
+ Description="Zed explorer command injector"
+ BackgroundColor="transparent"
+ Square150x150Logo="resources\logo_150x150.png"
+ Square44x44Logo="resources\logo_70x70.png">
+ </uap:VisualElements>
+ <Extensions>
+ <desktop4:Extension Category="windows.fileExplorerContextMenus">
+ <desktop4:FileExplorerContextMenus>
+ <desktop5:ItemType Type="Directory">
+ <desktop5:Verb Id="OpenWithZed" Clsid="6a1f6b13-3b82-48a1-9e06-7bb0a6d0bffd" />
+ </desktop5:ItemType>
+ <desktop5:ItemType Type="Directory\Background">
+ <desktop5:Verb Id="OpenWithZed" Clsid="6a1f6b13-3b82-48a1-9e06-7bb0a6d0bffd" />
+ </desktop5:ItemType>
+ <desktop5:ItemType Type="*">
+ <desktop5:Verb Id="OpenWithZed" Clsid="6a1f6b13-3b82-48a1-9e06-7bb0a6d0bffd" />
+ </desktop5:ItemType>
+ </desktop4:FileExplorerContextMenus>
+ </desktop4:Extension>
+ <com:Extension Category="windows.comServer">
+ <com:ComServer>
+ <com:SurrogateServer DisplayName="Zed">
+ <com:Class Id="6a1f6b13-3b82-48a1-9e06-7bb0a6d0bffd" Path="zed_explorer_command_injector.dll" ThreadingModel="STA"/>
+ </com:SurrogateServer>
+ </com:ComServer>
+ </com:Extension>
+ </Extensions>
+ </Application>
+ </Applications>
+</Package>
@@ -0,0 +1,28 @@
+[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]
+workspace-hack.workspace = true
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -0,0 +1,201 @@
+#![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<IShellItemArray>) -> Result<windows_core::PWSTR> {
+ let command_description =
+ retrieve_command_description().unwrap_or(HSTRING::from("Open with Zed"));
+ unsafe { SHStrDupW(&command_description) }
+ }
+
+ fn GetIcon(&self, _: Ref<IShellItemArray>) -> Result<windows_core::PWSTR> {
+ let Some(zed_exe) = get_zed_exe_path() else {
+ return Err(E_FAIL.into());
+ };
+ unsafe { SHStrDupW(&HSTRING::from(zed_exe)) }
+ }
+
+ fn GetToolTip(&self, _: Ref<IShellItemArray>) -> Result<windows_core::PWSTR> {
+ Err(E_NOTIMPL.into())
+ }
+
+ fn GetCanonicalName(&self) -> Result<windows_core::GUID> {
+ Ok(GUID::zeroed())
+ }
+
+ fn GetState(&self, _: Ref<IShellItemArray>, _: BOOL) -> Result<u32> {
+ Ok(ECS_ENABLED.0 as _)
+ }
+
+ fn Invoke(&self, psiitemarray: Ref<IShellItemArray>, _: Ref<IBindCtx>) -> 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()? };
+ std::process::Command::new(&zed_exe)
+ .arg(&item_path)
+ .spawn()
+ .map_err(|_| E_INVALIDARG)?;
+ }
+
+ Ok(())
+ }
+
+ fn GetFlags(&self) -> Result<u32> {
+ Ok(ECF_DEFAULT.0 as _)
+ }
+
+ fn EnumSubCommands(&self) -> Result<IEnumExplorerCommand> {
+ Err(E_NOTIMPL.into())
+ }
+}
+
+#[implement(IClassFactory)]
+struct ExplorerCommandInjectorFactory;
+
+impl IClassFactory_Impl for ExplorerCommandInjectorFactory_Impl {
+ fn CreateInstance(
+ &self,
+ punkouter: Ref<windows_core::IUnknown>,
+ 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<PathBuf> {
+ 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<String> {
+ get_zed_install_folder().map(|path| path.join("Zed.exe").to_string_lossy().to_string())
+}
+
+#[inline]
+fn retrieve_command_description() -> Result<HSTRING> {
+ #[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")
+}
@@ -1,5 +1,6 @@
use crate::{
- ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, parse_wasm_extension_version,
+ ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, build_debug_adapter_schema_path,
+ parse_wasm_extension_version,
};
use anyhow::{Context as _, Result, bail};
use async_compression::futures::bufread::GzipDecoder;
@@ -99,12 +100,8 @@ impl ExtensionBuilder {
}
for (debug_adapter_name, meta) in &mut extension_manifest.debug_adapters {
- let debug_adapter_relative_schema_path =
- meta.schema_path.clone().unwrap_or_else(|| {
- Path::new("debug_adapter_schemas")
- .join(Path::new(debug_adapter_name.as_ref()).with_extension("json"))
- });
- let debug_adapter_schema_path = extension_dir.join(debug_adapter_relative_schema_path);
+ 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(|| {
@@ -286,7 +286,8 @@ pub trait ExtensionLanguageServerProxy: Send + Sync + 'static {
&self,
language: &LanguageName,
language_server_id: &LanguageServerName,
- );
+ cx: &mut App,
+ ) -> Task<Result<()>>;
fn update_language_server_status(
&self,
@@ -313,12 +314,13 @@ impl ExtensionLanguageServerProxy for ExtensionHostProxy {
&self,
language: &LanguageName,
language_server_id: &LanguageServerName,
- ) {
+ cx: &mut App,
+ ) -> Task<Result<()>> {
let Some(proxy) = self.language_server_proxy.read().clone() else {
- return;
+ return Task::ready(Ok(()));
};
- proxy.remove_language_server(language, language_server_id)
+ proxy.remove_language_server(language, language_server_id, cx)
}
fn update_language_server_status(
@@ -350,6 +352,8 @@ impl ExtensionSnippetProxy for ExtensionHostProxy {
pub trait ExtensionSlashCommandProxy: Send + Sync + 'static {
fn register_slash_command(&self, extension: Arc<dyn Extension>, command: SlashCommand);
+
+ fn unregister_slash_command(&self, command_name: Arc<str>);
}
impl ExtensionSlashCommandProxy for ExtensionHostProxy {
@@ -360,6 +364,14 @@ impl ExtensionSlashCommandProxy for ExtensionHostProxy {
proxy.register_slash_command(extension, command)
}
+
+ fn unregister_slash_command(&self, command_name: Arc<str>) {
+ let Some(proxy) = self.slash_command_proxy.read().clone() else {
+ return;
+ };
+
+ proxy.unregister_slash_command(command_name)
+ }
}
pub trait ExtensionContextServerProxy: Send + Sync + 'static {
@@ -398,6 +410,8 @@ impl ExtensionContextServerProxy for ExtensionHostProxy {
pub trait ExtensionIndexedDocsProviderProxy: Send + Sync + 'static {
fn register_indexed_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>);
+
+ fn unregister_indexed_docs_provider(&self, provider_id: Arc<str>);
}
impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy {
@@ -408,6 +422,14 @@ impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy {
proxy.register_indexed_docs_provider(extension, provider_id)
}
+
+ fn unregister_indexed_docs_provider(&self, provider_id: Arc<str>) {
+ let Some(proxy) = self.indexed_docs_provider_proxy.read().clone() else {
+ return;
+ };
+
+ proxy.unregister_indexed_docs_provider(provider_id)
+ }
}
pub trait ExtensionDebugAdapterProviderProxy: Send + Sync + 'static {
@@ -130,6 +130,22 @@ impl ExtensionManifest {
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<str>,
+ 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"))
+ })
}
/// A capability for an extension.
@@ -320,6 +336,29 @@ mod tests {
}
}
+ #[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_exact_match() {
let manifest = ExtensionManifest {
@@ -289,6 +289,24 @@ async fn copy_extension_resources(
}
}
+ 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(())
}
@@ -20,6 +20,7 @@ use extension::{
ExtensionSnippetProxy, ExtensionThemeProxy,
};
use fs::{Fs, RemoveOptions};
+use futures::future::join_all;
use futures::{
AsyncReadExt as _, Future, FutureExt as _, StreamExt as _,
channel::{
@@ -54,7 +55,7 @@ use std::{
time::{Duration, Instant},
};
use url::Url;
-use util::ResultExt;
+use util::{ResultExt, paths::RemotePathBuf};
use wasm_host::{
WasmExtension, WasmHost,
wit::{is_supported_wasm_api_version, wasm_api_version_range},
@@ -178,7 +179,13 @@ pub struct ExtensionIndexLanguageEntry {
pub grammar: Option<Arc<str>>,
}
-actions!(zed, [ReloadExtensions]);
+actions!(
+ zed,
+ [
+ /// Reloads all installed extensions.
+ ReloadExtensions
+ ]
+);
pub fn init(
extension_host_proxy: Arc<ExtensionHostProxy>,
@@ -854,8 +861,8 @@ impl ExtensionStore {
btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove),
};
- cx.spawn(async move |this, cx| {
- let _finish = cx.on_drop(&this, {
+ 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());
@@ -870,22 +877,39 @@ impl ExtensionStore {
ignore_if_not_exists: true,
},
)
- .await?;
+ .await
+ .with_context(|| format!("Removing extension dir {extension_dir:?}"))?;
- // todo(windows)
- // Stop the server here.
- this.update(cx, |this, cx| this.reload(None, cx))?.await;
+ extension_store
+ .update(cx, |extension_store, cx| extension_store.reload(None, cx))?
+ .await;
- fs.remove_dir(
- &work_dir,
- RemoveOptions {
- recursive: true,
- ignore_if_not_exists: true,
- },
- )
- .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}");
+ }
+ }
+ }
+ }
- this.update(cx, |_, cx| {
+ extension_store.update(cx, |_, cx| {
cx.emit(Event::ExtensionUninstalled(extension_id.clone()));
if let Some(events) = ExtensionEvents::try_global(cx) {
if let Some(manifest) = extension_manifest {
@@ -1137,27 +1161,38 @@ impl ExtensionStore {
})
.collect::<Vec<_>>();
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.iter() {
+ for (language_server_name, config) in &extension.manifest.language_servers {
for language in config.languages() {
- self.proxy
- .remove_language_server(&language, language_server_name);
+ server_removal_tasks.push(self.proxy.remove_language_server(
+ &language,
+ language_server_name,
+ cx,
+ ));
}
}
- for (server_id, _) in extension.manifest.context_servers.iter() {
+ for (server_id, _) in &extension.manifest.context_servers {
self.proxy.unregister_context_server(server_id.clone(), cx);
}
- for (adapter, _) in extension.manifest.debug_adapters.iter() {
+ for (adapter, _) in &extension.manifest.debug_adapters {
self.proxy.unregister_debug_adapter(adapter.clone());
}
- for (locator, _) in extension.manifest.debug_locators.iter() {
+ for (locator, _) in &extension.manifest.debug_locators {
self.proxy.unregister_debug_locator(locator.clone());
}
+ for (command_name, _) in &extension.manifest.slash_commands {
+ self.proxy.unregister_slash_command(command_name.clone());
+ }
+ for (provider_id, _) in &extension.manifest.indexed_docs_providers {
+ self.proxy
+ .unregister_indexed_docs_provider(provider_id.clone());
+ }
}
self.wasm_extensions
@@ -1262,14 +1297,15 @@ impl ExtensionStore {
cx.background_spawn({
let fs = fs.clone();
async move {
- for theme_path in themes_to_add.into_iter() {
+ 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.into_iter() {
+ 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
@@ -1633,6 +1669,23 @@ impl ExtensionStore {
}
}
+ 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 {
+ match schema_path.parent() {
+ Some(parent) => fs.create_dir(&tmp_dir.join(parent)).await?,
+ None => {}
+ }
+ fs.copy_file(
+ &src_dir.join(schema_path),
+ &tmp_dir.join(schema_path),
+ fs::CopyOptions::default(),
+ )
+ .await?
+ }
+ }
+
Ok(())
})
}
@@ -1647,7 +1700,7 @@ impl ExtensionStore {
.extensions
.iter()
.filter_map(|(id, entry)| {
- if entry.manifest.language_servers.is_empty() {
+ if !entry.manifest.allow_remote_load() {
return None;
}
Some(proto::Extension {
@@ -1666,6 +1719,7 @@ impl ExtensionStore {
.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()?;
@@ -1678,7 +1732,10 @@ impl ExtensionStore {
)
})?
.await?;
- let dest_dir = PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id);
+ let dest_dir = RemotePathBuf::new(
+ PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id),
+ path_style,
+ );
log::info!("Uploading extension {}", missing_extension.clone().id);
client
@@ -1695,7 +1752,7 @@ impl ExtensionStore {
client
.update(cx, |client, _cx| {
client.proto_client().request(proto::InstallExtension {
- tmp_dir: dest_dir.to_string_lossy().to_string(),
+ tmp_dir: dest_dir.to_proto(),
extension: Some(missing_extension),
})
})?
@@ -11,6 +11,7 @@ use futures::{AsyncReadExt, StreamExt, io::BufReader};
use gpui::{AppContext as _, SemanticVersion, TestAppContext};
use http_client::{FakeHttpClient, Response};
use language::{BinaryStatus, LanguageMatcher, LanguageRegistry};
+use language_extension::LspAccess;
use lsp::LanguageServerName;
use node_runtime::NodeRuntime;
use parking_lot::Mutex;
@@ -271,7 +272,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
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(proxy.clone(), language_registry.clone());
+ language_extension::init(LspAccess::Noop, proxy.clone(), language_registry.clone());
let node_runtime = NodeRuntime::unavailable();
let store = cx.new(|cx| {
@@ -554,7 +555,11 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
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(proxy.clone(), language_registry.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();
@@ -815,7 +820,6 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
extension_store
.update(cx, |store, cx| store.reload(Some("gleam".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)
@@ -1,13 +1,17 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::{Context as _, Result};
-use client::{TypedEnvelope, proto};
+use client::{
+ TypedEnvelope,
+ proto::{self, FromProto},
+};
use collections::{HashMap, HashSet};
use extension::{
- Extension, ExtensionHostProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy,
- ExtensionManifest,
+ 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};
@@ -125,7 +129,7 @@ impl HeadlessExtensionStore {
let manifest = Arc::new(ExtensionManifest::load(fs.clone(), &extension_dir).await?);
- debug_assert!(!manifest.languages.is_empty() || !manifest.language_servers.is_empty());
+ debug_assert!(!manifest.languages.is_empty() || manifest.allow_remote_load());
if manifest.version.as_ref() != extension.version.as_str() {
anyhow::bail!(
@@ -165,12 +169,13 @@ impl HeadlessExtensionStore {
})?;
}
- if manifest.language_servers.is_empty() {
+ if !manifest.allow_remote_load() {
return Ok(());
}
- let wasm_extension: Arc<dyn Extension> =
- Arc::new(WasmExtension::load(extension_dir, &manifest, wasm_host.clone(), &cx).await?);
+ let wasm_extension: Arc<dyn Extension> = Arc::new(
+ WasmExtension::load(extension_dir.clone(), &manifest, wasm_host.clone(), &cx).await?,
+ );
for (language_server_id, language_server_config) in &manifest.language_servers {
for language in language_server_config.languages() {
@@ -186,6 +191,28 @@ impl HeadlessExtensionStore {
);
})?;
}
+ 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(())
@@ -204,18 +231,27 @@ impl HeadlessExtensionStore {
.unwrap_or_default();
self.proxy.remove_languages(&languages_to_remove, &[]);
- for (language_server_name, language) in self
+ let servers_to_remove = self
.loaded_language_servers
.remove(extension_id)
- .unwrap_or_default()
- {
- self.proxy
- .remove_language_server(&language, &language_server_name);
- }
-
+ .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.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 {
@@ -224,6 +260,7 @@ impl HeadlessExtensionStore {
},
)
.await
+ .with_context(|| format!("Removing directory {path:?}"))
})
}
@@ -305,7 +342,7 @@ impl HeadlessExtensionStore {
version: extension.version,
dev: extension.dev,
},
- PathBuf::from(envelope.payload.tmp_dir),
+ PathBuf::from_proto(envelope.payload.tmp_dir),
cx,
)
})?
@@ -54,7 +54,7 @@ pub struct WasmHost {
main_thread_message_tx: mpsc::UnboundedSender<MainThreadCall>,
}
-#[derive(Clone)]
+#[derive(Clone, Debug)]
pub struct WasmExtension {
tx: UnboundedSender<ExtensionCall>,
pub manifest: Arc<ExtensionManifest>,
@@ -63,6 +63,12 @@ pub struct WasmExtension {
pub zed_api_version: SemanticVersion,
}
+impl Drop for WasmExtension {
+ fn drop(&mut self) {
+ self.tx.close_channel();
+ }
+}
+
#[async_trait]
impl extension::Extension for WasmExtension {
fn manifest(&self) -> Arc<ExtensionManifest> {
@@ -742,7 +748,6 @@ impl WasmExtension {
{
let (return_tx, return_rx) = oneshot::channel();
self.tx
- .clone()
.unbounded_send(Box::new(move |extension, store| {
async {
let result = f(extension, store).await;
@@ -999,7 +999,7 @@ impl Extension {
) -> Result<Result<DebugRequest, String>> {
match self {
Extension::V0_6_0(ext) => {
- let build_config_template = resolved_build_task.into();
+ let build_config_template = resolved_build_task.try_into()?;
let dap_request = ext
.call_run_dap_locator(store, &locator_name, &build_config_template)
.await?
@@ -299,15 +299,17 @@ impl From<extension::DebugScenario> for DebugScenario {
}
}
-impl From<SpawnInTerminal> for ResolvedTask {
- fn from(value: SpawnInTerminal) -> Self {
- Self {
+impl TryFrom<SpawnInTerminal> for ResolvedTask {
+ type Error = anyhow::Error;
+
+ fn try_from(value: SpawnInTerminal) -> Result<Self, Self::Error> {
+ Ok(Self {
label: value.label,
- command: value.command,
+ command: value.command.context("missing command")?,
args: value.args,
env: value.env.into_iter().collect(),
cwd: value.cwd.map(|s| s.to_string_lossy().into_owned()),
- }
+ })
}
}
@@ -54,6 +54,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
("nu", &["nu"]),
("ocaml", &["ml", "mli"]),
("php", &["php"]),
+ ("powershell", &["ps1", "psm1"]),
("prisma", &["prisma"]),
("proto", &["proto"]),
("purescript", &["purs"]),
@@ -38,7 +38,13 @@ use crate::extension_version_selector::{
ExtensionVersionSelector, ExtensionVersionSelectorDelegate,
};
-actions!(zed, [InstallDevExtension]);
+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| {
@@ -712,24 +718,34 @@ impl ExtensionsPage {
}
parent.child(
- h_flex().gap_2().children(
+ h_flex().gap_1().children(
extension
.manifest
.provides
.iter()
- .map(|provides| {
- div()
- .bg(cx.theme().colors().element_background)
- .px_0p5()
- .border_1()
- .border_color(cx.theme().colors().border)
- .rounded_sm()
- .child(
- Label::new(extension_provides_label(
- *provides,
- ))
- .size(LabelSize::XSmall),
- )
+ .filter_map(|provides| {
+ match provides {
+ ExtensionProvides::SlashCommands
+ | ExtensionProvides::IndexedDocsProviders => {
+ return None;
+ }
+ _ => {}
+ }
+
+ Some(
+ div()
+ .px_1()
+ .border_1()
+ .rounded_sm()
+ .border_color(cx.theme().colors().border)
+ .bg(cx.theme().colors().element_background)
+ .child(
+ Label::new(extension_provides_label(
+ *provides,
+ ))
+ .size(LabelSize::XSmall),
+ ),
+ )
})
.collect::<Vec<_>>(),
),
@@ -738,8 +754,7 @@ impl ExtensionsPage {
)
.child(
h_flex()
- .gap_2()
- .justify_between()
+ .gap_1()
.children(buttons.upgrade)
.children(buttons.configure)
.child(buttons.install_or_uninstall),
@@ -1446,23 +1461,30 @@ impl Render for ExtensionsPage {
this.change_provides_filter(None, cx);
})),
)
- .children(ExtensionProvides::iter().map(|provides| {
+ .children(ExtensionProvides::iter().filter_map(|provides| {
+ match provides {
+ ExtensionProvides::SlashCommands
+ | ExtensionProvides::IndexedDocsProviders => return None,
+ _ => {}
+ }
+
let label = extension_provides_label(provides);
- Button::new(
- SharedString::from(format!("filter-category-{}", label)),
- label,
+ let button_id = SharedString::from(format!("filter-category-{}", label));
+
+ Some(
+ Button::new(button_id, label)
+ .style(if self.provides_filter == Some(provides) {
+ ButtonStyle::Filled
+ } else {
+ ButtonStyle::Subtle
+ })
+ .toggle_state(self.provides_filter == Some(provides))
+ .on_click({
+ cx.listener(move |this, _event, _, cx| {
+ this.change_provides_filter(Some(provides), cx);
+ })
+ }),
)
- .style(if self.provides_filter == Some(provides) {
- ButtonStyle::Filled
- } else {
- ButtonStyle::Subtle
- })
- .toggle_state(self.provides_filter == Some(provides))
- .on_click({
- cx.listener(move |this, _event, _, cx| {
- this.change_provides_filter(Some(provides), cx);
- })
- })
})),
)
.child(self.render_feature_upsells(cx))
@@ -92,6 +92,12 @@ impl FeatureFlag for JjUiFeatureFlag {
const NAME: &'static str = "jj-ui";
}
+pub struct AcpFeatureFlag;
+
+impl FeatureFlag for AcpFeatureFlag {
+ const NAME: &'static str = "acp";
+}
+
pub trait FeatureFlagViewExt<V: 'static> {
fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
where
@@ -11,9 +11,13 @@ pub mod system_specs;
actions!(
zed,
[
+ /// Copies system specifications to the clipboard for bug reports.
CopySystemSpecsIntoClipboard,
+ /// Opens email client to send feedback to Zed support.
EmailZed,
+ /// Opens the Zed repository on GitHub.
OpenZedRepo,
+ /// Opens the feature request form.
RequestFeature,
]
);
@@ -47,7 +47,14 @@ use workspace::{
actions!(
file_finder,
- [SelectPrevious, ToggleFilterMenu, ToggleSplitMenu]
+ [
+ /// Selects the previous item in the file finder.
+ SelectPrevious,
+ /// Toggles the file filter menu.
+ ToggleFilterMenu,
+ /// Toggles the split direction menu.
+ ToggleSplitMenu
+ ]
);
impl ModalView for FileFinder {
@@ -15,16 +15,14 @@ use std::{
};
use ui::{Context, LabelLike, ListItem, Window};
use ui::{HighlightedLabel, ListItemSpacing, prelude::*};
-use util::{maybe, paths::compare_paths};
+use util::{
+ maybe,
+ paths::{PathStyle, compare_paths},
+};
use workspace::Workspace;
pub(crate) struct OpenPathPrompt;
-#[cfg(target_os = "windows")]
-const PROMPT_ROOT: &str = "C:\\";
-#[cfg(not(target_os = "windows"))]
-const PROMPT_ROOT: &str = "/";
-
#[derive(Debug)]
pub struct OpenPathDelegate {
tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
@@ -34,6 +32,8 @@ pub struct OpenPathDelegate {
string_matches: Vec<StringMatch>,
cancel_flag: Arc<AtomicBool>,
should_dismiss: bool,
+ prompt_root: String,
+ path_style: PathStyle,
replace_prompt: Task<()>,
}
@@ -42,6 +42,7 @@ impl OpenPathDelegate {
tx: oneshot::Sender<Option<Vec<PathBuf>>>,
lister: DirectoryLister,
creating_path: bool,
+ path_style: PathStyle,
) -> Self {
Self {
tx: Some(tx),
@@ -53,6 +54,11 @@ impl OpenPathDelegate {
string_matches: Vec::new(),
cancel_flag: Arc::new(AtomicBool::new(false)),
should_dismiss: true,
+ prompt_root: match path_style {
+ PathStyle::Posix => "/".to_string(),
+ PathStyle::Windows => "C:\\".to_string(),
+ },
+ path_style,
replace_prompt: Task::ready(()),
}
}
@@ -185,7 +191,8 @@ impl OpenPathPrompt {
cx: &mut Context<Workspace>,
) {
workspace.toggle_modal(window, cx, |window, cx| {
- let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path);
+ let delegate =
+ OpenPathDelegate::new(tx, lister.clone(), creating_path, PathStyle::current());
let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.));
let query = lister.default_query(cx);
picker.set_query(query, window, cx);
@@ -226,18 +233,7 @@ impl PickerDelegate for OpenPathDelegate {
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let lister = &self.lister;
- let last_item = Path::new(&query)
- .file_name()
- .unwrap_or_default()
- .to_string_lossy();
- let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
- (dir.to_string(), last_item.into_owned())
- } else {
- (query, String::new())
- };
- if dir == "" {
- dir = PROMPT_ROOT.to_string();
- }
+ let (dir, suffix) = get_dir_and_suffix(query, self.path_style);
let query = match &self.directory_state {
DirectoryState::List { parent_path, .. } => {
@@ -266,6 +262,7 @@ impl PickerDelegate for OpenPathDelegate {
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
+ let parent_path_is_root = self.prompt_root == dir;
cx.spawn_in(window, async move |this, cx| {
if let Some(query) = query {
let paths = query.await;
@@ -279,7 +276,7 @@ impl PickerDelegate for OpenPathDelegate {
DirectoryState::None { create: false }
| DirectoryState::List { .. } => match paths {
Ok(paths) => DirectoryState::List {
- entries: path_candidates(&dir, paths),
+ entries: path_candidates(parent_path_is_root, paths),
parent_path: dir.clone(),
error: None,
},
@@ -292,7 +289,7 @@ impl PickerDelegate for OpenPathDelegate {
DirectoryState::None { create: true }
| DirectoryState::Create { .. } => match paths {
Ok(paths) => {
- let mut entries = path_candidates(&dir, paths);
+ let mut entries = path_candidates(parent_path_is_root, paths);
let mut exists = false;
let mut is_dir = false;
let mut new_id = None;
@@ -488,6 +485,7 @@ impl PickerDelegate for OpenPathDelegate {
_: &mut Context<Picker<Self>>,
) -> Option<String> {
let candidate = self.get_entry(self.selected_index)?;
+ let path_style = self.path_style;
Some(
maybe!({
match &self.directory_state {
@@ -496,7 +494,7 @@ impl PickerDelegate for OpenPathDelegate {
parent_path,
candidate.path.string,
if candidate.is_dir {
- MAIN_SEPARATOR_STR
+ path_style.separator()
} else {
""
}
@@ -506,7 +504,7 @@ impl PickerDelegate for OpenPathDelegate {
parent_path,
candidate.path.string,
if candidate.is_dir {
- MAIN_SEPARATOR_STR
+ path_style.separator()
} else {
""
}
@@ -527,8 +525,8 @@ impl PickerDelegate for OpenPathDelegate {
DirectoryState::None { .. } => return,
DirectoryState::List { parent_path, .. } => {
let confirmed_path =
- if parent_path == PROMPT_ROOT && candidate.path.string.is_empty() {
- PathBuf::from(PROMPT_ROOT)
+ if parent_path == &self.prompt_root && candidate.path.string.is_empty() {
+ PathBuf::from(&self.prompt_root)
} else {
Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
.join(&candidate.path.string)
@@ -548,8 +546,8 @@ impl PickerDelegate for OpenPathDelegate {
return;
}
let prompted_path =
- if parent_path == PROMPT_ROOT && user_input.file.string.is_empty() {
- PathBuf::from(PROMPT_ROOT)
+ if parent_path == &self.prompt_root && user_input.file.string.is_empty() {
+ PathBuf::from(&self.prompt_root)
} else {
Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
.join(&user_input.file.string)
@@ -652,8 +650,8 @@ impl PickerDelegate for OpenPathDelegate {
.inset(true)
.toggle_state(selected)
.child(HighlightedLabel::new(
- if parent_path == PROMPT_ROOT {
- format!("{}{}", PROMPT_ROOT, candidate.path.string)
+ if parent_path == &self.prompt_root {
+ format!("{}{}", self.prompt_root, candidate.path.string)
} else {
candidate.path.string.clone()
},
@@ -665,10 +663,10 @@ impl PickerDelegate for OpenPathDelegate {
user_input,
..
} => {
- let (label, delta) = if parent_path == PROMPT_ROOT {
+ let (label, delta) = if parent_path == &self.prompt_root {
(
- format!("{}{}", PROMPT_ROOT, candidate.path.string),
- PROMPT_ROOT.len(),
+ format!("{}{}", self.prompt_root, candidate.path.string),
+ self.prompt_root.len(),
)
} else {
(candidate.path.string.clone(), 0)
@@ -751,8 +749,11 @@ impl PickerDelegate for OpenPathDelegate {
}
}
-fn path_candidates(parent_path: &String, mut children: Vec<DirectoryItem>) -> Vec<CandidateInfo> {
- if *parent_path == PROMPT_ROOT {
+fn path_candidates(
+ parent_path_is_root: bool,
+ mut children: Vec<DirectoryItem>,
+) -> Vec<CandidateInfo> {
+ if parent_path_is_root {
children.push(DirectoryItem {
is_dir: true,
path: PathBuf::default(),
@@ -769,3 +770,128 @@ fn path_candidates(parent_path: &String, mut children: Vec<DirectoryItem>) -> Ve
})
.collect()
}
+
+#[cfg(target_os = "windows")]
+fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
+ let last_item = Path::new(&query)
+ .file_name()
+ .unwrap_or_default()
+ .to_string_lossy();
+ let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
+ (dir.to_string(), last_item.into_owned())
+ } else {
+ (query.to_string(), String::new())
+ };
+ match path_style {
+ PathStyle::Posix => {
+ if dir.is_empty() {
+ dir = "/".to_string();
+ }
+ }
+ PathStyle::Windows => {
+ if dir.len() < 3 {
+ dir = "C:\\".to_string();
+ }
+ }
+ }
+ (dir, suffix)
+}
+
+#[cfg(not(target_os = "windows"))]
+fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
+ match path_style {
+ PathStyle::Posix => {
+ let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
+ (query[..index].to_string(), query[index + 1..].to_string())
+ } else {
+ (query, String::new())
+ };
+ if !dir.ends_with('/') {
+ dir.push('/');
+ }
+ (dir, suffix)
+ }
+ PathStyle::Windows => {
+ let (mut dir, suffix) = if let Some(index) = query.rfind('\\') {
+ (query[..index].to_string(), query[index + 1..].to_string())
+ } else {
+ (query, String::new())
+ };
+ if dir.len() < 3 {
+ dir = "C:\\".to_string();
+ }
+ if !dir.ends_with('\\') {
+ dir.push('\\');
+ }
+ (dir, suffix)
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use util::paths::PathStyle;
+
+ use crate::open_path_prompt::get_dir_and_suffix;
+
+ #[test]
+ fn test_get_dir_and_suffix_with_windows_style() {
+ let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows);
+ assert_eq!(dir, "C:\\");
+ assert_eq!(suffix, "");
+
+ let (dir, suffix) = get_dir_and_suffix("C:".into(), PathStyle::Windows);
+ assert_eq!(dir, "C:\\");
+ assert_eq!(suffix, "");
+
+ let (dir, suffix) = get_dir_and_suffix("C:\\".into(), PathStyle::Windows);
+ assert_eq!(dir, "C:\\");
+ assert_eq!(suffix, "");
+
+ let (dir, suffix) = get_dir_and_suffix("C:\\Use".into(), PathStyle::Windows);
+ assert_eq!(dir, "C:\\");
+ assert_eq!(suffix, "Use");
+
+ let (dir, suffix) =
+ get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows);
+ assert_eq!(dir, "C:\\Users\\Junkui\\");
+ assert_eq!(suffix, "Docum");
+
+ let (dir, suffix) =
+ get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows);
+ assert_eq!(dir, "C:\\Users\\Junkui\\");
+ assert_eq!(suffix, "Documents");
+
+ let (dir, suffix) =
+ get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
+ assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
+ assert_eq!(suffix, "");
+ }
+
+ #[test]
+ fn test_get_dir_and_suffix_with_posix_style() {
+ let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
+ assert_eq!(dir, "/");
+ assert_eq!(suffix, "");
+
+ let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
+ assert_eq!(dir, "/");
+ assert_eq!(suffix, "");
+
+ let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
+ assert_eq!(dir, "/");
+ assert_eq!(suffix, "Use");
+
+ let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
+ assert_eq!(dir, "/Users/Junkui/");
+ assert_eq!(suffix, "Docum");
+
+ let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
+ assert_eq!(dir, "/Users/Junkui/");
+ assert_eq!(suffix, "Documents");
+
+ let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
+ assert_eq!(dir, "/Users/Junkui/Documents/");
+ assert_eq!(suffix, "");
+ }
+}
@@ -5,7 +5,7 @@ use picker::{Picker, PickerDelegate};
use project::Project;
use serde_json::json;
use ui::rems;
-use util::path;
+use util::{path, paths::PathStyle};
use workspace::{AppState, Workspace};
use crate::OpenPathDelegate;
@@ -37,7 +37,7 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
- let (picker, cx) = build_open_path_prompt(project, false, cx);
+ let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
let query = path!("/root");
insert_query(query, &picker, cx).await;
@@ -111,7 +111,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
- let (picker, cx) = build_open_path_prompt(project, false, cx);
+ let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
// Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
let query = path!("/root");
@@ -186,7 +186,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
}
#[gpui::test]
-#[cfg(target_os = "windows")]
+#[cfg_attr(not(target_os = "windows"), ignore)]
async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
@@ -204,7 +204,7 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
- let (picker, cx) = build_open_path_prompt(project, false, cx);
+ let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
// Support both forward and backward slashes.
let query = "C:/root/";
@@ -251,6 +251,47 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
);
}
+#[gpui::test]
+#[cfg_attr(not(target_os = "windows"), ignore)]
+async fn test_open_path_prompt_on_windows_with_remote(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/root",
+ json!({
+ "a": "A",
+ "dir1": {},
+ "dir2": {}
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+
+ let (picker, cx) = build_open_path_prompt(project, false, PathStyle::Posix, cx);
+
+ let query = "/root/";
+ insert_query(query, &picker, cx).await;
+ assert_eq!(
+ collect_match_candidates(&picker, cx),
+ vec!["a", "dir1", "dir2"]
+ );
+ assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/a");
+
+ // Confirm completion for the query "/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash.
+ let query = "/root/d";
+ insert_query(query, &picker, cx).await;
+ assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
+ assert_eq!(confirm_completion(query, 1, &picker, cx), "/root/dir2/");
+
+ let query = "/root/d";
+ insert_query(query, &picker, cx).await;
+ assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
+ assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/dir1/");
+}
+
#[gpui::test]
async fn test_new_path_prompt(cx: &mut TestAppContext) {
let app_state = init_test(cx);
@@ -278,7 +319,7 @@ async fn test_new_path_prompt(cx: &mut TestAppContext) {
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
- let (picker, cx) = build_open_path_prompt(project, true, cx);
+ let (picker, cx) = build_open_path_prompt(project, true, PathStyle::current(), cx);
insert_query(path!("/root"), &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
@@ -315,11 +356,12 @@ fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
fn build_open_path_prompt(
project: Entity<Project>,
creating_path: bool,
+ path_style: PathStyle,
cx: &mut TestAppContext,
) -> (Entity<Picker<OpenPathDelegate>>, &mut VisualTestContext) {
let (tx, _) = futures::channel::oneshot::channel();
let lister = project::DirectoryLister::Project(project.clone());
- let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path);
+ let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, path_style);
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
(
@@ -1,7 +1,7 @@
-use crate::FakeFs;
+use crate::{FakeFs, Fs};
use anyhow::{Context as _, Result};
use collections::{HashMap, HashSet};
-use futures::future::{self, BoxFuture};
+use futures::future::{self, BoxFuture, join_all};
use git::{
blame::Blame,
repository::{
@@ -356,18 +356,46 @@ impl GitRepository for FakeGitRepository {
fn stage_paths(
&self,
- _paths: Vec<RepoPath>,
+ paths: Vec<RepoPath>,
_env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
- unimplemented!()
+ Box::pin(async move {
+ let contents = paths
+ .into_iter()
+ .map(|path| {
+ let abs_path = self.dot_git_path.parent().unwrap().join(&path);
+ Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
+ })
+ .collect::<Vec<_>>();
+ let contents = join_all(contents).await;
+ self.with_state_async(true, move |state| {
+ for (path, content) in contents {
+ if let Some(content) = content {
+ state.index_contents.insert(path, content);
+ } else {
+ state.index_contents.remove(&path);
+ }
+ }
+ Ok(())
+ })
+ .await
+ })
}
fn unstage_paths(
&self,
- _paths: Vec<RepoPath>,
+ paths: Vec<RepoPath>,
_env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
- unimplemented!()
+ self.with_state_async(true, move |state| {
+ for path in paths {
+ match state.head_contents.get(&path) {
+ Some(content) => state.index_contents.insert(path, content.clone()),
+ None => state.index_contents.remove(&path),
+ };
+ }
+ Ok(())
+ })
}
fn commit(
@@ -31,38 +31,66 @@ actions!(
git,
[
// per-hunk
+ /// Toggles the staged state of the hunk or status entry at cursor.
ToggleStaged,
+ /// Stage status entries between an anchor entry and the cursor.
+ StageRange,
+ /// Stages the current hunk and moves to the next one.
StageAndNext,
+ /// Unstages the current hunk and moves to the next one.
UnstageAndNext,
+ /// Restores the selected hunks to their original state.
#[action(deprecated_aliases = ["editor::RevertSelectedHunks"])]
Restore,
// per-file
+ /// Shows git blame information for the current file.
#[action(deprecated_aliases = ["editor::ToggleGitBlame"])]
Blame,
+ /// Stages the current file.
StageFile,
+ /// Unstages the current file.
UnstageFile,
// repo-wide
+ /// Stages all changes in the repository.
StageAll,
+ /// Unstages all changes in the repository.
UnstageAll,
+ /// Restores all tracked files to their last committed state.
RestoreTrackedFiles,
+ /// Moves all untracked files to trash.
TrashUntrackedFiles,
+ /// Undoes the last commit, keeping changes in the working directory.
Uncommit,
+ /// Pushes commits to the remote repository.
Push,
+ /// Pushes commits to a specific remote branch.
PushTo,
+ /// Force pushes commits to the remote repository.
ForcePush,
+ /// Pulls changes from the remote repository.
Pull,
+ /// Fetches changes from the remote repository.
Fetch,
+ /// Fetches changes from a specific remote.
FetchFrom,
+ /// Creates a new commit with staged changes.
Commit,
+ /// Amends the last commit with staged changes.
Amend,
+ /// Cancels the current git operation.
Cancel,
+ /// Expands the commit message editor.
ExpandCommitEditor,
+ /// Generates a commit message using AI.
GenerateCommitMessage,
+ /// Initializes a new git repository.
Init,
+ /// Opens all modified files in the editor.
OpenModifiedFiles,
]
);
+/// Restores a file to its last committed state, discarding local changes.
#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = git, deprecated_aliases = ["editor::RevertFile"])]
#[serde(deny_unknown_fields)]
@@ -11,10 +11,7 @@ use gpui::{
use language::{Anchor, Buffer, BufferId};
use project::{ConflictRegion, ConflictSet, ConflictSetUpdate, ProjectItem as _};
use std::{ops::Range, sync::Arc};
-use ui::{
- ActiveTheme, AnyElement, Element as _, StatefulInteractiveElement, Styled,
- StyledTypography as _, Window, div, h_flex, rems,
-};
+use ui::{ActiveTheme, Element as _, Styled, Window, prelude::*};
use util::{ResultExt as _, debug_panic, maybe};
pub(crate) struct ConflictAddon {
@@ -300,7 +297,6 @@ fn conflicts_updated(
move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx)
}),
priority: 0,
- render_in_minimap: true,
})
}
let new_block_ids = editor.insert_blocks(blocks, None, cx);
@@ -391,20 +387,15 @@ fn render_conflict_buttons(
cx: &mut BlockContext,
) -> AnyElement {
h_flex()
+ .id(cx.block_id)
.h(cx.line_height)
- .items_end()
.ml(cx.margins.gutter.width)
- .id(cx.block_id)
- .gap_0p5()
+ .items_end()
+ .gap_1()
+ .bg(cx.theme().colors().editor_background)
.child(
- div()
- .id("ours")
- .px_1()
- .child("Take Ours")
- .rounded_t(rems(0.2))
- .text_ui_sm(cx)
- .hover(|this| this.bg(cx.theme().colors().element_background))
- .cursor_pointer()
+ Button::new("head", "Use HEAD")
+ .label_size(LabelSize::Small)
.on_click({
let editor = editor.clone();
let conflict = conflict.clone();
@@ -423,14 +414,8 @@ fn render_conflict_buttons(
}),
)
.child(
- div()
- .id("theirs")
- .px_1()
- .child("Take Theirs")
- .rounded_t(rems(0.2))
- .text_ui_sm(cx)
- .hover(|this| this.bg(cx.theme().colors().element_background))
- .cursor_pointer()
+ Button::new("origin", "Use Origin")
+ .label_size(LabelSize::Small)
.on_click({
let editor = editor.clone();
let conflict = conflict.clone();
@@ -449,14 +434,8 @@ fn render_conflict_buttons(
}),
)
.child(
- div()
- .id("both")
- .px_1()
- .child("Take Both")
- .rounded_t(rems(0.2))
- .text_ui_sm(cx)
- .hover(|this| this.bg(cx.theme().colors().element_background))
- .cursor_pointer()
+ Button::new("both", "Use Both")
+ .label_size(LabelSize::Small)
.on_click({
let editor = editor.clone();
let conflict = conflict.clone();
@@ -30,10 +30,9 @@ use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles
use gpui::{
Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
- ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
- MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task,
- Transformation, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, percentage,
- uniform_list,
+ ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, Point,
+ PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle,
+ WeakEntity, actions, anchored, deferred, percentage, uniform_list,
};
use itertools::Itertools;
use language::{Buffer, File};
@@ -48,7 +47,7 @@ use panel::{
PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
panel_icon_button,
};
-use project::git_store::RepositoryEvent;
+use project::git_store::{RepositoryEvent, RepositoryId};
use project::{
Fs, Project, ProjectPath,
git_store::{GitStoreEvent, Repository},
@@ -77,11 +76,17 @@ use zed_llm_client::CompletionIntent;
actions!(
git_panel,
[
+ /// Closes the git panel.
Close,
+ /// Toggles focus on the git panel.
ToggleFocus,
+ /// Opens the git panel menu.
OpenMenu,
+ /// Focuses on the commit message editor.
FocusEditor,
+ /// Focuses on the changes list.
FocusChanges,
+ /// Toggles automatic co-author suggestions.
ToggleFillCoAuthors,
]
);
@@ -206,14 +211,14 @@ impl GitHeaderEntry {
#[derive(Debug, PartialEq, Eq, Clone)]
enum GitListEntry {
- GitStatusEntry(GitStatusEntry),
+ Status(GitStatusEntry),
Header(GitHeaderEntry),
}
impl GitListEntry {
fn status_entry(&self) -> Option<&GitStatusEntry> {
match self {
- GitListEntry::GitStatusEntry(entry) => Some(entry),
+ GitListEntry::Status(entry) => Some(entry),
_ => None,
}
}
@@ -317,7 +322,6 @@ pub struct GitPanel {
pub(crate) commit_editor: Entity<Editor>,
conflicted_count: usize,
conflicted_staged_count: usize,
- current_modifiers: Modifiers,
add_coauthors: bool,
generate_commit_message_task: Option<Task<Option<()>>>,
entries: Vec<GitListEntry>,
@@ -349,9 +353,16 @@ pub struct GitPanel {
show_placeholders: bool,
local_committer: Option<GitCommitter>,
local_committer_task: Option<Task<()>>,
+ bulk_staging: Option<BulkStaging>,
_settings_subscription: Subscription,
}
+#[derive(Clone, Debug, PartialEq, Eq)]
+struct BulkStaging {
+ repo_id: RepositoryId,
+ anchor: RepoPath,
+}
+
const MAX_PANEL_EDITOR_LINES: usize = 6;
pub(crate) fn commit_message_editor(
@@ -491,7 +502,6 @@ impl GitPanel {
commit_editor,
conflicted_count: 0,
conflicted_staged_count: 0,
- current_modifiers: window.modifiers(),
add_coauthors: true,
generate_commit_message_task: None,
entries: Vec::new(),
@@ -523,6 +533,7 @@ impl GitPanel {
entry_count: 0,
horizontal_scrollbar,
vertical_scrollbar,
+ bulk_staging: None,
_settings_subscription,
};
@@ -729,16 +740,6 @@ impl GitPanel {
}
}
- fn handle_modifiers_changed(
- &mut self,
- event: &ModifiersChangedEvent,
- _: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.current_modifiers = event.modifiers;
- cx.notify();
- }
-
fn scroll_to_selected_entry(&mut self, cx: &mut Context<Self>) {
if let Some(selected_entry) = self.selected_entry {
self.scroll_handle
@@ -1259,10 +1260,18 @@ impl GitPanel {
return;
};
let (stage, repo_paths) = match entry {
- GitListEntry::GitStatusEntry(status_entry) => {
+ GitListEntry::Status(status_entry) => {
if status_entry.status.staging().is_fully_staged() {
+ if let Some(op) = self.bulk_staging.clone()
+ && op.anchor == status_entry.repo_path
+ {
+ self.bulk_staging = None;
+ }
+
(false, vec![status_entry.clone()])
} else {
+ self.set_bulk_staging_anchor(status_entry.repo_path.clone(), cx);
+
(true, vec![status_entry.clone()])
}
}
@@ -1377,6 +1386,13 @@ impl GitPanel {
}
}
+ fn stage_range(&mut self, _: &git::StageRange, _window: &mut Window, cx: &mut Context<Self>) {
+ let Some(index) = self.selected_entry else {
+ return;
+ };
+ self.stage_bulk(index, cx);
+ }
+
fn stage_selected(&mut self, _: &git::StageFile, _window: &mut Window, cx: &mut Context<Self>) {
let Some(selected_entry) = self.get_selected_entry() else {
return;
@@ -1824,6 +1840,7 @@ impl GitPanel {
tool_choice: None,
stop: Vec::new(),
temperature,
+ thinking_allowed: false,
};
let stream = model.stream_completion_text(request, &cx);
@@ -2442,6 +2459,11 @@ impl GitPanel {
}
fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
+ let bulk_staging = self.bulk_staging.take();
+ let last_staged_path_prev_index = bulk_staging
+ .as_ref()
+ .and_then(|op| self.entry_by_path(&op.anchor, cx));
+
self.entries.clear();
self.single_staged_entry.take();
self.single_tracked_entry.take();
@@ -2458,7 +2480,7 @@ impl GitPanel {
let mut changed_entries = Vec::new();
let mut new_entries = Vec::new();
let mut conflict_entries = Vec::new();
- let mut last_staged = None;
+ let mut single_staged_entry = None;
let mut staged_count = 0;
let mut max_width_item: Option<(RepoPath, usize)> = None;
@@ -2496,7 +2518,7 @@ impl GitPanel {
if staging.has_staged() {
staged_count += 1;
- last_staged = Some(entry.clone());
+ single_staged_entry = Some(entry.clone());
}
let width_estimate = Self::item_width_estimate(
@@ -2527,27 +2549,27 @@ impl GitPanel {
let mut pending_staged_count = 0;
let mut last_pending_staged = None;
- let mut pending_status_for_last_staged = None;
+ let mut pending_status_for_single_staged = None;
for pending in self.pending.iter() {
if pending.target_status == TargetStatus::Staged {
pending_staged_count += pending.entries.len();
last_pending_staged = pending.entries.iter().next().cloned();
}
- if let Some(last_staged) = &last_staged {
+ if let Some(single_staged) = &single_staged_entry {
if pending
.entries
.iter()
- .any(|entry| entry.repo_path == last_staged.repo_path)
+ .any(|entry| entry.repo_path == single_staged.repo_path)
{
- pending_status_for_last_staged = Some(pending.target_status);
+ pending_status_for_single_staged = Some(pending.target_status);
}
}
}
if conflict_entries.len() == 0 && staged_count == 1 && pending_staged_count == 0 {
- match pending_status_for_last_staged {
+ match pending_status_for_single_staged {
Some(TargetStatus::Staged) | None => {
- self.single_staged_entry = last_staged;
+ self.single_staged_entry = single_staged_entry;
}
_ => {}
}
@@ -2563,11 +2585,8 @@ impl GitPanel {
self.entries.push(GitListEntry::Header(GitHeaderEntry {
header: Section::Conflict,
}));
- self.entries.extend(
- conflict_entries
- .into_iter()
- .map(GitListEntry::GitStatusEntry),
- );
+ self.entries
+ .extend(conflict_entries.into_iter().map(GitListEntry::Status));
}
if changed_entries.len() > 0 {
@@ -2576,31 +2595,39 @@ impl GitPanel {
header: Section::Tracked,
}));
}
- self.entries.extend(
- changed_entries
- .into_iter()
- .map(GitListEntry::GitStatusEntry),
- );
+ self.entries
+ .extend(changed_entries.into_iter().map(GitListEntry::Status));
}
if new_entries.len() > 0 {
self.entries.push(GitListEntry::Header(GitHeaderEntry {
header: Section::New,
}));
self.entries
- .extend(new_entries.into_iter().map(GitListEntry::GitStatusEntry));
+ .extend(new_entries.into_iter().map(GitListEntry::Status));
}
if let Some((repo_path, _)) = max_width_item {
self.max_width_item_index = self.entries.iter().position(|entry| match entry {
- GitListEntry::GitStatusEntry(git_status_entry) => {
- git_status_entry.repo_path == repo_path
- }
+ GitListEntry::Status(git_status_entry) => git_status_entry.repo_path == repo_path,
GitListEntry::Header(_) => false,
});
}
self.update_counts(repo);
+ let bulk_staging_anchor_new_index = bulk_staging
+ .as_ref()
+ .filter(|op| op.repo_id == repo.id)
+ .and_then(|op| self.entry_by_path(&op.anchor, cx));
+ if bulk_staging_anchor_new_index == last_staged_path_prev_index
+ && let Some(index) = bulk_staging_anchor_new_index
+ && let Some(entry) = self.entries.get(index)
+ && let Some(entry) = entry.status_entry()
+ && self.entry_staging(entry) == StageStatus::Staged
+ {
+ self.bulk_staging = bulk_staging;
+ }
+
self.select_first_entry_if_none(cx);
let suggested_commit_message = self.suggest_commit_message(cx);
@@ -2838,7 +2865,7 @@ impl GitPanel {
PopoverMenu::new(id.into())
.trigger(
- IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical)
+ IconButton::new("overflow-menu-trigger", IconName::Ellipsis)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
)
@@ -2959,15 +2986,20 @@ impl GitPanel {
&self,
id: impl Into<ElementId>,
keybinding_target: Option<FocusHandle>,
+ cx: &mut Context<Self>,
) -> impl IntoElement {
PopoverMenu::new(id.into())
.trigger(
ui::ButtonLike::new_rounded_right("commit-split-button-right")
.layer(ui::ElevationIndex::ModalSurface)
- .size(ui::ButtonSize::None)
+ .size(ButtonSize::None)
.child(
- div()
+ h_flex()
.px_1()
+ .h_full()
+ .justify_center()
+ .border_l_1()
+ .border_color(cx.theme().colors().border)
.child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
),
)
@@ -3060,6 +3092,7 @@ impl GitPanel {
Some(
self.panel_header_container(window, cx)
.px_2()
+ .justify_between()
.child(
panel_button(change_string)
.color(Color::Muted)
@@ -3074,23 +3107,25 @@ impl GitPanel {
})
}),
)
- .child(div().flex_grow()) // spacer
- .child(self.render_overflow_menu("overflow_menu"))
- .child(div().w_2()) // another spacer
.child(
- panel_filled_button(text)
- .tooltip(Tooltip::for_action_title_in(
- tooltip,
- action.as_ref(),
- &self.focus_handle,
- ))
- .disabled(self.entry_count == 0)
- .on_click(move |_, _, cx| {
- let action = action.boxed_clone();
- cx.defer(move |cx| {
- cx.dispatch_action(action.as_ref());
- })
- }),
+ h_flex()
+ .gap_1()
+ .child(self.render_overflow_menu("overflow_menu"))
+ .child(
+ panel_filled_button(text)
+ .tooltip(Tooltip::for_action_title_in(
+ tooltip,
+ action.as_ref(),
+ &self.focus_handle,
+ ))
+ .disabled(self.entry_count == 0)
+ .on_click(move |_, _, cx| {
+ let action = action.boxed_clone();
+ cx.defer(move |cx| {
+ cx.dispatch_action(action.as_ref());
+ })
+ }),
+ ),
),
)
}
@@ -3168,7 +3203,7 @@ impl GitPanel {
.w_full()
.h(max_height + footer_size)
.border_t_1()
- .border_color(cx.theme().colors().border_variant)
+ .border_color(cx.theme().colors().border)
.cursor_text()
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
window.focus(&this.commit_editor.focus_handle(cx));
@@ -3253,6 +3288,7 @@ impl GitPanel {
let (can_commit, tooltip) = self.configure_commit_button(cx);
let title = self.commit_button_title();
let commit_tooltip_focus_handle = self.commit_editor.focus_handle(cx);
+
div()
.id("commit-wrapper")
.on_hover(cx.listener(move |this, hovered, _, cx| {
@@ -3365,6 +3401,7 @@ impl GitPanel {
self.render_git_commit_menu(
ElementId::Name(format!("split-button-right-{}", title).into()),
Some(commit_tooltip_focus_handle.clone()),
+ cx,
)
.into_any_element(),
))
@@ -3409,8 +3446,8 @@ impl GitPanel {
fn render_pending_amend(&self, cx: &mut Context<Self>) -> impl IntoElement {
div()
- .py_2()
- .px(px(8.))
+ .p_2()
+ .border_t_1()
.border_color(cx.theme().colors().border)
.child(
Label::new(
@@ -3425,22 +3462,21 @@ impl GitPanel {
let branch = active_repository.read(cx).branch.as_ref()?;
let commit = branch.most_recent_commit.as_ref()?.clone();
let workspace = self.workspace.clone();
-
let this = cx.entity();
+
Some(
h_flex()
- .items_center()
- .py_2()
- .px(px(8.))
- .border_color(cx.theme().colors().border)
+ .py_1p5()
+ .px_2()
.gap_1p5()
+ .justify_between()
+ .border_t_1()
+ .border_color(cx.theme().colors().border.opacity(0.8))
.child(
div()
.flex_grow()
.overflow_hidden()
- .items_center()
.max_w(relative(0.85))
- .h_full()
.child(
Label::new(commit.subject.clone())
.size(LabelSize::Small)
@@ -3474,12 +3510,11 @@ impl GitPanel {
}
}),
)
- .child(div().flex_1())
.when(commit.has_parent, |this| {
let has_unstaged = self.has_unstaged_changes();
this.child(
panel_icon_button("undo", IconName::Undo)
- .icon_size(IconSize::Small)
+ .icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip(move |window, cx| {
Tooltip::with_meta(
@@ -3501,43 +3536,38 @@ impl GitPanel {
}
fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
- h_flex()
- .h_full()
- .flex_grow()
- .justify_center()
- .items_center()
- .child(
- v_flex()
- .gap_2()
- .child(h_flex().w_full().justify_around().child(
- if self.active_repository.is_some() {
- "No changes to commit"
- } else {
- "No Git repositories"
- },
- ))
- .children({
- let worktree_count = self.project.read(cx).visible_worktrees(cx).count();
- (worktree_count > 0 && self.active_repository.is_none()).then(|| {
- h_flex().w_full().justify_around().child(
- panel_filled_button("Initialize Repository")
- .tooltip(Tooltip::for_action_title_in(
- "git init",
- &git::Init,
- &self.focus_handle,
- ))
- .on_click(move |_, _, cx| {
- cx.defer(move |cx| {
- cx.dispatch_action(&git::Init);
- })
- }),
- )
- })
+ h_flex().h_full().flex_grow().justify_center().child(
+ v_flex()
+ .gap_2()
+ .child(h_flex().w_full().justify_around().child(
+ if self.active_repository.is_some() {
+ "No changes to commit"
+ } else {
+ "No Git repositories"
+ },
+ ))
+ .children({
+ let worktree_count = self.project.read(cx).visible_worktrees(cx).count();
+ (worktree_count > 0 && self.active_repository.is_none()).then(|| {
+ h_flex().w_full().justify_around().child(
+ panel_filled_button("Initialize Repository")
+ .tooltip(Tooltip::for_action_title_in(
+ "git init",
+ &git::Init,
+ &self.focus_handle,
+ ))
+ .on_click(move |_, _, cx| {
+ cx.defer(move |cx| {
+ cx.dispatch_action(&git::Init);
+ })
+ }),
+ )
})
- .text_ui_sm(cx)
- .mx_auto()
- .text_color(Color::Placeholder.color(cx)),
- )
+ })
+ .text_ui_sm(cx)
+ .mx_auto()
+ .text_color(Color::Placeholder.color(cx)),
+ )
}
fn render_vertical_scrollbar(
@@ -3733,7 +3763,7 @@ impl GitPanel {
for ix in range {
match &this.entries.get(ix) {
- Some(GitListEntry::GitStatusEntry(entry)) => {
+ Some(GitListEntry::Status(entry)) => {
items.push(this.render_entry(
ix,
entry,
@@ -3990,8 +4020,6 @@ impl GitPanel {
let marked = self.marked_entries.contains(&ix);
let status_style = GitPanelSettings::get_global(cx).status_style;
let status = entry.status;
- let modifiers = self.current_modifiers;
- let shift_held = modifiers.shift;
let has_conflict = status.is_conflicted();
let is_modified = status.is_modified();
@@ -4110,12 +4138,6 @@ impl GitPanel {
cx.stop_propagation();
},
)
- // .on_secondary_mouse_down(cx.listener(
- // move |this, event: &MouseDownEvent, window, cx| {
- // this.deploy_entry_context_menu(event.position, ix, window, cx);
- // cx.stop_propagation();
- // },
- // ))
.child(
div()
.id(checkbox_wrapper_id)
@@ -4127,46 +4149,35 @@ impl GitPanel {
.disabled(!has_write_access)
.fill()
.elevation(ElevationIndex::Surface)
- .on_click({
+ .on_click_ext({
let entry = entry.clone();
- cx.listener(move |this, _, window, cx| {
- if !has_write_access {
- return;
- }
- this.toggle_staged_for_entry(
- &GitListEntry::GitStatusEntry(entry.clone()),
- window,
- cx,
- );
- cx.stop_propagation();
- })
+ let this = cx.weak_entity();
+ move |_, click, window, cx| {
+ this.update(cx, |this, cx| {
+ if !has_write_access {
+ return;
+ }
+ if click.modifiers().shift {
+ this.stage_bulk(ix, cx);
+ } else {
+ this.toggle_staged_for_entry(
+ &GitListEntry::Status(entry.clone()),
+ window,
+ cx,
+ );
+ }
+ cx.stop_propagation();
+ })
+ .ok();
+ }
})
.tooltip(move |window, cx| {
let is_staged = entry_staging.is_fully_staged();
let action = if is_staged { "Unstage" } else { "Stage" };
- let tooltip_name = if shift_held {
- format!("{} section", action)
- } else {
- action.to_string()
- };
-
- let meta = if shift_held {
- format!(
- "Release shift to {} single entry",
- action.to_lowercase()
- )
- } else {
- format!("Shift click to {} section", action.to_lowercase())
- };
+ let tooltip_name = action.to_string();
- Tooltip::with_meta(
- tooltip_name,
- Some(&ToggleStaged),
- meta,
- window,
- cx,
- )
+ Tooltip::for_action(tooltip_name, &ToggleStaged, window, cx)
}),
),
)
@@ -4232,6 +4243,41 @@ impl GitPanel {
panel
})
}
+
+ fn stage_bulk(&mut self, mut index: usize, cx: &mut Context<'_, Self>) {
+ let Some(op) = self.bulk_staging.as_ref() else {
+ return;
+ };
+ let Some(mut anchor_index) = self.entry_by_path(&op.anchor, cx) else {
+ return;
+ };
+ if let Some(entry) = self.entries.get(index)
+ && let Some(entry) = entry.status_entry()
+ {
+ self.set_bulk_staging_anchor(entry.repo_path.clone(), cx);
+ }
+ if index < anchor_index {
+ std::mem::swap(&mut index, &mut anchor_index);
+ }
+ let entries = self
+ .entries
+ .get(anchor_index..=index)
+ .unwrap_or_default()
+ .iter()
+ .filter_map(|entry| entry.status_entry().cloned())
+ .collect::<Vec<_>>();
+ self.change_file_stage(true, entries, cx);
+ }
+
+ fn set_bulk_staging_anchor(&mut self, path: RepoPath, cx: &mut Context<'_, GitPanel>) {
+ let Some(repo) = self.active_repository.as_ref() else {
+ return;
+ };
+ self.bulk_staging = Some(BulkStaging {
+ repo_id: repo.read(cx).id,
+ anchor: path,
+ });
+ }
}
fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
@@ -4269,9 +4315,9 @@ impl Render for GitPanel {
.id("git_panel")
.key_context(self.dispatch_context(window, cx))
.track_focus(&self.focus_handle)
- .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.when(has_write_access && !project.is_read_only(cx), |this| {
this.on_action(cx.listener(Self::toggle_staged_for_selected))
+ .on_action(cx.listener(Self::stage_range))
.on_action(cx.listener(GitPanel::commit))
.on_action(cx.listener(GitPanel::amend))
.on_action(cx.listener(GitPanel::cancel))
@@ -4615,7 +4661,7 @@ impl RenderOnce for PanelRepoFooter {
})
.trigger_with_tooltip(
repo_selector_trigger.disabled(single_repo).truncate(true),
- Tooltip::text("Switch active repository"),
+ Tooltip::text("Switch Active Repository"),
)
.anchor(Corner::BottomLeft)
.into_any_element();
@@ -4943,7 +4989,7 @@ impl Component for PanelRepoFooter {
#[cfg(test)]
mod tests {
- use git::status::StatusCode;
+ use git::status::{StatusCode, UnmergedStatus, UnmergedStatusCode};
use gpui::{TestAppContext, VisualTestContext};
use project::{FakeFs, WorktreeSettings};
use serde_json::json;
@@ -5042,13 +5088,13 @@ mod tests {
GitListEntry::Header(GitHeaderEntry {
header: Section::Tracked
}),
- GitListEntry::GitStatusEntry(GitStatusEntry {
+ GitListEntry::Status(GitStatusEntry {
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
repo_path: "crates/gpui/gpui.rs".into(),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
}),
- GitListEntry::GitStatusEntry(GitStatusEntry {
+ GitListEntry::Status(GitStatusEntry {
abs_path: path!("/root/zed/crates/util/util.rs").into(),
repo_path: "crates/util/util.rs".into(),
status: StatusCode::Modified.worktree(),
@@ -5057,54 +5103,6 @@ mod tests {
],
);
- // TODO(cole) restore this once repository deduplication is implemented properly.
- //cx.update_window_entity(&panel, |panel, window, cx| {
- // panel.select_last(&Default::default(), window, cx);
- // assert_eq!(panel.selected_entry, Some(2));
- // panel.open_diff(&Default::default(), window, cx);
- //});
- //cx.run_until_parked();
-
- //let worktree_roots = workspace.update(cx, |workspace, cx| {
- // workspace
- // .worktrees(cx)
- // .map(|worktree| worktree.read(cx).abs_path())
- // .collect::<Vec<_>>()
- //});
- //pretty_assertions::assert_eq!(
- // worktree_roots,
- // vec![
- // Path::new(path!("/root/zed/crates/gpui")).into(),
- // Path::new(path!("/root/zed/crates/util/util.rs")).into(),
- // ]
- //);
-
- //project.update(cx, |project, cx| {
- // let git_store = project.git_store().read(cx);
- // // The repo that comes from the single-file worktree can't be selected through the UI.
- // let filtered_entries = filtered_repository_entries(git_store, cx)
- // .iter()
- // .map(|repo| repo.read(cx).worktree_abs_path.clone())
- // .collect::<Vec<_>>();
- // assert_eq!(
- // filtered_entries,
- // [Path::new(path!("/root/zed/crates/gpui")).into()]
- // );
- // // But we can select it artificially here.
- // let repo_from_single_file_worktree = git_store
- // .repositories()
- // .values()
- // .find(|repo| {
- // repo.read(cx).worktree_abs_path.as_ref()
- // == Path::new(path!("/root/zed/crates/util/util.rs"))
- // })
- // .unwrap()
- // .clone();
-
- // // Paths still make sense when we somehow activate a repo that comes from a single-file worktree.
- // repo_from_single_file_worktree.update(cx, |repo, cx| repo.set_as_active_repository(cx));
- //});
-
let handle = cx.update_window_entity(&panel, |panel, _, _| {
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
});
@@ -5117,13 +5115,13 @@ mod tests {
GitListEntry::Header(GitHeaderEntry {
header: Section::Tracked
}),
- GitListEntry::GitStatusEntry(GitStatusEntry {
+ GitListEntry::Status(GitStatusEntry {
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
repo_path: "crates/gpui/gpui.rs".into(),
status: StatusCode::Modified.worktree(),
staging: StageStatus::Unstaged,
}),
- GitListEntry::GitStatusEntry(GitStatusEntry {
+ GitListEntry::Status(GitStatusEntry {
abs_path: path!("/root/zed/crates/util/util.rs").into(),
repo_path: "crates/util/util.rs".into(),
status: StatusCode::Modified.worktree(),
@@ -5132,4 +5130,196 @@ mod tests {
],
);
}
+
+ #[gpui::test]
+ async fn test_bulk_staging(cx: &mut TestAppContext) {
+ use GitListEntry::*;
+
+ init_test(cx);
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/root",
+ json!({
+ "project": {
+ ".git": {},
+ "src": {
+ "main.rs": "fn main() {}",
+ "lib.rs": "pub fn hello() {}",
+ "utils.rs": "pub fn util() {}"
+ },
+ "tests": {
+ "test.rs": "fn test() {}"
+ },
+ "new_file.txt": "new content",
+ "another_new.rs": "// new file",
+ "conflict.txt": "conflicted content"
+ }
+ }),
+ )
+ .await;
+
+ fs.set_status_for_repo(
+ Path::new(path!("/root/project/.git")),
+ &[
+ (Path::new("src/main.rs"), StatusCode::Modified.worktree()),
+ (Path::new("src/lib.rs"), StatusCode::Modified.worktree()),
+ (Path::new("tests/test.rs"), StatusCode::Modified.worktree()),
+ (Path::new("new_file.txt"), FileStatus::Untracked),
+ (Path::new("another_new.rs"), FileStatus::Untracked),
+ (Path::new("src/utils.rs"), FileStatus::Untracked),
+ (
+ Path::new("conflict.txt"),
+ UnmergedStatus {
+ first_head: UnmergedStatusCode::Updated,
+ second_head: UnmergedStatusCode::Updated,
+ }
+ .into(),
+ ),
+ ],
+ );
+
+ let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await;
+ let workspace =
+ cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+ cx.read(|cx| {
+ project
+ .read(cx)
+ .worktrees(cx)
+ .nth(0)
+ .unwrap()
+ .read(cx)
+ .as_local()
+ .unwrap()
+ .scan_complete()
+ })
+ .await;
+
+ cx.executor().run_until_parked();
+
+ let panel = workspace.update(cx, GitPanel::new).unwrap();
+
+ let handle = cx.update_window_entity(&panel, |panel, _, _| {
+ std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
+ });
+ cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
+ handle.await;
+
+ let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
+ #[rustfmt::skip]
+ pretty_assertions::assert_matches!(
+ entries.as_slice(),
+ &[
+ Header(GitHeaderEntry { header: Section::Conflict }),
+ Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
+ Header(GitHeaderEntry { header: Section::Tracked }),
+ Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
+ Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
+ Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
+ Header(GitHeaderEntry { header: Section::New }),
+ Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
+ Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
+ Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
+ ],
+ );
+
+ let second_status_entry = entries[3].clone();
+ panel.update_in(cx, |panel, window, cx| {
+ panel.toggle_staged_for_entry(&second_status_entry, window, cx);
+ });
+
+ panel.update_in(cx, |panel, window, cx| {
+ panel.selected_entry = Some(7);
+ panel.stage_range(&git::StageRange, window, cx);
+ });
+
+ cx.read(|cx| {
+ project
+ .read(cx)
+ .worktrees(cx)
+ .nth(0)
+ .unwrap()
+ .read(cx)
+ .as_local()
+ .unwrap()
+ .scan_complete()
+ })
+ .await;
+
+ cx.executor().run_until_parked();
+
+ let handle = cx.update_window_entity(&panel, |panel, _, _| {
+ std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
+ });
+ cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
+ handle.await;
+
+ let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
+ #[rustfmt::skip]
+ pretty_assertions::assert_matches!(
+ entries.as_slice(),
+ &[
+ Header(GitHeaderEntry { header: Section::Conflict }),
+ Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
+ Header(GitHeaderEntry { header: Section::Tracked }),
+ Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
+ Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
+ Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
+ Header(GitHeaderEntry { header: Section::New }),
+ Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
+ Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
+ Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
+ ],
+ );
+
+ let third_status_entry = entries[4].clone();
+ panel.update_in(cx, |panel, window, cx| {
+ panel.toggle_staged_for_entry(&third_status_entry, window, cx);
+ });
+
+ panel.update_in(cx, |panel, window, cx| {
+ panel.selected_entry = Some(9);
+ panel.stage_range(&git::StageRange, window, cx);
+ });
+
+ cx.read(|cx| {
+ project
+ .read(cx)
+ .worktrees(cx)
+ .nth(0)
+ .unwrap()
+ .read(cx)
+ .as_local()
+ .unwrap()
+ .scan_complete()
+ })
+ .await;
+
+ cx.executor().run_until_parked();
+
+ let handle = cx.update_window_entity(&panel, |panel, _, _| {
+ std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
+ });
+ cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
+ handle.await;
+
+ let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
+ #[rustfmt::skip]
+ pretty_assertions::assert_matches!(
+ entries.as_slice(),
+ &[
+ Header(GitHeaderEntry { header: Section::Conflict }),
+ Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
+ Header(GitHeaderEntry { header: Section::Tracked }),
+ Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
+ Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
+ Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
+ Header(GitHeaderEntry { header: Section::New }),
+ Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
+ Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
+ Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
+ ],
+ );
+ }
}
@@ -31,7 +31,13 @@ pub mod project_diff;
pub(crate) mod remote_output;
pub mod repository_selector;
-actions!(git, [ResetOnboarding]);
+actions!(
+ git,
+ [
+ /// Resets the git onboarding state to show the tutorial again.
+ ResetOnboarding
+ ]
+);
pub fn init(cx: &mut App) {
GitPanelSettings::register(cx);
@@ -41,7 +41,15 @@ use workspace::{
searchable::SearchableItemHandle,
};
-actions!(git, [Diff, Add]);
+actions!(
+ git,
+ [
+ /// Shows the diff between the working directory and the index.
+ Diff,
+ /// Adds files to the git staging area.
+ Add
+ ]
+);
pub struct ProjectDiff {
project: Entity<Project>,
@@ -120,7 +120,18 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ
}
RemoteAction::Push(branch_name, remote_ref) => {
if output.stderr.contains("* [new branch]") {
- let style = if output.stderr.contains("Create a pull request") {
+ let pr_hints = [
+ // GitHub
+ "Create a pull request",
+ // Bitbucket
+ "Create pull request",
+ // GitLab
+ "create a merge request",
+ ];
+ let style = if pr_hints
+ .iter()
+ .any(|indicator| output.stderr.contains(indicator))
+ {
let finder = LinkFinder::new();
let first_link = finder
.links(&output.stderr)
@@ -154,3 +165,109 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_push_new_branch_pull_request() {
+ let action = RemoteAction::Push(
+ SharedString::new("test_branch"),
+ Remote {
+ name: SharedString::new("test_remote"),
+ },
+ );
+
+ let output = RemoteCommandOutput {
+ stdout: String::new(),
+ stderr: String::from(
+ "
+ Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
+ remote:
+ remote: Create a pull request for 'test' on GitHub by visiting:
+ remote: https://example.com/test/test/pull/new/test
+ remote:
+ To example.com:test/test.git
+ * [new branch] test -> test
+ ",
+ ),
+ };
+
+ let msg = format_output(&action, output);
+
+ if let SuccessStyle::PushPrLink { link } = &msg.style {
+ assert_eq!(link, "https://example.com/test/test/pull/new/test");
+ } else {
+ panic!("Expected PushPrLink variant");
+ }
+ }
+
+ #[test]
+ fn test_push_new_branch_merge_request() {
+ let action = RemoteAction::Push(
+ SharedString::new("test_branch"),
+ Remote {
+ name: SharedString::new("test_remote"),
+ },
+ );
+
+ let output = RemoteCommandOutput {
+ stdout: String::new(),
+ stderr: String::from("
+ Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
+ remote:
+ remote: To create a merge request for test, visit:
+ remote: https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test
+ remote:
+ To example.com:test/test.git
+ * [new branch] test -> test
+ "),
+ };
+
+ let msg = format_output(&action, output);
+
+ if let SuccessStyle::PushPrLink { link } = &msg.style {
+ assert_eq!(
+ link,
+ "https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test"
+ );
+ } else {
+ panic!("Expected PushPrLink variant");
+ }
+ }
+
+ #[test]
+ fn test_push_new_branch_no_link() {
+ let action = RemoteAction::Push(
+ SharedString::new("test_branch"),
+ Remote {
+ name: SharedString::new("test_remote"),
+ },
+ );
+
+ let output = RemoteCommandOutput {
+ stdout: String::new(),
+ stderr: String::from(
+ "
+ To http://example.com/test/test.git
+ * [new branch] test -> test
+ ",
+ ),
+ };
+
+ let msg = format_output(&action, output);
+
+ if let SuccessStyle::ToastWithLog { output } = &msg.style {
+ assert_eq!(
+ output.stderr,
+ "
+ To http://example.com/test/test.git
+ * [new branch] test -> test
+ "
+ );
+ } else {
+ panic!("Expected ToastWithLog variant");
+ }
+ }
+}
@@ -66,6 +66,7 @@ x11 = [
"x11-clipboard",
"filedescriptor",
"open",
+ "scap?/x11",
]
screen-capture = [
"scap",
@@ -150,6 +151,9 @@ metal.workspace = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies]
pathfinder_geometry = "0.5"
+[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies]
+scap = { workspace = true, optional = true }
+
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
# Always used
flume = "0.11"
@@ -168,7 +172,6 @@ cosmic-text = { version = "0.14.0", optional = true }
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5474cfad4b719a72ec8ed2cb7327b2b01fd10568", features = [
"source-fontconfig-dlopen",
], optional = true }
-scap = { workspace = true, optional = true }
calloop = { version = "0.13.0" }
filedescriptor = { version = "0.8.2", optional = true }
@@ -220,7 +223,7 @@ blade-macros.workspace = true
flume = "0.11"
rand.workspace = true
windows.workspace = true
-windows-core = "0.61"
+windows-core.workspace = true
windows-numerics = "0.2"
windows-registry = "0.5"
@@ -487,7 +487,7 @@ impl Element for TextElement {
let font_size = style.font_size.to_pixels(window.rem_size());
let line = window
.text_system()
- .shape_line(display_text, font_size, &runs);
+ .shape_line(display_text, font_size, &runs, None);
let cursor_pos = line.x_for_index(cursor);
let (selection, cursor) = if selected_range.is_empty() {
@@ -156,6 +156,10 @@ impl Render for Shadow {
.w_full()
.children(vec![
example("None", Shadow::base()),
+ // 2Xsmall shadow
+ example("2X Small", Shadow::base().shadow_2xs()),
+ // Xsmall shadow
+ example("Extra Small", Shadow::base().shadow_xs()),
// Small shadow
example("Small", Shadow::base().shadow_sm()),
// Medium shadow
@@ -150,6 +150,15 @@ pub trait Action: Any + Send {
{
None
}
+
+ /// The documentation for this action, if any. When using the derive macro for actions
+ /// this will be automatically generated from the doc comments on the action struct.
+ fn documentation() -> Option<&'static str>
+ where
+ Self: Sized,
+ {
+ None
+ }
}
impl std::fmt::Debug for dyn Action {
@@ -216,6 +225,7 @@ pub(crate) struct ActionRegistry {
all_names: Vec<&'static str>, // So we can return a static slice.
deprecated_aliases: HashMap<&'static str, &'static str>, // deprecated name -> preferred name
deprecation_messages: HashMap<&'static str, &'static str>, // action name -> deprecation message
+ documentation: HashMap<&'static str, &'static str>, // action name -> documentation
}
impl Default for ActionRegistry {
@@ -223,6 +233,7 @@ impl Default for ActionRegistry {
let mut this = ActionRegistry {
by_name: Default::default(),
names_by_type_id: Default::default(),
+ documentation: Default::default(),
all_names: Default::default(),
deprecated_aliases: Default::default(),
deprecation_messages: Default::default(),
@@ -254,6 +265,7 @@ pub struct MacroActionData {
pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option<schemars::Schema>,
pub deprecated_aliases: &'static [&'static str],
pub deprecation_message: Option<&'static str>,
+ pub documentation: Option<&'static str>,
}
inventory::collect!(MacroActionBuilder);
@@ -276,6 +288,7 @@ impl ActionRegistry {
json_schema: A::action_json_schema,
deprecated_aliases: A::deprecated_aliases(),
deprecation_message: A::deprecation_message(),
+ documentation: A::documentation(),
});
}
@@ -316,6 +329,9 @@ impl ActionRegistry {
if let Some(deprecation_msg) = action.deprecation_message {
self.deprecation_messages.insert(name, deprecation_msg);
}
+ if let Some(documentation) = action.documentation {
+ self.documentation.insert(name, documentation);
+ }
}
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
@@ -377,18 +393,20 @@ impl ActionRegistry {
pub fn deprecation_messages(&self) -> &HashMap<&'static str, &'static str> {
&self.deprecation_messages
}
+
+ pub fn documentation(&self) -> &HashMap<&'static str, &'static str> {
+ &self.documentation
+ }
}
/// Generate a list of all the registered actions.
/// Useful for transforming the list of available actions into a
/// format suited for static analysis such as in validating keymaps, or
/// generating documentation.
-pub fn generate_list_of_all_registered_actions() -> Vec<MacroActionData> {
- let mut actions = Vec::new();
- for builder in inventory::iter::<MacroActionBuilder> {
- actions.push(builder.0());
- }
- actions
+pub fn generate_list_of_all_registered_actions() -> impl Iterator<Item = MacroActionData> {
+ inventory::iter::<MacroActionBuilder>
+ .into_iter()
+ .map(|builder| builder.0())
}
mod no_action {
@@ -272,6 +272,7 @@ pub struct App {
// TypeId is the type of the event that the listener callback expects
pub(crate) event_listeners: SubscriberSet<EntityId, (TypeId, Listener)>,
pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>,
+ pub(crate) keystroke_interceptors: SubscriberSet<(), KeystrokeObserver>,
pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>,
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
@@ -344,6 +345,7 @@ impl App {
event_listeners: SubscriberSet::new(),
release_listeners: SubscriberSet::new(),
keystroke_observers: SubscriberSet::new(),
+ keystroke_interceptors: SubscriberSet::new(),
keyboard_layout_observers: SubscriberSet::new(),
global_observers: SubscriberSet::new(),
quit_observers: SubscriberSet::new(),
@@ -1248,11 +1250,7 @@ impl App {
.downcast::<T>()
.unwrap()
.update(cx, |entity_state, cx| {
- if let Some(window) = window {
- on_new(entity_state, Some(window), cx);
- } else {
- on_new(entity_state, None, cx);
- }
+ on_new(entity_state, window.as_deref_mut(), cx)
})
},
),
@@ -1322,6 +1320,32 @@ impl App {
)
}
+ /// Register a callback to be invoked when a keystroke is received by the application
+ /// in any window. Note that this fires _before_ all other action and event mechanisms have resolved
+ /// unlike [`App::observe_keystrokes`] which fires after. This means that `cx.stop_propagation` calls
+ /// within interceptors will prevent action dispatch
+ pub fn intercept_keystrokes(
+ &mut self,
+ mut f: impl FnMut(&KeystrokeEvent, &mut Window, &mut App) + 'static,
+ ) -> Subscription {
+ fn inner(
+ keystroke_interceptors: &SubscriberSet<(), KeystrokeObserver>,
+ handler: KeystrokeObserver,
+ ) -> Subscription {
+ let (subscription, activate) = keystroke_interceptors.insert((), handler);
+ activate();
+ subscription
+ }
+
+ inner(
+ &mut self.keystroke_interceptors,
+ Box::new(move |event, window, cx| {
+ f(event, window, cx);
+ true
+ }),
+ )
+ }
+
/// Register key bindings.
pub fn bind_keys(&mut self, bindings: impl IntoIterator<Item = KeyBinding>) {
self.keymap.borrow_mut().add_bindings(bindings);
@@ -1403,11 +1427,16 @@ impl App {
self.actions.deprecated_aliases()
}
- /// Get a list of all action deprecation messages.
+ /// Get a map from an action name to the deprecation messages.
pub fn action_deprecation_messages(&self) -> &HashMap<&'static str, &'static str> {
self.actions.deprecation_messages()
}
+ /// Get a map from an action name to the documentation.
+ pub fn action_documentation(&self) -> &HashMap<&'static str, &'static str> {
+ self.actions.documentation()
+ }
+
/// Register a callback to be invoked when the application is about to quit.
/// It is not possible to cancel the quit event at this point.
pub fn on_app_quit<Fut>(
@@ -12,18 +12,13 @@ use std::{
/// Convert an RGB hex color code number to a color type
pub fn rgb(hex: u32) -> Rgba {
- 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 [_, r, g, b] = hex.to_be_bytes().map(|b| (b as f32) / 255.0);
Rgba { r, g, b, a: 1.0 }
}
/// Convert an RGBA hex color code number to [`Rgba`]
pub fn rgba(hex: u32) -> Rgba {
- let r = ((hex >> 24) & 0xFF) as f32 / 255.0;
- let g = ((hex >> 16) & 0xFF) as f32 / 255.0;
- let b = ((hex >> 8) & 0xFF) as f32 / 255.0;
- let a = (hex & 0xFF) as f32 / 255.0;
+ let [r, g, b, a] = hex.to_be_bytes().map(|b| (b as f32) / 255.0);
Rgba { r, g, b, a }
}
@@ -63,14 +58,14 @@ impl Rgba {
if other.a >= 1.0 {
other
} else if other.a <= 0.0 {
- return *self;
+ *self
} else {
- return Rgba {
+ Rgba {
r: (self.r * (1.0 - other.a)) + (other.r * other.a),
g: (self.g * (1.0 - other.a)) + (other.g * other.a),
b: (self.b * (1.0 - other.a)) + (other.b * other.a),
a: self.a,
- };
+ }
}
}
}
@@ -494,12 +489,12 @@ impl Hsla {
if alpha >= 1.0 {
other
} else if alpha <= 0.0 {
- return self;
+ self
} else {
let converted_self = Rgba::from(self);
let converted_other = Rgba::from(other);
let blended_rgb = converted_self.blend(converted_other);
- return Hsla::from(blended_rgb);
+ Hsla::from(blended_rgb)
}
}
@@ -903,7 +903,7 @@ pub trait InteractiveElement: Sized {
/// Apply the given style when the given data type is dragged over this element
fn drag_over<S: 'static>(
mut self,
- f: impl 'static + Fn(StyleRefinement, &S, &Window, &App) -> StyleRefinement,
+ f: impl 'static + Fn(StyleRefinement, &S, &mut Window, &mut App) -> StyleRefinement,
) -> Self {
self.interactivity().drag_over_styles.push((
TypeId::of::<S>(),
@@ -7,8 +7,8 @@
use crate::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId,
Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
- ListSizingBehavior, Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled, Window,
- point, size,
+ ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, StyleRefinement, Styled,
+ Window, point, size,
};
use smallvec::SmallVec;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -88,6 +88,8 @@ pub enum ScrollStrategy {
/// May not be possible if there's not enough list items above the item scrolled to:
/// in this case, the element will be placed at the closest possible position.
Center,
+ /// Scrolls the element to be at the given item index from the top of the viewport.
+ ToPosition(usize),
}
#[derive(Clone, Debug, Default)]
@@ -140,6 +142,15 @@ impl UniformListScrollHandle {
.map(|(ix, _)| ix)
.unwrap_or_else(|| this.base_handle.logical_scroll_top().0)
}
+
+ /// Checks if the list can be scrolled vertically.
+ pub fn is_scrollable(&self) -> bool {
+ if let Some(size) = self.0.borrow().last_item_size {
+ size.contents.height > size.item.height
+ } else {
+ false
+ }
+ }
}
impl Styled for UniformList {
@@ -345,6 +356,15 @@ impl Element for UniformList {
}
}
}
+ ScrollStrategy::ToPosition(sticky_index) => {
+ let target_y_in_viewport = item_height * sticky_index;
+ let target_scroll_top = item_top - target_y_in_viewport;
+ let max_scroll_top =
+ (content_height - list_height).max(Pixels::ZERO);
+ let new_scroll_top =
+ target_scroll_top.clamp(Pixels::ZERO, max_scroll_top);
+ updated_scroll_offset.y = -new_scroll_top;
+ }
}
scroll_offset = *updated_scroll_offset
}
@@ -354,6 +374,7 @@ impl Element for UniformList {
let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
/ item_height)
.ceil() as usize;
+
let visible_range = first_visible_element_ix
..cmp::min(last_visible_element_ix, self.item_count);
@@ -409,6 +430,7 @@ impl Element for UniformList {
let mut decoration = decoration.as_ref().compute(
visible_range.clone(),
bounds,
+ scroll_offset,
item_height,
self.item_count,
window,
@@ -476,6 +498,7 @@ pub trait UniformListDecoration {
&self,
visible_range: Range<usize>,
bounds: Bounds<Pixels>,
+ scroll_offset: Point<Pixels>,
item_height: Pixels,
item_count: usize,
window: &mut Window,
@@ -26,8 +26,13 @@ mod windows;
#[cfg(all(
feature = "screen-capture",
- any(target_os = "linux", target_os = "freebsd"),
- any(feature = "wayland", feature = "x11"),
+ any(
+ target_os = "windows",
+ all(
+ any(target_os = "linux", target_os = "freebsd"),
+ any(feature = "wayland", feature = "x11"),
+ )
+ )
))]
pub(crate) mod scap_screen_capture;
@@ -13,6 +13,9 @@ pub struct Keystroke {
/// key is the character printed on the key that was pressed
/// e.g. for option-s, key is "s"
+ /// On layouts that do not have ascii keys (e.g. Thai)
+ /// this will be the ASCII-equivalent character (q instead of ๆ),
+ /// and the typed character will be present in key_char.
pub key: String,
/// key_char is the character that could have been typed when
@@ -55,7 +58,7 @@ impl Keystroke {
///
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
/// both possibilities for self against the target.
- pub(crate) fn should_match(&self, target: &Keystroke) -> bool {
+ pub fn should_match(&self, target: &Keystroke) -> bool {
#[cfg(not(target_os = "windows"))]
if let Some(key_char) = self
.key_char
@@ -200,8 +200,8 @@ impl<P: LinuxClient + 'static> Platform for P {
app_path = app_path.display()
);
- // execute the script using /bin/bash
- let restart_process = Command::new("/bin/bash")
+ let restart_process = Command::new("/usr/bin/env")
+ .arg("bash")
.arg("-c")
.arg(script)
.process_group(0)
@@ -706,6 +706,60 @@ pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) {
}
}
+#[cfg(any(feature = "wayland", feature = "x11"))]
+fn guess_ascii(keycode: Keycode, shift: bool) -> Option<char> {
+ let c = match (keycode.raw(), shift) {
+ (24, _) => 'q',
+ (25, _) => 'w',
+ (26, _) => 'e',
+ (27, _) => 'r',
+ (28, _) => 't',
+ (29, _) => 'y',
+ (30, _) => 'u',
+ (31, _) => 'i',
+ (32, _) => 'o',
+ (33, _) => 'p',
+ (34, false) => '[',
+ (34, true) => '{',
+ (35, false) => ']',
+ (35, true) => '}',
+ (38, _) => 'a',
+ (39, _) => 's',
+ (40, _) => 'd',
+ (41, _) => 'f',
+ (42, _) => 'g',
+ (43, _) => 'h',
+ (44, _) => 'j',
+ (45, _) => 'k',
+ (46, _) => 'l',
+ (47, false) => ';',
+ (47, true) => ':',
+ (48, false) => '\'',
+ (48, true) => '"',
+ (49, false) => '`',
+ (49, true) => '~',
+ (51, false) => '\\',
+ (51, true) => '|',
+ (52, _) => 'z',
+ (53, _) => 'x',
+ (54, _) => 'c',
+ (55, _) => 'v',
+ (56, _) => 'b',
+ (57, _) => 'n',
+ (58, _) => 'm',
+ (59, false) => ',',
+ (59, true) => '>',
+ (60, false) => '.',
+ (60, true) => '<',
+ (61, false) => '/',
+ (61, true) => '?',
+
+ _ => return None,
+ };
+
+ Some(c)
+}
+
#[cfg(any(feature = "wayland", feature = "x11"))]
impl crate::Keystroke {
pub(super) fn from_xkb(
@@ -773,6 +827,8 @@ impl crate::Keystroke {
let name = xkb::keysym_get_name(key_sym).to_lowercase();
if key_sym.is_keypad_key() {
name.replace("kp_", "")
+ } else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) {
+ String::from(key_en)
} else {
name
}
@@ -77,6 +77,8 @@ pub(crate) const XINPUT_ALL_DEVICES: xinput::DeviceId = 0;
/// terminology is both archaic and unclear.
pub(crate) const XINPUT_ALL_DEVICE_GROUPS: xinput::DeviceId = 1;
+const GPUI_X11_SCALE_FACTOR_ENV: &str = "GPUI_X11_SCALE_FACTOR";
+
pub(crate) struct WindowRef {
window: X11WindowStatePtr,
refresh_state: Option<RefreshState>,
@@ -424,12 +426,7 @@ impl X11Client {
let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection)
.context("Failed to create resource database")?;
- let scale_factor = resource_database
- .get_value("Xft.dpi", "Xft.dpi")
- .ok()
- .flatten()
- .map(|dpi: f32| dpi / 96.0)
- .unwrap_or(1.0);
+ let scale_factor = get_scale_factor(&xcb_connection, &resource_database, x_root_index);
let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database)
.context("Failed to initialize cursor theme handler")?
.reply()
@@ -2272,3 +2269,253 @@ fn create_invisible_cursor(
xcb_flush(connection);
Ok(cursor)
}
+
+enum DpiMode {
+ Randr,
+ Scale(f32),
+ NotSet,
+}
+
+fn get_scale_factor(
+ connection: &XCBConnection,
+ resource_database: &Database,
+ screen_index: usize,
+) -> f32 {
+ let env_dpi = std::env::var(GPUI_X11_SCALE_FACTOR_ENV)
+ .ok()
+ .map(|var| {
+ if var.to_lowercase() == "randr" {
+ DpiMode::Randr
+ } else if let Ok(scale) = var.parse::<f32>() {
+ if valid_scale_factor(scale) {
+ DpiMode::Scale(scale)
+ } else {
+ panic!(
+ "`{}` must be a positive normal number or `randr`. Got `{}`",
+ GPUI_X11_SCALE_FACTOR_ENV, var
+ );
+ }
+ } else if var.is_empty() {
+ DpiMode::NotSet
+ } else {
+ panic!(
+ "`{}` must be a positive number or `randr`. Got `{}`",
+ GPUI_X11_SCALE_FACTOR_ENV, var
+ );
+ }
+ })
+ .unwrap_or(DpiMode::NotSet);
+
+ match env_dpi {
+ DpiMode::Scale(scale) => {
+ log::info!(
+ "Using scale factor from {}: {}",
+ GPUI_X11_SCALE_FACTOR_ENV,
+ scale
+ );
+ return scale;
+ }
+ DpiMode::Randr => {
+ if let Some(scale) = get_randr_scale_factor(connection, screen_index) {
+ log::info!(
+ "Using RandR scale factor from {}=randr: {}",
+ GPUI_X11_SCALE_FACTOR_ENV,
+ scale
+ );
+ return scale;
+ }
+ log::warn!("Failed to calculate RandR scale factor, falling back to default");
+ return 1.0;
+ }
+ DpiMode::NotSet => {}
+ }
+
+ // TODO: Use scale factor from XSettings here
+
+ if let Some(dpi) = resource_database
+ .get_value::<f32>("Xft.dpi", "Xft.dpi")
+ .ok()
+ .flatten()
+ {
+ let scale = dpi / 96.0; // base dpi
+ log::info!("Using scale factor from Xft.dpi: {}", scale);
+ return scale;
+ }
+
+ if let Some(scale) = get_randr_scale_factor(connection, screen_index) {
+ log::info!("Using RandR scale factor: {}", scale);
+ return scale;
+ }
+
+ log::info!("Using default scale factor: 1.0");
+ 1.0
+}
+
+fn get_randr_scale_factor(connection: &XCBConnection, screen_index: usize) -> Option<f32> {
+ let root = connection.setup().roots.get(screen_index)?.root;
+
+ let version_cookie = connection.randr_query_version(1, 6).ok()?;
+ let version_reply = version_cookie.reply().ok()?;
+ if version_reply.major_version < 1
+ || (version_reply.major_version == 1 && version_reply.minor_version < 5)
+ {
+ return legacy_get_randr_scale_factor(connection, root); // for randr <1.5
+ }
+
+ let monitors_cookie = connection.randr_get_monitors(root, true).ok()?; // true for active only
+ let monitors_reply = monitors_cookie.reply().ok()?;
+
+ let mut fallback_scale: Option<f32> = None;
+ for monitor in monitors_reply.monitors {
+ if monitor.width_in_millimeters == 0 || monitor.height_in_millimeters == 0 {
+ continue;
+ }
+ let scale_factor = get_dpi_factor(
+ (monitor.width as u32, monitor.height as u32),
+ (
+ monitor.width_in_millimeters as u64,
+ monitor.height_in_millimeters as u64,
+ ),
+ );
+ if monitor.primary {
+ return Some(scale_factor);
+ } else if fallback_scale.is_none() {
+ fallback_scale = Some(scale_factor);
+ }
+ }
+
+ fallback_scale
+}
+
+fn legacy_get_randr_scale_factor(connection: &XCBConnection, root: u32) -> Option<f32> {
+ let primary_cookie = connection.randr_get_output_primary(root).ok()?;
+ let primary_reply = primary_cookie.reply().ok()?;
+ let primary_output = primary_reply.output;
+
+ let primary_output_cookie = connection
+ .randr_get_output_info(primary_output, x11rb::CURRENT_TIME)
+ .ok()?;
+ let primary_output_info = primary_output_cookie.reply().ok()?;
+
+ // try primary
+ if primary_output_info.connection == randr::Connection::CONNECTED
+ && primary_output_info.mm_width > 0
+ && primary_output_info.mm_height > 0
+ && primary_output_info.crtc != 0
+ {
+ let crtc_cookie = connection
+ .randr_get_crtc_info(primary_output_info.crtc, x11rb::CURRENT_TIME)
+ .ok()?;
+ let crtc_info = crtc_cookie.reply().ok()?;
+
+ if crtc_info.width > 0 && crtc_info.height > 0 {
+ let scale_factor = get_dpi_factor(
+ (crtc_info.width as u32, crtc_info.height as u32),
+ (
+ primary_output_info.mm_width as u64,
+ primary_output_info.mm_height as u64,
+ ),
+ );
+ return Some(scale_factor);
+ }
+ }
+
+ // fallback: full scan
+ let resources_cookie = connection.randr_get_screen_resources_current(root).ok()?;
+ let screen_resources = resources_cookie.reply().ok()?;
+
+ let mut crtc_cookies = Vec::with_capacity(screen_resources.crtcs.len());
+ for &crtc in &screen_resources.crtcs {
+ if let Ok(cookie) = connection.randr_get_crtc_info(crtc, x11rb::CURRENT_TIME) {
+ crtc_cookies.push((crtc, cookie));
+ }
+ }
+
+ let mut crtc_infos: HashMap<randr::Crtc, randr::GetCrtcInfoReply> = HashMap::default();
+ let mut valid_outputs: HashSet<randr::Output> = HashSet::new();
+ for (crtc, cookie) in crtc_cookies {
+ if let Ok(reply) = cookie.reply() {
+ if reply.width > 0 && reply.height > 0 && !reply.outputs.is_empty() {
+ crtc_infos.insert(crtc, reply.clone());
+ valid_outputs.extend(&reply.outputs);
+ }
+ }
+ }
+
+ if valid_outputs.is_empty() {
+ return None;
+ }
+
+ let mut output_cookies = Vec::with_capacity(valid_outputs.len());
+ for &output in &valid_outputs {
+ if let Ok(cookie) = connection.randr_get_output_info(output, x11rb::CURRENT_TIME) {
+ output_cookies.push((output, cookie));
+ }
+ }
+ let mut output_infos: HashMap<randr::Output, randr::GetOutputInfoReply> = HashMap::default();
+ for (output, cookie) in output_cookies {
+ if let Ok(reply) = cookie.reply() {
+ output_infos.insert(output, reply);
+ }
+ }
+
+ let mut fallback_scale: Option<f32> = None;
+ for crtc_info in crtc_infos.values() {
+ for &output in &crtc_info.outputs {
+ if let Some(output_info) = output_infos.get(&output) {
+ if output_info.connection != randr::Connection::CONNECTED {
+ continue;
+ }
+
+ if output_info.mm_width == 0 || output_info.mm_height == 0 {
+ continue;
+ }
+
+ let scale_factor = get_dpi_factor(
+ (crtc_info.width as u32, crtc_info.height as u32),
+ (output_info.mm_width as u64, output_info.mm_height as u64),
+ );
+
+ if output != primary_output && fallback_scale.is_none() {
+ fallback_scale = Some(scale_factor);
+ }
+ }
+ }
+ }
+
+ fallback_scale
+}
+
+fn get_dpi_factor((width_px, height_px): (u32, u32), (width_mm, height_mm): (u64, u64)) -> f32 {
+ let ppmm = ((width_px as f64 * height_px as f64) / (width_mm as f64 * height_mm as f64)).sqrt(); // pixels per mm
+
+ const MM_PER_INCH: f64 = 25.4;
+ const BASE_DPI: f64 = 96.0;
+ const QUANTIZE_STEP: f64 = 12.0; // e.g. 1.25 = 15/12, 1.5 = 18/12, 1.75 = 21/12, 2.0 = 24/12
+ const MIN_SCALE: f64 = 1.0;
+ const MAX_SCALE: f64 = 20.0;
+
+ let dpi_factor =
+ ((ppmm * (QUANTIZE_STEP * MM_PER_INCH / BASE_DPI)).round() / QUANTIZE_STEP).max(MIN_SCALE);
+
+ let validated_factor = if dpi_factor <= MAX_SCALE {
+ dpi_factor
+ } else {
+ MIN_SCALE
+ };
+
+ if valid_scale_factor(validated_factor as f32) {
+ validated_factor as f32
+ } else {
+ log::warn!(
+ "Calculated DPI factor {} is invalid, using 1.0",
+ validated_factor
+ );
+ 1.0
+ }
+}
+
+#[inline]
+fn valid_scale_factor(scale_factor: f32) -> bool {
+ scale_factor.is_sign_positive() && scale_factor.is_normal()
+}
@@ -7,7 +7,7 @@ use super::{
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
- MacDisplay, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
+ MacDisplay, MacWindow, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, PlatformDisplay,
PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task,
WindowAppearance, WindowParams, hash,
};
@@ -170,6 +170,7 @@ pub(crate) struct MacPlatformState {
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
finish_launching: Option<Box<dyn FnOnce()>>,
dock_menu: Option<id>,
+ menus: Option<Vec<OwnedMenu>>,
}
impl Default for MacPlatform {
@@ -207,6 +208,7 @@ impl MacPlatform {
finish_launching: None,
dock_menu: None,
on_keyboard_layout_change: None,
+ menus: None,
}))
}
@@ -226,7 +228,7 @@ impl MacPlatform {
unsafe fn create_menu_bar(
&self,
- menus: Vec<Menu>,
+ menus: &Vec<Menu>,
delegate: id,
actions: &mut Vec<Box<dyn Action>>,
keymap: &Keymap,
@@ -241,7 +243,7 @@ impl MacPlatform {
menu.setTitle_(menu_title);
menu.setDelegate_(delegate);
- for item_config in menu_config.items {
+ for item_config in &menu_config.items {
menu.addItem_(Self::create_menu_item(
item_config,
delegate,
@@ -277,7 +279,7 @@ impl MacPlatform {
dock_menu.setDelegate_(delegate);
for item_config in menu_items {
dock_menu.addItem_(Self::create_menu_item(
- item_config,
+ &item_config,
delegate,
actions,
keymap,
@@ -289,7 +291,7 @@ impl MacPlatform {
}
unsafe fn create_menu_item(
- item: MenuItem,
+ item: &MenuItem,
delegate: id,
actions: &mut Vec<Box<dyn Action>>,
keymap: &Keymap,
@@ -399,7 +401,7 @@ impl MacPlatform {
let tag = actions.len() as NSInteger;
let _: () = msg_send![item, setTag: tag];
- actions.push(action);
+ actions.push(action.boxed_clone());
item
}
MenuItem::Submenu(Menu { name, items }) => {
@@ -865,10 +867,15 @@ impl Platform for MacPlatform {
let app: id = msg_send![APP_CLASS, sharedApplication];
let mut state = self.0.lock();
let actions = &mut state.menu_actions;
- let menu = self.create_menu_bar(menus, NSWindow::delegate(app), actions, keymap);
+ let menu = self.create_menu_bar(&menus, NSWindow::delegate(app), actions, keymap);
drop(state);
app.setMainMenu_(menu);
}
+ self.0.lock().menus = Some(menus.into_iter().map(|menu| menu.owned()).collect());
+ }
+
+ fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
+ self.0.lock().menus.clone()
}
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {
@@ -26,4 +26,7 @@ pub(crate) use wrapper::*;
pub(crate) use windows::Win32::Foundation::HWND;
+#[cfg(feature = "screen-capture")]
+pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
+#[cfg(not(feature = "screen-capture"))]
pub(crate) type PlatformScreenCaptureFrame = ();
@@ -93,7 +93,7 @@ pub(crate) fn handle_msg(
WM_IME_STARTCOMPOSITION => handle_ime_position(handle, state_ptr),
WM_IME_COMPOSITION => handle_ime_composition(handle, lparam, state_ptr),
WM_SETCURSOR => handle_set_cursor(handle, lparam, state_ptr),
- WM_SETTINGCHANGE => handle_system_settings_changed(handle, lparam, state_ptr),
+ WM_SETTINGCHANGE => handle_system_settings_changed(handle, wparam, lparam, state_ptr),
WM_INPUTLANGCHANGE => handle_input_language_changed(lparam, state_ptr),
WM_GPUI_CURSOR_STYLE_CHANGED => handle_cursor_changed(lparam, state_ptr),
_ => None,
@@ -466,12 +466,7 @@ fn handle_keyup_msg(
}
fn handle_char_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
- let Some(input) = char::from_u32(wparam.0 as u32)
- .filter(|c| !c.is_control())
- .map(String::from)
- else {
- return Some(1);
- };
+ let input = parse_char_message(wparam, &state_ptr)?;
with_input_handler(&state_ptr, |input_handler| {
input_handler.replace_text_in_range(None, &input);
});
@@ -1152,37 +1147,23 @@ fn handle_set_cursor(
fn handle_system_settings_changed(
handle: HWND,
+ wparam: WPARAM,
lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
- let mut lock = state_ptr.state.borrow_mut();
- let display = lock.display;
- // system settings
- lock.system_settings.update(display);
- // mouse double click
- lock.click_state.system_update();
- // window border offset
- lock.border_offset.update(handle).log_err();
- drop(lock);
-
- // lParam is a pointer to a string that indicates the area containing the system parameter
- // that was changed.
- let parameter = PCWSTR::from_raw(lparam.0 as _);
- if unsafe { !parameter.is_null() && !parameter.is_empty() } {
- if let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() {
- log::info!("System settings changed: {}", parameter_string);
- match parameter_string.as_str() {
- "ImmersiveColorSet" => {
- handle_system_theme_changed(handle, state_ptr);
- }
- _ => {}
- }
- }
- }
-
+ if wparam.0 != 0 {
+ let mut lock = state_ptr.state.borrow_mut();
+ let display = lock.display;
+ lock.system_settings.update(display, wparam.0);
+ lock.click_state.system_update(wparam.0);
+ lock.border_offset.update(handle).log_err();
+ } else {
+ handle_system_theme_changed(handle, lparam, state_ptr)?;
+ };
// Force to trigger WM_NCCALCSIZE event to ensure that we handle auto hide
// taskbar correctly.
notify_frame_changed(handle);
+
Some(0)
}
@@ -1199,17 +1180,34 @@ fn handle_system_command(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -
fn handle_system_theme_changed(
handle: HWND,
+ lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
- let mut callback = state_ptr
- .state
- .borrow_mut()
- .callbacks
- .appearance_changed
- .take()?;
- callback();
- state_ptr.state.borrow_mut().callbacks.appearance_changed = Some(callback);
- configure_dwm_dark_mode(handle);
+ // lParam is a pointer to a string that indicates the area containing the system parameter
+ // that was changed.
+ let parameter = PCWSTR::from_raw(lparam.0 as _);
+ if unsafe { !parameter.is_null() && !parameter.is_empty() } {
+ if let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() {
+ log::info!("System settings changed: {}", parameter_string);
+ match parameter_string.as_str() {
+ "ImmersiveColorSet" => {
+ let new_appearance = system_appearance()
+ .context("unable to get system appearance when handling ImmersiveColorSet")
+ .log_err()?;
+ let mut lock = state_ptr.state.borrow_mut();
+ if new_appearance != lock.appearance {
+ lock.appearance = new_appearance;
+ let mut callback = lock.callbacks.appearance_changed.take()?;
+ drop(lock);
+ callback();
+ state_ptr.state.borrow_mut().callbacks.appearance_changed = Some(callback);
+ configure_dwm_dark_mode(handle, new_appearance);
+ }
+ }
+ _ => {}
+ }
+ }
+ }
Some(0)
}
@@ -1225,6 +1223,38 @@ fn handle_input_language_changed(
Some(0)
}
+#[inline]
+fn parse_char_message(wparam: WPARAM, state_ptr: &Rc<WindowsWindowStatePtr>) -> Option<String> {
+ let code_point = wparam.loword();
+ let mut lock = state_ptr.state.borrow_mut();
+ // https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G2630
+ match code_point {
+ 0xD800..=0xDBFF => {
+ // High surrogate, wait for low surrogate
+ lock.pending_surrogate = Some(code_point);
+ None
+ }
+ 0xDC00..=0xDFFF => {
+ if let Some(high_surrogate) = lock.pending_surrogate.take() {
+ // Low surrogate, combine with pending high surrogate
+ String::from_utf16(&[high_surrogate, code_point]).ok()
+ } else {
+ // Invalid low surrogate without a preceding high surrogate
+ log::warn!(
+ "Received low surrogate without a preceding high surrogate: {code_point:x}"
+ );
+ None
+ }
+ }
+ _ => {
+ lock.pending_surrogate = None;
+ char::from_u32(code_point as u32)
+ .filter(|c| !c.is_control())
+ .map(|c| c.to_string())
+ }
+ }
+}
+
#[inline]
fn translate_message(handle: HWND, wparam: WPARAM, lparam: LPARAM) {
let msg = MSG {
@@ -1267,6 +1297,10 @@ where
capslock: current_capslock(),
}))
}
+ VK_PACKET => {
+ translate_message(handle, wparam, lparam);
+ None
+ }
VK_CAPITAL => {
let capslock = current_capslock();
if state
@@ -130,11 +130,13 @@ pub(crate) fn generate_key_char(
let mut buffer = [0; 8];
let len = unsafe { ToUnicode(vkey.0 as u32, scan_code, Some(&state), &mut buffer, 1 << 2) };
- if len > 0 {
- let candidate = String::from_utf16_lossy(&buffer[..len as usize]);
- if !candidate.is_empty() && !candidate.chars().next().unwrap().is_control() {
- return Some(candidate);
- }
+ match len {
+ len if len > 0 => String::from_utf16(&buffer[..len as usize])
+ .ok()
+ .filter(|candidate| {
+ !candidate.is_empty() && !candidate.chars().next().unwrap().is_control()
+ }),
+ len if len < 0 => String::from_utf16(&buffer[..(-len as usize)]).ok(),
+ _ => None,
}
- None
}
@@ -434,16 +434,14 @@ impl Platform for WindowsPlatform {
#[cfg(feature = "screen-capture")]
fn is_screen_capture_supported(&self) -> bool {
- false
+ true
}
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
- let (mut tx, rx) = oneshot::channel();
- tx.send(Err(anyhow!("screen capture not implemented"))).ok();
- rx
+ crate::platform::scap_screen_capture::scap_screen_sources(&self.foreground_executor)
}
fn active_window(&self) -> Option<AnyWindowHandle> {
@@ -32,14 +32,32 @@ pub(crate) struct MouseWheelSettings {
impl WindowsSystemSettings {
pub(crate) fn new(display: WindowsDisplay) -> Self {
let mut settings = Self::default();
- settings.update(display);
+ settings.init(display);
settings
}
- pub(crate) fn update(&mut self, display: WindowsDisplay) {
+ fn init(&mut self, display: WindowsDisplay) {
self.mouse_wheel_settings.update();
self.auto_hide_taskbar_position = AutoHideTaskbarPosition::new(display).log_err().flatten();
}
+
+ pub(crate) fn update(&mut self, display: WindowsDisplay, wparam: usize) {
+ match wparam {
+ // SPI_SETWORKAREA
+ 47 => self.update_taskbar_position(display),
+ // SPI_GETWHEELSCROLLLINES, SPI_GETWHEELSCROLLCHARS
+ 104 | 108 => self.update_mouse_wheel_settings(),
+ _ => {}
+ }
+ }
+
+ fn update_mouse_wheel_settings(&mut self) {
+ self.mouse_wheel_settings.update();
+ }
+
+ fn update_taskbar_position(&mut self, display: WindowsDisplay) {
+ self.auto_hide_taskbar_position = AutoHideTaskbarPosition::new(display).log_err().flatten();
+ }
}
impl MouseWheelSettings {
@@ -144,8 +144,8 @@ pub(crate) fn load_cursor(style: CursorStyle) -> Option<HCURSOR> {
}
/// This function is used to configure the dark mode for the window built-in title bar.
-pub(crate) fn configure_dwm_dark_mode(hwnd: HWND) {
- let dark_mode_enabled: BOOL = match system_appearance().log_err().unwrap_or_default() {
+pub(crate) fn configure_dwm_dark_mode(hwnd: HWND, appearance: WindowAppearance) {
+ let dark_mode_enabled: BOOL = match appearance {
WindowAppearance::Dark | WindowAppearance::VibrantDark => true.into(),
WindowAppearance::Light | WindowAppearance::VibrantLight => false.into(),
};
@@ -37,11 +37,13 @@ pub struct WindowsWindowState {
pub min_size: Option<Size<Pixels>>,
pub fullscreen_restore_bounds: Bounds<Pixels>,
pub border_offset: WindowBorderOffset,
+ pub appearance: WindowAppearance,
pub scale_factor: f32,
pub restore_from_minimized: Option<Box<dyn FnMut(RequestFrameOptions)>>,
pub callbacks: Callbacks,
pub input_handler: Option<PlatformInputHandler>,
+ pub pending_surrogate: Option<u16>,
pub last_reported_modifiers: Option<Modifiers>,
pub last_reported_capslock: Option<Capslock>,
pub system_key_handled: bool,
@@ -84,6 +86,7 @@ impl WindowsWindowState {
display: WindowsDisplay,
gpu_context: &BladeContext,
min_size: Option<Size<Pixels>>,
+ appearance: WindowAppearance,
) -> Result<Self> {
let scale_factor = {
let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32;
@@ -103,6 +106,7 @@ impl WindowsWindowState {
let renderer = windows_renderer::init(gpu_context, hwnd, transparent)?;
let callbacks = Callbacks::default();
let input_handler = None;
+ let pending_surrogate = None;
let last_reported_modifiers = None;
let last_reported_capslock = None;
let system_key_handled = false;
@@ -118,11 +122,13 @@ impl WindowsWindowState {
logical_size,
fullscreen_restore_bounds,
border_offset,
+ appearance,
scale_factor,
restore_from_minimized,
min_size,
callbacks,
input_handler,
+ pending_surrogate,
last_reported_modifiers,
last_reported_capslock,
system_key_handled,
@@ -206,6 +212,7 @@ impl WindowsWindowStatePtr {
context.display,
context.gpu_context,
context.min_size,
+ context.appearance,
)?);
Ok(Rc::new_cyclic(|this| Self {
@@ -338,6 +345,7 @@ struct WindowCreateContext<'a> {
main_receiver: flume::Receiver<Runnable>,
gpu_context: &'a BladeContext,
main_thread_id_win32: u32,
+ appearance: WindowAppearance,
}
impl WindowsWindow {
@@ -387,6 +395,7 @@ impl WindowsWindow {
} else {
WindowsDisplay::primary_monitor().unwrap()
};
+ let appearance = system_appearance().unwrap_or_default();
let mut context = WindowCreateContext {
inner: None,
handle,
@@ -403,6 +412,7 @@ impl WindowsWindow {
main_receiver,
gpu_context,
main_thread_id_win32,
+ appearance,
};
let lpparam = Some(&context as *const _ as *const _);
let creation_result = unsafe {
@@ -426,7 +436,7 @@ impl WindowsWindow {
let state_ptr = context.inner.take().unwrap()?;
let hwnd = creation_result?;
register_drag_drop(state_ptr.clone())?;
- configure_dwm_dark_mode(hwnd);
+ configure_dwm_dark_mode(hwnd, appearance);
state_ptr.state.borrow_mut().border_offset.update(hwnd)?;
let placement = retrieve_window_placement(
hwnd,
@@ -543,7 +553,7 @@ impl PlatformWindow for WindowsWindow {
}
fn appearance(&self) -> WindowAppearance {
- system_appearance().log_err().unwrap_or_default()
+ self.0.state.borrow().appearance
}
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
@@ -951,7 +961,7 @@ impl IDropTarget_Impl for WindowsDragDropHandler_Impl {
}
}
-#[derive(Debug)]
+#[derive(Debug, Clone, Copy)]
pub(crate) struct ClickState {
button: MouseButton,
last_click: Instant,
@@ -993,10 +1003,25 @@ impl ClickState {
self.current_count
}
- pub fn system_update(&mut self) {
- self.double_click_spatial_tolerance_width = unsafe { GetSystemMetrics(SM_CXDOUBLECLK) };
- self.double_click_spatial_tolerance_height = unsafe { GetSystemMetrics(SM_CYDOUBLECLK) };
- self.double_click_interval = Duration::from_millis(unsafe { GetDoubleClickTime() } as u64);
+ pub fn system_update(&mut self, wparam: usize) {
+ match wparam {
+ // SPI_SETDOUBLECLKWIDTH
+ 29 => {
+ self.double_click_spatial_tolerance_width =
+ unsafe { GetSystemMetrics(SM_CXDOUBLECLK) }
+ }
+ // SPI_SETDOUBLECLKHEIGHT
+ 30 => {
+ self.double_click_spatial_tolerance_height =
+ unsafe { GetSystemMetrics(SM_CYDOUBLECLK) }
+ }
+ // SPI_SETDOUBLECLICKTIME
+ 32 => {
+ self.double_click_interval =
+ Duration::from_millis(unsafe { GetDoubleClickTime() } as u64)
+ }
+ _ => {}
+ }
}
#[inline]
@@ -357,6 +357,7 @@ impl WindowTextSystem {
text: SharedString,
font_size: Pixels,
runs: &[TextRun],
+ force_width: Option<Pixels>,
) -> ShapedLine {
debug_assert!(
text.find('\n').is_none(),
@@ -384,7 +385,7 @@ impl WindowTextSystem {
});
}
- let layout = self.layout_line(&text, font_size, runs);
+ let layout = self.layout_line(&text, font_size, runs, force_width);
ShapedLine {
layout,
@@ -524,6 +525,7 @@ impl WindowTextSystem {
text: Text,
font_size: Pixels,
runs: &[TextRun],
+ force_width: Option<Pixels>,
) -> Arc<LineLayout>
where
Text: AsRef<str>,
@@ -544,9 +546,9 @@ impl WindowTextSystem {
});
}
- let layout = self
- .line_layout_cache
- .layout_line(text, font_size, &font_runs);
+ let layout =
+ self.line_layout_cache
+ .layout_line_internal(text, font_size, &font_runs, force_width);
font_runs.clear();
self.font_runs_pool.lock().push(font_runs);
@@ -482,6 +482,7 @@ impl LineLayoutCache {
font_size,
runs,
wrap_width,
+ force_width: None,
} as &dyn AsCacheKeyRef;
let current_frame = self.current_frame.upgradable_read();
@@ -516,6 +517,7 @@ impl LineLayoutCache {
font_size,
runs: SmallVec::from(runs),
wrap_width,
+ force_width: None,
});
let mut current_frame = self.current_frame.write();
@@ -534,6 +536,20 @@ impl LineLayoutCache {
font_size: Pixels,
runs: &[FontRun],
) -> Arc<LineLayout>
+ where
+ Text: AsRef<str>,
+ SharedString: From<Text>,
+ {
+ self.layout_line_internal(text, font_size, runs, None)
+ }
+
+ pub fn layout_line_internal<Text>(
+ &self,
+ text: Text,
+ font_size: Pixels,
+ runs: &[FontRun],
+ force_width: Option<Pixels>,
+ ) -> Arc<LineLayout>
where
Text: AsRef<str>,
SharedString: From<Text>,
@@ -543,6 +559,7 @@ impl LineLayoutCache {
font_size,
runs,
wrap_width: None,
+ force_width,
} as &dyn AsCacheKeyRef;
let current_frame = self.current_frame.upgradable_read();
@@ -557,16 +574,30 @@ impl LineLayoutCache {
layout
} else {
let text = SharedString::from(text);
- let layout = Arc::new(
- self.platform_text_system
- .layout_line(&text, font_size, runs),
- );
+ let mut layout = self
+ .platform_text_system
+ .layout_line(&text, font_size, runs);
+
+ if let Some(force_width) = force_width {
+ let mut glyph_pos = 0;
+ for run in layout.runs.iter_mut() {
+ for glyph in run.glyphs.iter_mut() {
+ if (glyph.position.x - glyph_pos * force_width).abs() > px(1.) {
+ glyph.position.x = glyph_pos * force_width;
+ }
+ glyph_pos += 1;
+ }
+ }
+ }
+
let key = Arc::new(CacheKey {
text,
font_size,
runs: SmallVec::from(runs),
wrap_width: None,
+ force_width,
});
+ let layout = Arc::new(layout);
current_frame.lines.insert(key.clone(), layout.clone());
current_frame.used_lines.push(key);
layout
@@ -591,6 +622,7 @@ struct CacheKey {
font_size: Pixels,
runs: SmallVec<[FontRun; 1]>,
wrap_width: Option<Pixels>,
+ force_width: Option<Pixels>,
}
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
@@ -599,6 +631,7 @@ struct CacheKeyRef<'a> {
font_size: Pixels,
runs: &'a [FontRun],
wrap_width: Option<Pixels>,
+ force_width: Option<Pixels>,
}
impl PartialEq for (dyn AsCacheKeyRef + '_) {
@@ -622,6 +655,7 @@ impl AsCacheKeyRef for CacheKey {
font_size: self.font_size,
runs: self.runs.as_slice(),
wrap_width: self.wrap_width,
+ force_width: self.force_width,
}
}
}
@@ -1369,6 +1369,31 @@ impl Window {
});
}
+ pub(crate) fn dispatch_keystroke_interceptors(
+ &mut self,
+ event: &dyn Any,
+ context_stack: Vec<KeyContext>,
+ cx: &mut App,
+ ) {
+ let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() else {
+ return;
+ };
+
+ cx.keystroke_interceptors
+ .clone()
+ .retain(&(), move |callback| {
+ (callback)(
+ &KeystrokeEvent {
+ keystroke: key_down_event.keystroke.clone(),
+ action: None,
+ context_stack: context_stack.clone(),
+ },
+ self,
+ cx,
+ )
+ });
+ }
+
/// Schedules the given function to be run at the end of the current effect cycle, allowing entities
/// that are currently on the stack to be returned to the app.
pub fn defer(&self, cx: &mut App, f: impl FnOnce(&mut Window, &mut App) + 'static) {
@@ -3522,6 +3547,13 @@ impl Window {
return;
};
+ cx.propagate_event = true;
+ self.dispatch_keystroke_interceptors(event, self.context_stack(), cx);
+ if !cx.propagate_event {
+ self.finish_dispatch_key_event(event, dispatch_path, self.context_stack(), cx);
+ return;
+ }
+
let mut currently_pending = self.pending_input.take().unwrap_or_default();
if currently_pending.focus.is_some() && currently_pending.focus != self.focus {
currently_pending = PendingInput::default();
@@ -3570,7 +3602,6 @@ impl Window {
return;
}
- cx.propagate_event = true;
for binding in match_result.bindings {
self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx);
if !cx.propagate_event {
@@ -14,6 +14,7 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream {
let mut no_register = false;
let mut namespace = None;
let mut deprecated = None;
+ let mut doc_str: Option<String> = None;
for attr in &input.attrs {
if attr.path().is_ident("action") {
@@ -74,6 +75,22 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream {
Ok(())
})
.unwrap_or_else(|e| panic!("in #[action] attribute: {}", e));
+ } else if attr.path().is_ident("doc") {
+ use syn::{Expr::Lit, ExprLit, Lit::Str, Meta, MetaNameValue};
+ if let Meta::NameValue(MetaNameValue {
+ value:
+ Lit(ExprLit {
+ lit: Str(ref lit_str),
+ ..
+ }),
+ ..
+ }) = attr.meta
+ {
+ let doc = lit_str.value();
+ let doc_str = doc_str.get_or_insert_default();
+ doc_str.push_str(doc.trim());
+ doc_str.push('\n');
+ }
}
}
@@ -122,6 +139,13 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream {
quote! { None }
};
+ let documentation_fn_body = if let Some(doc) = doc_str {
+ let doc = doc.trim();
+ quote! { Some(#doc) }
+ } else {
+ quote! { None }
+ };
+
let registration = if no_register {
quote! {}
} else {
@@ -171,6 +195,10 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream {
fn deprecation_message() -> Option<&'static str> {
#deprecation_fn_body
}
+
+ fn documentation() -> Option<&'static str> {
+ #documentation_fn_body
+ }
}
})
}
@@ -34,6 +34,7 @@ pub(crate) fn generate_register_action(type_name: &Ident) -> TokenStream2 {
json_schema: <#type_name as gpui::Action>::action_json_schema,
deprecated_aliases: <#type_name as gpui::Action>::deprecated_aliases(),
deprecation_message: <#type_name as gpui::Action>::deprecation_message(),
+ documentation: <#type_name as gpui::Action>::documentation(),
}
}
@@ -407,7 +407,22 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
/// Sets the box shadow of the element.
/// [Docs](https://tailwindcss.com/docs/box-shadow)
- #visibility fn shadow_sm(mut self) -> Self {
+ #visibility fn shadow_2xs(mut self) -> Self {
+ use gpui::{BoxShadow, hsla, point, px};
+ use std::vec;
+
+ self.style().box_shadow = Some(vec![BoxShadow {
+ color: hsla(0., 0., 0., 0.05),
+ offset: point(px(0.), px(1.)),
+ blur_radius: px(0.),
+ spread_radius: px(0.),
+ }]);
+ self
+ }
+
+ /// Sets the box shadow of the element.
+ /// [Docs](https://tailwindcss.com/docs/box-shadow)
+ #visibility fn shadow_xs(mut self) -> Self {
use gpui::{BoxShadow, hsla, point, px};
use std::vec;
@@ -420,6 +435,29 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
self
}
+ /// Sets the box shadow of the element.
+ /// [Docs](https://tailwindcss.com/docs/box-shadow)
+ #visibility fn shadow_sm(mut self) -> Self {
+ use gpui::{BoxShadow, hsla, point, px};
+ use std::vec;
+
+ self.style().box_shadow = Some(vec![
+ BoxShadow {
+ color: hsla(0., 0., 0., 0.1),
+ offset: point(px(0.), px(1.)),
+ blur_radius: px(3.),
+ spread_radius: px(0.),
+ },
+ BoxShadow {
+ color: hsla(0., 0., 0., 0.1),
+ offset: point(px(0.), px(1.)),
+ blur_radius: px(2.),
+ spread_radius: px(-1.),
+ }
+ ]);
+ self
+ }
+
/// Sets the box shadow of the element.
/// [Docs](https://tailwindcss.com/docs/box-shadow)
#visibility fn shadow_md(mut self) -> Self {
@@ -428,7 +466,7 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
self.style().box_shadow = Some(vec![
BoxShadow {
- color: hsla(0.5, 0., 0., 0.1),
+ color: hsla(0., 0., 0., 0.1),
offset: point(px(0.), px(4.)),
blur_radius: px(6.),
spread_radius: px(-1.),
@@ -119,8 +119,10 @@ impl MarkdownWriter {
.push_back(current_element.clone());
}
- for child in node.children.borrow().iter() {
- self.visit_node(child, handlers)?;
+ if self.current_element_stack.len() < 200 {
+ for child in node.children.borrow().iter() {
+ self.visit_node(child, handlers)?;
+ }
}
if let Some(current_element) = current_element {
@@ -229,7 +229,7 @@ impl HttpClientWithUrl {
pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
let base_url = self.base_url();
let base_api_url = match base_url.as_ref() {
- "https://zed.dev" => "https://llm.zed.dev",
+ "https://zed.dev" => "https://cloud.zed.dev",
"https://staging.zed.dev" => "https://llm-staging.zed.dev",
"http://localhost:3000" => "http://localhost:8787",
other => other,
@@ -13,6 +13,7 @@ pub enum IconName {
AiBedrock,
AiDeepSeek,
AiEdit,
+ AiGemini,
AiGoogle,
AiLmStudio,
AiMistral,
@@ -65,7 +66,6 @@ pub enum IconName {
Circle,
CircleOff,
CircleHelp,
- Clipboard,
Close,
Cloud,
Code,
@@ -117,7 +117,6 @@ pub enum IconName {
File,
FileCode,
FileCreate,
- FileDelete,
FileDiff,
FileDoc,
FileGeneric,
@@ -164,6 +163,7 @@ pub enum IconName {
ListTree,
ListX,
LoadCircle,
+ LocationEdit,
LockOutlined,
LspDebug,
LspRestart,
@@ -191,6 +191,7 @@ pub enum IconName {
Play,
PlayAlt,
PlayBug,
+ PlayFilled,
Plus,
PocketKnife,
Power,
@@ -214,7 +215,6 @@ pub enum IconName {
Scissors,
Screen,
ScrollText,
- SearchCode,
SearchSelection,
SelectAll,
Send,
@@ -249,9 +249,23 @@ pub enum IconName {
SwatchBook,
Tab,
Terminal,
+ TerminalAlt,
TextSnippet,
ThumbsDown,
ThumbsUp,
+ ToolBulb,
+ ToolCopy,
+ ToolDeleteFile,
+ ToolDiagnostics,
+ ToolFolder,
+ ToolHammer,
+ ToolNotification,
+ ToolPencil,
+ ToolRead,
+ ToolRegex,
+ ToolSearch,
+ ToolTerminal,
+ ToolWeb,
Trash,
TrashAlt,
Triangle,
@@ -29,6 +29,11 @@ impl ExtensionIndexedDocsProviderProxy for IndexedDocsRegistryProxy {
ProviderId(provider_id),
)));
}
+
+ fn unregister_indexed_docs_provider(&self, provider_id: Arc<str>) {
+ self.indexed_docs_registry
+ .unregister_provider(&ProviderId(provider_id));
+ }
}
pub struct ExtensionIndexedDocsProvider {
@@ -52,6 +52,10 @@ impl IndexedDocsRegistry {
);
}
+ pub fn unregister_provider(&self, provider_id: &ProviderId) {
+ self.stores_by_provider.write().remove(provider_id);
+ }
+
pub fn get_provider_store(&self, provider_id: ProviderId) -> Option<Arc<IndexedDocsStore>> {
self.stores_by_provider.read().get(&provider_id).cloned()
}
@@ -37,7 +37,13 @@ use zed_actions::OpenBrowser;
use zed_llm_client::UsageLimit;
use zeta::RateCompletions;
-actions!(edit_prediction, [ToggleMenu]);
+actions!(
+ edit_prediction,
+ [
+ /// Toggles the inline completion menu.
+ ToggleMenu
+ ]
+);
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
@@ -829,10 +835,6 @@ impl InlineCompletionButton {
cx.notify();
}
-
- pub fn toggle_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.popover_menu_handle.toggle(window, cx);
- }
}
impl StatusItemView for InlineCompletionButton {
@@ -8,7 +8,15 @@ use util::ResultExt;
use workspace::notifications::{DetachAndPromptErr, NotificationId};
use workspace::{Toast, Workspace};
-actions!(cli, [Install, RegisterZedScheme]);
+actions!(
+ cli,
+ [
+ /// Installs the Zed CLI tool to the system PATH.
+ Install,
+ /// Registers the zed:// URL scheme handler.
+ RegisterZedScheme
+ ]
+);
async fn install_script(cx: &AsyncApp) -> Result<PathBuf> {
let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??;
@@ -13,7 +13,13 @@ use std::{
};
use workspace::{AppState, OpenVisible, Workspace};
-actions!(journal, [NewJournalEntry]);
+actions!(
+ journal,
+ [
+ /// Creates a new journal entry for today.
+ NewJournalEntry
+ ]
+);
/// Settings specific to journaling
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
@@ -334,6 +334,9 @@ impl LanguageRegistry {
if let Some(adapters) = state.lsp_adapters.get_mut(language_name) {
adapters.retain(|adapter| &adapter.name != name)
}
+ state.all_lsp_adapters.remove(name);
+ state.available_lsp_adapters.remove(name);
+
state.version += 1;
state.reload_count += 1;
*state.subscription.0.borrow_mut() = ();
@@ -18,10 +18,10 @@ use serde::{
use settings::{
ParameterizedJsonSchema, Settings, SettingsLocation, SettingsSources, SettingsStore,
- replace_subschema,
};
use shellexpand;
use std::{borrow::Cow, num::NonZeroU32, path::Path, slice, sync::Arc};
+use util::schemars::replace_subschema;
use util::serde::default_true;
/// Initializes the language settings.
@@ -321,7 +321,7 @@ inventory::submit! {
let language_settings_content_ref = generator
.subschema_for::<LanguageSettingsContent>()
.to_value();
- let schema = json_schema!({
+ replace_subschema::<LanguageToSettingsMap>(generator, || json_schema!({
"type": "object",
"properties": params
.language_names
@@ -333,8 +333,7 @@ inventory::submit! {
)
})
.collect::<serde_json::Map<_, _>>()
- });
- replace_subschema::<LanguageToSettingsMap>(generator, schema)
+ }))
}
}
}
@@ -21,6 +21,7 @@ fs.workspace = true
gpui.workspace = true
language.workspace = true
lsp.workspace = true
+project.workspace = true
serde.workspace = true
serde_json.workspace = true
util.workspace = true
@@ -6,21 +6,24 @@ use std::sync::Arc;
use anyhow::{Context as _, Result};
use async_trait::async_trait;
-use collections::HashMap;
+use collections::{HashMap, HashSet};
use extension::{Extension, ExtensionLanguageServerProxy, WorktreeDelegate};
use fs::Fs;
-use futures::{Future, FutureExt};
-use gpui::AsyncApp;
+use futures::{Future, FutureExt, future::join_all};
+use gpui::{App, AppContext, AsyncApp, Task};
use language::{
BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore,
LspAdapter, LspAdapterDelegate,
};
-use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName};
+use lsp::{
+ CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName,
+ LanguageServerSelector,
+};
use serde::Serialize;
use serde_json::Value;
use util::{ResultExt, fs::make_file_executable, maybe};
-use crate::LanguageServerRegistryProxy;
+use crate::{LanguageServerRegistryProxy, LspAccess};
/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
struct WorktreeDelegateAdapter(pub Arc<dyn LspAdapterDelegate>);
@@ -71,10 +74,50 @@ impl ExtensionLanguageServerProxy for LanguageServerRegistryProxy {
fn remove_language_server(
&self,
language: &LanguageName,
- language_server_id: &LanguageServerName,
- ) {
+ language_server_name: &LanguageServerName,
+ cx: &mut App,
+ ) -> Task<Result<()>> {
self.language_registry
- .remove_lsp_adapter(language, language_server_id);
+ .remove_lsp_adapter(language, language_server_name);
+
+ let mut tasks = Vec::new();
+ match &self.lsp_access {
+ LspAccess::ViaLspStore(lsp_store) => lsp_store.update(cx, |lsp_store, cx| {
+ let stop_task = lsp_store.stop_language_servers_for_buffers(
+ Vec::new(),
+ HashSet::from_iter([LanguageServerSelector::Name(
+ language_server_name.clone(),
+ )]),
+ cx,
+ );
+ tasks.push(stop_task);
+ }),
+ LspAccess::ViaWorkspaces(lsp_store_provider) => {
+ if let Ok(lsp_stores) = lsp_store_provider(cx) {
+ for lsp_store in lsp_stores {
+ lsp_store.update(cx, |lsp_store, cx| {
+ let stop_task = lsp_store.stop_language_servers_for_buffers(
+ Vec::new(),
+ HashSet::from_iter([LanguageServerSelector::Name(
+ language_server_name.clone(),
+ )]),
+ cx,
+ );
+ tasks.push(stop_task);
+ });
+ }
+ }
+ }
+ LspAccess::Noop => {}
+ }
+
+ cx.background_spawn(async move {
+ let results = join_all(tasks).await;
+ for result in results {
+ result?;
+ }
+ Ok(())
+ })
}
fn update_language_server_status(
@@ -5,13 +5,26 @@ use std::sync::Arc;
use anyhow::Result;
use extension::{ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy};
+use gpui::{App, Entity};
use language::{LanguageMatcher, LanguageName, LanguageRegistry, LoadedLanguage};
+use project::LspStore;
+
+#[derive(Clone)]
+pub enum LspAccess {
+ ViaLspStore(Entity<LspStore>),
+ ViaWorkspaces(Arc<dyn Fn(&mut App) -> Result<Vec<Entity<LspStore>>> + Send + Sync + 'static>),
+ Noop,
+}
pub fn init(
+ lsp_access: LspAccess,
extension_host_proxy: Arc<ExtensionHostProxy>,
language_registry: Arc<LanguageRegistry>,
) {
- let language_server_registry_proxy = LanguageServerRegistryProxy { language_registry };
+ let language_server_registry_proxy = LanguageServerRegistryProxy {
+ language_registry,
+ lsp_access,
+ };
extension_host_proxy.register_grammar_proxy(language_server_registry_proxy.clone());
extension_host_proxy.register_language_proxy(language_server_registry_proxy.clone());
extension_host_proxy.register_language_server_proxy(language_server_registry_proxy);
@@ -20,6 +33,7 @@ pub fn init(
#[derive(Clone)]
struct LanguageServerRegistryProxy {
language_registry: Arc<LanguageRegistry>,
+ lsp_access: LspAccess,
}
impl ExtensionGrammarProxy for LanguageServerRegistryProxy {
@@ -26,7 +26,7 @@ use std::time::Duration;
use std::{fmt, io};
use thiserror::Error;
use util::serde::is_default;
-use zed_llm_client::CompletionRequestStatus;
+use zed_llm_client::{CompletionMode, CompletionRequestStatus};
pub use crate::model::*;
pub use crate::rate_limiter::*;
@@ -462,6 +462,10 @@ pub trait LanguageModel: Send + Sync {
}
fn max_token_count(&self) -> u64;
+ /// Returns the maximum token count for this model in burn mode (If `supports_burn_mode` is `false` this returns `None`)
+ fn max_token_count_in_burn_mode(&self) -> Option<u64> {
+ None
+ }
fn max_output_tokens(&self) -> Option<u64> {
None
}
@@ -557,6 +561,18 @@ pub trait LanguageModel: Send + Sync {
}
}
+pub trait LanguageModelExt: LanguageModel {
+ fn max_token_count_for_mode(&self, mode: CompletionMode) -> u64 {
+ match mode {
+ CompletionMode::Normal => self.max_token_count(),
+ CompletionMode::Max => self
+ .max_token_count_in_burn_mode()
+ .unwrap_or_else(|| self.max_token_count()),
+ }
+ }
+}
+impl LanguageModelExt for dyn LanguageModel {}
+
pub trait LanguageModelTool: 'static + DeserializeOwned + JsonSchema {
fn name() -> String;
fn description() -> String;
@@ -391,6 +391,7 @@ pub struct LanguageModelRequest {
pub tool_choice: Option<LanguageModelToolChoice>,
pub stop: Vec<String>,
pub temperature: Option<f32>,
+ pub thinking_allowed: bool,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
@@ -663,7 +663,9 @@ pub fn into_anthropic(
} else {
Some(anthropic::StringOrContents::String(system_message))
},
- thinking: if let AnthropicModelMode::Thinking { budget_tokens } = mode {
+ thinking: if request.thinking_allowed
+ && let AnthropicModelMode::Thinking { budget_tokens } = mode
+ {
Some(anthropic::Thinking::Enabled { budget_tokens })
} else {
None
@@ -1108,6 +1110,7 @@ mod tests {
temperature: None,
tools: vec![],
tool_choice: None,
+ thinking_allowed: true,
};
let anthropic_request = into_anthropic(
@@ -799,7 +799,9 @@ pub fn into_bedrock(
max_tokens: max_output_tokens,
system: Some(system_message),
tools: Some(tool_config),
- thinking: if let BedrockModelMode::Thinking { budget_tokens } = mode {
+ thinking: if request.thinking_allowed
+ && let BedrockModelMode::Thinking { budget_tokens } = mode
+ {
Some(bedrock::Thinking::Enabled { budget_tokens })
} else {
None
@@ -164,46 +164,9 @@ impl State {
}
let response = Self::fetch_models(client, llm_api_token).await?;
- cx.update(|cx| {
- this.update(cx, |this, cx| {
- let mut models = Vec::new();
-
- for model in response.models {
- models.push(Arc::new(model.clone()));
-
- // Right now we represent thinking variants of models as separate models on the client,
- // so we need to insert variants for any model that supports thinking.
- if model.supports_thinking {
- models.push(Arc::new(zed_llm_client::LanguageModel {
- id: zed_llm_client::LanguageModelId(
- format!("{}-thinking", model.id).into(),
- ),
- display_name: format!("{} Thinking", model.display_name),
- ..model
- }));
- }
- }
-
- this.default_model = models
- .iter()
- .find(|model| model.id == response.default_model)
- .cloned();
- this.default_fast_model = models
- .iter()
- .find(|model| model.id == response.default_fast_model)
- .cloned();
- this.recommended_models = response
- .recommended_models
- .iter()
- .filter_map(|id| models.iter().find(|model| &model.id == id))
- .cloned()
- .collect();
- this.models = models;
- cx.notify();
- })
- })??;
-
- anyhow::Ok(())
+ this.update(cx, |this, cx| {
+ this.update_models(response, cx);
+ })
})
.await
.context("failed to fetch Zed models")
@@ -214,12 +177,15 @@ impl State {
}),
_llm_token_subscription: cx.subscribe(
&refresh_llm_token_listener,
- |this, _listener, _event, cx| {
+ move |this, _listener, _event, cx| {
let client = this.client.clone();
let llm_api_token = this.llm_api_token.clone();
- cx.spawn(async move |_this, _cx| {
+ cx.spawn(async move |this, cx| {
llm_api_token.refresh(&client).await?;
- anyhow::Ok(())
+ let response = Self::fetch_models(client, llm_api_token).await?;
+ this.update(cx, |this, cx| {
+ this.update_models(response, cx);
+ })
})
.detach_and_log_err(cx);
},
@@ -262,6 +228,41 @@ impl State {
}));
}
+ fn update_models(&mut self, response: ListModelsResponse, cx: &mut Context<Self>) {
+ let mut models = Vec::new();
+
+ for model in response.models {
+ models.push(Arc::new(model.clone()));
+
+ // Right now we represent thinking variants of models as separate models on the client,
+ // so we need to insert variants for any model that supports thinking.
+ if model.supports_thinking {
+ models.push(Arc::new(zed_llm_client::LanguageModel {
+ id: zed_llm_client::LanguageModelId(format!("{}-thinking", model.id).into()),
+ display_name: format!("{} Thinking", model.display_name),
+ ..model
+ }));
+ }
+ }
+
+ self.default_model = models
+ .iter()
+ .find(|model| model.id == response.default_model)
+ .cloned();
+ self.default_fast_model = models
+ .iter()
+ .find(|model| model.id == response.default_fast_model)
+ .cloned();
+ self.recommended_models = response
+ .recommended_models
+ .iter()
+ .filter_map(|id| models.iter().find(|model| &model.id == id))
+ .cloned()
+ .collect();
+ self.models = models;
+ cx.notify();
+ }
+
async fn fetch_models(
client: Arc<Client>,
llm_api_token: LlmApiToken,
@@ -730,6 +731,13 @@ impl LanguageModel for CloudLanguageModel {
self.model.max_token_count as u64
}
+ fn max_token_count_in_burn_mode(&self) -> Option<u64> {
+ self.model
+ .max_token_count_in_max_mode
+ .filter(|_| self.model.supports_max_mode)
+ .map(|max_token_count| max_token_count as u64)
+ }
+
fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
match &self.model.provider {
zed_llm_client::LanguageModelProvider::Anthropic => {
@@ -828,6 +836,7 @@ impl LanguageModel for CloudLanguageModel {
let intent = request.intent;
let mode = request.mode;
let app_version = cx.update(|cx| AppVersion::global(cx)).ok();
+ let thinking_allowed = request.thinking_allowed;
match self.model.provider {
zed_llm_client::LanguageModelProvider::Anthropic => {
let request = into_anthropic(
@@ -835,7 +844,7 @@ impl LanguageModel for CloudLanguageModel {
self.model.id.to_string(),
1.0,
self.model.max_output_tokens as u64,
- if self.model.id.0.ends_with("-thinking") {
+ if thinking_allowed && self.model.id.0.ends_with("-thinking") {
AnthropicModelMode::Thinking {
budget_tokens: Some(4_096),
}
@@ -30,6 +30,7 @@ use settings::SettingsStore;
use std::time::Duration;
use ui::prelude::*;
use util::debug_panic;
+use zed_llm_client::CompletionIntent;
use super::anthropic::count_anthropic_tokens;
use super::google::count_google_tokens;
@@ -268,6 +269,19 @@ impl LanguageModel for CopilotChatLanguageModel {
LanguageModelCompletionError,
>,
> {
+ let is_user_initiated = request.intent.is_none_or(|intent| match intent {
+ CompletionIntent::UserPrompt
+ | CompletionIntent::ThreadContextSummarization
+ | CompletionIntent::InlineAssist
+ | CompletionIntent::TerminalInlineAssist
+ | CompletionIntent::GenerateGitCommitMessage => true,
+
+ CompletionIntent::ToolResults
+ | CompletionIntent::ThreadSummarization
+ | CompletionIntent::CreateFile
+ | CompletionIntent::EditFile => false,
+ });
+
let copilot_request = match into_copilot_chat(&self.model, request) {
Ok(request) => request,
Err(err) => return futures::future::ready(Err(err.into())).boxed(),
@@ -276,7 +290,8 @@ impl LanguageModel for CopilotChatLanguageModel {
let request_limiter = self.request_limiter.clone();
let future = cx.spawn(async move |cx| {
- let request = CopilotChat::stream_completion(copilot_request, cx.clone());
+ let request =
+ CopilotChat::stream_completion(copilot_request, is_user_initiated, cx.clone());
request_limiter
.stream(async move {
let response = request.await?;
@@ -559,11 +559,11 @@ pub fn into_google(
stop_sequences: Some(request.stop),
max_output_tokens: None,
temperature: request.temperature.map(|t| t as f64).or(Some(1.0)),
- thinking_config: match mode {
- GoogleModelMode::Thinking { budget_tokens } => {
+ thinking_config: match (request.thinking_allowed, mode) {
+ (true, GoogleModelMode::Thinking { budget_tokens }) => {
budget_tokens.map(|thinking_budget| ThinkingConfig { thinking_budget })
}
- GoogleModelMode::Default => None,
+ _ => None,
},
top_p: None,
top_k: None,
@@ -911,6 +911,7 @@ mod tests {
intent: None,
mode: None,
stop: vec![],
+ thinking_allowed: true,
};
let mistral_request = into_mistral(request, "mistral-small-latest".into(), None);
@@ -943,6 +944,7 @@ mod tests {
intent: None,
mode: None,
stop: vec![],
+ thinking_allowed: true,
};
let mistral_request = into_mistral(request, "pixtral-12b-latest".into(), None);
@@ -334,7 +334,10 @@ impl OllamaLanguageModel {
temperature: request.temperature.or(Some(1.0)),
..Default::default()
}),
- think: self.model.supports_thinking,
+ think: self
+ .model
+ .supports_thinking
+ .map(|supports_thinking| supports_thinking && request.thinking_allowed),
tools: request.tools.into_iter().map(tool_into_ollama).collect(),
}
}
@@ -999,6 +999,7 @@ mod tests {
tool_choice: None,
stop: vec![],
temperature: None,
+ thinking_allowed: true,
};
// Validate that all models are supported by tiktoken-rs
@@ -523,7 +523,9 @@ pub fn into_open_router(
None
},
usage: open_router::RequestUsage { include: true },
- reasoning: if let OpenRouterModelMode::Thinking { budget_tokens } = model.mode {
+ reasoning: if request.thinking_allowed
+ && let OpenRouterModelMode::Thinking { budget_tokens } = model.mode
+ {
Some(open_router::Reasoning {
effort: None,
max_tokens: budget_tokens,
@@ -19,7 +19,13 @@ use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
use util::ResultExt;
use workspace::{ModalView, Workspace};
-actions!(language_selector, [Toggle]);
+actions!(
+ language_selector,
+ [
+ /// Toggles the language selector modal.
+ Toggle
+ ]
+);
pub fn init(cx: &mut App) {
cx.observe_new(LanguageSelector::register).detach();
@@ -24,7 +24,6 @@ gpui.workspace = true
itertools.workspace = true
language.workspace = true
lsp.workspace = true
-picker.workspace = true
project.workspace = true
serde_json.workspace = true
settings.workspace = true
@@ -13,7 +13,13 @@ use ui::{
};
use workspace::{Item, SplitDirection, Workspace};
-actions!(dev, [OpenKeyContextView]);
+actions!(
+ dev,
+ [
+ /// Opens the key context view for debugging keybindings.
+ OpenKeyContextView
+ ]
+);
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _, _| {
@@ -204,7 +204,13 @@ pub(crate) struct LogMenuItem {
pub server_kind: LanguageServerKind,
}
-actions!(dev, [OpenLanguageServerLogs]);
+actions!(
+ dev,
+ [
+ /// Opens the language server protocol logs viewer.
+ OpenLanguageServerLogs
+ ]
+);
pub(super) struct GlobalLogStore(pub WeakEntity<LogStore>);
@@ -1325,6 +1331,7 @@ impl Render for LspLogToolbarItemView {
let Some(log_view) = self.log_view.clone() else {
return div();
};
+
let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
let menu_rows = log_view.menu_items(cx).unwrap_or_default();
let current_server_id = log_view.current_server_id;
@@ -1338,6 +1345,7 @@ impl Render for LspLogToolbarItemView {
None
}
});
+
let available_language_servers: Vec<_> = menu_rows
.into_iter()
.map(|row| {
@@ -1349,21 +1357,28 @@ impl Render for LspLogToolbarItemView {
)
})
.collect();
+
let log_toolbar_view = cx.entity().clone();
+
let lsp_menu = PopoverMenu::new("LspLogView")
.anchor(Corner::TopLeft)
- .trigger(Button::new(
- "language_server_menu_header",
- current_server
- .as_ref()
- .map(|row| {
- Cow::Owned(format!(
- "{} ({})",
- row.server_name.0, row.worktree_root_name,
- ))
- })
- .unwrap_or_else(|| "No server selected".into()),
- ))
+ .trigger(
+ Button::new(
+ "language_server_menu_header",
+ current_server
+ .as_ref()
+ .map(|row| {
+ Cow::Owned(format!(
+ "{} ({})",
+ row.server_name.0, row.worktree_root_name,
+ ))
+ })
+ .unwrap_or_else(|| "No server selected".into()),
+ )
+ .icon(IconName::ChevronDown)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted),
+ )
.menu({
let log_view = log_view.clone();
move |window, cx| {
@@ -1407,6 +1422,7 @@ impl Render for LspLogToolbarItemView {
.into()
}
});
+
let view_selector = current_server.map(|server| {
let server_id = server.server_id;
let is_remote = server.server_kind.is_remote();
@@ -1414,10 +1430,12 @@ impl Render for LspLogToolbarItemView {
let log_view = log_view.clone();
PopoverMenu::new("LspViewSelector")
.anchor(Corner::TopLeft)
- .trigger(Button::new(
- "language_server_menu_header",
- server.selected_entry.label(),
- ))
+ .trigger(
+ Button::new("language_server_menu_header", server.selected_entry.label())
+ .icon(IconName::ChevronDown)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted),
+ )
.menu(move |window, cx| {
let log_toolbar_view = log_toolbar_view.clone();
let log_view = log_view.clone();
@@ -1488,11 +1506,14 @@ impl Render for LspLogToolbarItemView {
}))
})
});
+
h_flex()
.size_full()
+ .gap_1()
.justify_between()
.child(
h_flex()
+ .gap_0p5()
.child(lsp_menu)
.children(view_selector)
.child(
@@ -1502,10 +1523,15 @@ impl Render for LspLogToolbarItemView {
div().child(
PopoverMenu::new("lsp-trace-level-menu")
.anchor(Corner::TopLeft)
- .trigger(Button::new(
- "language_server_trace_level_selector",
- "Trace level",
- ))
+ .trigger(
+ Button::new(
+ "language_server_trace_level_selector",
+ "Trace level",
+ )
+ .icon(IconName::ChevronDown)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted),
+ )
.menu({
let log_view = log_view.clone();
@@ -1565,10 +1591,15 @@ impl Render for LspLogToolbarItemView {
div().child(
PopoverMenu::new("lsp-log-level-menu")
.anchor(Corner::TopLeft)
- .trigger(Button::new(
- "language_server_log_level_selector",
- "Log level",
- ))
+ .trigger(
+ Button::new(
+ "language_server_log_level_selector",
+ "Log level",
+ )
+ .icon(IconName::ChevronDown)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted),
+ )
.menu({
let log_view = log_view.clone();
@@ -1629,23 +1660,19 @@ impl Render for LspLogToolbarItemView {
),
)
.child(
- div()
- .child(
- Button::new("clear_log_button", "Clear").on_click(cx.listener(
- |this, _, window, cx| {
- if let Some(log_view) = this.log_view.as_ref() {
- log_view.update(cx, |log_view, cx| {
- log_view.editor.update(cx, |editor, cx| {
- editor.set_read_only(false);
- editor.clear(window, cx);
- editor.set_read_only(true);
- });
- })
- }
- },
- )),
- )
- .ml_2(),
+ Button::new("clear_log_button", "Clear").on_click(cx.listener(
+ |this, _, window, cx| {
+ if let Some(log_view) = this.log_view.as_ref() {
+ log_view.update(cx, |log_view, cx| {
+ log_view.editor.update(cx, |editor, cx| {
+ editor.set_read_only(false);
+ editor.clear(window, cx);
+ editor.set_read_only(true);
+ });
+ })
+ }
+ },
+ )),
)
}
}
@@ -1,54 +1,64 @@
-use std::{collections::hash_map, path::PathBuf, sync::Arc, time::Duration};
+use std::{collections::hash_map, path::PathBuf, rc::Rc, time::Duration};
use client::proto;
use collections::{HashMap, HashSet};
use editor::{Editor, EditorEvent};
use feature_flags::FeatureFlagAppExt as _;
-use gpui::{
- Corner, DismissEvent, Entity, Focusable as _, MouseButton, Subscription, Task, WeakEntity,
- actions,
-};
+use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions};
use language::{BinaryStatus, BufferId, LocalFile, ServerHealth};
use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
-use picker::{Picker, PickerDelegate, popover_menu::PickerPopoverMenu};
use project::{LspStore, LspStoreEvent, project_settings::ProjectSettings};
use settings::{Settings as _, SettingsStore};
-use ui::{Context, Indicator, PopoverMenuHandle, Tooltip, Window, prelude::*};
+use ui::{
+ Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide,
+ Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, Window, prelude::*,
+};
use workspace::{StatusItemView, Workspace};
use crate::lsp_log::GlobalLogStore;
-actions!(lsp_tool, [ToggleMenu]);
+actions!(
+ lsp_tool,
+ [
+ /// Toggles the language server tool menu.
+ ToggleMenu
+ ]
+);
pub struct LspTool {
- state: Entity<PickerState>,
- popover_menu_handle: PopoverMenuHandle<Picker<LspPickerDelegate>>,
- lsp_picker: Option<Entity<Picker<LspPickerDelegate>>>,
+ server_state: Entity<LanguageServerState>,
+ popover_menu_handle: PopoverMenuHandle<ContextMenu>,
+ lsp_menu: Option<Entity<ContextMenu>>,
+ lsp_menu_refresh: Task<()>,
_subscriptions: Vec<Subscription>,
}
-struct PickerState {
+#[derive(Debug)]
+struct LanguageServerState {
+ items: Vec<LspItem>,
+ other_servers_start_index: Option<usize>,
workspace: WeakEntity<Workspace>,
lsp_store: WeakEntity<LspStore>,
active_editor: Option<ActiveEditor>,
language_servers: LanguageServers,
}
-#[derive(Debug)]
-pub struct LspPickerDelegate {
- state: Entity<PickerState>,
- selected_index: usize,
- items: Vec<LspItem>,
- other_servers_start_index: Option<usize>,
-}
-
struct ActiveEditor {
editor: WeakEntity<Editor>,
_editor_subscription: Subscription,
editor_buffers: HashSet<BufferId>,
}
+impl std::fmt::Debug for ActiveEditor {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("ActiveEditor")
+ .field("editor", &self.editor)
+ .field("editor_buffers", &self.editor_buffers)
+ .finish_non_exhaustive()
+ }
+}
+
#[derive(Debug, Default, Clone)]
struct LanguageServers {
health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>,
@@ -98,192 +108,187 @@ impl LanguageServerHealthStatus {
}
}
-impl LspPickerDelegate {
- fn regenerate_items(&mut self, cx: &mut Context<Picker<Self>>) {
- self.state.update(cx, |state, cx| {
- let editor_buffers = state
- .active_editor
- .as_ref()
- .map(|active_editor| active_editor.editor_buffers.clone())
- .unwrap_or_default();
- let editor_buffer_paths = editor_buffers
- .iter()
- .filter_map(|buffer_id| {
- let buffer_path = state
- .lsp_store
- .update(cx, |lsp_store, cx| {
- Some(
- project::File::from_dyn(
- lsp_store
- .buffer_store()
+impl LanguageServerState {
+ fn fill_menu(&self, mut menu: ContextMenu, cx: &mut Context<Self>) -> ContextMenu {
+ menu = menu.align_popover_bottom();
+ let lsp_logs = cx
+ .try_global::<GlobalLogStore>()
+ .and_then(|lsp_logs| lsp_logs.0.upgrade());
+ let lsp_store = self.lsp_store.upgrade();
+ let Some((lsp_logs, lsp_store)) = lsp_logs.zip(lsp_store) else {
+ return menu;
+ };
+
+ let mut first_button_encountered = false;
+ for (i, item) in self.items.iter().enumerate() {
+ if let LspItem::ToggleServersButton { restart } = item {
+ let label = if *restart {
+ "Restart All Servers"
+ } else {
+ "Stop All Servers"
+ };
+ let restart = *restart;
+ let button = ContextMenuEntry::new(label).handler({
+ let state = cx.entity();
+ move |_, cx| {
+ let lsp_store = state.read(cx).lsp_store.clone();
+ lsp_store
+ .update(cx, |lsp_store, cx| {
+ if restart {
+ let Some(workspace) = state.read(cx).workspace.upgrade() else {
+ return;
+ };
+ let project = workspace.read(cx).project().clone();
+ let buffer_store = project.read(cx).buffer_store().clone();
+ let worktree_store = project.read(cx).worktree_store();
+
+ let buffers = state
.read(cx)
- .get(*buffer_id)?
+ .language_servers
+ .servers_per_buffer_abs_path
+ .keys()
+ .filter_map(|abs_path| {
+ worktree_store.read(cx).find_worktree(abs_path, cx)
+ })
+ .filter_map(|(worktree, relative_path)| {
+ let entry =
+ worktree.read(cx).entry_for_path(&relative_path)?;
+ project.read(cx).path_for_entry(entry.id, cx)
+ })
+ .filter_map(|project_path| {
+ buffer_store.read(cx).get_by_path(&project_path)
+ })
+ .collect();
+ let selectors = state
.read(cx)
- .file(),
- )?
- .abs_path(cx),
- )
- })
- .ok()??;
- Some(buffer_path)
- })
- .collect::<Vec<_>>();
-
- let mut servers_with_health_checks = HashSet::default();
- let mut server_ids_with_health_checks = HashSet::default();
- let mut buffer_servers =
- Vec::with_capacity(state.language_servers.health_statuses.len());
- let mut other_servers =
- Vec::with_capacity(state.language_servers.health_statuses.len());
- let buffer_server_ids = editor_buffer_paths
- .iter()
- .filter_map(|buffer_path| {
- state
- .language_servers
- .servers_per_buffer_abs_path
- .get(buffer_path)
- })
- .flatten()
- .fold(HashMap::default(), |mut acc, (server_id, name)| {
- match acc.entry(*server_id) {
- hash_map::Entry::Occupied(mut o) => {
- let old_name: &mut Option<&LanguageServerName> = o.get_mut();
- if old_name.is_none() {
- *old_name = name.as_ref();
- }
- }
- hash_map::Entry::Vacant(v) => {
- v.insert(name.as_ref());
- }
+ .items
+ .iter()
+ // Do not try to use IDs as we have stopped all servers already, when allowing to restart them all
+ .flat_map(|item| match item {
+ LspItem::ToggleServersButton { .. } => None,
+ LspItem::WithHealthCheck(_, status, ..) => Some(
+ LanguageServerSelector::Name(status.name.clone()),
+ ),
+ LspItem::WithBinaryStatus(_, server_name, ..) => Some(
+ LanguageServerSelector::Name(server_name.clone()),
+ ),
+ })
+ .collect();
+ lsp_store.restart_language_servers_for_buffers(
+ buffers, selectors, cx,
+ );
+ } else {
+ lsp_store.stop_all_language_servers(cx);
+ }
+ })
+ .ok();
}
- acc
});
- for (server_id, server_state) in &state.language_servers.health_statuses {
- let binary_status = state
- .language_servers
- .binary_statuses
- .get(&server_state.name);
- servers_with_health_checks.insert(&server_state.name);
- server_ids_with_health_checks.insert(*server_id);
- if buffer_server_ids.contains_key(server_id) {
- buffer_servers.push(ServerData::WithHealthCheck(
- *server_id,
- server_state,
- binary_status,
- ));
- } else {
- other_servers.push(ServerData::WithHealthCheck(
- *server_id,
- server_state,
- binary_status,
- ));
+ if !first_button_encountered {
+ menu = menu.separator();
+ first_button_encountered = true;
}
- }
+ menu = menu.item(button);
+ continue;
+ };
- let mut can_stop_all = false;
- let mut can_restart_all = true;
+ let Some(server_info) = item.server_info() else {
+ continue;
+ };
- for (server_name, status) in state
- .language_servers
- .binary_statuses
- .iter()
- .filter(|(name, _)| !servers_with_health_checks.contains(name))
- {
- match status.status {
- BinaryStatus::None => {
- can_restart_all = false;
- can_stop_all = true;
- }
- BinaryStatus::CheckingForUpdate => {
- can_restart_all = false;
- }
- BinaryStatus::Downloading => {
- can_restart_all = false;
- }
- BinaryStatus::Starting => {
- can_restart_all = false;
- }
- BinaryStatus::Stopping => {
- can_restart_all = false;
- }
- BinaryStatus::Stopped => {}
- BinaryStatus::Failed { .. } => {}
- }
+ let workspace = self.workspace.clone();
+ let server_selector = server_info.server_selector();
+ // TODO currently, Zed remote does not work well with the LSP logs
+ // https://github.com/zed-industries/zed/issues/28557
+ let has_logs = lsp_store.read(cx).as_local().is_some()
+ && lsp_logs.read(cx).has_server_logs(&server_selector);
+
+ let status_color = server_info
+ .binary_status
+ .and_then(|binary_status| match binary_status.status {
+ BinaryStatus::None => None,
+ BinaryStatus::CheckingForUpdate
+ | BinaryStatus::Downloading
+ | BinaryStatus::Starting => Some(Color::Modified),
+ BinaryStatus::Stopping => Some(Color::Disabled),
+ BinaryStatus::Stopped => Some(Color::Disabled),
+ BinaryStatus::Failed { .. } => Some(Color::Error),
+ })
+ .or_else(|| {
+ Some(match server_info.health? {
+ ServerHealth::Ok => Color::Success,
+ ServerHealth::Warning => Color::Warning,
+ ServerHealth::Error => Color::Error,
+ })
+ })
+ .unwrap_or(Color::Success);
- let matching_server_id = state
- .language_servers
- .servers_per_buffer_abs_path
- .iter()
- .filter(|(path, _)| editor_buffer_paths.contains(path))
- .flat_map(|(_, server_associations)| server_associations.iter())
- .find_map(|(id, name)| {
- if name.as_ref() == Some(server_name) {
- Some(*id)
- } else {
- None
- }
- });
- if let Some(server_id) = matching_server_id {
- buffer_servers.push(ServerData::WithBinaryStatus(
- Some(server_id),
- server_name,
- status,
- ));
- } else {
- other_servers.push(ServerData::WithBinaryStatus(None, server_name, status));
- }
+ if self
+ .other_servers_start_index
+ .is_some_and(|index| index == i)
+ {
+ menu = menu.separator().header("Other Buffers");
}
- buffer_servers.sort_by_key(|data| data.name().clone());
- other_servers.sort_by_key(|data| data.name().clone());
-
- let mut other_servers_start_index = None;
- let mut new_lsp_items =
- Vec::with_capacity(buffer_servers.len() + other_servers.len() + 1);
- new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item));
- if !new_lsp_items.is_empty() {
- other_servers_start_index = Some(new_lsp_items.len());
- }
- new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item));
- if !new_lsp_items.is_empty() {
- if can_stop_all {
- new_lsp_items.push(LspItem::ToggleServersButton { restart: false });
- } else if can_restart_all {
- new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
- }
+ if i == 0 && self.other_servers_start_index.is_some() {
+ menu = menu.header("Current Buffer");
}
- self.items = new_lsp_items;
- self.other_servers_start_index = other_servers_start_index;
- });
- }
-
- fn server_info(&self, ix: usize) -> Option<ServerInfo> {
- match self.items.get(ix)? {
- LspItem::ToggleServersButton { .. } => None,
- LspItem::WithHealthCheck(
- language_server_id,
- language_server_health_status,
- language_server_binary_status,
- ) => Some(ServerInfo {
- name: language_server_health_status.name.clone(),
- id: Some(*language_server_id),
- health: language_server_health_status.health(),
- binary_status: language_server_binary_status.clone(),
- message: language_server_health_status.message(),
- }),
- LspItem::WithBinaryStatus(
- server_id,
- language_server_name,
- language_server_binary_status,
- ) => Some(ServerInfo {
- name: language_server_name.clone(),
- id: *server_id,
- health: None,
- binary_status: Some(language_server_binary_status.clone()),
- message: language_server_binary_status.message.clone(),
- }),
+ menu = menu.item(ContextMenuItem::custom_entry(
+ move |_, _| {
+ h_flex()
+ .group("menu_item")
+ .w_full()
+ .gap_2()
+ .justify_between()
+ .child(
+ h_flex()
+ .gap_2()
+ .child(Indicator::dot().color(status_color))
+ .child(Label::new(server_info.name.0.clone())),
+ )
+ .child(
+ h_flex()
+ .visible_on_hover("menu_item")
+ .child(
+ Label::new("View Logs")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .child(
+ Icon::new(IconName::ChevronRight)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ ),
+ )
+ .into_any_element()
+ },
+ {
+ let lsp_logs = lsp_logs.clone();
+ move |window, cx| {
+ if !has_logs {
+ cx.propagate();
+ return;
+ }
+ lsp_logs.update(cx, |lsp_logs, cx| {
+ lsp_logs.open_server_trace(
+ workspace.clone(),
+ server_selector.clone(),
+ window,
+ cx,
+ );
+ });
+ }
+ },
+ server_info.message.map(|server_message| {
+ DocumentationAside::new(
+ DocumentationSide::Right,
+ Rc::new(move |_| Label::new(server_message.clone()).into_any_element()),
+ )
+ }),
+ ));
}
+ menu
}
}
@@ -369,6 +374,36 @@ enum LspItem {
},
}
+impl LspItem {
+ fn server_info(&self) -> Option<ServerInfo> {
+ match self {
+ LspItem::ToggleServersButton { .. } => None,
+ LspItem::WithHealthCheck(
+ language_server_id,
+ language_server_health_status,
+ language_server_binary_status,
+ ) => Some(ServerInfo {
+ name: language_server_health_status.name.clone(),
+ id: Some(*language_server_id),
+ health: language_server_health_status.health(),
+ binary_status: language_server_binary_status.clone(),
+ message: language_server_health_status.message(),
+ }),
+ LspItem::WithBinaryStatus(
+ server_id,
+ language_server_name,
+ language_server_binary_status,
+ ) => Some(ServerInfo {
+ name: language_server_name.clone(),
+ id: *server_id,
+ health: None,
+ binary_status: Some(language_server_binary_status.clone()),
+ message: language_server_binary_status.message.clone(),
+ }),
+ }
+ }
+}
+
impl ServerData<'_> {
fn name(&self) -> &LanguageServerName {
match self {
@@ -389,267 +424,21 @@ impl ServerData<'_> {
}
}
-impl PickerDelegate for LspPickerDelegate {
- type ListItem = AnyElement;
-
- fn match_count(&self) -> usize {
- self.items.len()
- }
-
- fn selected_index(&self) -> usize {
- self.selected_index
- }
-
- fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
- self.selected_index = ix;
- cx.notify();
- }
-
- fn update_matches(
- &mut self,
- _: String,
- _: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) -> Task<()> {
- cx.spawn(async move |lsp_picker, cx| {
- cx.background_executor()
- .timer(Duration::from_millis(30))
- .await;
- lsp_picker
- .update(cx, |lsp_picker, cx| {
- lsp_picker.delegate.regenerate_items(cx);
- })
- .ok();
- })
- }
-
- fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
- Arc::default()
- }
-
- fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
- if let Some(LspItem::ToggleServersButton { restart }) = self.items.get(self.selected_index)
- {
- let lsp_store = self.state.read(cx).lsp_store.clone();
- lsp_store
- .update(cx, |lsp_store, cx| {
- if *restart {
- let Some(workspace) = self.state.read(cx).workspace.upgrade() else {
- return;
- };
- let project = workspace.read(cx).project().clone();
- let buffer_store = project.read(cx).buffer_store().clone();
- let worktree_store = project.read(cx).worktree_store();
-
- let buffers = self
- .state
- .read(cx)
- .language_servers
- .servers_per_buffer_abs_path
- .keys()
- .filter_map(|abs_path| {
- worktree_store.read(cx).find_worktree(abs_path, cx)
- })
- .filter_map(|(worktree, relative_path)| {
- let entry = worktree.read(cx).entry_for_path(&relative_path)?;
- project.read(cx).path_for_entry(entry.id, cx)
- })
- .filter_map(|project_path| {
- buffer_store.read(cx).get_by_path(&project_path)
- })
- .collect();
- let selectors = self
- .items
- .iter()
- // Do not try to use IDs as we have stopped all servers already, when allowing to restart them all
- .flat_map(|item| match item {
- LspItem::ToggleServersButton { .. } => None,
- LspItem::WithHealthCheck(_, status, ..) => {
- Some(LanguageServerSelector::Name(status.name.clone()))
- }
- LspItem::WithBinaryStatus(_, server_name, ..) => {
- Some(LanguageServerSelector::Name(server_name.clone()))
- }
- })
- .collect();
- lsp_store.restart_language_servers_for_buffers(buffers, selectors, cx);
- } else {
- lsp_store.stop_all_language_servers(cx);
- }
- })
- .ok();
- }
-
- let Some(server_selector) = self
- .server_info(self.selected_index)
- .map(|info| info.server_selector())
- else {
- return;
- };
- let lsp_logs = cx.global::<GlobalLogStore>().0.clone();
- let lsp_store = self.state.read(cx).lsp_store.clone();
- let workspace = self.state.read(cx).workspace.clone();
- lsp_logs
- .update(cx, |lsp_logs, cx| {
- let has_logs = lsp_store
- .update(cx, |lsp_store, _| {
- lsp_store.as_local().is_some() && lsp_logs.has_server_logs(&server_selector)
- })
- .unwrap_or(false);
- if has_logs {
- lsp_logs.open_server_trace(workspace, server_selector, window, cx);
- }
- })
- .ok();
- }
-
- fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
- cx.emit(DismissEvent);
- }
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- _: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- let rendered_match = h_flex().px_1().gap_1();
- let rendered_match_contents = h_flex()
- .id(("lsp-item", ix))
- .w_full()
- .px_2()
- .gap_2()
- .when(selected, |server_entry| {
- server_entry.bg(cx.theme().colors().element_hover)
- })
- .hover(|s| s.bg(cx.theme().colors().element_hover));
-
- if let Some(LspItem::ToggleServersButton { restart }) = self.items.get(ix) {
- let label = Label::new(if *restart {
- "Restart All Servers"
- } else {
- "Stop All Servers"
- });
- return Some(
- rendered_match
- .child(rendered_match_contents.child(label))
- .into_any_element(),
- );
- }
-
- let server_info = self.server_info(ix)?;
- let workspace = self.state.read(cx).workspace.clone();
- let lsp_logs = cx.global::<GlobalLogStore>().0.upgrade()?;
- let lsp_store = self.state.read(cx).lsp_store.upgrade()?;
- let server_selector = server_info.server_selector();
-
- // TODO currently, Zed remote does not work well with the LSP logs
- // https://github.com/zed-industries/zed/issues/28557
- let has_logs = lsp_store.read(cx).as_local().is_some()
- && lsp_logs.read(cx).has_server_logs(&server_selector);
-
- let status_color = server_info
- .binary_status
- .and_then(|binary_status| match binary_status.status {
- BinaryStatus::None => None,
- BinaryStatus::CheckingForUpdate
- | BinaryStatus::Downloading
- | BinaryStatus::Starting => Some(Color::Modified),
- BinaryStatus::Stopping => Some(Color::Disabled),
- BinaryStatus::Stopped => Some(Color::Disabled),
- BinaryStatus::Failed { .. } => Some(Color::Error),
- })
- .or_else(|| {
- Some(match server_info.health? {
- ServerHealth::Ok => Color::Success,
- ServerHealth::Warning => Color::Warning,
- ServerHealth::Error => Color::Error,
- })
- })
- .unwrap_or(Color::Success);
-
- Some(
- rendered_match
- .child(
- rendered_match_contents
- .child(Indicator::dot().color(status_color))
- .child(Label::new(server_info.name.0.clone()))
- .when_some(
- server_info.message.clone(),
- |server_entry, server_message| {
- server_entry.tooltip(Tooltip::text(server_message.clone()))
- },
- ),
- )
- .when_else(
- has_logs,
- |server_entry| {
- server_entry.on_mouse_down(MouseButton::Left, {
- let workspace = workspace.clone();
- let lsp_logs = lsp_logs.downgrade();
- let server_selector = server_selector.clone();
- move |_, window, cx| {
- lsp_logs
- .update(cx, |lsp_logs, cx| {
- lsp_logs.open_server_trace(
- workspace.clone(),
- server_selector.clone(),
- window,
- cx,
- );
- })
- .ok();
- }
- })
- },
- |div| div.cursor_default(),
- )
- .into_any_element(),
- )
- }
-
- fn render_editor(
- &self,
- editor: &Entity<Editor>,
- _: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) -> Div {
- div().child(div().track_focus(&editor.focus_handle(cx)))
- }
-
- fn separators_after_indices(&self) -> Vec<usize> {
- if self.items.is_empty() {
- return Vec::new();
- }
- let mut indices = vec![self.items.len().saturating_sub(2)];
- if let Some(other_servers_start_index) = self.other_servers_start_index {
- if other_servers_start_index > 0 {
- indices.insert(0, other_servers_start_index - 1);
- indices.dedup();
- }
- }
- indices
- }
-}
-
impl LspTool {
pub fn new(
workspace: &Workspace,
- popover_menu_handle: PopoverMenuHandle<Picker<LspPickerDelegate>>,
+ popover_menu_handle: PopoverMenuHandle<ContextMenu>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let settings_subscription =
cx.observe_global_in::<SettingsStore>(window, move |lsp_tool, window, cx| {
if ProjectSettings::get_global(cx).global_lsp_settings.button {
- if lsp_tool.lsp_picker.is_none() {
- lsp_tool.lsp_picker =
- Some(Self::new_lsp_picker(lsp_tool.state.clone(), window, cx));
- cx.notify();
+ if lsp_tool.lsp_menu.is_none() {
+ lsp_tool.refresh_lsp_menu(true, window, cx);
return;
}
- } else if lsp_tool.lsp_picker.take().is_some() {
+ } else if lsp_tool.lsp_menu.take().is_some() {
cx.notify();
}
});
@@ -660,17 +449,20 @@ impl LspTool {
lsp_tool.on_lsp_store_event(e, window, cx)
});
- let state = cx.new(|_| PickerState {
+ let state = cx.new(|_| LanguageServerState {
workspace: workspace.weak_handle(),
+ items: Vec::new(),
+ other_servers_start_index: None,
lsp_store: lsp_store.downgrade(),
active_editor: None,
language_servers: LanguageServers::default(),
});
Self {
- state,
+ server_state: state,
popover_menu_handle,
- lsp_picker: None,
+ lsp_menu: None,
+ lsp_menu_refresh: Task::ready(()),
_subscriptions: vec![settings_subscription, lsp_store_subscription],
}
}
@@ -681,7 +473,7 @@ impl LspTool {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let Some(lsp_picker) = self.lsp_picker.clone() else {
+ if self.lsp_menu.is_none() {
return;
};
let mut updated = false;
@@ -714,7 +506,7 @@ impl LspTool {
BinaryStatus::Failed { error }
}
};
- self.state.update(cx, |state, _| {
+ self.server_state.update(cx, |state, _| {
state.language_servers.update_binary_status(
binary_status,
status_update.message.as_deref(),
@@ -731,7 +523,7 @@ impl LspTool {
proto::ServerHealth::Warning => ServerHealth::Warning,
proto::ServerHealth::Error => ServerHealth::Error,
};
- self.state.update(cx, |state, _| {
+ self.server_state.update(cx, |state, _| {
state.language_servers.update_server_health(
*language_server_id,
health,
@@ -750,7 +542,7 @@ impl LspTool {
message: proto::update_language_server::Variant::RegisteredForBuffer(update),
..
} => {
- self.state.update(cx, |state, _| {
+ self.server_state.update(cx, |state, _| {
state
.language_servers
.servers_per_buffer_abs_path
@@ -764,27 +556,202 @@ impl LspTool {
};
if updated {
- lsp_picker.update(cx, |lsp_picker, cx| {
- lsp_picker.refresh(window, cx);
- });
+ self.refresh_lsp_menu(false, window, cx);
}
}
- fn new_lsp_picker(
- state: Entity<PickerState>,
+ fn regenerate_items(&mut self, cx: &mut App) {
+ self.server_state.update(cx, |state, cx| {
+ let editor_buffers = state
+ .active_editor
+ .as_ref()
+ .map(|active_editor| active_editor.editor_buffers.clone())
+ .unwrap_or_default();
+ let editor_buffer_paths = editor_buffers
+ .iter()
+ .filter_map(|buffer_id| {
+ let buffer_path = state
+ .lsp_store
+ .update(cx, |lsp_store, cx| {
+ Some(
+ project::File::from_dyn(
+ lsp_store
+ .buffer_store()
+ .read(cx)
+ .get(*buffer_id)?
+ .read(cx)
+ .file(),
+ )?
+ .abs_path(cx),
+ )
+ })
+ .ok()??;
+ Some(buffer_path)
+ })
+ .collect::<Vec<_>>();
+
+ let mut servers_with_health_checks = HashSet::default();
+ let mut server_ids_with_health_checks = HashSet::default();
+ let mut buffer_servers =
+ Vec::with_capacity(state.language_servers.health_statuses.len());
+ let mut other_servers =
+ Vec::with_capacity(state.language_servers.health_statuses.len());
+ let buffer_server_ids = editor_buffer_paths
+ .iter()
+ .filter_map(|buffer_path| {
+ state
+ .language_servers
+ .servers_per_buffer_abs_path
+ .get(buffer_path)
+ })
+ .flatten()
+ .fold(HashMap::default(), |mut acc, (server_id, name)| {
+ match acc.entry(*server_id) {
+ hash_map::Entry::Occupied(mut o) => {
+ let old_name: &mut Option<&LanguageServerName> = o.get_mut();
+ if old_name.is_none() {
+ *old_name = name.as_ref();
+ }
+ }
+ hash_map::Entry::Vacant(v) => {
+ v.insert(name.as_ref());
+ }
+ }
+ acc
+ });
+ for (server_id, server_state) in &state.language_servers.health_statuses {
+ let binary_status = state
+ .language_servers
+ .binary_statuses
+ .get(&server_state.name);
+ servers_with_health_checks.insert(&server_state.name);
+ server_ids_with_health_checks.insert(*server_id);
+ if buffer_server_ids.contains_key(server_id) {
+ buffer_servers.push(ServerData::WithHealthCheck(
+ *server_id,
+ server_state,
+ binary_status,
+ ));
+ } else {
+ other_servers.push(ServerData::WithHealthCheck(
+ *server_id,
+ server_state,
+ binary_status,
+ ));
+ }
+ }
+
+ let mut can_stop_all = !state.language_servers.health_statuses.is_empty();
+ let mut can_restart_all = state.language_servers.health_statuses.is_empty();
+ for (server_name, status) in state
+ .language_servers
+ .binary_statuses
+ .iter()
+ .filter(|(name, _)| !servers_with_health_checks.contains(name))
+ {
+ match status.status {
+ BinaryStatus::None => {
+ can_restart_all = false;
+ can_stop_all |= true;
+ }
+ BinaryStatus::CheckingForUpdate => {
+ can_restart_all = false;
+ can_stop_all = false;
+ }
+ BinaryStatus::Downloading => {
+ can_restart_all = false;
+ can_stop_all = false;
+ }
+ BinaryStatus::Starting => {
+ can_restart_all = false;
+ can_stop_all = false;
+ }
+ BinaryStatus::Stopping => {
+ can_restart_all = false;
+ can_stop_all = false;
+ }
+ BinaryStatus::Stopped => {}
+ BinaryStatus::Failed { .. } => {}
+ }
+
+ let matching_server_id = state
+ .language_servers
+ .servers_per_buffer_abs_path
+ .iter()
+ .filter(|(path, _)| editor_buffer_paths.contains(path))
+ .flat_map(|(_, server_associations)| server_associations.iter())
+ .find_map(|(id, name)| {
+ if name.as_ref() == Some(server_name) {
+ Some(*id)
+ } else {
+ None
+ }
+ });
+ if let Some(server_id) = matching_server_id {
+ buffer_servers.push(ServerData::WithBinaryStatus(
+ Some(server_id),
+ server_name,
+ status,
+ ));
+ } else {
+ other_servers.push(ServerData::WithBinaryStatus(None, server_name, status));
+ }
+ }
+
+ buffer_servers.sort_by_key(|data| data.name().clone());
+ other_servers.sort_by_key(|data| data.name().clone());
+
+ let mut other_servers_start_index = None;
+ let mut new_lsp_items =
+ Vec::with_capacity(buffer_servers.len() + other_servers.len() + 1);
+ new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item));
+ if !new_lsp_items.is_empty() {
+ other_servers_start_index = Some(new_lsp_items.len());
+ }
+ new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item));
+ if !new_lsp_items.is_empty() {
+ if can_stop_all {
+ new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
+ new_lsp_items.push(LspItem::ToggleServersButton { restart: false });
+ } else if can_restart_all {
+ new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
+ }
+ }
+
+ state.items = new_lsp_items;
+ state.other_servers_start_index = other_servers_start_index;
+ });
+ }
+
+ fn refresh_lsp_menu(
+ &mut self,
+ create_if_empty: bool,
window: &mut Window,
cx: &mut Context<Self>,
- ) -> Entity<Picker<LspPickerDelegate>> {
- cx.new(|cx| {
- let mut delegate = LspPickerDelegate {
- selected_index: 0,
- other_servers_start_index: None,
- items: Vec::new(),
- state,
- };
- delegate.regenerate_items(cx);
- Picker::list(delegate, window, cx)
- })
+ ) {
+ if create_if_empty || self.lsp_menu.is_some() {
+ let state = self.server_state.clone();
+ self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_tool, cx| {
+ cx.background_executor()
+ .timer(Duration::from_millis(30))
+ .await;
+ lsp_tool
+ .update_in(cx, |lsp_tool, window, cx| {
+ lsp_tool.regenerate_items(cx);
+ let menu = ContextMenu::build(window, cx, |menu, _, cx| {
+ state.update(cx, |state, cx| state.fill_menu(menu, cx))
+ });
+ lsp_tool.lsp_menu = Some(menu.clone());
+ lsp_tool.popover_menu_handle.refresh_menu(
+ window,
+ cx,
+ Rc::new(move |_, _| Some(menu.clone())),
+ );
+ cx.notify();
+ })
+ .ok();
+ });
+ }
}
}
@@ -15,7 +15,13 @@ use workspace::{
item::{Item, ItemHandle},
};
-actions!(dev, [OpenSyntaxTreeView]);
+actions!(
+ dev,
+ [
+ /// Opens the syntax tree view for the current file.
+ OpenSyntaxTreeView
+ ]
+);
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _, _| {
@@ -25,7 +25,7 @@
receiver: (parameter_list
"(" @context
(parameter_declaration
- name: (_) @name
+ name: (_) @context
type: (_) @context)
")" @context)
name: (field_identifier) @name
@@ -14,6 +14,15 @@
"(" @context
")" @context)) @item
+(generator_function_declaration
+ "async"? @context
+ "function" @context
+ "*" @context
+ name: (_) @name
+ parameters: (formal_parameters
+ "(" @context
+ ")" @context)) @item
+
(interface_declaration
"interface" @context
name: (_) @name) @item
@@ -1,9 +1,8 @@
(comment) @comment.inclusive
-[
- (string)
- (template_string)
-] @string
+(string) @string
+
+(template_string (string_fragment) @string)
(jsx_element) @element
@@ -8,7 +8,8 @@ use futures::StreamExt;
use gpui::{App, AsyncApp, Task};
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
use language::{
- ContextProvider, LanguageRegistry, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
+ ContextProvider, LanguageRegistry, LanguageToolchainStore, LocalFile as _, LspAdapter,
+ LspAdapterDelegate,
};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
@@ -65,13 +66,14 @@ impl ContextProvider for JsonTaskProvider {
.ok()?
.await
.ok()?;
+ let path = cx.update(|cx| file.abs_path(cx)).ok()?.as_path().into();
let task_templates = if is_package_json {
let package_json = serde_json_lenient::from_str::<
HashMap<String, serde_json_lenient::Value>,
>(&contents.text)
.ok()?;
- let package_json = PackageJsonData::new(file.path.clone(), package_json);
+ let package_json = PackageJsonData::new(path, package_json);
let command = package_json.package_manager.unwrap_or("npm").to_owned();
package_json
.scripts
@@ -270,6 +272,7 @@ impl JsonLspAdapter {
#[cfg(debug_assertions)]
fn generate_inspector_style_schema() -> serde_json_lenient::Value {
let schema = schemars::generate::SchemaSettings::draft2019_09()
+ .with_transform(util::schemars::DefaultDenyUnknownFields)
.into_generator()
.root_schema_for::<gpui::StyleRefinement>();
@@ -34,5 +34,4 @@ decrease_indent_patterns = [
{ pattern = "^\\s*else\\b.*:", valid_after = ["if", "elif", "for", "while", "except"] },
{ pattern = "^\\s*except\\b.*:", valid_after = ["try", "except"] },
{ pattern = "^\\s*finally\\b.*:", valid_after = ["try", "except", "else"] },
- { pattern = "^\\s*case\\b.*:", valid_after = ["match", "case"] }
]
@@ -14,4 +14,4 @@
(else_clause) @start.else
(except_clause) @start.except
(finally_clause) @start.finally
-(case_pattern) @start.case
+(case_clause) @start.case
@@ -18,6 +18,15 @@
"(" @context
")" @context)) @item
+(generator_function_declaration
+ "async"? @context
+ "function" @context
+ "*" @context
+ name: (_) @name
+ parameters: (formal_parameters
+ "(" @context
+ ")" @context)) @item
+
(interface_declaration
"interface" @context
name: (_) @name) @item
@@ -1,9 +1,8 @@
(comment) @comment.inclusive
-[
- (string)
- (template_string)
-] @string
+(string) @string
+
+(template_string (string_fragment) @string)
(jsx_element) @element
@@ -221,15 +221,30 @@ impl PackageJsonData {
});
}
+ let script_name_counts: HashMap<_, usize> =
+ self.scripts
+ .iter()
+ .fold(HashMap::default(), |mut acc, (_, script)| {
+ *acc.entry(script).or_default() += 1;
+ acc
+ });
for (path, script) in &self.scripts {
+ let label = if script_name_counts.get(script).copied().unwrap_or_default() > 1
+ && let Some(parent) = path.parent().and_then(|parent| parent.file_name())
+ {
+ let parent = parent.to_string_lossy();
+ format!("{parent}/package.json > {script}")
+ } else {
+ format!("package.json > {script}")
+ };
task_templates.0.push(TaskTemplate {
- label: format!("package.json > {script}",),
+ label,
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec!["run".to_owned(), script.to_owned()],
tags: vec!["package-script".into()],
cwd: Some(
path.parent()
- .unwrap_or(Path::new(""))
+ .unwrap_or(Path::new("/"))
.to_string_lossy()
.to_string(),
),
@@ -848,7 +863,7 @@ impl LspAdapter for EsLintLspAdapter {
},
"experimental": {
"useFlatConfig": use_flat_config,
- },
+ }
});
let override_options = cx.update(|cx| {
@@ -1014,6 +1029,7 @@ mod tests {
use language::language_settings;
use project::{FakeFs, Project};
use serde_json::json;
+ use task::TaskTemplates;
use unindent::Unindent;
use util::path;
@@ -1059,6 +1075,62 @@ mod tests {
);
}
+ #[gpui::test]
+ async fn test_generator_function_outline(cx: &mut TestAppContext) {
+ let language = crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into());
+
+ let text = r#"
+ function normalFunction() {
+ console.log("normal");
+ }
+
+ function* simpleGenerator() {
+ yield 1;
+ yield 2;
+ }
+
+ async function* asyncGenerator() {
+ yield await Promise.resolve(1);
+ }
+
+ function* generatorWithParams(start, end) {
+ for (let i = start; i <= end; i++) {
+ yield i;
+ }
+ }
+
+ class TestClass {
+ *methodGenerator() {
+ yield "method";
+ }
+
+ async *asyncMethodGenerator() {
+ yield "async method";
+ }
+ }
+ "#
+ .unindent();
+
+ let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
+ let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
+ assert_eq!(
+ outline
+ .items
+ .iter()
+ .map(|item| (item.text.as_str(), item.depth))
+ .collect::<Vec<_>>(),
+ &[
+ ("function normalFunction()", 0),
+ ("function* simpleGenerator()", 0),
+ ("async function* asyncGenerator()", 0),
+ ("function* generatorWithParams( )", 0),
+ ("class TestClass", 0),
+ ("*methodGenerator()", 1),
+ ("async *asyncMethodGenerator()", 1),
+ ]
+ );
+ }
+
#[gpui::test]
async fn test_package_json_discovery(executor: BackgroundExecutor, cx: &mut TestAppContext) {
cx.update(|cx| {
@@ -1135,5 +1207,42 @@ mod tests {
package_manager: None,
}
);
+
+ let mut task_templates = TaskTemplates::default();
+ package_json_data.fill_task_templates(&mut task_templates);
+ let task_templates = task_templates
+ .0
+ .into_iter()
+ .map(|template| (template.label, template.cwd))
+ .collect::<Vec<_>>();
+ pretty_assertions::assert_eq!(
+ task_templates,
+ [
+ (
+ "vitest file test".into(),
+ Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()),
+ ),
+ (
+ "vitest test $ZED_SYMBOL".into(),
+ Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()),
+ ),
+ (
+ "mocha file test".into(),
+ Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()),
+ ),
+ (
+ "mocha test $ZED_SYMBOL".into(),
+ Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()),
+ ),
+ (
+ "root/package.json > test".into(),
+ Some(path!("/root").into())
+ ),
+ (
+ "sub/package.json > test".into(),
+ Some(path!("/root/sub").into())
+ ),
+ ]
+ );
}
}
@@ -18,6 +18,15 @@
"(" @context
")" @context)) @item
+(generator_function_declaration
+ "async"? @context
+ "function" @context
+ "*" @context
+ name: (_) @name
+ parameters: (formal_parameters
+ "(" @context
+ ")" @context)) @item
+
(interface_declaration
"interface" @context
name: (_) @name) @item
@@ -1,6 +1,9 @@
(comment) @comment.inclusive
+
(string) @string
+(template_string (string_fragment) @string)
+
(_ value: (call_expression
function: (identifier) @function_name_before_type_arguments
type_arguments: (type_arguments)))
@@ -12,6 +12,6 @@ brackets = [
auto_indent_on_paste = false
auto_indent_using_last_non_empty_line = false
-increase_indent_pattern = ":\\s*[|>]?\\s*$"
+increase_indent_pattern = "^[^#]*:\\s*[|>]?\\s*$"
prettier_parser_name = "yaml"
tab_size = 2
@@ -25,7 +25,7 @@ async-trait.workspace = true
collections.workspace = true
cpal.workspace = true
futures.workspace = true
-gpui = { workspace = true, features = ["x11", "wayland"] }
+gpui = { workspace = true, features = ["screen-capture", "x11", "wayland", "windows-manifest"] }
gpui_tokio.workspace = true
http_client_tls.workspace = true
image.workspace = true
@@ -45,7 +45,7 @@ livekit = { rev = "d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4", git = "https://git
"__rustls-tls"
] }
-[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
+[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies]
scap.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
@@ -3,36 +3,16 @@ use collections::HashMap;
mod remote_video_track_view;
pub use remote_video_track_view::{RemoteVideoTrackView, RemoteVideoTrackViewEvent};
-#[cfg(not(any(
- test,
- feature = "test-support",
- any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")
-)))]
+#[cfg(not(any(test, feature = "test-support", target_os = "freebsd")))]
mod livekit_client;
-#[cfg(not(any(
- test,
- feature = "test-support",
- any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")
-)))]
+#[cfg(not(any(test, feature = "test-support", target_os = "freebsd")))]
pub use livekit_client::*;
-#[cfg(any(
- test,
- feature = "test-support",
- any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")
-))]
+#[cfg(any(test, feature = "test-support", target_os = "freebsd"))]
mod mock_client;
-#[cfg(any(
- test,
- feature = "test-support",
- any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")
-))]
+#[cfg(any(test, feature = "test-support", target_os = "freebsd"))]
pub mod test;
-#[cfg(any(
- test,
- feature = "test-support",
- any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")
-))]
+#[cfg(any(test, feature = "test-support", target_os = "freebsd"))]
pub use mock_client::*;
#[derive(Debug, Clone)]
@@ -585,10 +585,10 @@ fn video_frame_buffer_from_webrtc(buffer: Box<dyn VideoBuffer>) -> Option<Remote
if start_ptr.is_null() {
return None;
}
- let bgra_frame_slice = std::slice::from_raw_parts_mut(start_ptr, byte_len);
+ let argb_frame_slice = std::slice::from_raw_parts_mut(start_ptr, byte_len);
buffer.to_argb(
- VideoFormatType::ARGB, // For some reason, this displays correctly while RGBA (the correct format) does not
- bgra_frame_slice,
+ VideoFormatType::ARGB,
+ argb_frame_slice,
stride,
width as i32,
height as i32,
@@ -596,12 +596,13 @@ fn video_frame_buffer_from_webrtc(buffer: Box<dyn VideoBuffer>) -> Option<Remote
Vec::from_raw_parts(start_ptr, byte_len, byte_len)
};
+ // TODO: Unclear why providing argb_image to RgbaImage works properly.
+ let image = RgbaImage::from_raw(width, height, argb_image)
+ .with_context(|| "Bug: not enough bytes allocated for image.")
+ .log_err()?;
+
Some(Arc::new(RenderImage::new(SmallVec::from_elem(
- Frame::new(
- RgbaImage::from_raw(width, height, argb_image)
- .with_context(|| "Bug: not enough bytes allocated for image.")
- .log_err()?,
- ),
+ Frame::new(image),
1,
))))
}
@@ -617,9 +618,9 @@ fn video_frame_buffer_to_webrtc(frame: ScreenCaptureFrame) -> Option<impl AsRef<
}
}
-#[cfg(any(target_os = "linux", target_os = "freebsd"))]
+#[cfg(not(target_os = "macos"))]
fn video_frame_buffer_to_webrtc(frame: ScreenCaptureFrame) -> Option<impl AsRef<dyn VideoBuffer>> {
- use libwebrtc::native::yuv_helper::argb_to_nv12;
+ use libwebrtc::native::yuv_helper::{abgr_to_nv12, argb_to_nv12};
use livekit::webrtc::prelude::NV12Buffer;
match frame.0 {
scap::frame::Frame::BGRx(frame) => {
@@ -638,6 +639,22 @@ fn video_frame_buffer_to_webrtc(frame: ScreenCaptureFrame) -> Option<impl AsRef<
);
Some(buffer)
}
+ scap::frame::Frame::RGBx(frame) => {
+ let mut buffer = NV12Buffer::new(frame.width as u32, frame.height as u32);
+ let (stride_y, stride_uv) = buffer.strides();
+ let (data_y, data_uv) = buffer.data_mut();
+ abgr_to_nv12(
+ &frame.data,
+ frame.width as u32 * 4,
+ data_y,
+ stride_y,
+ data_uv,
+ stride_uv,
+ frame.width,
+ frame.height,
+ );
+ Some(buffer)
+ }
scap::frame::Frame::YUVFrame(yuvframe) => {
let mut buffer = NV12Buffer::with_strides(
yuvframe.width as u32,
@@ -659,11 +676,6 @@ fn video_frame_buffer_to_webrtc(frame: ScreenCaptureFrame) -> Option<impl AsRef<
}
}
-#[cfg(target_os = "windows")]
-fn video_frame_buffer_to_webrtc(_frame: ScreenCaptureFrame) -> Option<impl AsRef<dyn VideoBuffer>> {
- None as Option<Box<dyn VideoBuffer>>
-}
-
trait DeviceChangeListenerApi: Stream<Item = ()> + Sized {
fn new(input: bool) -> Result<Self>;
}
@@ -141,7 +141,15 @@ pub type CodeBlockRenderFn = Arc<
pub type CodeBlockTransformFn =
Arc<dyn Fn(AnyDiv, Range<usize>, CodeBlockMetadata, &mut Window, &App) -> AnyDiv>;
-actions!(markdown, [Copy, CopyAsMarkdown]);
+actions!(
+ markdown,
+ [
+ /// Copies the selected text to the clipboard.
+ Copy,
+ /// Copies the selected text as markdown to the clipboard.
+ CopyAsMarkdown
+ ]
+);
impl Markdown {
pub fn new(
@@ -9,10 +9,15 @@ pub mod markdown_renderer;
actions!(
markdown,
[
+ /// Scrolls up by one page in the markdown preview.
MovePageUp,
+ /// Scrolls down by one page in the markdown preview.
MovePageDown,
+ /// 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
]
);
@@ -12,13 +12,21 @@ 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,
]
@@ -93,3 +93,9 @@ pub(crate) mod m_2025_06_27 {
pub(crate) use settings::SETTINGS_PATTERNS;
}
+
+pub(crate) mod m_2025_07_08 {
+ mod settings;
+
+ pub(crate) use settings::SETTINGS_PATTERNS;
+}
@@ -0,0 +1,37 @@
+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<usize>, 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,
+ }
+}
@@ -160,6 +160,10 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
migrations::m_2025_06_27::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_06_27,
),
+ (
+ migrations::m_2025_07_08::SETTINGS_PATTERNS,
+ &SETTINGS_QUERY_2025_07_08,
+ ),
];
run_migrations(text, migrations)
}
@@ -270,6 +274,10 @@ 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
+);
// custom query
static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
@@ -0,0 +1,25 @@
+[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
+workspace-hack.workspace = true
+
+[target.'cfg(target_os = "windows")'.dependencies]
+anyhow.workspace = true
+async-io = "2.4"
+windows.workspace = true
+
+[dev-dependencies]
+tempfile.workspace = true
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -0,0 +1,69 @@
+#[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<crate::UnixListener>);
+
+ impl UnixListener {
+ pub fn bind<P: AsRef<Path>>(path: P) -> Result<Self> {
+ 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<crate::UnixStream>);
+
+ impl UnixStream {
+ pub async fn connect<P: AsRef<Path>>(path: P) -> Result<Self> {
+ 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<Result<usize>> {
+ 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<Result<usize>> {
+ Pin::new(&mut self.0).poll_write(cx, buf)
+ }
+
+ fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
+ Pin::new(&mut self.0).poll_flush(cx)
+ }
+
+ fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
+ Pin::new(&mut self.0).poll_close(cx)
+ }
+ }
+}
@@ -0,0 +1,45 @@
+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<P: AsRef<Path>>(path: P) -> Result<Self> {
+ 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 _) }
+ }
+}
@@ -0,0 +1,107 @@
+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(())
+ })
+ }
+}
@@ -0,0 +1,59 @@
+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<Self> {
+ 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<Self> {
+ 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<usize> {
+ map_ret(unsafe { recv(self.0, buf, SEND_RECV_FLAGS::default()) })
+ }
+
+ pub(crate) fn send(&self, buf: &[u8]) -> Result<usize> {
+ map_ret(unsafe { send(self.0, buf, SEND_RECV_FLAGS::default()) })
+ }
+}
+
+impl Drop for UnixSocket {
+ fn drop(&mut self) {
+ unsafe { closesocket(self.0) };
+ }
+}
@@ -0,0 +1,60 @@
+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<P: AsRef<Path>>(path: P) -> Result<Self> {
+ 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<usize> {
+ self.0.recv(buf)
+ }
+}
+
+impl Write for UnixStream {
+ fn write(&mut self, buf: &[u8]) -> Result<usize> {
+ 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 _) }
+ }
+}
@@ -0,0 +1,76 @@
+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<P: AsRef<Path>>(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<usize> {
+ 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
+}
@@ -65,17 +65,28 @@ 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,
- ToggleFocus,
+ /// Unfolds the selected directory.
UnfoldDirectory,
+ /// Toggles focus on the outline panel.
+ ToggleFocus,
]
);
@@ -4573,53 +4584,52 @@ impl OutlinePanel {
.track_scroll(self.scroll_handle.clone())
.when(show_indent_guides, |list| {
list.with_decoration(
- ui::indent_guides(
- cx.entity().clone(),
- px(indent_size),
- IndentGuideColors::panel(cx),
- |outline_panel, range, _, _| {
- let entries = outline_panel.cached_entries.get(range);
- if let Some(entries) = entries {
- entries.into_iter().map(|item| item.depth).collect()
- } else {
- smallvec::SmallVec::new()
- }
- },
- )
- .with_render_fn(
- cx.entity().clone(),
- move |outline_panel, params, _, _| {
- const LEFT_OFFSET: Pixels = px(14.);
-
- let indent_size = params.indent_size;
- let item_height = params.item_height;
- let active_indent_guide_ix = find_active_indent_guide_ix(
- outline_panel,
- ¶ms.indent_guides,
- );
+ ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
+ .with_compute_indents_fn(
+ cx.entity().clone(),
+ |outline_panel, range, _, _| {
+ let entries = outline_panel.cached_entries.get(range);
+ if let Some(entries) = entries {
+ entries.into_iter().map(|item| item.depth).collect()
+ } else {
+ smallvec::SmallVec::new()
+ }
+ },
+ )
+ .with_render_fn(
+ cx.entity().clone(),
+ move |outline_panel, params, _, _| {
+ const LEFT_OFFSET: Pixels = px(14.);
+
+ let indent_size = params.indent_size;
+ let item_height = params.item_height;
+ let active_indent_guide_ix = find_active_indent_guide_ix(
+ outline_panel,
+ ¶ms.indent_guides,
+ );
- params
- .indent_guides
- .into_iter()
- .enumerate()
- .map(|(ix, layout)| {
- let bounds = Bounds::new(
- point(
- layout.offset.x * indent_size + LEFT_OFFSET,
- layout.offset.y * item_height,
- ),
- size(px(1.), layout.length * item_height),
- );
- ui::RenderedIndentGuide {
- bounds,
- layout,
- is_active: active_indent_guide_ix == Some(ix),
- hitbox: None,
- }
- })
- .collect()
- },
- ),
+ params
+ .indent_guides
+ .into_iter()
+ .enumerate()
+ .map(|(ix, layout)| {
+ let bounds = Bounds::new(
+ point(
+ layout.offset.x * indent_size + LEFT_OFFSET,
+ layout.offset.y * item_height,
+ ),
+ size(px(1.), layout.length * item_height),
+ );
+ ui::RenderedIndentGuide {
+ bounds,
+ layout,
+ is_active: active_indent_guide_ix == Some(ix),
+ hitbox: None,
+ }
+ })
+ .collect()
+ },
+ ),
)
})
};
@@ -5,7 +5,15 @@ use settings::Settings;
use theme::ThemeSettings;
use ui::{Tab, prelude::*};
-actions!(panel, [NextPanelTab, PreviousPanelTab]);
+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 {
@@ -59,10 +67,10 @@ pub fn panel_filled_button(label: impl Into<SharedString>) -> ui::Button {
pub fn panel_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton {
let id = ElementId::Name(id.into());
- ui::IconButton::new(id, icon)
+
+ IconButton::new(id, icon)
// TODO: Change this once we use on_surface_bg in button_like
.layer(ui::ElevationIndex::ModalSurface)
- .size(ui::ButtonSize::Compact)
}
pub fn panel_filled_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton {
@@ -352,6 +352,14 @@ pub fn debug_adapters_dir() -> &'static PathBuf {
DEBUG_ADAPTERS_DIR.get_or_init(|| data_dir().join("debug_adapters"))
}
+/// Returns the path to the agent servers directory
+///
+/// This is where agent servers are downloaded to
+pub fn agent_servers_dir() -> &'static PathBuf {
+ static AGENT_SERVERS_DIR: OnceLock<PathBuf> = OnceLock::new();
+ AGENT_SERVERS_DIR.get_or_init(|| data_dir().join("agent_servers"))
+}
+
/// Returns the path to the Copilot directory.
pub fn copilot_dir() -> &'static PathBuf {
static COPILOT_DIR: OnceLock<PathBuf> = OnceLock::new();
@@ -34,7 +34,13 @@ pub enum Direction {
Down,
}
-actions!(picker, [ConfirmCompletion]);
+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.
@@ -31,6 +31,7 @@ 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
@@ -72,6 +73,7 @@ settings.workspace = true
sha2.workspace = true
shellexpand.workspace = true
shlex.workspace = true
+smallvec.workspace = true
smol.workspace = true
snippet.workspace = true
snippet_provider.workspace = true
@@ -21,7 +21,13 @@ pub fn init(cx: &mut App) {
extension::init(cx);
}
-actions!(context_server, [Restart]);
+actions!(
+ context_server,
+ [
+ /// Restarts the context server.
+ Restart
+ ]
+);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContextServerStatus {
@@ -165,6 +171,15 @@ impl ContextServerStore {
)
}
+ /// Returns all configured context server ids, regardless of enabled state.
+ pub fn configured_server_ids(&self) -> Vec<ContextServerId> {
+ self.context_server_settings
+ .keys()
+ .cloned()
+ .map(ContextServerId)
+ .collect()
+ }
+
#[cfg(any(test, feature = "test-support"))]
pub fn test(
registry: Entity<ContextServerDescriptorRegistry>,
@@ -812,9 +827,9 @@ mod tests {
.await;
let executor = cx.executor();
- let registry = cx.new(|_| {
+ let registry = cx.new(|cx| {
let mut registry = ContextServerDescriptorRegistry::new();
- registry.register_context_server_descriptor(SERVER_1_ID.into(), fake_descriptor_1);
+ registry.register_context_server_descriptor(SERVER_1_ID.into(), fake_descriptor_1, cx);
registry
});
let store = cx.new(|cx| {
@@ -103,19 +103,20 @@ struct ContextServerDescriptorRegistryProxy {
impl ExtensionContextServerProxy for ContextServerDescriptorRegistryProxy {
fn register_context_server(&self, extension: Arc<dyn Extension>, id: Arc<str>, cx: &mut App) {
self.context_server_factory_registry
- .update(cx, |registry, _| {
+ .update(cx, |registry, cx| {
registry.register_context_server_descriptor(
id.clone(),
Arc::new(ContextServerDescriptor { id, extension })
as Arc<dyn registry::ContextServerDescriptor>,
+ cx,
)
});
}
fn unregister_context_server(&self, server_id: Arc<str>, cx: &mut App) {
self.context_server_factory_registry
- .update(cx, |registry, _| {
- registry.unregister_context_server_descriptor_by_id(&server_id)
+ .update(cx, |registry, cx| {
+ registry.unregister_context_server_descriptor_by_id(&server_id, cx)
});
}
}
@@ -4,7 +4,7 @@ use anyhow::Result;
use collections::HashMap;
use context_server::ContextServerCommand;
use extension::ContextServerConfiguration;
-use gpui::{App, AppContext as _, AsyncApp, Entity, Global, Task};
+use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Global, Task};
use crate::worktree_store::WorktreeStore;
@@ -66,12 +66,19 @@ impl ContextServerDescriptorRegistry {
&mut self,
id: Arc<str>,
descriptor: Arc<dyn ContextServerDescriptor>,
+ cx: &mut Context<Self>,
) {
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) {
+ pub fn unregister_context_server_descriptor_by_id(
+ &mut self,
+ server_id: &str,
+ cx: &mut Context<Self>,
+ ) {
self.context_servers.remove(server_id);
+ cx.notify();
}
}
@@ -15,7 +15,9 @@ 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;
@@ -1,6 +1,7 @@
use std::sync::Arc;
use anyhow::{Context as _, Ok, Result};
+use base64::Engine;
use dap::{
Capabilities, ContinueArguments, ExceptionFilterOptions, InitializeRequestArguments,
InitializeRequestArgumentsPathFormat, NextArguments, SetVariableResponse, SourceBreakpoint,
@@ -10,6 +11,7 @@ use dap::{
proto_conversions::ProtoConversion,
requests::{Continue, Next},
};
+
use rpc::proto;
use serde_json::Value;
use util::ResultExt;
@@ -17,6 +19,8 @@ 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
@@ -33,7 +37,6 @@ pub trait LocalDapCommand: 'static + Send + Sync + std::fmt::Debug {
pub trait DapCommand: LocalDapCommand {
type ProtoRequest: 'static + Send;
type ProtoResponse: 'static + Send;
- const CACHEABLE: bool = false;
#[allow(dead_code)]
fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId;
@@ -811,7 +814,7 @@ impl DapCommand for RestartCommand {
}
}
-#[derive(Debug, Hash, PartialEq, Eq)]
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct VariablesCommand {
pub variables_reference: u64,
pub filter: Option<VariablesArgumentsFilter>,
@@ -823,6 +826,7 @@ pub struct VariablesCommand {
impl LocalDapCommand for VariablesCommand {
type Response = Vec<Variable>;
type DapRequest = dap::requests::Variables;
+ const CACHEABLE: bool = true;
fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments {
dap::VariablesArguments {
@@ -845,7 +849,6 @@ impl LocalDapCommand for VariablesCommand {
impl DapCommand for VariablesCommand {
type ProtoRequest = proto::VariablesRequest;
type ProtoResponse = proto::DapVariables;
- const CACHEABLE: bool = true;
fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId {
SessionId::from_proto(request.client_id)
@@ -1041,6 +1044,7 @@ pub(crate) struct ModulesCommand;
impl LocalDapCommand for ModulesCommand {
type Response = Vec<dap::Module>;
type DapRequest = dap::requests::Modules;
+ const CACHEABLE: bool = true;
fn is_supported(capabilities: &Capabilities) -> bool {
capabilities.supports_modules_request.unwrap_or_default()
@@ -1064,7 +1068,6 @@ impl LocalDapCommand for ModulesCommand {
impl DapCommand for ModulesCommand {
type ProtoRequest = proto::DapModulesRequest;
type ProtoResponse = proto::DapModulesResponse;
- const CACHEABLE: bool = true;
fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId {
SessionId::from_proto(request.client_id)
@@ -1113,6 +1116,7 @@ pub(crate) struct LoadedSourcesCommand;
impl LocalDapCommand for LoadedSourcesCommand {
type Response = Vec<dap::Source>;
type DapRequest = dap::requests::LoadedSources;
+ const CACHEABLE: bool = true;
fn is_supported(capabilities: &Capabilities) -> bool {
capabilities
@@ -1134,7 +1138,6 @@ impl LocalDapCommand for LoadedSourcesCommand {
impl DapCommand for LoadedSourcesCommand {
type ProtoRequest = proto::DapLoadedSourcesRequest;
type ProtoResponse = proto::DapLoadedSourcesResponse;
- const CACHEABLE: bool = true;
fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId {
SessionId::from_proto(request.client_id)
@@ -1187,6 +1190,7 @@ pub(crate) struct StackTraceCommand {
impl LocalDapCommand for StackTraceCommand {
type Response = Vec<dap::StackFrame>;
type DapRequest = dap::requests::StackTrace;
+ const CACHEABLE: bool = true;
fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments {
dap::StackTraceArguments {
@@ -1208,7 +1212,6 @@ impl LocalDapCommand for StackTraceCommand {
impl DapCommand for StackTraceCommand {
type ProtoRequest = proto::DapStackTraceRequest;
type ProtoResponse = proto::DapStackTraceResponse;
- const CACHEABLE: bool = true;
fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest {
proto::DapStackTraceRequest {
@@ -1258,6 +1261,7 @@ pub(crate) struct ScopesCommand {
impl LocalDapCommand for ScopesCommand {
type Response = Vec<dap::Scope>;
type DapRequest = dap::requests::Scopes;
+ const CACHEABLE: bool = true;
fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments {
dap::ScopesArguments {
@@ -1276,7 +1280,6 @@ impl LocalDapCommand for ScopesCommand {
impl DapCommand for ScopesCommand {
type ProtoRequest = proto::DapScopesRequest;
type ProtoResponse = proto::DapScopesResponse;
- const CACHEABLE: bool = true;
fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest {
proto::DapScopesRequest {
@@ -1313,6 +1316,7 @@ impl DapCommand for ScopesCommand {
impl LocalDapCommand for super::session::CompletionsQuery {
type Response = dap::CompletionsResponse;
type DapRequest = dap::requests::Completions;
+ const CACHEABLE: bool = true;
fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments {
dap::CompletionsArguments {
@@ -1340,7 +1344,6 @@ impl LocalDapCommand for super::session::CompletionsQuery {
impl DapCommand for super::session::CompletionsQuery {
type ProtoRequest = proto::DapCompletionRequest;
type ProtoResponse = proto::DapCompletionResponse;
- const CACHEABLE: bool = true;
fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest {
proto::DapCompletionRequest {
@@ -1477,6 +1480,7 @@ pub(crate) struct ThreadsCommand;
impl LocalDapCommand for ThreadsCommand {
type Response = Vec<dap::Thread>;
type DapRequest = dap::requests::Threads;
+ const CACHEABLE: bool = true;
fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments {
dap::ThreadsArgument {}
@@ -1493,7 +1497,6 @@ impl LocalDapCommand for ThreadsCommand {
impl DapCommand for ThreadsCommand {
type ProtoRequest = proto::DapThreadsRequest;
type ProtoResponse = proto::DapThreadsResponse;
- const CACHEABLE: bool = true;
fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest {
proto::DapThreadsRequest {
@@ -1665,6 +1668,130 @@ impl LocalDapCommand for SetBreakpoints {
Ok(message.breakpoints)
}
}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub enum DataBreakpointContext {
+ Variable {
+ variables_reference: u64,
+ name: String,
+ bytes: Option<u64>,
+ },
+ Expression {
+ expression: String,
+ frame_id: Option<u64>,
+ },
+ Address {
+ address: String,
+ bytes: Option<u64>,
+ },
+}
+
+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<DataBreakpointContext>,
+ pub mode: Option<String>,
+}
+
+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) -> <Self::DapRequest as dap::requests::Request>::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: <Self::DapRequest as dap::requests::Request>::Response,
+ ) -> Result<Self::Response> {
+ Ok(message)
+ }
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub(crate) struct SetDataBreakpointsCommand {
+ pub breakpoints: Vec<dap::DataBreakpoint>,
+}
+
+impl LocalDapCommand for SetDataBreakpointsCommand {
+ type Response = Vec<dap::Breakpoint>;
+ type DapRequest = dap::requests::SetDataBreakpoints;
+
+ fn is_supported(capabilities: &Capabilities) -> bool {
+ capabilities.supports_data_breakpoints.unwrap_or(false)
+ }
+
+ fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments {
+ dap::SetDataBreakpointsArguments {
+ breakpoints: self.breakpoints.clone(),
+ }
+ }
+
+ fn response_from_dap(
+ &self,
+ message: <Self::DapRequest as dap::requests::Request>::Response,
+ ) -> Result<Self::Response> {
+ Ok(message.breakpoints)
+ }
+}
+
#[derive(Clone, Debug, Hash, PartialEq)]
pub(super) enum SetExceptionBreakpoints {
Plain {
@@ -1712,6 +1839,7 @@ pub(super) struct LocationsCommand {
impl LocalDapCommand for LocationsCommand {
type Response = dap::LocationsResponse;
type DapRequest = dap::requests::Locations;
+ const CACHEABLE: bool = true;
fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments {
dap::LocationsArguments {
@@ -1731,8 +1859,6 @@ impl DapCommand for LocationsCommand {
type ProtoRequest = proto::DapLocationsRequest;
type ProtoResponse = proto::DapLocationsResponse;
- const CACHEABLE: bool = true;
-
fn client_id_from_proto(message: &Self::ProtoRequest) -> SessionId {
SessionId::from_proto(message.session_id)
}
@@ -1774,3 +1900,76 @@ impl DapCommand for LocationsCommand {
})
}
}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub(crate) struct ReadMemory {
+ pub(crate) memory_reference: String,
+ pub(crate) offset: Option<u64>,
+ pub(crate) count: u64,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub(crate) struct ReadMemoryResponse {
+ pub(super) address: Arc<str>,
+ pub(super) unreadable_bytes: Option<u64>,
+ 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) -> <Self::DapRequest as dap::requests::Request>::Arguments {
+ dap::ReadMemoryArguments {
+ memory_reference: self.memory_reference.clone(),
+ offset: self.offset,
+ count: self.count,
+ }
+ }
+
+ fn response_from_dap(
+ &self,
+ message: <Self::DapRequest as dap::requests::Request>::Response,
+ ) -> Result<Self::Response> {
+ 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) -> <Self::DapRequest as dap::requests::Request>::Arguments {
+ self.clone()
+ }
+
+ fn response_from_dap(
+ &self,
+ message: <Self::DapRequest as dap::requests::Request>::Response,
+ ) -> Result<Self::Response> {
+ Ok(message)
+ }
+}
@@ -6,6 +6,7 @@ use super::{
};
use crate::{
InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState,
+ debugger::session::SessionQuirks,
project_settings::ProjectSettings,
terminals::{SshCommand, wrap_for_ssh},
worktree_store::WorktreeStore,
@@ -14,15 +15,13 @@ use anyhow::{Context as _, Result, anyhow};
use async_trait::async_trait;
use collections::HashMap;
use dap::{
- Capabilities, CompletionItem, CompletionsArguments, DapRegistry, DebugRequest,
- EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, Source, StackFrameId,
+ Capabilities, DapRegistry, DebugRequest, EvaluateArgumentsContext, StackFrameId,
adapters::{
DapDelegate, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments,
},
client::SessionId,
inline_value::VariableLookupKind,
messages::Message,
- requests::{Completions, Evaluate},
};
use fs::Fs;
use futures::{
@@ -35,11 +34,12 @@ use http_client::HttpClient;
use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind};
use node_runtime::NodeRuntime;
-use remote::SshRemoteClient;
+use remote::{SshRemoteClient, ssh_session::SshArgs};
use rpc::{
AnyProtoClient, TypedEnvelope,
proto::{self},
};
+use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsLocation, WorktreeId};
use std::{
borrow::Borrow,
@@ -93,10 +93,23 @@ pub struct DapStore {
worktree_store: Entity<WorktreeStore>,
sessions: BTreeMap<SessionId, Entity<Session>>,
next_session_id: u32,
+ adapter_options: BTreeMap<DebugAdapterName, Arc<PersistedAdapterOptions>>,
}
impl EventEmitter<DapStoreEvent> 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<String, PersistedExceptionBreakpoint>,
+}
+
impl DapStore {
pub fn init(client: &AnyProtoClient, cx: &mut App) {
static ADD_LOCATORS: Once = Once::new();
@@ -173,6 +186,7 @@ impl DapStore {
breakpoint_store,
worktree_store,
sessions: Default::default(),
+ adapter_options: Default::default(),
}
}
@@ -240,11 +254,16 @@ impl DapStore {
cx.spawn(async move |_, cx| {
let response = request.await?;
let binary = DebugAdapterBinary::from_proto(response)?;
- let mut ssh_command = ssh_client.read_with(cx, |ssh, _| {
- anyhow::Ok(SshCommand {
- arguments: ssh.ssh_args().context("SSH arguments not found")?,
- })
- })??;
+ let (mut ssh_command, envs, path_style) =
+ ssh_client.read_with(cx, |ssh, _| {
+ let (SshArgs { arguments, envs }, path_style) =
+ ssh.ssh_info().context("SSH arguments not found")?;
+ anyhow::Ok((
+ SshCommand { arguments },
+ envs.unwrap_or_default(),
+ path_style,
+ ))
+ })??;
let mut connection = None;
if let Some(c) = binary.connection {
@@ -269,12 +288,13 @@ impl DapStore {
binary.cwd.as_deref(),
binary.envs,
None,
+ path_style,
);
Ok(DebugAdapterBinary {
command: Some(program),
arguments: args,
- envs: HashMap::default(),
+ envs,
cwd: None,
connection,
request_args: binary.request_args,
@@ -366,10 +386,11 @@ impl DapStore {
pub fn new_session(
&mut self,
- label: SharedString,
+ label: Option<SharedString>,
adapter: DebugAdapterName,
task_context: TaskContext,
parent_session: Option<Entity<Session>>,
+ quirks: SessionQuirks,
cx: &mut Context<Self>,
) -> Entity<Session> {
let session_id = SessionId(util::post_inc(&mut self.next_session_id));
@@ -387,6 +408,7 @@ impl DapStore {
label,
adapter,
task_context,
+ quirks,
cx,
);
@@ -520,65 +542,6 @@ impl DapStore {
))
}
- pub fn evaluate(
- &self,
- session_id: &SessionId,
- stack_frame_id: u64,
- expression: String,
- context: EvaluateArgumentsContext,
- source: Option<Source>,
- cx: &mut Context<Self>,
- ) -> Task<Result<EvaluateResponse>> {
- let Some(client) = self
- .session_by_id(session_id)
- .and_then(|client| client.read(cx).adapter_client())
- else {
- return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id)));
- };
-
- cx.background_executor().spawn(async move {
- client
- .request::<Evaluate>(EvaluateArguments {
- expression: expression.clone(),
- frame_id: Some(stack_frame_id),
- context: Some(context),
- format: None,
- line: None,
- column: None,
- source,
- })
- .await
- })
- }
-
- pub fn completions(
- &self,
- session_id: &SessionId,
- stack_frame_id: u64,
- text: String,
- completion_column: u64,
- cx: &mut Context<Self>,
- ) -> Task<Result<Vec<CompletionItem>>> {
- let Some(client) = self
- .session_by_id(session_id)
- .and_then(|client| client.read(cx).adapter_client())
- else {
- return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id)));
- };
-
- cx.background_executor().spawn(async move {
- Ok(client
- .request::<Completions>(CompletionsArguments {
- frame_id: Some(stack_frame_id),
- line: None,
- text,
- column: completion_column,
- })
- .await?
- .targets)
- })
- }
-
pub fn resolve_inline_value_locations(
&self,
session: Entity<Session>,
@@ -600,6 +563,11 @@ impl DapStore {
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
@@ -607,7 +575,7 @@ impl DapStore {
index -= 1;
}
value.truncate(index);
- value.push_str("...");
+ value.push_str("…");
}
format!(": {}", value)
@@ -853,6 +821,45 @@ impl DapStore {
})
})
}
+
+ pub fn sync_adapter_options(
+ &mut self,
+ session: &Entity<Session>,
+ cx: &App,
+ ) -> Arc<PersistedAdapterOptions> {
+ 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<Arc<PersistedAdapterOptions>> {
+ self.adapter_options.get(name).cloned()
+ }
+
+ pub fn all_adapter_options(&self) -> &BTreeMap<DebugAdapterName, Arc<PersistedAdapterOptions>> {
+ &self.adapter_options
+ }
}
#[derive(Clone)]
@@ -119,7 +119,7 @@ impl DapLocator for CargoLocator {
.context("Couldn't get cwd from debug config which is needed for locators")?;
let builder = ShellBuilder::new(true, &build_config.shell).non_interactive();
let (program, args) = builder.build(
- "cargo".into(),
+ Some("cargo".into()),
&build_config
.args
.iter()
@@ -0,0 +1,384 @@
+//! 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<MappedPageContents>),
+}
+
+impl PageContents {
+ #[cfg(test)]
+ fn mapped(contents: Vec<u8>) -> 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<PageAddress>,
+ ) -> impl Iterator<Item = PageAddress> {
+ 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<PageAddress, PageContents>,
+}
+
+/// 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<u8>);
+
+impl Memory {
+ pub(super) fn new() -> Self {
+ Self {
+ pages: Default::default(),
+ }
+ }
+
+ pub(super) fn memory_range_to_page_range(
+ range: RangeInclusive<MemoryAddress>,
+ ) -> RangeInclusive<PageAddress> {
+ 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<MemoryPageBuilder> {
+ 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<MemoryAddress>) -> MemoryIterator {
+ let pages = Self::memory_range_to_page_range(range.clone());
+ let pages = self
+ .pages
+ .range(pages)
+ .map(|(address, page)| (*address, page.clone()))
+ .collect::<Vec<_>>();
+ 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<UnknownMemory> {
+ 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<MappedPageContents>) -> Box<dyn Iterator<Item = MemoryCell>> {
+ 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<dyn Iterator<Item = MemoryCell>>
+ }
+ 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<dyn Iterator<Item = MemoryCell>>)>,
+ pages: std::vec::IntoIter<(PageAddress, PageContents)>,
+}
+
+impl MemoryIterator {
+ fn new(
+ range: RangeInclusive<MemoryAddress>,
+ 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<Self::Item> {
+ if self.start > self.end {
+ return None;
+ }
+ if let Some((current_page_address, current_memory_chunk)) = self.current_known_page.as_mut()
+ {
+ if current_page_address.0 <= self.start {
+ if let Some(next_cell) = current_memory_chunk.next() {
+ self.start += 1;
+ return Some(next_cell);
+ } else {
+ self.current_known_page.take();
+ }
+ }
+ }
+ if !self.fetch_next_page() {
+ self.start += 1;
+ return 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::<Vec<_>>();
+ 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::<Vec<_>>();
+ 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::<Vec<_>>();
+ 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::<Vec<_>>();
+ let expected = (20..=30)
+ .map(|val| MemoryCell(Some(val)))
+ .collect::<Vec<_>>();
+ assert_eq!(actual.len(), expected.len());
+ assert_eq!(actual, expected);
+ }
+}
@@ -1,18 +1,21 @@
use crate::debugger::breakpoint_store::BreakpointSessionState;
+use crate::debugger::dap_command::{DataBreakpointContext, ReadMemory};
+use crate::debugger::memory::{self, Memory, MemoryIterator, MemoryPageBuilder, PageAddress};
use super::breakpoint_store::{
BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint,
};
use super::dap_command::{
- self, Attach, ConfigurationDone, ContinueCommand, DapCommand, DisconnectCommand,
+ self, Attach, ConfigurationDone, ContinueCommand, DataBreakpointInfoCommand, DisconnectCommand,
EvaluateCommand, Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, LocationsCommand,
ModulesCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand,
- ScopesCommand, SetExceptionBreakpoints, SetVariableValueCommand, StackTraceCommand,
- StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand,
- TerminateThreadsCommand, ThreadsCommand, VariablesCommand,
+ ScopesCommand, SetDataBreakpointsCommand, SetExceptionBreakpoints, SetVariableValueCommand,
+ StackTraceCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand,
+ TerminateCommand, TerminateThreadsCommand, ThreadsCommand, VariablesCommand,
};
use super::dap_store::DapStore;
use anyhow::{Context as _, Result, anyhow};
+use base64::Engine;
use collections::{HashMap, HashSet, IndexMap};
use dap::adapters::{DebugAdapterBinary, DebugAdapterName};
use dap::messages::Response;
@@ -26,7 +29,7 @@ use dap::{
use dap::{
ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEvent, OutputEventCategory,
RunInTerminalRequestArguments, StackFramePresentationHint, StartDebuggingRequestArguments,
- StartDebuggingRequestArgumentsRequest, VariablePresentationHint,
+ StartDebuggingRequestArgumentsRequest, VariablePresentationHint, WriteMemoryArguments,
};
use futures::SinkExt;
use futures::channel::mpsc::UnboundedSender;
@@ -42,6 +45,7 @@ use serde_json::Value;
use smol::stream::StreamExt;
use std::any::TypeId;
use std::collections::BTreeMap;
+use std::ops::RangeInclusive;
use std::u64;
use std::{
any::Any,
@@ -52,7 +56,7 @@ use std::{
};
use task::TaskContext;
use text::{PointUtf16, ToPointUtf16};
-use util::ResultExt;
+use util::{ResultExt, maybe};
use worktree::Worktree;
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)]
@@ -134,8 +138,15 @@ pub struct Watcher {
pub presentation_hint: Option<VariablePresentationHint>,
}
-pub enum Mode {
- Building,
+#[derive(Debug, Clone, PartialEq)]
+pub struct DataBreakpointState {
+ pub dap: dap::DataBreakpoint,
+ pub is_enabled: bool,
+ pub context: Arc<DataBreakpointContext>,
+}
+
+pub enum SessionState {
+ Building(Option<Task<Result<()>>>),
Running(RunningMode),
}
@@ -151,6 +162,12 @@ pub struct RunningMode {
messages_tx: UnboundedSender<Message>,
}
+#[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
@@ -409,17 +426,6 @@ impl RunningMode {
};
let configuration_done_supported = ConfigurationDone::is_supported(capabilities);
- let exception_filters = capabilities
- .exception_breakpoint_filters
- .as_ref()
- .map(|exception_filters| {
- exception_filters
- .iter()
- .filter(|filter| filter.default == Some(true))
- .cloned()
- .collect::<Vec<_>>()
- })
- .unwrap_or_default();
// From spec (on initialization sequence):
// client sends a setExceptionBreakpoints request if one or more exceptionBreakpointFilters have been defined (or if supportsConfigurationDoneRequest is not true)
//
@@ -434,10 +440,20 @@ impl RunningMode {
.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 |_, cx| {
- let breakpoint_store =
- dap_store.read_with(cx, |dap_store, _| dap_store.breakpoint_store().clone())?;
+ 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))?
@@ -471,7 +487,25 @@ impl RunningMode {
})?;
if should_send_exception_breakpoints {
- this.send_exception_breakpoints(exception_filters, supports_exception_filters)
+ _ = 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();
}
@@ -537,15 +571,15 @@ impl RunningMode {
}
}
-impl Mode {
- pub(super) fn request_dap<R: DapCommand>(&self, request: R) -> Task<Result<R::Response>>
+impl SessionState {
+ pub(super) fn request_dap<R: LocalDapCommand>(&self, request: R) -> Task<Result<R::Response>>
where
<R::DapRequest as dap::requests::Request>::Response: 'static,
<R::DapRequest as dap::requests::Request>::Arguments: 'static + Send,
{
match self {
- Mode::Running(debug_adapter_client) => debug_adapter_client.request(request),
- Mode::Building => Task::ready(Err(anyhow!(
+ SessionState::Running(debug_adapter_client) => debug_adapter_client.request(request),
+ SessionState::Building(_) => Task::ready(Err(anyhow!(
"no adapter running to send request: {request:?}"
))),
}
@@ -554,13 +588,13 @@ impl Mode {
/// Did this debug session stop at least once?
pub(crate) fn has_ever_stopped(&self) -> bool {
match self {
- Mode::Building => false,
- Mode::Running(running_mode) => running_mode.has_ever_stopped,
+ SessionState::Building(_) => false,
+ SessionState::Running(running_mode) => running_mode.has_ever_stopped,
}
}
fn stopped(&mut self) {
- if let Mode::Running(running) = self {
+ if let SessionState::Running(running) = self {
running.has_ever_stopped = true;
}
}
@@ -637,9 +671,9 @@ type IsEnabled = bool;
pub struct OutputToken(pub usize);
/// Represents a current state of a single debug adapter and provides ways to mutate it.
pub struct Session {
- pub mode: Mode,
+ pub mode: SessionState,
id: SessionId,
- label: SharedString,
+ label: Option<SharedString>,
adapter: DebugAdapterName,
pub(super) capabilities: Capabilities,
child_session_ids: HashSet<SessionId>,
@@ -659,8 +693,12 @@ pub struct Session {
pub(crate) breakpoint_store: Entity<BreakpointStore>,
ignore_breakpoints: bool,
exception_breakpoints: BTreeMap<String, (ExceptionBreakpointsFilter, IsEnabled)>,
+ data_breakpoints: BTreeMap<String, DataBreakpointState>,
background_tasks: Vec<Task<()>>,
+ restart_task: Option<Task<()>>,
task_context: TaskContext,
+ memory: memory::Memory,
+ quirks: SessionQuirks,
}
trait CacheableCommand: Any + Send + Sync {
@@ -671,7 +709,7 @@ trait CacheableCommand: Any + Send + Sync {
impl<T> CacheableCommand for T
where
- T: DapCommand + PartialEq + Eq + Hash,
+ T: LocalDapCommand + PartialEq + Eq + Hash,
{
fn dyn_eq(&self, rhs: &dyn CacheableCommand) -> bool {
(rhs as &dyn Any)
@@ -690,7 +728,7 @@ where
pub(crate) struct RequestSlot(Arc<dyn CacheableCommand>);
-impl<T: DapCommand + PartialEq + Eq + Hash> From<T> for RequestSlot {
+impl<T: LocalDapCommand + PartialEq + Eq + Hash> From<T> for RequestSlot {
fn from(request: T) -> Self {
Self(Arc::new(request))
}
@@ -750,6 +788,7 @@ pub enum SessionEvent {
request: RunInTerminalRequestArguments,
sender: mpsc::Sender<Result<u32>>,
},
+ DataBreakpointInfo,
ConsoleOutput,
}
@@ -774,9 +813,10 @@ impl Session {
breakpoint_store: Entity<BreakpointStore>,
session_id: SessionId,
parent_session: Option<Entity<Session>>,
- label: SharedString,
+ label: Option<SharedString>,
adapter: DebugAdapterName,
task_context: TaskContext,
+ quirks: SessionQuirks,
cx: &mut App,
) -> Entity<Self> {
cx.new::<Self>(|cx| {
@@ -802,10 +842,9 @@ impl Session {
BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {}
})
.detach();
- // cx.on_app_quit(Self::on_app_quit).detach();
let this = Self {
- mode: Mode::Building,
+ mode: SessionState::Building(None),
id: session_id,
child_session_ids: HashSet::default(),
parent_session,
@@ -821,14 +860,18 @@ impl Session {
loaded_sources: Vec::default(),
threads: IndexMap::default(),
background_tasks: Vec::default(),
+ restart_task: None,
locations: Default::default(),
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,
};
this
@@ -841,8 +884,8 @@ impl Session {
pub fn worktree(&self) -> Option<Entity<Worktree>> {
match &self.mode {
- Mode::Building => None,
- Mode::Running(local_mode) => local_mode.worktree.upgrade(),
+ SessionState::Building(_) => None,
+ SessionState::Running(local_mode) => local_mode.worktree.upgrade(),
}
}
@@ -901,7 +944,18 @@ impl Session {
)
.await?;
this.update(cx, |this, cx| {
- this.mode = Mode::Running(mode);
+ match &mut this.mode {
+ SessionState::Building(task) if task.is_some() => {
+ task.take().unwrap().detach_and_log_err(cx);
+ }
+ _ => {
+ debug_assert!(
+ this.parent_session.is_some(),
+ "Booting a root debug session without a boot task"
+ );
+ }
+ };
+ this.mode = SessionState::Running(mode);
cx.emit(SessionStateEvent::Running);
})?;
@@ -994,8 +1048,8 @@ impl Session {
pub fn binary(&self) -> Option<&DebugAdapterBinary> {
match &self.mode {
- Mode::Building => None,
- Mode::Running(running_mode) => Some(&running_mode.binary),
+ SessionState::Building(_) => None,
+ SessionState::Running(running_mode) => Some(&running_mode.binary),
}
}
@@ -1003,7 +1057,7 @@ impl Session {
self.adapter.clone()
}
- pub fn label(&self) -> SharedString {
+ pub fn label(&self) -> Option<SharedString> {
self.label.clone()
}
@@ -1016,7 +1070,7 @@ impl Session {
cx.spawn(async move |this, cx| {
while let Some(output) = rx.next().await {
- this.update(cx, |this, cx| {
+ this.update(cx, |this, _| {
let event = dap::OutputEvent {
category: None,
output,
@@ -1028,7 +1082,7 @@ impl Session {
data: None,
location_reference: None,
};
- this.push_output(event, cx);
+ this.push_output(event);
})?;
}
anyhow::Ok(())
@@ -1040,26 +1094,26 @@ impl Session {
pub fn is_started(&self) -> bool {
match &self.mode {
- Mode::Building => false,
- Mode::Running(running) => running.is_started,
+ SessionState::Building(_) => false,
+ SessionState::Running(running) => running.is_started,
}
}
pub fn is_building(&self) -> bool {
- matches!(self.mode, Mode::Building)
+ matches!(self.mode, SessionState::Building(_))
}
pub fn as_running_mut(&mut self) -> Option<&mut RunningMode> {
match &mut self.mode {
- Mode::Running(local_mode) => Some(local_mode),
- Mode::Building => None,
+ SessionState::Running(local_mode) => Some(local_mode),
+ SessionState::Building(_) => None,
}
}
pub fn as_running(&self) -> Option<&RunningMode> {
match &self.mode {
- Mode::Running(local_mode) => Some(local_mode),
- Mode::Building => None,
+ SessionState::Running(local_mode) => Some(local_mode),
+ SessionState::Building(_) => None,
}
}
@@ -1201,7 +1255,7 @@ impl Session {
let adapter_id = self.adapter().to_string();
let request = Initialize { adapter_id };
- let Mode::Running(running) = &self.mode else {
+ let SessionState::Running(running) = &self.mode else {
return Task::ready(Err(anyhow!(
"Cannot send initialize request, task still building"
)));
@@ -1233,18 +1287,7 @@ impl Session {
Ok(capabilities) => {
this.update(cx, |session, cx| {
session.capabilities = capabilities;
- let filters = session
- .capabilities
- .exception_breakpoint_filters
- .clone()
- .unwrap_or_default();
- for filter in filters {
- let default = filter.default.unwrap_or_default();
- session
- .exception_breakpoints
- .entry(filter.filter.clone())
- .or_insert_with(|| (filter, default));
- }
+
cx.emit(SessionEvent::CapabilitiesLoaded);
})?;
return Ok(());
@@ -1261,10 +1304,12 @@ impl Session {
cx: &mut Context<Self>,
) -> Task<Result<()>> {
match &self.mode {
- Mode::Running(local_mode) => {
+ SessionState::Running(local_mode) => {
local_mode.initialize_sequence(&self.capabilities, initialize_rx, dap_store, cx)
}
- Mode::Building => Task::ready(Err(anyhow!("cannot initialize, still building"))),
+ SessionState::Building(_) => {
+ Task::ready(Err(anyhow!("cannot initialize, still building")))
+ }
}
}
@@ -1275,7 +1320,7 @@ impl Session {
cx: &mut Context<Self>,
) {
match &mut self.mode {
- Mode::Running(local_mode) => {
+ SessionState::Running(local_mode) => {
if !matches!(
self.thread_states.thread_state(active_thread_id),
Some(ThreadStatus::Stopped)
@@ -1299,7 +1344,7 @@ impl Session {
})
.detach();
}
- Mode::Building => {}
+ SessionState::Building(_) => {}
}
}
@@ -1458,7 +1503,7 @@ impl Session {
return;
}
- self.push_output(event, cx);
+ self.push_output(event);
cx.notify();
}
Events::Breakpoint(event) => self.breakpoint_store.update(cx, |store, _| {
@@ -1526,7 +1571,7 @@ impl Session {
}
/// 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<T: DapCommand + PartialEq + Eq + Hash>(
+ fn fetch<T: LocalDapCommand + PartialEq + Eq + Hash>(
&mut self,
request: T,
process_result: impl FnOnce(&mut Self, Result<T::Response>, &mut Context<Self>) + 'static,
@@ -1577,9 +1622,9 @@ impl Session {
}
}
- fn request_inner<T: DapCommand + PartialEq + Eq + Hash>(
+ fn request_inner<T: LocalDapCommand + PartialEq + Eq + Hash>(
capabilities: &Capabilities,
- mode: &Mode,
+ mode: &SessionState,
request: T,
process_result: impl FnOnce(
&mut Self,
@@ -1613,7 +1658,7 @@ impl Session {
})
}
- fn request<T: DapCommand + PartialEq + Eq + Hash>(
+ fn request<T: LocalDapCommand + PartialEq + Eq + Hash>(
&self,
request: T,
process_result: impl FnOnce(
@@ -1627,7 +1672,7 @@ impl Session {
Self::request_inner(&self.capabilities, &self.mode, request, process_result, cx)
}
- fn invalidate_command_type<Command: DapCommand>(&mut self) {
+ fn invalidate_command_type<Command: LocalDapCommand>(&mut self) {
self.requests.remove(&std::any::TypeId::of::<Command>());
}
@@ -1635,6 +1680,12 @@ impl Session {
self.invalidate_command_type::<ModulesCommand>();
self.invalidate_command_type::<LoadedSourcesCommand>();
self.invalidate_command_type::<ThreadsCommand>();
+ self.invalidate_command_type::<DataBreakpointInfoCommand>();
+ self.invalidate_command_type::<ReadMemory>();
+ 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) {
@@ -1645,10 +1696,9 @@ impl Session {
});
}
- fn push_output(&mut self, event: OutputEvent, cx: &mut Context<Self>) {
+ fn push_output(&mut self, event: OutputEvent) {
self.output.push_back(event);
self.output_token.0 += 1;
- cx.emit(SessionEvent::ConsoleOutput);
}
pub fn any_stopped_thread(&self) -> bool {
@@ -1708,6 +1758,135 @@ impl Session {
&self.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<u64>,
+ evaluate_name: &str,
+ cx: &mut Context<Self>,
+ ) -> Task<Option<u64>> {
+ 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<u64>,
+ expression: String,
+ cx: &mut Context<Self>,
+ ) -> Task<Option<String>> {
+ 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
+ })
+ }
+
+ pub fn write_memory(&mut self, address: u64, data: &[u8], cx: &mut Context<Self>) {
+ 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::<ReadMemory>();
+ this.invalidate_command_type::<VariablesCommand>();
+ cx.emit(SessionEvent::Variables);
+ response.ok()
+ },
+ cx,
+ )
+ .detach();
+ }
+ pub fn read_memory(
+ &mut self,
+ range: RangeInclusive<u64>,
+ cx: &mut Context<Self>,
+ ) -> 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<Self>) {
+ _ = 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<Self>,
+ ) {
+ 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
}
@@ -1738,6 +1917,10 @@ impl Session {
}
}
+ pub fn data_breakpoints(&self) -> impl Iterator<Item = &DataBreakpointState> {
+ self.data_breakpoints.values()
+ }
+
pub fn exception_breakpoints(
&self,
) -> impl Iterator<Item = &(ExceptionBreakpointsFilter, IsEnabled)> {
@@ -1771,6 +1954,45 @@ impl Session {
}
}
+ 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<Self>) {
+ 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<DataBreakpointContext>,
+ data_id: String,
+ dap: dap::DataBreakpoint,
+ cx: &mut Context<Self>,
+ ) {
+ 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
}
@@ -1809,7 +2031,7 @@ impl Session {
Some(())
}
- fn on_step_response<T: DapCommand + PartialEq + Eq + Hash>(
+ fn on_step_response<T: LocalDapCommand + PartialEq + Eq + Hash>(
thread_id: ThreadId,
) -> impl FnOnce(&mut Self, Result<T::Response>, &mut Context<Self>) -> Option<T::Response> + 'static
{
@@ -1865,18 +2087,30 @@ impl Session {
}
pub fn restart(&mut self, args: Option<Value>, cx: &mut Context<Self>) {
- if self.capabilities.supports_restart_request.unwrap_or(false) && !self.is_terminated() {
- self.request(
- RestartCommand {
- raw: args.unwrap_or(Value::Null),
- },
- Self::fallback_to_manual_restart,
- cx,
- )
- .detach();
- } else {
- cx.emit(SessionStateEvent::Restart);
+ 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<Self>) -> Task<()> {
@@ -1888,34 +2122,47 @@ impl Session {
self.thread_states.exit_all_threads();
cx.notify();
- let task = 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,
- )
+ let task = match &mut self.mode {
+ 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::Building(build_task) => {
+ build_task.take();
+ Task::ready(Some(()))
+ }
};
cx.emit(SessionStateEvent::Shutdown);
- cx.spawn(async move |_, _| {
+ cx.spawn(async move |this, cx| {
task.await;
+ let _ = this.update(cx, |this, _| {
+ if let Some(adapter_client) = this.adapter_client() {
+ adapter_client.kill();
+ }
+ });
})
}
@@ -1936,12 +2183,14 @@ impl Session {
}
pub fn continue_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
+ let supports_single_thread_execution_requests =
+ self.capabilities.supports_single_thread_execution_requests;
self.thread_states.continue_thread(thread_id);
self.request(
ContinueCommand {
args: ContinueArguments {
thread_id: thread_id.0,
- single_thread: Some(true),
+ single_thread: supports_single_thread_execution_requests,
},
},
Self::on_step_response::<ContinueCommand>(thread_id),
@@ -1952,8 +2201,8 @@ impl Session {
pub fn adapter_client(&self) -> Option<Arc<DebugAdapterClient>> {
match self.mode {
- Mode::Running(ref local) => Some(local.client.clone()),
- Mode::Building => None,
+ SessionState::Running(ref local) => Some(local.client.clone()),
+ SessionState::Building(_) => None,
}
}
@@ -2305,6 +2554,20 @@ impl Session {
.unwrap_or_default()
}
+ pub fn data_breakpoint_info(
+ &mut self,
+ context: Arc<DataBreakpointContext>,
+ mode: Option<String>,
+ cx: &mut Context<Self>,
+ ) -> Task<Option<dap::DataBreakpointInfoResponse>> {
+ let command = DataBreakpointInfoCommand {
+ context: context.clone(),
+ mode,
+ };
+
+ self.request(command, |_, response, _| response.ok(), cx)
+ }
+
pub fn set_variable_value(
&mut self,
stack_frame_id: u64,
@@ -2323,6 +2586,8 @@ impl Session {
move |this, response, cx| {
let response = response.log_err()?;
this.invalidate_command_type::<VariablesCommand>();
+ this.invalidate_command_type::<ReadMemory>();
+ this.memory.clear(cx.background_executor());
this.refresh_watchers(stack_frame_id, cx);
cx.emit(SessionEvent::Variables);
Some(response)
@@ -2352,7 +2617,7 @@ impl Session {
data: None,
location_reference: None,
};
- self.push_output(event, cx);
+ self.push_output(event);
let request = self.mode.request_dap(EvaluateCommand {
expression,
context,
@@ -2362,6 +2627,8 @@ impl Session {
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::<ReadMemory>();
match response {
Ok(response) => {
let event = dap::OutputEvent {
@@ -2375,7 +2642,7 @@ impl Session {
data: None,
location_reference: None,
};
- this.push_output(event, cx);
+ this.push_output(event);
}
Err(e) => {
let event = dap::OutputEvent {
@@ -2389,7 +2656,7 @@ impl Session {
data: None,
location_reference: None,
};
- this.push_output(event, cx);
+ this.push_output(event);
}
};
cx.notify();
@@ -2417,7 +2684,7 @@ impl Session {
}
pub fn is_attached(&self) -> bool {
- let Mode::Running(local_mode) = &self.mode else {
+ let SessionState::Running(local_mode) = &self.mode else {
return false;
};
local_mode.binary.request_args.request == StartDebuggingRequestArgumentsRequest::Attach
@@ -2455,4 +2722,8 @@ impl Session {
pub fn thread_state(&self, thread_id: ThreadId) -> Option<ThreadStatus> {
self.thread_states.thread_state(thread_id)
}
+
+ pub fn quirks(&self) -> SessionQuirks {
+ self.quirks
+ }
}
@@ -84,7 +84,7 @@ impl ProjectEnvironment {
self.get_worktree_environment(worktree, cx)
}
- pub(crate) fn get_worktree_environment(
+ pub fn get_worktree_environment(
&mut self,
worktree: Entity<Worktree>,
cx: &mut Context<Self>,
@@ -118,7 +118,7 @@ impl ProjectEnvironment {
/// 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(crate) fn get_directory_environment(
+ pub fn get_directory_environment(
&mut self,
abs_path: Arc<Path>,
cx: &mut Context<Self>,
@@ -3822,7 +3822,7 @@ impl GetDocumentDiagnostics {
code,
code_description: match diagnostic.code_description {
Some(code_description) => Some(CodeDescription {
- href: lsp::Url::parse(&code_description).unwrap(),
+ href: Some(lsp::Url::parse(&code_description).unwrap()),
}),
None => None,
},
@@ -3898,7 +3898,7 @@ impl GetDocumentDiagnostics {
tags,
code_description: diagnostic
.code_description
- .map(|desc| desc.href.to_string()),
+ .and_then(|desc| desc.href.map(|url| url.to_string())),
message: diagnostic.message,
data: diagnostic.data.as_ref().map(|data| data.to_string()),
})
@@ -6043,7 +6043,9 @@ impl LspStore {
);
server.request::<lsp::request::ResolveCompletionItem>(*lsp_completion.clone())
}
- CompletionSource::BufferWord { .. } | CompletionSource::Custom => {
+ CompletionSource::BufferWord { .. }
+ | CompletionSource::Dap { .. }
+ | CompletionSource::Custom => {
return Ok(());
}
}
@@ -6195,7 +6197,9 @@ impl LspStore {
}
serde_json::to_string(lsp_completion).unwrap().into_bytes()
}
- CompletionSource::Custom | CompletionSource::BufferWord { .. } => {
+ CompletionSource::Custom
+ | CompletionSource::Dap { .. }
+ | CompletionSource::BufferWord { .. } => {
return Ok(());
}
}
@@ -9126,7 +9130,13 @@ impl LspStore {
}
};
- let lsp::ProgressParamsValue::WorkDone(progress) = progress.value;
+ let progress = match progress.value {
+ lsp::ProgressParamsValue::WorkDone(progress) => progress,
+ lsp::ProgressParamsValue::WorkspaceDiagnostic(_) => {
+ return;
+ }
+ };
+
let language_server_status =
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
status
@@ -9708,29 +9718,31 @@ impl LspStore {
} 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(
+ 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,
- ))
- }
- proto::language_server_selector::Selector::Name(name) => {
- LanguageServerSelector::Name(LanguageServerName(
- SharedString::from(name),
- ))
- }
+ ) => LanguageServerSelector::Id(LanguageServerId::from_proto(
+ server_id,
+ )),
+ proto::language_server_selector::Selector::Name(name) => {
+ LanguageServerSelector::Name(LanguageServerName(
+ SharedString::from(name),
+ ))
+ }
+ })
})
- })
- .collect(),
- cx,
- );
+ .collect(),
+ cx,
+ )
+ .detach_and_log_err(cx);
}
})?;
@@ -10286,9 +10298,9 @@ impl LspStore {
pub fn stop_language_servers_for_buffers(
&mut self,
buffers: Vec<Entity<Buffer>>,
- also_restart_servers: HashSet<LanguageServerSelector>,
+ also_stop_servers: HashSet<LanguageServerSelector>,
cx: &mut Context<Self>,
- ) {
+ ) -> Task<Result<()>> {
if let Some((client, project_id)) = self.upstream_client() {
let request = client.request(proto::StopLanguageServers {
project_id,
@@ -10296,7 +10308,7 @@ impl LspStore {
.into_iter()
.map(|b| b.read(cx).remote_id().to_proto())
.collect(),
- also_servers: also_restart_servers
+ also_servers: also_stop_servers
.into_iter()
.map(|selector| {
let selector = match selector {
@@ -10318,24 +10330,31 @@ impl LspStore {
.collect(),
all: false,
});
- cx.background_spawn(request).detach_and_log_err(cx);
+ cx.background_spawn(async move {
+ let _ = request.await?;
+ Ok(())
+ })
} else {
- self.stop_local_language_servers_for_buffers(&buffers, also_restart_servers, cx)
- .detach();
+ 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<Buffer>],
- also_restart_servers: HashSet<LanguageServerSelector>,
+ also_stop_servers: HashSet<LanguageServerSelector>,
cx: &mut Context<Self>,
) -> 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_restart_servers
+ let mut language_servers_to_stop = also_stop_servers
.into_iter()
.flat_map(|selector| match selector {
LanguageServerSelector::Id(id) => Some(id),
@@ -10499,7 +10518,7 @@ impl LspStore {
code_description: diagnostic
.code_description
.as_ref()
- .map(|d| d.href.clone()),
+ .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)
@@ -10526,7 +10545,7 @@ impl LspStore {
code_description: diagnostic
.code_description
.as_ref()
- .map(|c| c.href.clone()),
+ .and_then(|d| d.href.clone()),
severity: DiagnosticSeverity::INFORMATION,
markdown: adapter.as_ref().and_then(|adapter| {
adapter.diagnostic_message_to_markdown(&info.message)
@@ -10667,6 +10686,21 @@ impl LspStore {
}
// 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.0];
+ if let Some(local) = self.as_local() {
+ // Find all worktrees that have this server in their language server tree
+ for (worktree_id, servers) in &local.lsp_tree.read(cx).instances {
+ if *worktree_id != key.0 {
+ for (_, server_map) in &servers.roots {
+ if server_map.contains_key(&key.1) {
+ worktrees_using_server.push(*worktree_id);
+ }
+ }
+ }
+ }
+ }
+
let mut buffer_paths_registered = Vec::new();
self.buffer_store.clone().update(cx, |buffer_store, cx| {
for buffer_handle in buffer_store.buffers() {
@@ -10680,7 +10714,7 @@ impl LspStore {
None => continue,
};
- if file.worktree.read(cx).id() != key.0
+ if !worktrees_using_server.contains(&file.worktree.read(cx).id())
|| !self
.languages
.lsp_adapters(&language.name())
@@ -11081,6 +11115,10 @@ impl LspStore {
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
@@ -11135,6 +11173,11 @@ impl LspStore {
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),
},
})
@@ -117,7 +117,7 @@ use text::{Anchor, BufferId, Point};
use toolchain_store::EmptyToolchainStore;
use util::{
ResultExt as _,
- paths::{SanitizedPath, compare_paths},
+ paths::{PathStyle, RemotePathBuf, SanitizedPath, compare_paths},
};
use worktree::{CreatedEntry, Snapshot, Traversal};
pub use worktree::{
@@ -456,6 +456,10 @@ pub enum CompletionSource {
/// Whether this completion has been resolved, to ensure it happens once per completion.
resolved: bool,
},
+ Dap {
+ /// The sort text for this completion.
+ sort_text: String,
+ },
Custom,
BufferWord {
word_range: Range<Anchor>,
@@ -1155,9 +1159,11 @@ impl Project {
let snippets =
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
- let ssh_proto = ssh.read(cx).proto_client();
- let worktree_store =
- cx.new(|_| WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID));
+ let (ssh_proto, path_style) =
+ ssh.read_with(cx, |ssh, _| (ssh.proto_client(), ssh.path_style()));
+ let worktree_store = cx.new(|_| {
+ WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID, path_style)
+ });
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
.detach();
@@ -1406,8 +1412,15 @@ impl Project {
let remote_id = response.payload.project_id;
let role = response.payload.role();
+ // todo(zjk)
+ // Set the proper path style based on the remote
let worktree_store = cx.new(|_| {
- WorktreeStore::remote(true, client.clone().into(), response.payload.project_id)
+ WorktreeStore::remote(
+ true,
+ client.clone().into(),
+ response.payload.project_id,
+ PathStyle::Posix,
+ )
})?;
let buffer_store = cx.new(|cx| {
BufferStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx)
@@ -3204,9 +3217,11 @@ impl Project {
also_restart_servers: HashSet<LanguageServerSelector>,
cx: &mut Context<Self>,
) {
- self.lsp_store.update(cx, |lsp_store, cx| {
- lsp_store.stop_language_servers_for_buffers(buffers, also_restart_servers, cx)
- })
+ self.lsp_store
+ .update(cx, |lsp_store, cx| {
+ lsp_store.stop_language_servers_for_buffers(buffers, also_restart_servers, cx)
+ })
+ .detach_and_log_err(cx);
}
pub fn cancel_language_server_work_for_buffers(
@@ -3349,8 +3364,14 @@ impl Project {
cx: &mut Context<Self>,
) -> Task<Result<Vec<LocationLink>>> {
let position = position.to_point_utf16(buffer.read(cx));
- self.lsp_store.update(cx, |lsp_store, cx| {
+ let guard = self.retain_remotely_created_models(cx);
+ let task = self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.definitions(buffer, position, cx)
+ });
+ cx.spawn(async move |_, _| {
+ let result = task.await;
+ drop(guard);
+ result
})
}
@@ -3361,8 +3382,14 @@ impl Project {
cx: &mut Context<Self>,
) -> Task<Result<Vec<LocationLink>>> {
let position = position.to_point_utf16(buffer.read(cx));
- self.lsp_store.update(cx, |lsp_store, cx| {
+ let guard = self.retain_remotely_created_models(cx);
+ let task = self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.declarations(buffer, position, cx)
+ });
+ cx.spawn(async move |_, _| {
+ let result = task.await;
+ drop(guard);
+ result
})
}
@@ -3373,8 +3400,14 @@ impl Project {
cx: &mut Context<Self>,
) -> Task<Result<Vec<LocationLink>>> {
let position = position.to_point_utf16(buffer.read(cx));
- self.lsp_store.update(cx, |lsp_store, cx| {
+ let guard = self.retain_remotely_created_models(cx);
+ let task = self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.type_definitions(buffer, position, cx)
+ });
+ cx.spawn(async move |_, _| {
+ let result = task.await;
+ drop(guard);
+ result
})
}
@@ -3385,8 +3418,14 @@ impl Project {
cx: &mut Context<Self>,
) -> Task<Result<Vec<LocationLink>>> {
let position = position.to_point_utf16(buffer.read(cx));
- self.lsp_store.update(cx, |lsp_store, cx| {
+ let guard = self.retain_remotely_created_models(cx);
+ let task = self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.implementations(buffer, position, cx)
+ });
+ cx.spawn(async move |_, _| {
+ let result = task.await;
+ drop(guard);
+ result
})
}
@@ -3397,8 +3436,14 @@ impl Project {
cx: &mut Context<Self>,
) -> Task<Result<Vec<Location>>> {
let position = position.to_point_utf16(buffer.read(cx));
- self.lsp_store.update(cx, |lsp_store, cx| {
+ let guard = self.retain_remotely_created_models(cx);
+ let task = self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.references(buffer, position, cx)
+ });
+ cx.spawn(async move |_, _| {
+ let result = task.await;
+ drop(guard);
+ result
})
}
@@ -4035,7 +4080,8 @@ impl Project {
})
})
} else if let Some(ssh_client) = self.ssh_client.as_ref() {
- let request_path = Path::new(path);
+ let path_style = ssh_client.read(cx).path_style();
+ let request_path = RemotePathBuf::from_str(path, path_style);
let request = ssh_client
.read(cx)
.proto_client()
@@ -326,6 +326,79 @@ impl DiagnosticSeverity {
}
}
+/// Determines the severity of the diagnostic that should be moved to.
+#[derive(PartialEq, PartialOrd, Clone, Copy, Debug, Eq, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum GoToDiagnosticSeverity {
+ /// Errors
+ Error = 3,
+ /// Warnings
+ Warning = 2,
+ /// Information
+ Information = 1,
+ /// Hints
+ Hint = 0,
+}
+
+impl From<lsp::DiagnosticSeverity> for GoToDiagnosticSeverity {
+ fn from(severity: lsp::DiagnosticSeverity) -> Self {
+ match severity {
+ lsp::DiagnosticSeverity::ERROR => Self::Error,
+ lsp::DiagnosticSeverity::WARNING => Self::Warning,
+ lsp::DiagnosticSeverity::INFORMATION => Self::Information,
+ lsp::DiagnosticSeverity::HINT => Self::Hint,
+ _ => Self::Error,
+ }
+ }
+}
+
+impl GoToDiagnosticSeverity {
+ pub fn min() -> Self {
+ Self::Hint
+ }
+
+ pub fn max() -> Self {
+ Self::Error
+ }
+}
+
+/// Allows filtering diagnostics that should be moved to.
+#[derive(PartialEq, Clone, Copy, Debug, Deserialize, JsonSchema)]
+#[serde(untagged)]
+pub enum GoToDiagnosticSeverityFilter {
+ /// Move to diagnostics of a specific severity.
+ Only(GoToDiagnosticSeverity),
+
+ /// Specify a range of severities to include.
+ Range {
+ /// Minimum severity to move to. Defaults no "error".
+ #[serde(default = "GoToDiagnosticSeverity::min")]
+ min: GoToDiagnosticSeverity,
+ /// Maximum severity to move to. Defaults to "hint".
+ #[serde(default = "GoToDiagnosticSeverity::max")]
+ max: GoToDiagnosticSeverity,
+ },
+}
+
+impl Default for GoToDiagnosticSeverityFilter {
+ fn default() -> Self {
+ Self::Range {
+ min: GoToDiagnosticSeverity::min(),
+ max: GoToDiagnosticSeverity::max(),
+ }
+ }
+}
+
+impl GoToDiagnosticSeverityFilter {
+ pub fn matches(&self, severity: lsp::DiagnosticSeverity) -> bool {
+ let severity: GoToDiagnosticSeverity = severity.into();
+ match self {
+ Self::Only(target) => *target == severity,
+ Self::Range { min, max } => severity >= *min && severity <= *max,
+ }
+ }
+}
+
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct GitSettings {
/// Whether or not to show the git gutter.
@@ -568,7 +568,7 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) {
.into_iter()
.map(|(source_kind, task)| {
let resolved = task.resolved;
- (source_kind, resolved.command)
+ (source_kind, resolved.command.unwrap())
})
.collect::<Vec<_>>(),
vec![(
@@ -404,6 +404,9 @@ impl SearchQuery {
let start = line_offset + mat.start();
let end = line_offset + mat.end();
matches.push(start..end);
+ if self.one_match_per_line() == Some(true) {
+ break;
+ }
}
line_offset += line.len() + 1;
@@ -72,18 +72,12 @@ impl SearchHistory {
}
pub fn next(&mut self, cursor: &mut SearchHistoryCursor) -> Option<&str> {
- let history_size = self.history.len();
- if history_size == 0 {
- return None;
- }
-
let selected = cursor.selection?;
- if selected == history_size - 1 {
- return None;
- }
let next_index = selected + 1;
+
+ let next = self.history.get(next_index)?;
cursor.selection = Some(next_index);
- Some(&self.history[next_index])
+ Some(next)
}
pub fn current(&self, cursor: &SearchHistoryCursor) -> Option<&str> {
@@ -92,25 +86,17 @@ impl SearchHistory {
.and_then(|selected_ix| self.history.get(selected_ix).map(|s| s.as_str()))
}
+ /// Get the previous history entry using the given `SearchHistoryCursor`.
+ /// Uses the last element in the history when there is no cursor.
pub fn previous(&mut self, cursor: &mut SearchHistoryCursor) -> Option<&str> {
- let history_size = self.history.len();
- if history_size == 0 {
- return None;
- }
-
let prev_index = match cursor.selection {
- Some(selected_index) => {
- if selected_index == 0 {
- return None;
- } else {
- selected_index - 1
- }
- }
- None => history_size - 1,
+ Some(index) => index.checked_sub(1)?,
+ None => self.history.len().checked_sub(1)?,
};
+ let previous = self.history.get(prev_index)?;
cursor.selection = Some(prev_index);
- Some(&self.history[prev_index])
+ Some(previous)
}
}
@@ -4,6 +4,7 @@ use collections::HashMap;
use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, Task, WeakEntity};
use itertools::Itertools;
use language::LanguageName;
+use remote::ssh_session::SshArgs;
use settings::{Settings, SettingsLocation};
use smol::channel::bounded;
use std::{
@@ -17,7 +18,10 @@ use terminal::{
TaskState, TaskStatus, Terminal, TerminalBuilder,
terminal_settings::{self, TerminalSettings, VenvSettings},
};
-use util::ResultExt;
+use util::{
+ ResultExt,
+ paths::{PathStyle, RemotePathBuf},
+};
pub struct Terminals {
pub(crate) local_handles: Vec<WeakEntity<terminal::Terminal>>,
@@ -47,6 +51,13 @@ impl SshCommand {
}
}
+pub struct SshDetails {
+ pub host: String,
+ pub ssh_command: SshCommand,
+ pub envs: Option<HashMap<String, String>>,
+ pub path_style: PathStyle,
+}
+
impl Project {
pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
let worktree = self
@@ -68,14 +79,16 @@ impl Project {
}
}
- pub fn ssh_details(&self, cx: &App) -> Option<(String, SshCommand)> {
+ pub fn ssh_details(&self, cx: &App) -> Option<SshDetails> {
if let Some(ssh_client) = &self.ssh_client {
let ssh_client = ssh_client.read(cx);
- if let Some(args) = ssh_client.ssh_args() {
- return Some((
- ssh_client.connection_options().host.clone(),
- SshCommand { arguments: args },
- ));
+ if let Some((SshArgs { arguments, envs }, path_style)) = ssh_client.ssh_info() {
+ return Some(SshDetails {
+ host: ssh_client.connection_options().host.clone(),
+ ssh_command: SshCommand { arguments },
+ envs,
+ path_style,
+ });
}
}
@@ -149,26 +162,35 @@ impl Project {
let settings = self.terminal_settings(&path, cx).clone();
let builder = ShellBuilder::new(ssh_details.is_none(), &settings.shell).non_interactive();
- let (command, args) = builder.build(command, &Vec::new());
+ let (command, args) = builder.build(Some(command), &Vec::new());
let mut env = self
.environment
.read(cx)
.get_cli_environment()
.unwrap_or_default();
- env.extend(settings.env.clone());
-
- match &self.ssh_details(cx) {
- Some((_, ssh_command)) => {
+ env.extend(settings.env);
+
+ match self.ssh_details(cx) {
+ Some(SshDetails {
+ ssh_command,
+ envs,
+ path_style,
+ ..
+ }) => {
let (command, args) = wrap_for_ssh(
- ssh_command,
+ &ssh_command,
Some((&command, &args)),
path.as_deref(),
env,
None,
+ path_style,
);
let mut command = std::process::Command::new(command);
command.args(args);
+ if let Some(envs) = envs {
+ command.envs(envs);
+ }
command
}
None => {
@@ -202,6 +224,7 @@ impl Project {
}
};
let ssh_details = this.ssh_details(cx);
+ let is_ssh_terminal = ssh_details.is_some();
let mut settings_location = None;
if let Some(path) = path.as_ref() {
@@ -224,13 +247,9 @@ impl Project {
.unwrap_or_default();
// Then extend it with the explicit env variables from the settings, so they take
// precedence.
- env.extend(settings.env.clone());
+ env.extend(settings.env);
- let local_path = if ssh_details.is_none() {
- path.clone()
- } else {
- None
- };
+ let local_path = if is_ssh_terminal { None } else { path.clone() };
let mut python_venv_activate_command = None;
@@ -241,8 +260,13 @@ impl Project {
this.python_activate_command(python_venv_directory, &settings.detect_venv);
}
- match &ssh_details {
- Some((host, ssh_command)) => {
+ match ssh_details {
+ Some(SshDetails {
+ host,
+ ssh_command,
+ envs,
+ path_style,
+ }) => {
log::debug!("Connecting to a remote server: {ssh_command:?}");
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
@@ -252,9 +276,18 @@ impl Project {
env.entry("TERM".to_string())
.or_insert_with(|| "xterm-256color".to_string());
- let (program, args) =
- wrap_for_ssh(&ssh_command, None, path.as_deref(), env, None);
+ let (program, args) = wrap_for_ssh(
+ &ssh_command,
+ None,
+ path.as_deref(),
+ env,
+ None,
+ path_style,
+ );
env = HashMap::default();
+ if let Some(envs) = envs {
+ env.extend(envs);
+ }
(
Option::<TaskState>::None,
Shell::WithArguments {
@@ -290,19 +323,31 @@ impl Project {
);
}
- match &ssh_details {
- Some((host, ssh_command)) => {
+ match ssh_details {
+ Some(SshDetails {
+ host,
+ ssh_command,
+ envs,
+ path_style,
+ }) => {
log::debug!("Connecting to a remote server: {ssh_command:?}");
env.entry("TERM".to_string())
.or_insert_with(|| "xterm-256color".to_string());
let (program, args) = wrap_for_ssh(
&ssh_command,
- Some((&spawn_task.command, &spawn_task.args)),
+ spawn_task
+ .command
+ .as_ref()
+ .map(|command| (command, &spawn_task.args)),
path.as_deref(),
env,
python_venv_directory.as_deref(),
+ path_style,
);
env = HashMap::default();
+ if let Some(envs) = envs {
+ env.extend(envs);
+ }
(
task_state,
Shell::WithArguments {
@@ -317,14 +362,16 @@ impl Project {
add_environment_path(&mut env, &venv_path.join("bin")).log_err();
}
- (
- task_state,
+ let shell = if let Some(program) = spawn_task.command {
Shell::WithArguments {
- program: spawn_task.command,
+ program,
args: spawn_task.args,
title_override: None,
- },
- )
+ }
+ } else {
+ Shell::System
+ };
+ (task_state, shell)
}
}
}
@@ -338,7 +385,7 @@ impl Project {
settings.cursor_shape.unwrap_or_default(),
settings.alternate_scroll,
settings.max_scroll_history_lines,
- ssh_details.is_some(),
+ is_ssh_terminal,
window,
completion_tx,
cx,
@@ -476,36 +523,47 @@ impl Project {
},
terminal_settings::ActivateScript::Nushell => "overlay use",
terminal_settings::ActivateScript::PowerShell => ".",
+ terminal_settings::ActivateScript::Pyenv => "pyenv",
_ => "source",
};
let activate_script_name = match venv_settings.activate_script {
- terminal_settings::ActivateScript::Default => "activate",
+ terminal_settings::ActivateScript::Default
+ | terminal_settings::ActivateScript::Pyenv => "activate",
terminal_settings::ActivateScript::Csh => "activate.csh",
terminal_settings::ActivateScript::Fish => "activate.fish",
terminal_settings::ActivateScript::Nushell => "activate.nu",
terminal_settings::ActivateScript::PowerShell => "activate.ps1",
};
- let path = venv_base_directory
- .join(match std::env::consts::OS {
- "windows" => "Scripts",
- _ => "bin",
- })
- .join(activate_script_name)
- .to_string_lossy()
- .to_string();
- let quoted = shlex::try_quote(&path).ok()?;
+
let line_ending = match std::env::consts::OS {
"windows" => "\r",
_ => "\n",
};
- smol::block_on(self.fs.metadata(path.as_ref()))
- .ok()
- .flatten()?;
- Some(format!(
- "{} {} ; clear{}",
- activate_keyword, quoted, line_ending
- ))
+ if venv_settings.venv_name.is_empty() {
+ let path = venv_base_directory
+ .join(match std::env::consts::OS {
+ "windows" => "Scripts",
+ _ => "bin",
+ })
+ .join(activate_script_name)
+ .to_string_lossy()
+ .to_string();
+ let quoted = shlex::try_quote(&path).ok()?;
+ smol::block_on(self.fs.metadata(path.as_ref()))
+ .ok()
+ .flatten()?;
+
+ Some(format!(
+ "{} {} ; clear{}",
+ activate_keyword, quoted, line_ending
+ ))
+ } else {
+ Some(format!(
+ "{activate_keyword} {activate_script_name} {name}; clear{line_ending}",
+ name = venv_settings.venv_name
+ ))
+ }
}
fn activate_python_virtual_environment(
@@ -528,6 +586,7 @@ pub fn wrap_for_ssh(
path: Option<&Path>,
env: HashMap<String, String>,
venv_directory: Option<&Path>,
+ path_style: PathStyle,
) -> (String, Vec<String>) {
let to_run = if let Some((command, args)) = command {
// DEFAULT_REMOTE_SHELL is '"${SHELL:-sh}"' so must not be escaped
@@ -550,24 +609,25 @@ pub fn wrap_for_ssh(
}
if let Some(venv_directory) = venv_directory {
if let Ok(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref()) {
- env_changes.push_str(&format!("PATH={}:$PATH ", str));
+ let path = RemotePathBuf::new(PathBuf::from(str.to_string()), path_style).to_string();
+ env_changes.push_str(&format!("PATH={}:$PATH ", path));
}
}
let commands = if let Some(path) = path {
- let path_string = path.to_string_lossy().to_string();
+ let path = RemotePathBuf::new(path.to_path_buf(), path_style).to_string();
// shlex will wrap the command in single quotes (''), disabling ~ expansion,
// replace ith with something that works
let tilde_prefix = "~/";
if path.starts_with(tilde_prefix) {
- let trimmed_path = path_string
+ let trimmed_path = path
.trim_start_matches("/")
.trim_start_matches("~")
.trim_start_matches("/");
format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}")
} else {
- format!("cd {path:?}; {env_changes} {to_run}")
+ format!("cd {path}; {env_changes} {to_run}")
}
} else {
format!("cd; {env_changes} {to_run}")
@@ -25,7 +25,10 @@ use smol::{
stream::StreamExt,
};
use text::ReplicaId;
-use util::{ResultExt, paths::SanitizedPath};
+use util::{
+ ResultExt,
+ paths::{PathStyle, RemotePathBuf, SanitizedPath},
+};
use worktree::{
Entry, ProjectEntryId, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId,
WorktreeSettings,
@@ -46,6 +49,7 @@ enum WorktreeStoreState {
Remote {
upstream_client: AnyProtoClient,
upstream_project_id: u64,
+ path_style: PathStyle,
},
}
@@ -100,6 +104,7 @@ impl WorktreeStore {
retain_worktrees: bool,
upstream_client: AnyProtoClient,
upstream_project_id: u64,
+ path_style: PathStyle,
) -> Self {
Self {
next_entry_id: Default::default(),
@@ -111,6 +116,7 @@ impl WorktreeStore {
state: WorktreeStoreState::Remote {
upstream_client,
upstream_project_id,
+ path_style,
},
}
}
@@ -214,17 +220,16 @@ impl WorktreeStore {
if !self.loading_worktrees.contains_key(&abs_path) {
let task = match &self.state {
WorktreeStoreState::Remote {
- upstream_client, ..
+ upstream_client,
+ path_style,
+ ..
} => {
if upstream_client.is_via_collab() {
Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab"))))
} else {
- self.create_ssh_worktree(
- upstream_client.clone(),
- abs_path.clone(),
- visible,
- cx,
- )
+ let abs_path =
+ RemotePathBuf::new(abs_path.as_path().to_path_buf(), *path_style);
+ self.create_ssh_worktree(upstream_client.clone(), abs_path, visible, cx)
}
}
WorktreeStoreState::Local { fs } => {
@@ -250,11 +255,12 @@ impl WorktreeStore {
fn create_ssh_worktree(
&mut self,
client: AnyProtoClient,
- abs_path: impl Into<SanitizedPath>,
+ abs_path: RemotePathBuf,
visible: bool,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Worktree>, Arc<anyhow::Error>>> {
- let mut abs_path = Into::<SanitizedPath>::into(abs_path).to_string();
+ let path_style = abs_path.path_style();
+ let mut abs_path = abs_path.to_string();
// If we start with `/~` that means the ssh path was something like `ssh://user@host/~/home-dir-folder/`
// in which case want to strip the leading the `/`.
// On the host-side, the `~` will get expanded.
@@ -265,10 +271,11 @@ impl WorktreeStore {
if abs_path.is_empty() {
abs_path = "~/".to_string();
}
+
cx.spawn(async move |this, cx| {
let this = this.upgrade().context("Dropped worktree store")?;
- let path = Path::new(abs_path.as_str());
+ let path = RemotePathBuf::new(abs_path.into(), path_style);
let response = client
.request(proto::AddWorktree {
project_id: SSH_PROJECT_ID,
@@ -23,7 +23,8 @@ use gpui::{
ListSizingBehavior, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
ParentElement, Pixels, Point, PromptLevel, Render, ScrollStrategy, Stateful, Styled,
Subscription, Task, UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred,
- div, point, px, size, transparent_white, uniform_list,
+ div, hsla, linear_color_stop, linear_gradient, point, px, size, transparent_white,
+ uniform_list,
};
use indexmap::IndexMap;
use language::DiagnosticSeverity;
@@ -32,6 +33,7 @@ use project::{
Entry, EntryKind, Fs, GitEntry, GitEntryRef, GitTraversal, Project, ProjectEntryId,
ProjectPath, Worktree, WorktreeId,
git_store::{GitStoreEvent, git_traversal::ChildEntriesGitIter},
+ project_settings::GoToDiagnosticSeverityFilter,
relativize_path,
};
use project_panel_settings::{
@@ -55,8 +57,8 @@ use std::{
use theme::ThemeSettings;
use ui::{
Color, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind, IndentGuideColors,
- IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, Scrollbar,
- ScrollbarState, Tooltip, prelude::*, v_flex,
+ IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, ScrollableHandle,
+ Scrollbar, ScrollbarState, StickyCandidate, Tooltip, prelude::*, v_flex,
};
use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths};
use workspace::{
@@ -173,6 +175,7 @@ struct EntryDetails {
is_editing: bool,
is_processing: bool,
is_cut: bool,
+ sticky: Option<StickyDetails>,
filename_text_color: Color,
diagnostic_severity: Option<DiagnosticSeverity>,
git_status: GitSummary,
@@ -181,6 +184,12 @@ struct EntryDetails {
canonical_path: Option<Arc<Path>>,
}
+#[derive(Debug, PartialEq, Eq, Clone)]
+struct StickyDetails {
+ sticky_index: usize,
+}
+
+/// Permanently deletes the selected file or directory.
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = project_panel)]
#[serde(deny_unknown_fields)]
@@ -189,6 +198,7 @@ struct Delete {
pub skip_prompt: bool,
}
+/// Moves the selected file or directory to the system trash.
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = project_panel)]
#[serde(deny_unknown_fields)]
@@ -197,35 +207,76 @@ struct Trash {
pub skip_prompt: bool,
}
+/// Selects the next entry with diagnostics.
+#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
+#[action(namespace = project_panel)]
+#[serde(deny_unknown_fields)]
+struct SelectNextDiagnostic {
+ #[serde(default)]
+ pub severity: GoToDiagnosticSeverityFilter,
+}
+
+/// Selects the previous entry with diagnostics.
+#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
+#[action(namespace = project_panel)]
+#[serde(deny_unknown_fields)]
+struct SelectPrevDiagnostic {
+ #[serde(default)]
+ pub severity: GoToDiagnosticSeverityFilter,
+}
+
actions!(
project_panel,
[
+ /// Expands the selected entry in the project tree.
ExpandSelectedEntry,
+ /// Collapses the selected entry in the project tree.
CollapseSelectedEntry,
+ /// Collapses all entries in the project tree.
CollapseAllEntries,
+ /// Creates a new directory.
NewDirectory,
+ /// Creates a new file.
NewFile,
+ /// Copies the selected file or directory.
Copy,
+ /// Duplicates the selected file or directory.
Duplicate,
+ /// Reveals the selected item in the system file manager.
RevealInFileManager,
+ /// Removes the selected folder from the project.
RemoveFromProject,
+ /// Opens the selected file with the system's default application.
OpenWithSystem,
+ /// Cuts the selected file or directory.
Cut,
+ /// Pastes the previously cut or copied item.
Paste,
+ /// Renames the selected file or directory.
Rename,
+ /// Opens the selected file in the editor.
Open,
+ /// Opens the selected file in a permanent tab.
OpenPermanent,
+ /// Toggles focus on the project panel.
ToggleFocus,
+ /// Toggles visibility of git-ignored files.
ToggleHideGitIgnore,
+ /// Starts a new search in the selected directory.
NewSearchInDirectory,
+ /// Unfolds the selected directory.
UnfoldDirectory,
+ /// Folds the selected directory.
FoldDirectory,
+ /// Selects the parent directory.
SelectParent,
+ /// Selects the next entry with git changes.
SelectNextGitEntry,
+ /// Selects the previous entry with git changes.
SelectPrevGitEntry,
- SelectNextDiagnostic,
- SelectPrevDiagnostic,
+ /// Selects the next directory.
SelectNextDirectory,
+ /// Selects the previous directory.
SelectPrevDirectory,
]
);
@@ -1918,7 +1969,7 @@ impl ProjectPanel {
fn select_prev_diagnostic(
&mut self,
- _: &SelectPrevDiagnostic,
+ action: &SelectPrevDiagnostic,
_: &mut Window,
cx: &mut Context<Self>,
) {
@@ -1937,7 +1988,8 @@ impl ProjectPanel {
&& entry.is_file()
&& self
.diagnostics
- .contains_key(&(worktree_id, entry.path.to_path_buf()))
+ .get(&(worktree_id, entry.path.to_path_buf()))
+ .is_some_and(|severity| action.severity.matches(*severity))
},
cx,
);
@@ -1953,7 +2005,7 @@ impl ProjectPanel {
fn select_next_diagnostic(
&mut self,
- _: &SelectNextDiagnostic,
+ action: &SelectNextDiagnostic,
_: &mut Window,
cx: &mut Context<Self>,
) {
@@ -1972,7 +2024,8 @@ impl ProjectPanel {
&& entry.is_file()
&& self
.diagnostics
- .contains_key(&(worktree_id, entry.path.to_path_buf()))
+ .get(&(worktree_id, entry.path.to_path_buf()))
+ .is_some_and(|severity| action.severity.matches(*severity))
},
cx,
);
@@ -3274,12 +3327,13 @@ impl ProjectPanel {
fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef<'_>)> {
let mut offset = 0;
for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
- if visible_worktree_entries.len() > offset + index {
+ let current_len = visible_worktree_entries.len();
+ if index < offset + current_len {
return visible_worktree_entries
- .get(index)
+ .get(index - offset)
.map(|entry| (*worktree_id, entry.to_ref()));
}
- offset += visible_worktree_entries.len();
+ offset += current_len;
}
None
}
@@ -3289,7 +3343,13 @@ impl ProjectPanel {
range: Range<usize>,
window: &mut Window,
cx: &mut Context<ProjectPanel>,
- mut callback: impl FnMut(&Entry, &HashSet<Arc<Path>>, &mut Window, &mut Context<ProjectPanel>),
+ mut callback: impl FnMut(
+ &Entry,
+ usize,
+ &HashSet<Arc<Path>>,
+ &mut Window,
+ &mut Context<ProjectPanel>,
+ ),
) {
let mut ix = 0;
for (_, visible_worktree_entries, entries_paths) in &self.visible_entries {
@@ -3310,8 +3370,10 @@ impl ProjectPanel {
.map(|e| (e.path.clone()))
.collect()
});
- for entry in visible_worktree_entries[entry_range].iter() {
- callback(&entry, entries, window, cx);
+ let base_index = ix + entry_range.start;
+ for (i, entry) in visible_worktree_entries[entry_range].iter().enumerate() {
+ let global_index = base_index + i;
+ callback(&entry, global_index, entries, window, cx);
}
ix = end_ix;
}
@@ -3336,22 +3398,13 @@ impl ProjectPanel {
}
let end_ix = range.end.min(ix + visible_worktree_entries.len());
- let (git_status_setting, show_file_icons, show_folder_icons) = {
+ let git_status_setting = {
let settings = ProjectPanelSettings::get_global(cx);
- (
- settings.git_status,
- settings.file_icons,
- settings.folder_icons,
- )
+ settings.git_status
};
if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
let snapshot = worktree.read(cx).snapshot();
let root_name = OsStr::new(snapshot.root_name());
- let expanded_entry_ids = self
- .expanded_dir_ids
- .get(&snapshot.id())
- .map(Vec::as_slice)
- .unwrap_or(&[]);
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
let entries = entries_paths.get_or_init(|| {
@@ -3364,80 +3417,17 @@ impl ProjectPanel {
let status = git_status_setting
.then_some(entry.git_summary)
.unwrap_or_default();
- 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, cx)
- } else {
- None
- }
- }
- _ => {
- if show_folder_icons {
- FileIcons::get_folder_icon(is_expanded, cx)
- } else {
- FileIcons::get_chevron_icon(is_expanded, cx)
- }
- }
- };
-
- let (depth, difference) =
- ProjectPanel::calculate_depth_and_difference(&entry, entries);
-
- let filename = match difference {
- diff if diff > 1 => entry
- .path
- .iter()
- .skip(entry.path.components().count() - diff)
- .collect::<PathBuf>()
- .to_str()
- .unwrap_or_default()
- .to_string(),
- _ => entry
- .path
- .file_name()
- .map(|name| name.to_string_lossy().into_owned())
- .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
- };
- let selection = SelectedEntry {
- worktree_id: snapshot.id(),
- entry_id: entry.id,
- };
- let is_marked = self.marked_entries.contains(&selection);
-
- let diagnostic_severity = self
- .diagnostics
- .get(&(*worktree_id, entry.path.to_path_buf()))
- .cloned();
-
- let filename_text_color =
- entry_git_aware_label_color(status, entry.is_ignored, is_marked);
-
- let mut details = EntryDetails {
- filename,
- icon,
- path: entry.path.clone(),
- depth,
- kind: entry.kind,
- is_ignored: entry.is_ignored,
- is_expanded,
- is_selected: self.selection == Some(selection),
- is_marked,
- is_editing: false,
- is_processing: false,
- is_cut: self
- .clipboard
- .as_ref()
- .map_or(false, |e| e.is_cut() && e.items().contains(&selection)),
- filename_text_color,
- diagnostic_severity,
- git_status: status,
- is_private: entry.is_private,
- worktree_id: *worktree_id,
- canonical_path: entry.canonical_path.clone(),
- };
+ let mut details = self.details_for_entry(
+ entry,
+ *worktree_id,
+ root_name,
+ entries,
+ status,
+ None,
+ window,
+ cx,
+ );
if let Some(edit_state) = &self.edit_state {
let is_edited_entry = if edit_state.is_new_entry() {
@@ -3849,6 +3839,8 @@ impl ProjectPanel {
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;
@@ -3962,8 +3954,15 @@ impl ProjectPanel {
}
};
+ 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(entry_id.to_proto() as usize)
+ .id(id.clone())
+ .relative()
.group(GROUP_NAME)
.cursor_pointer()
.rounded_none()
@@ -3972,141 +3971,147 @@ impl ProjectPanel {
.border_r_2()
.border_color(border_color)
.hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
- .on_drag_move::<ExternalPaths>(cx.listener(
- move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
- let is_current_target = this.drag_target_entry.as_ref()
- .map(|entry| entry.entry_id) == 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
+ .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))
+ .on_drag_move::<ExternalPaths>(cx.listener(
+ move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
+ let is_current_target = this.drag_target_entry.as_ref()
+ .map(|entry| entry.entry_id) == 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 {
- this.drag_target_entry = None;
+ return;
}
- return;
- }
- if is_current_target {
- return;
- }
+ 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;
+ };
- 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(DragTargetEntry {
+ entry_id,
+ highlight_entry_id,
+ });
+ this.marked_entries.clear();
+ },
+ ))
+ .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::<DraggedSelection>(cx.listener(
+ move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
+ let is_current_target = this.drag_target_entry.as_ref()
+ .map(|entry| entry.entry_id) == 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;
+ }
- this.drag_target_entry = Some(DragTargetEntry {
- entry_id,
- highlight_entry_id,
- });
- this.marked_entries.clear();
- },
- ))
- .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::<DraggedSelection>(cx.listener(
- move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
- let is_current_target = this.drag_target_entry.as_ref()
- .map(|entry| entry.entry_id) == 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;
}
- return;
- }
-
- if is_current_target {
- return;
- }
- let drag_state = event.drag(cx);
- 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;
- };
+ let drag_state = event.drag(cx);
+ 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(DragTargetEntry {
- entry_id,
- highlight_entry_id,
- });
- if drag_state.items().count() == 1 {
- this.marked_entries.clear();
- this.marked_entries.insert(drag_state.active_selection);
- }
- this.hover_expand_task.take();
+ this.drag_target_entry = Some(DragTargetEntry {
+ entry_id,
+ highlight_entry_id,
+ });
+ if drag_state.items().count() == 1 {
+ this.marked_entries.clear();
+ this.marked_entries.insert(drag_state.active_selection);
+ }
+ this.hover_expand_task.take();
- if !kind.is_dir()
- || this
- .expanded_dir_ids
- .get(&details.worktree_id)
- .map_or(false, |ids| ids.binary_search(&entry_id).is_ok())
- {
- return;
- }
+ if !kind.is_dir()
+ || this
+ .expanded_dir_ids
+ .get(&details.worktree_id)
+ .map_or(false, |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().map(|entry| entry.entry_id) == 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)),
- cx,
- );
- cx.notify();
- }
- })
- .ok();
- }));
- },
- ))
- .on_drag(
- dragged_selection,
- move |selection, click_offset, _window, cx| {
- cx.new(|_| DraggedProjectEntryView {
- details: details.clone(),
- click_offset,
- selection: selection.active_selection,
- selections: selection.marked_selections.clone(),
- })
- },
- )
- .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over))
- .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);
- }),
- )
+ 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().map(|entry| entry.entry_id) == 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)),
+ cx,
+ );
+ cx.notify();
+ }
+ })
+ .ok();
+ }));
+ },
+ ))
+ .on_drag(
+ dragged_selection,
+ move |selection, click_offset, _window, cx| {
+ cx.new(|_| DraggedProjectEntryView {
+ details: details.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| {
@@ -4138,7 +4143,7 @@ impl ProjectPanel {
current_selection.zip(target_selection)
{
let range_start = source_index.min(target_index);
- let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
+ let range_end = source_index.max(target_index) + 1;
let mut new_selections = BTreeSet::new();
this.for_each_visible_entry(
range_start..range_end,
@@ -4172,6 +4177,26 @@ impl ProjectPanel {
}
} else if kind.is_dir() {
this.marked_entries.clear();
+ if is_sticky {
+ if let Some((_, _, index)) = this.index_for_entry(entry_id, worktree_id) {
+ let strategy = sticky_index
+ .map(ScrollStrategy::ToPosition)
+ .unwrap_or(ScrollStrategy::Top);
+ this.scroll_handle.scroll_to_item(index, strategy);
+ 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 {
this.toggle_expand_all(entry_id, window, cx);
} else {
@@ -4187,7 +4212,7 @@ impl ProjectPanel {
}),
)
.child(
- ListItem::new(entry_id.to_proto() as usize)
+ ListItem::new(id)
.indent_level(depth)
.indent_step_size(px(settings.indent_size))
.spacing(match settings.entry_spacing {
@@ -4287,6 +4312,7 @@ impl ProjectPanel {
.collect::<Vec<_>>();
let components_len = components.len();
+ // TODO this can underflow
let active_index = components_len
- 1
- folded_ancestors.current_ancestor_depth;
@@ -4298,51 +4324,99 @@ impl ProjectPanel {
let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned();
this = this.child(
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);
- }
- }))
+ .when(!is_sticky, |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<DraggedSelection>, _, _| {
+ 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
+ .map_or(false, |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
.on_drag_move(cx.listener(
move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
- if event.bounds.contains(&event.event.position) {
+ if event.bounds.contains(&event.event.position) {
this.folded_directory_drag_target = Some(
FoldedDirectoryDragTarget {
entry_id,
- index: delimiter_target_index,
- is_delimiter_target: true,
+ index,
+ is_delimiter_target: false,
}
);
} else {
let is_current_target = this.folded_directory_drag_target
+ .as_ref()
.map_or(false, |target|
target.entry_id == entry_id &&
- target.index == delimiter_target_index &&
- target.is_delimiter_target
+ target.index == 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)
+ .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.map_or(false, |target|
+ target.entry_id == entry_id &&
+ target.index == index
+ ), |this| {
+ this.bg(item_colors.drag_over)
+ })
+ })
+ })
.on_click(cx.listener(move |this, _, _, cx| {
if index != active_index {
if let Some(folds) =
@@ -4354,48 +4428,6 @@ impl ProjectPanel {
}
}
}))
- .when(index != components_len - 1, |div|{
- let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
- div
- .on_drag_move(cx.listener(
- move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
- 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()
- .map_or(false, |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.map_or(false, |target|
- target.entry_id == entry_id &&
- target.index == index
- ), |this| {
- this.bg(item_colors.drag_over)
- })
- })
.child(
Label::new(component)
.single_line()
@@ -4467,6 +4499,108 @@ impl ProjectPanel {
)
}
+ fn details_for_entry(
+ &self,
+ entry: &Entry,
+ worktree_id: WorktreeId,
+ root_name: &OsStr,
+ entries_paths: &HashSet<Arc<Path>>,
+ git_status: GitSummary,
+ sticky: Option<StickyDetails>,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> 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
+ .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, cx)
+ } else {
+ None
+ }
+ }
+ _ => {
+ if show_folder_icons {
+ FileIcons::get_folder_icon(is_expanded, cx)
+ } else {
+ FileIcons::get_chevron_icon(is_expanded, cx)
+ }
+ }
+ };
+
+ let (depth, difference) =
+ ProjectPanel::calculate_depth_and_difference(&entry, entries_paths);
+
+ let filename = match difference {
+ diff if diff > 1 => entry
+ .path
+ .iter()
+ .skip(entry.path.components().count() - diff)
+ .collect::<PathBuf>()
+ .to_str()
+ .unwrap_or_default()
+ .to_string(),
+ _ => entry
+ .path
+ .file_name()
+ .map(|name| name.to_string_lossy().into_owned())
+ .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
+ };
+
+ let selection = SelectedEntry {
+ worktree_id,
+ entry_id: entry.id,
+ };
+ let is_marked = self.marked_entries.contains(&selection);
+ let is_selected = self.selection == Some(selection);
+
+ let diagnostic_severity = self
+ .diagnostics
+ .get(&(worktree_id, entry.path.to_path_buf()))
+ .cloned();
+
+ let filename_text_color =
+ entry_git_aware_label_color(git_status, entry.is_ignored, is_marked);
+
+ let is_cut = self
+ .clipboard
+ .as_ref()
+ .map_or(false, |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 render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !Self::should_show_scrollbar(cx)
|| !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
@@ -4721,6 +4855,131 @@ impl ProjectPanel {
}
None
}
+
+ fn render_sticky_entries(
+ &self,
+ child: StickyProjectPanelCandidate,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> 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_worktree_entries, entries_paths)) = self
+ .visible_entries
+ .iter()
+ .find(|(id, _, _)| *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 = entries_paths.get_or_init(|| {
+ visible_worktree_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) {
+ if 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 git_status_enabled = ProjectPanelSettings::get_global(cx).git_status;
+ let root_name = OsStr::new(worktree.root_name());
+
+ let git_summaries_by_id = if git_status_enabled {
+ visible_worktree_entries
+ .iter()
+ .map(|e| (e.id, e.git_summary))
+ .collect::<HashMap<_, _>>()
+ } 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 {
@@ -40,6 +40,7 @@ pub struct ProjectPanelSettings {
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 scrollbar: ScrollbarSettings,
@@ -150,6 +151,10 @@ pub struct ProjectPanelSettingsContent {
///
/// Default: false
pub hide_root: Option<bool>,
+ /// Whether to stick parent directories at top of the project panel.
+ ///
+ /// Default: true
+ pub sticky_scroll: Option<bool>,
}
impl Settings for ProjectPanelSettings {
@@ -27,3 +27,4 @@ prost-build.workspace = true
[dev-dependencies]
collections = { workspace = true, features = ["test-support"] }
+typed-path = "0.11"
@@ -535,7 +535,7 @@ message DebugScenario {
message SpawnInTerminal {
string label = 1;
- string command = 2;
+ optional string command = 2;
repeated string args = 3;
map<string, string> env = 4;
optional string cwd = 5;
@@ -294,6 +294,7 @@ message Commit {
optional string email = 5;
string message = 6;
optional CommitOptions options = 7;
+ reserved 8;
message CommitOptions {
bool amend = 1;
@@ -222,11 +222,13 @@ message Completion {
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;
}
}
@@ -127,51 +127,46 @@ pub trait ToProto {
fn to_proto(self) -> String;
}
-impl FromProto for PathBuf {
+#[inline]
+fn from_proto_path(proto: String) -> PathBuf {
#[cfg(target_os = "windows")]
- fn from_proto(proto: String) -> Self {
- proto.split("/").collect()
- }
+ let proto = proto.replace('/', "\\");
+
+ PathBuf::from(proto)
+}
+
+#[inline]
+fn to_proto_path(path: &Path) -> String {
+ #[cfg(target_os = "windows")]
+ let proto = path.to_string_lossy().replace('\\', "/");
#[cfg(not(target_os = "windows"))]
+ let proto = path.to_string_lossy().to_string();
+
+ proto
+}
+
+impl FromProto for PathBuf {
fn from_proto(proto: String) -> Self {
- PathBuf::from(proto)
+ from_proto_path(proto)
}
}
impl FromProto for Arc<Path> {
fn from_proto(proto: String) -> Self {
- PathBuf::from_proto(proto).into()
+ from_proto_path(proto).into()
}
}
impl ToProto for PathBuf {
- #[cfg(target_os = "windows")]
- fn to_proto(self) -> String {
- self.components()
- .map(|comp| comp.as_os_str().to_string_lossy().to_string())
- .collect::<Vec<_>>()
- .join("/")
- }
-
- #[cfg(not(target_os = "windows"))]
fn to_proto(self) -> String {
- self.to_string_lossy().to_string()
+ to_proto_path(&self)
}
}
impl ToProto for &Path {
- #[cfg(target_os = "windows")]
fn to_proto(self) -> String {
- self.components()
- .map(|comp| comp.as_os_str().to_string_lossy().to_string())
- .collect::<Vec<_>>()
- .join("/")
- }
-
- #[cfg(not(target_os = "windows"))]
- fn to_proto(self) -> String {
- self.to_string_lossy().to_string()
+ to_proto_path(self)
}
}
@@ -214,3 +209,103 @@ impl<T: RequestMessage> TypedEnvelope<T> {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use typed_path::{UnixPath, UnixPathBuf, WindowsPath, WindowsPathBuf};
+
+ fn windows_path_from_proto(proto: String) -> WindowsPathBuf {
+ let proto = proto.replace('/', "\\");
+ WindowsPathBuf::from(proto)
+ }
+
+ fn unix_path_from_proto(proto: String) -> UnixPathBuf {
+ UnixPathBuf::from(proto)
+ }
+
+ fn windows_path_to_proto(path: &WindowsPath) -> String {
+ path.to_string_lossy().replace('\\', "/")
+ }
+
+ fn unix_path_to_proto(path: &UnixPath) -> String {
+ path.to_string_lossy().to_string()
+ }
+
+ #[test]
+ fn test_path_proto_interop() {
+ const WINDOWS_PATHS: &[&str] = &[
+ "C:\\Users\\User\\Documents\\file.txt",
+ "C:/Program Files/App/app.exe",
+ "projects\\zed\\crates\\proto\\src\\typed_envelope.rs",
+ "projects/my project/src/main.rs",
+ ];
+ const UNIX_PATHS: &[&str] = &[
+ "/home/user/documents/file.txt",
+ "/usr/local/bin/my app/app",
+ "projects/zed/crates/proto/src/typed_envelope.rs",
+ "projects/my project/src/main.rs",
+ ];
+
+ // Windows path to proto and back
+ for &windows_path_str in WINDOWS_PATHS {
+ let windows_path = WindowsPathBuf::from(windows_path_str);
+ let proto = windows_path_to_proto(&windows_path);
+ let recovered_path = windows_path_from_proto(proto);
+ assert_eq!(windows_path, recovered_path);
+ assert_eq!(
+ recovered_path.to_string_lossy(),
+ windows_path_str.replace('/', "\\")
+ );
+ }
+ // Unix path to proto and back
+ for &unix_path_str in UNIX_PATHS {
+ let unix_path = UnixPathBuf::from(unix_path_str);
+ let proto = unix_path_to_proto(&unix_path);
+ let recovered_path = unix_path_from_proto(proto);
+ assert_eq!(unix_path, recovered_path);
+ assert_eq!(recovered_path.to_string_lossy(), unix_path_str);
+ }
+ // Windows host, Unix client, host sends Windows path to client
+ for &windows_path_str in WINDOWS_PATHS {
+ let windows_host_path = WindowsPathBuf::from(windows_path_str);
+ let proto = windows_path_to_proto(&windows_host_path);
+ let unix_client_received_path = unix_path_from_proto(proto);
+ let proto = unix_path_to_proto(&unix_client_received_path);
+ let windows_host_recovered_path = windows_path_from_proto(proto);
+ assert_eq!(windows_host_path, windows_host_recovered_path);
+ assert_eq!(
+ windows_host_recovered_path.to_string_lossy(),
+ windows_path_str.replace('/', "\\")
+ );
+ }
+ // Unix host, Windows client, host sends Unix path to client
+ for &unix_path_str in UNIX_PATHS {
+ let unix_host_path = UnixPathBuf::from(unix_path_str);
+ let proto = unix_path_to_proto(&unix_host_path);
+ let windows_client_received_path = windows_path_from_proto(proto);
+ let proto = windows_path_to_proto(&windows_client_received_path);
+ let unix_host_recovered_path = unix_path_from_proto(proto);
+ assert_eq!(unix_host_path, unix_host_recovered_path);
+ assert_eq!(unix_host_recovered_path.to_string_lossy(), unix_path_str);
+ }
+ }
+
+ // todo(zjk)
+ #[test]
+ fn test_unsolved_case() {
+ // Unix host, Windows client
+ // The Windows client receives a Unix path with backslashes in it, then
+ // sends it back to the host.
+ // This currently fails.
+ let unix_path = UnixPathBuf::from("/home/user/projects/my\\project/src/main.rs");
+ let proto = unix_path_to_proto(&unix_path);
+ let windows_client_received_path = windows_path_from_proto(proto);
+ let proto = windows_path_to_proto(&windows_client_received_path);
+ let unix_host_recovered_path = unix_path_from_proto(proto);
+ assert_ne!(unix_path, unix_host_recovered_path);
+ assert_eq!(
+ unix_host_recovered_path.to_string_lossy(),
+ "/home/user/projects/my/project/src/main.rs"
+ );
+ }
+}
@@ -28,9 +28,8 @@ use paths::user_ssh_config_file;
use picker::Picker;
use project::Fs;
use project::Project;
-use remote::SshConnectionOptions;
-use remote::SshRemoteClient;
use remote::ssh_session::ConnectionIdentifier;
+use remote::{SshConnectionOptions, SshRemoteClient};
use settings::Settings;
use settings::SettingsStore;
use settings::update_settings_file;
@@ -42,7 +41,10 @@ use ui::{
IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState,
Section, Tooltip, prelude::*,
};
-use util::ResultExt;
+use util::{
+ ResultExt,
+ paths::{PathStyle, RemotePathBuf},
+};
use workspace::OpenOptions;
use workspace::Toast;
use workspace::notifications::NotificationId;
@@ -142,20 +144,21 @@ impl ProjectPicker {
ix: usize,
connection: SshConnectionOptions,
project: Entity<Project>,
- home_dir: PathBuf,
+ home_dir: RemotePathBuf,
+ path_style: PathStyle,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<RemoteServerProjects>,
) -> Entity<Self> {
let (tx, rx) = oneshot::channel();
let lister = project::DirectoryLister::Project(project.clone());
- let delegate = file_finder::OpenPathDelegate::new(tx, lister, false);
+ 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_lossy().to_string(), window, cx);
+ picker.set_query(home_dir.to_string(), window, cx);
picker
});
let connection_string = connection.connection_string().into();
@@ -422,7 +425,8 @@ impl RemoteServerProjects {
ix: usize,
connection_options: remote::SshConnectionOptions,
project: Entity<Project>,
- home_dir: PathBuf,
+ home_dir: RemotePathBuf,
+ path_style: PathStyle,
window: &mut Window,
cx: &mut Context<Self>,
workspace: WeakEntity<Workspace>,
@@ -435,6 +439,7 @@ impl RemoteServerProjects {
connection_options,
project,
home_dir,
+ path_style,
workspace,
window,
cx,
@@ -589,15 +594,18 @@ impl RemoteServerProjects {
});
};
- let project = cx.update(|_, cx| {
- project::Project::ssh(
- 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 (path_style, project) = cx.update(|_, cx| {
+ (
+ session.read(cx).path_style(),
+ project::Project::ssh(
+ session,
+ app_state.client.clone(),
+ app_state.node_runtime.clone(),
+ app_state.user_store.clone(),
+ app_state.languages.clone(),
+ app_state.fs.clone(),
+ cx,
+ ),
)
})?;
@@ -605,7 +613,13 @@ impl RemoteServerProjects {
.read_with(cx, |project, cx| project.resolve_abs_path("~", cx))?
.await
.and_then(|path| path.into_abs_path())
- .unwrap_or(PathBuf::from("/"));
+ .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| {
@@ -617,6 +631,7 @@ impl RemoteServerProjects {
connection_options,
project,
home_dir,
+ path_style,
window,
cx,
weak,
@@ -49,7 +49,10 @@ use std::{
time::{Duration, Instant},
};
use tempfile::TempDir;
-use util::ResultExt;
+use util::{
+ ResultExt,
+ paths::{PathStyle, RemotePathBuf},
+};
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
@@ -59,7 +62,10 @@ pub struct SshProjectId(pub u64);
#[derive(Clone)]
pub struct SshSocket {
connection_options: SshConnectionOptions,
+ #[cfg(not(target_os = "windows"))]
socket_path: PathBuf,
+ #[cfg(target_os = "windows")]
+ envs: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
@@ -85,6 +91,11 @@ pub struct SshConnectionOptions {
pub upload_binary_over_ssh: bool,
}
+pub struct SshArgs {
+ pub arguments: Vec<String>,
+ pub envs: Option<HashMap<String, String>>,
+}
+
#[macro_export]
macro_rules! shell_script {
($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{
@@ -303,20 +314,6 @@ pub struct SshPlatform {
pub arch: &'static str,
}
-impl SshPlatform {
- pub fn triple(&self) -> Option<String> {
- Some(format!(
- "{}-{}",
- self.arch,
- match self.os {
- "linux" => "unknown-linux-gnu",
- "macos" => "apple-darwin",
- _ => return None,
- }
- ))
- }
-}
-
pub trait SshClientDelegate: Send + Sync {
fn ask_password(&self, prompt: String, tx: oneshot::Sender<String>, cx: &mut AsyncApp);
fn get_download_params(
@@ -338,6 +335,28 @@ pub trait SshClientDelegate: Send + Sync {
}
impl SshSocket {
+ #[cfg(not(target_os = "windows"))]
+ fn new(options: SshConnectionOptions, socket_path: PathBuf) -> Result<Self> {
+ Ok(Self {
+ connection_options: options,
+ socket_path,
+ })
+ }
+
+ #[cfg(target_os = "windows")]
+ fn new(options: SshConnectionOptions, temp_dir: &TempDir, secret: String) -> Result<Self> {
+ let askpass_script = temp_dir.path().join("askpass.bat");
+ std::fs::write(&askpass_script, "@ECHO OFF\necho %ZED_SSH_ASKPASS%")?;
+ let mut envs = HashMap::default();
+ envs.insert("SSH_ASKPASS_REQUIRE".into(), "force".into());
+ envs.insert("SSH_ASKPASS".into(), askpass_script.display().to_string());
+ envs.insert("ZED_SSH_ASKPASS".into(), secret);
+ Ok(Self {
+ connection_options: options,
+ envs,
+ })
+ }
+
// :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.
@@ -375,6 +394,7 @@ impl SshSocket {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
+ #[cfg(not(target_os = "windows"))]
fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
command
.stdin(Stdio::piped())
@@ -384,14 +404,68 @@ impl SshSocket {
.arg(format!("ControlPath={}", self.socket_path.display()))
}
- fn ssh_args(&self) -> Vec<String> {
- vec![
- "-o".to_string(),
- "ControlMaster=no".to_string(),
- "-o".to_string(),
- format!("ControlPath={}", self.socket_path.display()),
- self.connection_options.ssh_url(),
- ]
+ #[cfg(target_os = "windows")]
+ fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
+ command
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .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) -> SshArgs {
+ SshArgs {
+ arguments: vec![
+ "-o".to_string(),
+ "ControlMaster=no".to_string(),
+ "-o".to_string(),
+ format!("ControlPath={}", self.socket_path.display()),
+ self.connection_options.ssh_url(),
+ ],
+ envs: None,
+ }
+ }
+
+ #[cfg(target_os = "windows")]
+ fn ssh_args(&self) -> SshArgs {
+ SshArgs {
+ arguments: vec![self.connection_options.ssh_url()],
+ envs: Some(self.envs.clone()),
+ }
+ }
+
+ async fn platform(&self) -> Result<SshPlatform> {
+ let uname = self.run_command("sh", &["-c", "uname -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(SshPlatform { os, arch })
}
}
@@ -560,6 +634,7 @@ pub struct SshRemoteClient {
client: Arc<ChannelClient>,
unique_identifier: String,
connection_options: SshConnectionOptions,
+ path_style: PathStyle,
state: Arc<Mutex<Option<State>>>,
}
@@ -620,22 +695,25 @@ impl SshRemoteClient {
let client =
cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?;
- let this = cx.new(|_| Self {
- client: client.clone(),
- unique_identifier: unique_identifier.clone(),
- connection_options: connection_options.clone(),
- state: Arc::new(Mutex::new(Some(State::Connecting))),
- })?;
let ssh_connection = cx
.update(|cx| {
cx.update_default_global(|pool: &mut ConnectionPool, cx| {
- pool.connect(connection_options, &delegate, cx)
+ pool.connect(connection_options.clone(), &delegate, cx)
})
})?
.await
.map_err(|e| e.cloned())?;
+ let path_style = ssh_connection.path_style();
+ let this = cx.new(|_| Self {
+ client: client.clone(),
+ unique_identifier: unique_identifier.clone(),
+ connection_options,
+ path_style,
+ state: Arc::new(Mutex::new(Some(State::Connecting))),
+ })?;
+
let io_task = ssh_connection.start_proxy(
unique_identifier,
false,
@@ -1065,18 +1143,18 @@ impl SshRemoteClient {
self.client.subscribe_to_entity(remote_id, entity);
}
- pub fn ssh_args(&self) -> Option<Vec<String>> {
+ pub fn ssh_info(&self) -> Option<(SshArgs, PathStyle)> {
self.state
.lock()
.as_ref()
.and_then(|state| state.ssh_connection())
- .map(|ssh_connection| ssh_connection.ssh_args())
+ .map(|ssh_connection| (ssh_connection.ssh_args(), ssh_connection.path_style()))
}
pub fn upload_directory(
&self,
src_path: PathBuf,
- dest_path: PathBuf,
+ dest_path: RemotePathBuf,
cx: &App,
) -> Task<Result<()>> {
let state = self.state.lock();
@@ -1110,6 +1188,10 @@ impl SshRemoteClient {
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();
@@ -1288,12 +1370,19 @@ trait RemoteConnection: Send + Sync {
delegate: Arc<dyn SshClientDelegate>,
cx: &mut AsyncApp,
) -> Task<Result<i32>>;
- fn upload_directory(&self, src_path: PathBuf, dest_path: PathBuf, cx: &App)
- -> Task<Result<()>>;
+ fn upload_directory(
+ &self,
+ src_path: PathBuf,
+ dest_path: RemotePathBuf,
+ cx: &App,
+ ) -> Task<Result<()>>;
async fn kill(&self) -> Result<()>;
fn has_been_killed(&self) -> bool;
- fn ssh_args(&self) -> Vec<String>;
+ /// 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
+ fn ssh_args(&self) -> SshArgs;
fn connection_options(&self) -> SshConnectionOptions;
+ fn path_style(&self) -> PathStyle;
#[cfg(any(test, feature = "test-support"))]
fn simulate_disconnect(&self, _: &AsyncApp) {}
@@ -1302,7 +1391,9 @@ trait RemoteConnection: Send + Sync {
struct SshRemoteConnection {
socket: SshSocket,
master_process: Mutex<Option<Child>>,
- remote_binary_path: Option<PathBuf>,
+ remote_binary_path: Option<RemotePathBuf>,
+ ssh_platform: SshPlatform,
+ ssh_path_style: PathStyle,
_temp_dir: TempDir,
}
@@ -1321,7 +1412,7 @@ impl RemoteConnection for SshRemoteConnection {
self.master_process.lock().is_none()
}
- fn ssh_args(&self) -> Vec<String> {
+ fn ssh_args(&self) -> SshArgs {
self.socket.ssh_args()
}
@@ -1332,7 +1423,7 @@ impl RemoteConnection for SshRemoteConnection {
fn upload_directory(
&self,
src_path: PathBuf,
- dest_path: PathBuf,
+ dest_path: RemotePathBuf,
cx: &App,
) -> Task<Result<()>> {
let mut command = util::command::new_smol_command("scp");
@@ -1352,7 +1443,7 @@ impl RemoteConnection for SshRemoteConnection {
.arg(format!(
"{}:{}",
self.socket.connection_options.scp_url(),
- dest_path.display()
+ dest_path.to_string()
))
.output();
@@ -1363,7 +1454,7 @@ impl RemoteConnection for SshRemoteConnection {
output.status.success(),
"failed to upload directory {} -> {}: {}",
src_path.display(),
- dest_path.display(),
+ dest_path.to_string(),
String::from_utf8_lossy(&output.stderr)
);
@@ -1389,7 +1480,7 @@ impl RemoteConnection for SshRemoteConnection {
let mut start_proxy_command = shell_script!(
"exec {binary_path} proxy --identifier {identifier}",
- binary_path = &remote_binary_path.to_string_lossy(),
+ binary_path = &remote_binary_path.to_string(),
identifier = &unique_identifier,
);
@@ -1432,19 +1523,13 @@ impl RemoteConnection for SshRemoteConnection {
&cx,
)
}
-}
-impl SshRemoteConnection {
- #[cfg(not(unix))]
- async fn new(
- _connection_options: SshConnectionOptions,
- _delegate: Arc<dyn SshClientDelegate>,
- _cx: &mut AsyncApp,
- ) -> Result<Self> {
- anyhow::bail!("ssh is not supported on this platform");
+ fn path_style(&self) -> PathStyle {
+ self.ssh_path_style
}
+}
- #[cfg(unix)]
+impl SshRemoteConnection {
async fn new(
connection_options: SshConnectionOptions,
delegate: Arc<dyn SshClientDelegate>,
@@ -1470,27 +1555,38 @@ impl SshRemoteConnection {
// 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");
- let mut master_process = process::Command::new("ssh")
- .stdin(Stdio::null())
- .stdout(Stdio::piped())
- .stderr(Stdio::piped())
- .env("SSH_ASKPASS_REQUIRE", "force")
- .env("SSH_ASKPASS", &askpass.script_path())
- .args(connection_options.additional_args())
- .args([
+ let mut master_process = {
+ #[cfg(not(target_os = "windows"))]
+ let args = [
"-N",
"-o",
"ControlPersist=no",
"-o",
"ControlMaster=yes",
"-o",
- ])
- .arg(format!("ControlPath={}", socket_path.display()))
- .arg(&url)
- .kill_on_drop(true)
- .spawn()?;
+ ];
+ // 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
+ #[cfg(target_os = "windows")]
+ let args = ["-N"];
+ 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(connection_options.additional_args())
+ .args(args);
+ #[cfg(not(target_os = "windows"))]
+ master_process.arg(format!("ControlPath={}", socket_path.display()));
+ master_process.arg(&url).spawn()?
+ };
// Wait for this ssh process to close its stdout, indicating that authentication
// has completed.
let mut stdout = master_process.stdout.take().unwrap();
@@ -1529,11 +1625,16 @@ impl SshRemoteConnection {
anyhow::bail!(error_message);
}
+ #[cfg(not(target_os = "windows"))]
+ let socket = SshSocket::new(connection_options, socket_path)?;
+ #[cfg(target_os = "windows")]
+ let socket = SshSocket::new(connection_options, &temp_dir, askpass.get_password())?;
drop(askpass);
- let socket = SshSocket {
- connection_options,
- socket_path,
+ let ssh_platform = socket.platform().await?;
+ let ssh_path_style = match ssh_platform.os {
+ "windows" => PathStyle::Windows,
+ _ => PathStyle::Posix,
};
let mut this = Self {
@@ -1541,6 +1642,8 @@ impl SshRemoteConnection {
master_process: Mutex::new(Some(master_process)),
_temp_dir: temp_dir,
remote_binary_path: None,
+ ssh_path_style,
+ ssh_platform,
};
let (release_channel, version, commit) = cx.update(|cx| {
@@ -1558,37 +1661,6 @@ impl SshRemoteConnection {
Ok(this)
}
- async fn platform(&self) -> Result<SshPlatform> {
- let uname = self.socket.run_command("sh", &["-c", "uname -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(SshPlatform { os, arch })
- }
-
fn multiplex(
mut ssh_proxy_process: Child,
incoming_tx: UnboundedSender<Envelope>,
@@ -1699,11 +1771,10 @@ impl SshRemoteConnection {
version: SemanticVersion,
commit: Option<AppCommitSha>,
cx: &mut AsyncApp,
- ) -> Result<PathBuf> {
+ ) -> Result<RemotePathBuf> {
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(),
@@ -1714,19 +1785,23 @@ impl SshRemoteConnection {
release_channel.dev_name(),
version_str
);
- let dst_path = paths::remote_server_dir_relative().join(binary_name);
+ let dst_path = RemotePathBuf::new(
+ paths::remote_server_dir_relative().join(binary_name),
+ self.ssh_path_style,
+ );
let build_remote_server = std::env::var("ZED_BUILD_REMOTE_SERVER").ok();
#[cfg(debug_assertions)]
if let Some(build_remote_server) = build_remote_server {
- let src_path = self
- .build_local(build_remote_server, self.platform().await?, delegate, cx)
- .await?;
- let tmp_path = paths::remote_server_dir_relative().join(format!(
- "download-{}-{}",
- std::process::id(),
- src_path.file_name().unwrap().to_string_lossy()
- ));
+ let src_path = self.build_local(build_remote_server, delegate, cx).await?;
+ let tmp_path = RemotePathBuf::new(
+ paths::remote_server_dir_relative().join(format!(
+ "download-{}-{}",
+ std::process::id(),
+ src_path.file_name().unwrap().to_string_lossy()
+ )),
+ self.ssh_path_style,
+ );
self.upload_local_server_binary(&src_path, &tmp_path, delegate, cx)
.await?;
self.extract_server_binary(&dst_path, &tmp_path, delegate, cx)
@@ -1736,7 +1811,7 @@ impl SshRemoteConnection {
if self
.socket
- .run_command(&dst_path.to_string_lossy(), &["version"])
+ .run_command(&dst_path.to_string(), &["version"])
.await
.is_ok()
{
@@ -1754,16 +1829,17 @@ impl SshRemoteConnection {
_ => Ok(Some(AppVersion::global(cx))),
})??;
- let platform = self.platform().await?;
-
- let tmp_path_gz = PathBuf::from(format!(
- "{}-download-{}.gz",
- dst_path.to_string_lossy(),
- std::process::id()
- ));
+ let tmp_path_gz = RemotePathBuf::new(
+ PathBuf::from(format!(
+ "{}-download-{}.gz",
+ dst_path.to_string(),
+ std::process::id()
+ )),
+ self.ssh_path_style,
+ );
if !self.socket.connection_options.upload_binary_over_ssh {
if let Some((url, body)) = delegate
- .get_download_params(platform, release_channel, wanted_version, cx)
+ .get_download_params(self.ssh_platform, release_channel, wanted_version, cx)
.await?
{
match self
@@ -1786,7 +1862,7 @@ impl SshRemoteConnection {
}
let src_path = delegate
- .download_server_binary_locally(platform, release_channel, wanted_version, cx)
+ .download_server_binary_locally(self.ssh_platform, release_channel, wanted_version, cx)
.await?;
self.upload_local_server_binary(&src_path, &tmp_path_gz, delegate, cx)
.await?;
@@ -1799,7 +1875,7 @@ impl SshRemoteConnection {
&self,
url: &str,
body: &str,
- tmp_path_gz: &Path,
+ tmp_path_gz: &RemotePathBuf,
delegate: &Arc<dyn SshClientDelegate>,
cx: &mut AsyncApp,
) -> Result<()> {
@@ -1809,10 +1885,7 @@ impl SshRemoteConnection {
"sh",
&[
"-c",
- &shell_script!(
- "mkdir -p {parent}",
- parent = parent.to_string_lossy().as_ref()
- ),
+ &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
],
)
.await?;
@@ -1835,7 +1908,7 @@ impl SshRemoteConnection {
&body,
&url,
"-o",
- &tmp_path_gz.to_string_lossy(),
+ &tmp_path_gz.to_string(),
],
)
.await
@@ -1857,7 +1930,7 @@ impl SshRemoteConnection {
&body,
&url,
"-O",
- &tmp_path_gz.to_string_lossy(),
+ &tmp_path_gz.to_string(),
],
)
.await
@@ -1880,7 +1953,7 @@ impl SshRemoteConnection {
async fn upload_local_server_binary(
&self,
src_path: &Path,
- tmp_path_gz: &Path,
+ tmp_path_gz: &RemotePathBuf,
delegate: &Arc<dyn SshClientDelegate>,
cx: &mut AsyncApp,
) -> Result<()> {
@@ -1890,10 +1963,7 @@ impl SshRemoteConnection {
"sh",
&[
"-c",
- &shell_script!(
- "mkdir -p {parent}",
- parent = parent.to_string_lossy().as_ref()
- ),
+ &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
],
)
.await?;
@@ -1918,33 +1988,33 @@ impl SshRemoteConnection {
async fn extract_server_binary(
&self,
- dst_path: &Path,
- tmp_path: &Path,
+ dst_path: &RemotePathBuf,
+ tmp_path: &RemotePathBuf,
delegate: &Arc<dyn SshClientDelegate>,
cx: &mut AsyncApp,
) -> Result<()> {
delegate.set_status(Some("Extracting remote development server"), cx);
let server_mode = 0o755;
- let orig_tmp_path = tmp_path.to_string_lossy();
+ let orig_tmp_path = tmp_path.to_string();
let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") {
shell_script!(
"gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}",
server_mode = &format!("{:o}", server_mode),
- dst_path = &dst_path.to_string_lossy()
+ dst_path = &dst_path.to_string(),
)
} else {
shell_script!(
"chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}",
server_mode = &format!("{:o}", server_mode),
- dst_path = &dst_path.to_string_lossy()
+ dst_path = &dst_path.to_string()
)
};
self.socket.run_command("sh", &["-c", &script]).await?;
Ok(())
}
- async fn upload_file(&self, src_path: &Path, dest_path: &Path) -> Result<()> {
+ async fn upload_file(&self, src_path: &Path, dest_path: &RemotePathBuf) -> Result<()> {
log::debug!("uploading file {:?} to {:?}", src_path, dest_path);
let mut command = util::command::new_smol_command("scp");
let output = self
@@ -1961,7 +2031,7 @@ impl SshRemoteConnection {
.arg(format!(
"{}:{}",
self.socket.connection_options.scp_url(),
- dest_path.display()
+ dest_path.to_string()
))
.output()
.await?;
@@ -1970,7 +2040,7 @@ impl SshRemoteConnection {
output.status.success(),
"failed to upload file {} -> {}: {}",
src_path.display(),
- dest_path.display(),
+ dest_path.to_string(),
String::from_utf8_lossy(&output.stderr)
);
Ok(())
@@ -1980,11 +2050,11 @@ impl SshRemoteConnection {
async fn build_local(
&self,
build_remote_server: String,
- platform: SshPlatform,
delegate: &Arc<dyn SshClientDelegate>,
cx: &mut AsyncApp,
) -> Result<PathBuf> {
use smol::process::{Command, Stdio};
+ use std::env::VarError;
async fn run_cmd(command: &mut Command) -> Result<()> {
let output = command
@@ -1999,58 +2069,37 @@ impl SshRemoteConnection {
Ok(())
}
- 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(Command::new("cargo").args([
- "build",
- "--package",
- "remote_server",
- "--features",
- "debug-embed",
- "--target-dir",
- "target/remote_server",
- ]))
- .await?;
-
- delegate.set_status(Some("Compressing binary"), cx);
-
- run_cmd(Command::new("gzip").args([
- "-9",
- "-f",
- "target/remote_server/debug/remote_server",
- ]))
- .await?;
-
- let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz");
- return Ok(path);
- }
- let Some(triple) = platform.triple() else {
- anyhow::bail!("can't cross compile for: {:?}", platform);
+ let triple = format!(
+ "{}-{}",
+ self.ssh_platform.arch,
+ match self.ssh_platform.os {
+ "linux" => "unknown-linux-musl",
+ "macos" => "apple-darwin",
+ _ => anyhow::bail!("can't cross compile for: {:?}", self.ssh_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()
+ }
};
- smol::fs::create_dir_all("target/remote_server").await?;
-
- if build_remote_server.contains("cross") {
- delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
- log::info!("installing cross");
- run_cmd(Command::new("cargo").args([
- "install",
- "cross",
- "--git",
- "https://github.com/cross-rs/cross",
- ]))
- .await?;
+ if self.ssh_platform.os == "linux" {
+ rust_flags.push_str(" -C target-feature=+crt-static");
+ }
+ if build_remote_server.contains("mold") {
+ rust_flags.push_str(" -C link-arg=-fuse-ld=mold");
+ }
- delegate.set_status(
- Some(&format!(
- "Building remote server binary from source for {} with Docker",
- &triple
- )),
- cx,
- );
- log::info!("building remote server binary from source for {}", &triple);
+ if self.ssh_platform.arch == std::env::consts::ARCH
+ && self.ssh_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(
- Command::new("cross")
+ Command::new("cargo")
.args([
"build",
"--package",
@@ -2062,69 +2111,152 @@ impl SshRemoteConnection {
"--target",
&triple,
])
- .env(
- "CROSS_CONTAINER_OPTS",
- "--mount type=bind,src=./target,dst=/app/target",
- ),
+ .env("RUSTFLAGS", &rust_flags),
)
.await?;
} else {
- let which = cx
- .background_spawn(async move { which::which("zig") })
- .await;
+ if build_remote_server.contains("cross") {
+ #[cfg(target_os = "windows")]
+ use util::paths::SanitizedPath;
+
+ delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
+ log::info!("installing cross");
+ run_cmd(Command::new("cargo").args([
+ "install",
+ "cross",
+ "--git",
+ "https://github.com/cross-rs/cross",
+ ]))
+ .await?;
- if which.is_err() {
- anyhow::bail!(
- "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
+ delegate.set_status(
+ Some(&format!(
+ "Building remote server binary from source for {} with Docker",
+ &triple
+ )),
+ cx,
+ );
+ log::info!("building remote server binary from source for {}", &triple);
+
+ // On Windows, the binding needs to be set to the canonical path
+ #[cfg(target_os = "windows")]
+ let src =
+ SanitizedPath::from(smol::fs::canonicalize("./target").await?).to_glob_string();
+ #[cfg(not(target_os = "windows"))]
+ let src = "./target";
+ run_cmd(
+ Command::new("cross")
+ .args([
+ "build",
+ "--package",
+ "remote_server",
+ "--features",
+ "debug-embed",
+ "--target-dir",
+ "target/remote_server",
+ "--target",
+ &triple,
+ ])
+ .env(
+ "CROSS_CONTAINER_OPTS",
+ format!("--mount type=bind,src={src},dst=/app/target"),
+ )
+ .env("RUSTFLAGS", &rust_flags),
)
- }
+ .await?;
+ } else {
+ let which = cx
+ .background_spawn(async move { which::which("zig") })
+ .await;
- delegate.set_status(Some("Adding rustup target for cross-compilation"), cx);
- log::info!("adding rustup target");
- run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?;
+ if which.is_err() {
+ #[cfg(not(target_os = "windows"))]
+ {
+ anyhow::bail!(
+ "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
+ )
+ }
+ #[cfg(target_os = "windows")]
+ {
+ anyhow::bail!(
+ "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) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross"
+ )
+ }
+ }
- delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx);
- log::info!("installing cargo-zigbuild");
- run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?;
+ delegate.set_status(Some("Adding rustup target for cross-compilation"), cx);
+ log::info!("adding rustup target");
+ run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).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(Command::new("cargo").args([
- "zigbuild",
- "--package",
- "remote_server",
- "--features",
- "debug-embed",
- "--target-dir",
- "target/remote_server",
- "--target",
- &triple,
- ]))
- .await?;
+ delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx);
+ log::info!("installing cargo-zigbuild");
+ run_cmd(Command::new("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(
+ Command::new("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 mut path = format!("target/remote_server/{triple}/debug/remote_server").into();
- if !build_remote_server.contains("nocompress") {
+ let path = if !build_remote_server.contains("nocompress") {
delegate.set_status(Some("Compressing binary"), cx);
- run_cmd(Command::new("gzip").args([
- "-9",
- "-f",
- &format!("target/remote_server/{}/debug/remote_server", triple),
- ]))
- .await?;
+ #[cfg(not(target_os = "windows"))]
+ {
+ run_cmd(Command::new("gzip").args(["-9", "-f", &bin_path.to_string_lossy()]))
+ .await?;
+ }
+ #[cfg(target_os = "windows")]
+ {
+ // On Windows, we use 7z to compress the binary
+ let seven_zip = which::which("7z.exe").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(Command::new(seven_zip).args([
+ "a",
+ "-tgzip",
+ &gz_path,
+ &bin_path.to_string_lossy(),
+ ]))
+ .await?;
+ }
- path = std::env::current_dir()?.join(format!(
- "target/remote_server/{triple}/debug/remote_server.gz"
- ));
- }
+ let mut archive_path = bin_path;
+ archive_path.set_extension("gz");
+ std::env::current_dir()?.join(archive_path)
+ } else {
+ bin_path
+ };
- return Ok(path);
+ Ok(path)
}
}
@@ -2450,9 +2582,11 @@ mod fake {
use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task, TestAppContext};
use release_channel::ReleaseChannel;
use rpc::proto::Envelope;
+ use util::paths::{PathStyle, RemotePathBuf};
use super::{
- ChannelClient, RemoteConnection, SshClientDelegate, SshConnectionOptions, SshPlatform,
+ ChannelClient, RemoteConnection, SshArgs, SshClientDelegate, SshConnectionOptions,
+ SshPlatform,
};
pub(super) struct FakeRemoteConnection {
@@ -2488,13 +2622,17 @@ mod fake {
false
}
- fn ssh_args(&self) -> Vec<String> {
- Vec::new()
+ fn ssh_args(&self) -> SshArgs {
+ SshArgs {
+ arguments: Vec::new(),
+ envs: None,
+ }
}
+
fn upload_directory(
&self,
_src_path: PathBuf,
- _dest_path: PathBuf,
+ _dest_path: RemotePathBuf,
_cx: &App,
) -> Task<Result<()>> {
unreachable!()
@@ -37,6 +37,7 @@ 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
@@ -85,7 +86,7 @@ node_runtime = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
remote = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, features = ["test-support"] }
-lsp = { workspace = true, features=["test-support"] }
+lsp = { workspace = true, features = ["test-support"] }
unindent.workspace = true
serde_json.workspace = true
zlog.workspace = true
@@ -95,4 +96,4 @@ cargo_toml.workspace = true
toml.workspace = true
[package.metadata.cargo-machete]
-ignored = ["rust-embed", "paths"]
+ignored = ["git2", "rust-embed", "paths"]
@@ -77,7 +77,6 @@ impl HeadlessProject {
cx: &mut Context<Self>,
) -> Self {
debug_adapter_extension::init(proxy.clone(), cx);
- language_extension::init(proxy.clone(), languages.clone());
languages::init(languages.clone(), node_runtime.clone(), cx);
let worktree_store = cx.new(|cx| {
@@ -185,6 +184,11 @@ impl HeadlessProject {
});
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,
@@ -164,7 +164,7 @@ fn init_panic_hook() {
}),
app_version: format!("remote-server-{version}"),
app_commit_sha: option_env!("ZED_COMMIT_SHA").map(|sha| sha.into()),
- release_channel: release_channel.display_name().into(),
+ release_channel: release_channel.dev_name().into(),
target: env!("TARGET").to_owned().into(),
os_name: telemetry::os_name(),
os_version: Some(telemetry::os_version()),
@@ -656,7 +656,7 @@ impl Render for CodeCell {
// .bg(cx.theme().colors().editor_background)
// .border(px(1.))
// .border_color(cx.theme().colors().border)
- // .shadow_sm()
+ // .shadow_xs()
.children(content)
},
))),
@@ -28,12 +28,19 @@ 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,
]
);
@@ -25,6 +25,7 @@ use alacritty_terminal::{
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::*};
@@ -257,12 +258,18 @@ impl Render for TerminalOutput {
point: ic.point,
cell: ic.cell.clone(),
});
- let (cells, rects) =
- TerminalElement::layout_grid(grid, 0, &text_style, text_system, None, window, cx);
+ 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 = cells.iter().map(|c| c.point.line).max().unwrap_or(0) + 1;
+ 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());
@@ -290,15 +297,14 @@ impl Render for TerminalOutput {
);
}
- for cell in cells {
- cell.paint(
+ for batch in batched_text_runs {
+ batch.paint(
bounds.origin,
&terminal::TerminalBounds {
cell_width,
line_height: text_line_height,
bounds,
},
- bounds,
window,
cx,
);
@@ -106,7 +106,9 @@ impl TableView {
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).width;
+ 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);
@@ -118,7 +120,7 @@ impl TableView {
runs[0].len = content.len();
let cell_width = window
.text_system()
- .layout_line(&content, font_size, &runs)
+ .layout_line(&content, font_size, &runs, None)
.width;
width = width.max(cell_width)
@@ -16,13 +16,21 @@ 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
]
);
@@ -90,7 +90,6 @@ impl EditorBlock {
style: BlockStyle::Sticky,
render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()),
priority: 0,
- render_in_minimap: false,
};
let block_id = editor.insert_blocks([block], None, cx)[0];
@@ -37,7 +37,16 @@ pub fn init(cx: &mut App) {
actions!(
rules_library,
- [NewRule, DeleteRule, DuplicateRule, ToggleDefaultRule]
+ [
+ /// 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: &'static str = concat!(
@@ -972,6 +981,7 @@ impl RulesLibrary {
tool_choice: None,
stop: Vec::new(),
temperature: None,
+ thinking_allowed: true,
},
cx,
)
@@ -46,6 +46,7 @@ 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)]
@@ -58,7 +59,17 @@ pub struct Deploy {
pub selection_search_enabled: bool,
}
-actions!(buffer_search, [DeployReplace, Dismiss, FocusEditor]);
+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 {
@@ -928,6 +939,11 @@ impl BufferSearchBar {
});
}
+ pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.focus(&self.replacement_editor.focus_handle(cx), window, cx);
+ cx.notify();
+ }
+
pub fn search(
&mut self,
query: &str,
@@ -1081,6 +1097,21 @@ impl BufferSearchBar {
}
}
+ pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ 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() {
+ return;
+ }
+ searchable_item.update_matches(matches, window, cx);
+ searchable_item.activate_match(0, matches, window, cx);
+ }
+ }
+ }
+
pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
if let Some(matches) = self
@@ -47,7 +47,16 @@ use workspace::{
actions!(
project_search,
- [SearchInNew, ToggleFocus, NextField, ToggleFilters]
+ [
+ /// 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
+ ]
);
#[derive(Default)]
@@ -23,19 +23,35 @@ pub fn init(cx: &mut App) {
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,
]
);
@@ -1,9 +1,6 @@
use editor::EditorSettings;
use settings::Settings as _;
-use ui::{
- ButtonCommon, ButtonLike, Clickable, Color, Context, Icon, IconName, IconSize, ParentElement,
- Render, Styled, Tooltip, Window, h_flex,
-};
+use ui::{ButtonCommon, Clickable, Context, Render, Tooltip, Window, prelude::*};
use workspace::{ItemHandle, StatusItemView};
pub struct SearchButton;
@@ -16,18 +13,15 @@ impl SearchButton {
impl Render for SearchButton {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
- let button = h_flex().gap_2();
+ let button = div();
+
if !EditorSettings::get_global(cx).search.button {
- return button;
+ return button.w_0().invisible();
}
button.child(
- ButtonLike::new("project-search-indicator")
- .child(
- Icon::new(IconName::MagnifyingGlass)
- .size(IconSize::Small)
- .color(Color::Default),
- )
+ IconButton::new("project-search-indicator", IconName::MagnifyingGlass)
+ .icon_size(IconSize::Small)
.tooltip(|window, cx| {
Tooltip::for_action(
"Project Search",
@@ -570,6 +570,7 @@ impl SummaryIndex {
tool_choice: None,
stop: Vec::new(),
temperature: None,
+ thinking_allowed: true,
};
let code_len = code.len();
@@ -1,8 +1,8 @@
use std::fmt::{Display, Formatter};
+use crate::{Settings, SettingsSources, VsCodeSettings};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
/// Base key bindings scheme. Base keymaps can be overridden with user keymaps.
///
@@ -114,7 +114,7 @@ impl Settings for BaseKeymap {
sources.default.ok_or_else(Self::missing_default)
}
- fn import_from_vscode(_vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
+ fn import_from_vscode(_vscode: &VsCodeSettings, current: &mut Self::FileContent) {
*current = Some(BaseKeymap::VSCode);
}
}
@@ -63,7 +63,7 @@ pub struct KeymapSection {
/// current file extension are also supported - see [the
/// documentation](https://zed.dev/docs/key-bindings#contexts) for more details.
#[serde(default)]
- context: String,
+ 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.
@@ -426,8 +426,18 @@ impl KeymapFile {
}
}
+ /// Creates a JSON schema generator, suitable for generating json schemas
+ /// for actions
+ pub fn action_schema_generator() -> schemars::SchemaGenerator {
+ schemars::generate::SchemaSettings::draft2019_09().into_generator()
+ }
+
pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value {
- let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator();
+ // 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 deprecations = cx.deprecated_actions_to_preferred_actions();
@@ -597,15 +607,25 @@ impl KeymapFile {
mut keymap_contents: String,
tab_size: usize,
) -> Result<String> {
- // if trying to replace a keybinding that is not user-defined, treat it as an add operation
match operation {
+ // if trying to replace a keybinding that is not user-defined, treat it as an add operation
KeybindUpdateOperation::Replace {
- target_source,
+ target_keybind_source: target_source,
source,
..
} if target_source != KeybindSource::User => {
operation = KeybindUpdateOperation::Add(source);
}
+ // 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 {
+ mut target,
+ target_keybind_source,
+ } if target_keybind_source != KeybindSource::User => {
+ target.action_name = gpui::NoAction.name();
+ target.input.take();
+ operation = KeybindUpdateOperation::Add(target);
+ }
_ => {}
}
@@ -613,56 +633,117 @@ impl KeymapFile {
// 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)
+ else {
+ anyhow::bail!("Failed to find keybinding to remove");
+ };
+ let is_only_binding = keymap.0[index]
+ .bindings
+ .as_ref()
+ .map_or(true, |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,
+ )
+ .context("Failed to remove keybinding")?;
+ keymap_contents.replace_range(replace_range, &replace_value);
+ return Ok(keymap_contents);
+ }
+
if let KeybindUpdateOperation::Replace { source, target, .. } = operation {
- let mut found_index = None;
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")?;
- 'sections: for (index, section) in keymap.sections().enumerate() {
- if section.context != target.context.unwrap_or("") {
- continue;
- }
- if section.use_key_equivalents != target.use_key_equivalents {
- continue;
- }
- let Some(bindings) = §ion.bindings else {
- continue;
- };
- for (keystrokes, action) in bindings {
- let Ok(keystrokes) = keystrokes
- .split_whitespace()
- .map(Keystroke::parse)
- .collect::<Result<Vec<_>, _>>()
- else {
- continue;
- };
- if keystrokes != target.keystrokes {
- continue;
- }
- if action.0 != target_action_value {
- continue;
- }
- found_index = Some(index);
- break 'sections;
- }
- }
-
- if let Some(index) = found_index {
- let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
- &keymap_contents,
- &["bindings", &target.keystrokes_unparsed()],
- Some(&source_action_value),
- Some(&source.keystrokes_unparsed()),
- index,
- tab_size,
- )
- .context("Failed to replace keybinding")?;
- keymap_contents.replace_range(replace_range, &replace_value);
- return Ok(keymap_contents);
+ if let Some((index, keystrokes_str)) =
+ find_binding(&keymap, &target, &target_action_value)
+ {
+ 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,
+ )
+ .context("Failed to replace keybinding")?;
+ keymap_contents.replace_range(replace_range, &replace_value);
+
+ return Ok(keymap_contents);
+ } else if keymap.0[index]
+ .bindings
+ .as_ref()
+ .map_or(true, |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,
+ )
+ .context("Failed to replace keybinding")?;
+ 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,
+ )
+ .context("Failed to replace keybinding")?;
+ 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,
+ )
+ .context("Failed to replace keybinding")?;
+ keymap_contents.replace_range(replace_range, &replace_value);
+ operation = KeybindUpdateOperation::Add(source);
+ }
} else {
log::warn!(
"Failed to find keybinding to update `{:?} -> {}` creating new binding for `{:?} -> {}` instead",
@@ -699,6 +780,50 @@ impl KeymapFile {
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,
+ ) -> 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;
+ }
+ if section.use_key_equivalents != target.use_key_equivalents {
+ continue;
+ }
+ let Some(bindings) = §ion.bindings else {
+ continue;
+ };
+ for (keystrokes_str, action) in bindings {
+ let Ok(keystrokes) = keystrokes_str
+ .split_whitespace()
+ .map(Keystroke::parse)
+ .collect::<Result<Vec<_>, _>>()
+ else {
+ continue;
+ };
+ if keystrokes.len() != target.keystrokes.len()
+ || !keystrokes
+ .iter()
+ .zip(target.keystrokes)
+ .all(|(a, b)| a.should_match(b))
+ {
+ continue;
+ }
+ if &action.0 != target_action_value {
+ continue;
+ }
+ return Some((index, &keystrokes_str));
+ }
+ }
+ None
+ }
}
}
@@ -708,11 +833,16 @@ pub enum KeybindUpdateOperation<'a> {
source: KeybindUpdateTarget<'a>,
/// Describes the keybind to remove
target: KeybindUpdateTarget<'a>,
- target_source: KeybindSource,
+ target_keybind_source: KeybindSource,
},
Add(KeybindUpdateTarget<'a>),
+ Remove {
+ target: KeybindUpdateTarget<'a>,
+ target_keybind_source: KeybindSource,
+ },
}
+#[derive(Debug)]
pub struct KeybindUpdateTarget<'a> {
pub context: Option<&'a str>,
pub keystrokes: &'a [Keystroke],
@@ -997,7 +1127,7 @@ mod tests {
use_key_equivalents: false,
input: Some(r#"{"foo": "bar"}"#),
},
- target_source: KeybindSource::Base,
+ target_keybind_source: KeybindSource::Base,
},
r#"[
{
@@ -1023,14 +1153,14 @@ mod tests {
r#"[
{
"bindings": {
- "ctrl-a": "zed::SomeAction"
+ "a": "zed::SomeAction"
}
}
]"#
.unindent(),
KeybindUpdateOperation::Replace {
target: KeybindUpdateTarget {
- keystrokes: &parse_keystrokes("ctrl-a"),
+ keystrokes: &parse_keystrokes("a"),
action_name: "zed::SomeAction",
context: None,
use_key_equivalents: false,
@@ -1043,7 +1173,7 @@ mod tests {
use_key_equivalents: false,
input: Some(r#"{"foo": "bar"}"#),
},
- target_source: KeybindSource::User,
+ target_keybind_source: KeybindSource::User,
},
r#"[
{
@@ -1084,7 +1214,7 @@ mod tests {
use_key_equivalents: false,
input: None,
},
- target_source: KeybindSource::User,
+ target_keybind_source: KeybindSource::User,
},
r#"[
{
@@ -1127,7 +1257,7 @@ mod tests {
use_key_equivalents: false,
input: Some(r#"{"foo": "bar"}"#),
},
- target_source: KeybindSource::User,
+ target_keybind_source: KeybindSource::User,
},
r#"[
{
@@ -1145,5 +1275,201 @@ mod tests {
]"#
.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"),
+ use_key_equivalents: false,
+ input: None,
+ },
+ source: KeybindUpdateTarget {
+ keystrokes: &parse_keystrokes("c"),
+ action_name: "foo::baz",
+ context: Some("SomeOtherContext"),
+ use_key_equivalents: false,
+ input: 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"),
+ use_key_equivalents: false,
+ input: None,
+ },
+ source: KeybindUpdateTarget {
+ keystrokes: &parse_keystrokes("c"),
+ action_name: "foo::baz",
+ context: Some("SomeOtherContext"),
+ use_key_equivalents: false,
+ input: 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",
+ use_key_equivalents: false,
+ input: 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",
+ use_key_equivalents: false,
+ input: 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",
+ use_key_equivalents: false,
+ input: Some("true"),
+ },
+ target_keybind_source: KeybindSource::User,
+ },
+ r#"[
+ {
+ "context": "SomeContext",
+ "bindings": {
+ "b": "foo::baz",
+ }
+ },
+ {
+ "context": "SomeContext",
+ "bindings": {
+ "c": "foo::baz",
+ }
+ },
+ ]"#
+ .unindent(),
+ );
}
}
@@ -1,3 +1,4 @@
+mod base_keymap_setting;
mod editable_setting_control;
mod key_equivalents;
mod keymap_file;
@@ -11,6 +12,7 @@ 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 key_equivalents::*;
pub use keymap_file::{
@@ -71,6 +73,7 @@ pub fn init(cx: &mut App) {
.set_default_settings(&default_settings(), cx)
.unwrap();
cx.set_global(settings);
+ BaseKeymap::register(cx);
}
pub fn default_settings() -> Cow<'static, str> {
@@ -1,6 +1,5 @@
use anyhow::Result;
use gpui::App;
-use schemars::{JsonSchema, Schema, transform::transform_subschemas};
use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value;
use std::{ops::Range, sync::LazyLock};
@@ -21,72 +20,6 @@ pub struct ParameterizedJsonSchema {
inventory::collect!(ParameterizedJsonSchema);
-const DEFS_PATH: &str = "#/$defs/";
-
-/// Replaces the JSON schema definition for some type, and returns a reference to it.
-pub fn replace_subschema<T: JsonSchema>(
- generator: &mut schemars::SchemaGenerator,
- schema: schemars::Schema,
-) -> schemars::Schema {
- // The key in definitions may not match T::schema_name() if multiple types have the same name.
- // This is a workaround for there being no straightforward way to get the key used for a type -
- // see https://github.com/GREsau/schemars/issues/449
- let ref_schema = generator.subschema_for::<T>();
- if let Some(serde_json::Value::String(definition_pointer)) = ref_schema.get("$ref") {
- if let Some(definition_name) = definition_pointer.strip_prefix(DEFS_PATH) {
- generator
- .definitions_mut()
- .insert(definition_name.to_string(), schema.to_value());
- return ref_schema;
- } else {
- log::error!(
- "bug: expected `$ref` field to start with {DEFS_PATH}, \
- got {definition_pointer}"
- );
- }
- } else {
- log::error!("bug: expected `$ref` field in result of `subschema_for`");
- }
- // fallback on just using the schema name, which could collide.
- let schema_name = T::schema_name();
- generator
- .definitions_mut()
- .insert(schema_name.to_string(), schema.to_value());
- Schema::new_ref(format!("{DEFS_PATH}{schema_name}"))
-}
-
-/// Adds a new JSON schema definition and returns a reference to it. **Panics** if the name is
-/// already in use.
-pub fn add_new_subschema(
- generator: &mut schemars::SchemaGenerator,
- name: &str,
- schema: Value,
-) -> Schema {
- let old_definition = generator.definitions_mut().insert(name.to_string(), schema);
- assert_eq!(old_definition, None);
- schemars::Schema::new_ref(format!("{DEFS_PATH}{name}"))
-}
-
-/// Defaults `additionalProperties` to `true`, as if `#[schemars(deny_unknown_fields)]` was on every
-/// struct. Skips structs that have `additionalProperties` set (such as if #[serde(flatten)] is used
-/// on a map).
-#[derive(Clone)]
-pub struct DefaultDenyUnknownFields;
-
-impl schemars::transform::Transform for DefaultDenyUnknownFields {
- fn transform(&mut self, schema: &mut schemars::Schema) {
- if let Some(object) = schema.as_object_mut() {
- if object.contains_key("properties")
- && !object.contains_key("additionalProperties")
- && !object.contains_key("unevaluatedProperties")
- {
- object.insert("additionalProperties".to_string(), false.into());
- }
- }
- transform_subschemas(self, schema);
- }
-}
-
pub fn update_value_in_json_text<'a>(
text: &mut String,
key_path: &mut Vec<&'a str>,
@@ -420,29 +353,58 @@ pub fn replace_top_level_array_value_in_json_text(
let range = cursor.node().range();
let indent_width = range.start_point.column;
let offset = range.start_byte;
- let value_str = &text[range.start_byte..range.end_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;
- let (mut replace_range, mut replace_value) =
- replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key);
+ if new_value.is_none() && key_path.is_empty() {
+ let mut remove_range = text_range.clone();
+ 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') {
+ if 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;
+ }
+ }
+ return Ok((remove_range, String::new()));
+ } else {
+ 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;
+ 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);
- // replace_value.push('\n');
- } 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, " ");
+ if needs_indent {
+ let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width);
+ replace_value = replace_value.replace('\n', &increased_indent);
+ // replace_value.push('\n');
+ } 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, " ");
+ }
}
- }
- return Ok((replace_range, replace_value));
+ return Ok((replace_range, replace_value));
+ }
}
pub fn append_top_level_array_value_in_json_text(
@@ -1072,14 +1034,14 @@ mod tests {
input: impl ToString,
index: usize,
key_path: &[&str],
- value: Value,
+ value: Option<Value>,
expected: impl ToString,
) {
let input = input.to_string();
let result = replace_top_level_array_value_in_json_text(
&input,
key_path,
- Some(&value),
+ value.as_ref(),
None,
index,
4,
@@ -1090,10 +1052,10 @@ mod tests {
pretty_assertions::assert_eq!(expected.to_string(), result_str);
}
- check_array_replace(r#"[1, 3, 3]"#, 1, &[], json!(2), r#"[1, 2, 3]"#);
- check_array_replace(r#"[1, 3, 3]"#, 2, &[], json!(2), r#"[1, 3, 2]"#);
- check_array_replace(r#"[1, 3, 3,]"#, 3, &[], json!(2), r#"[1, 3, 3, 2]"#);
- check_array_replace(r#"[1, 3, 3,]"#, 100, &[], json!(2), r#"[1, 3, 3, 2]"#);
+ 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,
@@ -1103,7 +1065,7 @@ mod tests {
.unindent(),
1,
&[],
- json!({"foo": "bar", "baz": "qux"}),
+ Some(json!({"foo": "bar", "baz": "qux"})),
r#"[
1,
{
@@ -1118,7 +1080,7 @@ mod tests {
r#"[1, 3, 3,]"#,
1,
&[],
- json!({"foo": "bar", "baz": "qux"}),
+ Some(json!({"foo": "bar", "baz": "qux"})),
r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
);
@@ -1126,7 +1088,7 @@ mod tests {
r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1,
&["baz"],
- json!({"qux": "quz"}),
+ Some(json!({"qux": "quz"})),
r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#,
);
@@ -1141,7 +1103,7 @@ mod tests {
]"#,
1,
&["baz"],
- json!({"qux": "quz"}),
+ Some(json!({"qux": "quz"})),
r#"[
1,
{
@@ -1167,7 +1129,7 @@ mod tests {
]"#,
1,
&["baz"],
- json!("qux"),
+ Some(json!("qux")),
r#"[
1,
{
@@ -1194,7 +1156,7 @@ mod tests {
]"#,
1,
&["baz"],
- json!("qux"),
+ Some(json!("qux")),
r#"[
1,
{
@@ -1218,7 +1180,7 @@ mod tests {
]"#,
2,
&[],
- json!("replaced"),
+ Some(json!("replaced")),
r#"[
1,
// This is element 2
@@ -1236,7 +1198,7 @@ mod tests {
.unindent(),
0,
&[],
- json!("first"),
+ Some(json!("first")),
r#"[
// Empty array with comment
"first"
@@ -1247,7 +1209,7 @@ mod tests {
r#"[]"#.unindent(),
0,
&[],
- json!("first"),
+ Some(json!("first")),
r#"[
"first"
]"#
@@ -1264,7 +1226,7 @@ mod tests {
]"#,
0,
&[],
- json!({"new": "object"}),
+ Some(json!({"new": "object"})),
r#"[
// Leading comment
// Another leading comment
@@ -1284,7 +1246,7 @@ mod tests {
]"#,
1,
&[],
- json!("deep"),
+ Some(json!("deep")),
r#"[
1,
"deep",
@@ -1297,7 +1259,7 @@ mod tests {
r#"[1,2, 3, 4]"#,
2,
&[],
- json!("spaced"),
+ Some(json!("spaced")),
r#"[1,2, "spaced", 4]"#,
);
@@ -1310,7 +1272,7 @@ mod tests {
]"#,
1,
&[],
- json!(["a", "b", "c", "d"]),
+ Some(json!(["a", "b", "c", "d"])),
r#"[
[1, 2, 3],
[
@@ -1335,7 +1297,7 @@ mod tests {
]"#,
0,
&[],
- json!("updated"),
+ Some(json!("updated")),
r#"[
/*
* This is a
@@ -1351,7 +1313,7 @@ mod tests {
r#"[true, false, true]"#,
1,
&[],
- json!(null),
+ Some(json!(null)),
r#"[true, null, true]"#,
);
@@ -1360,7 +1322,7 @@ mod tests {
r#"[42]"#,
0,
&[],
- json!({"answer": 42}),
+ Some(json!({"answer": 42})),
r#"[{ "answer": 42 }]"#,
);
@@ -1374,7 +1336,7 @@ mod tests {
.unindent(),
10,
&[],
- json!(123),
+ Some(json!(123)),
r#"[
// Comment 1
// Comment 2
@@ -1383,6 +1345,54 @@ mod tests {
]"#
.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(),
+ );
}
#[test]
@@ -6,7 +6,7 @@ use futures::{FutureExt, StreamExt, channel::mpsc, future::LocalBoxFuture};
use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal};
use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name};
-use schemars::{JsonSchema, json_schema};
+use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::{Value, json};
use smallvec::SmallVec;
@@ -18,14 +18,16 @@ use std::{
str::{self, FromStr},
sync::Arc,
};
-
-use util::{ResultExt as _, merge_non_null_json_value_into};
+use util::{
+ ResultExt as _, merge_non_null_json_value_into,
+ schemars::{DefaultDenyUnknownFields, add_new_subschema},
+};
pub type EditorconfigProperties = ec4rs::Properties;
use crate::{
- DefaultDenyUnknownFields, ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings,
- WorktreeId, add_new_subschema, parse_json_with_comments, update_value_in_json_text,
+ ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings, WorktreeId,
+ parse_json_with_comments, update_value_in_json_text,
};
/// A value that can be defined as a user setting.
@@ -1019,19 +1021,19 @@ impl SettingsStore {
.unwrap()
.remove("additionalProperties");
- let mut root_schema = if let Some(meta_schema) = generator.settings().meta_schema.as_ref() {
- json_schema!({ "$schema": meta_schema.to_string() })
- } else {
- json_schema!({})
- };
-
- // "unevaluatedProperties: false" to report unknown fields.
- root_schema.insert("unevaluatedProperties".to_string(), false.into());
-
- // Settings file contents matches ZedSettings + overrides for each release stage.
- root_schema.insert(
- "allOf".to_string(),
- json!([
+ let meta_schema = generator
+ .settings()
+ .meta_schema
+ .as_ref()
+ .expect("meta_schema should be present in schemars settings")
+ .to_string();
+
+ json!({
+ "$schema": meta_schema,
+ "title": "Zed Settings",
+ "unevaluatedProperties": false,
+ // ZedSettings + settings overrides for each release stage
+ "allOf": [
zed_settings_ref,
{
"properties": {
@@ -1041,12 +1043,9 @@ impl SettingsStore {
"preview": zed_release_stage_settings_ref,
}
}
- ]),
- );
-
- root_schema.insert("$defs".to_string(), definitions.into());
-
- root_schema.to_value()
+ ],
+ "$defs": definitions,
+ })
}
fn recompute_values(
@@ -26,15 +26,19 @@ gpui.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
+notifications.workspace = true
paths.workspace = true
project.workspace = true
schemars.workspace = true
search.workspace = true
serde.workspace = true
+serde_json.workspace = true
settings.workspace = true
theme.workspace = true
tree-sitter-json.workspace = true
+tree-sitter-rust.workspace = true
ui.workspace = true
+ui_input.workspace = true
util.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
@@ -1,26 +1,35 @@
-use std::{ops::Range, sync::Arc};
+use std::{
+ ops::{Not, Range},
+ sync::Arc,
+};
use anyhow::{Context as _, anyhow};
-use collections::HashSet;
-use editor::{Editor, EditorEvent};
+use collections::{HashMap, HashSet};
+use editor::{CompletionProvider, Editor, EditorEvent};
use feature_flags::FeatureFlagViewExt;
use fs::Fs;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
- AppContext as _, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
- FontWeight, Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText,
- Subscription, WeakEntity, actions, div, transparent_black,
+ Action, Animation, AnimationExt, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent,
+ Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero, KeyContext,
+ KeyDownEvent, Keystroke, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy,
+ ScrollWheelEvent, StyledText, Subscription, WeakEntity, actions, anchored, deferred, div,
};
-use language::{Language, LanguageConfig};
-use settings::KeybindSource;
+use language::{Language, LanguageConfig, ToOffset as _};
+use notifications::status_toast::{StatusToast, ToastIcon};
+use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets};
use util::ResultExt;
use ui::{
- ActiveTheme as _, App, BorrowAppContext, ContextMenu, ParentElement as _, Render, SharedString,
- Styled as _, Window, prelude::*, right_click_menu,
+ ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, Modal, ModalFooter, ModalHeader,
+ ParentElement as _, Render, Section, SharedString, Styled as _, Tooltip, Window, prelude::*,
+};
+use ui_input::SingleLineInput;
+use workspace::{
+ Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,
+ register_serializable_item,
};
-use workspace::{Item, ModalView, SerializableItem, Workspace, register_serializable_item};
use crate::{
SettingsUiFeatureFlag,
@@ -28,10 +37,36 @@ use crate::{
ui_components::table::{Table, TableInteractionState},
};
-actions!(zed, [OpenKeymapEditor]);
+const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>");
+
+actions!(
+ zed,
+ [
+ /// Opens the keymap editor.
+ OpenKeymapEditor
+ ]
+);
const KEYMAP_EDITOR_NAMESPACE: &'static str = "keymap_editor";
-actions!(keymap_editor, [EditBinding, CopyAction, CopyContext]);
+actions!(
+ keymap_editor,
+ [
+ /// Edits the selected key binding.
+ EditBinding,
+ /// Creates a new key binding for the selected action.
+ CreateBinding,
+ /// Deletes the selected key binding.
+ DeleteBinding,
+ /// Copies the action name to clipboard.
+ CopyAction,
+ /// Copies the context predicate to clipboard.
+ CopyContext,
+ /// Toggles Conflict Filtering
+ ToggleConflictFilter,
+ /// Toggle Keystroke search
+ ToggleKeystrokeSearch,
+ ]
+);
pub fn init(cx: &mut App) {
let keymap_event_channel = KeymapEventChannel::new();
@@ -39,20 +74,30 @@ pub fn init(cx: &mut App) {
cx.on_action(|_: &OpenKeymapEditor, cx| {
workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| {
- let existing = workspace
- .active_pane()
- .read(cx)
- .items()
- .find_map(|item| item.downcast::<KeymapEditor>());
-
- if let Some(existing) = existing {
- workspace.activate_item(&existing, true, true, window, cx);
- } else {
- let keymap_editor =
- cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
- workspace.add_item_to_active_pane(Box::new(keymap_editor), None, true, window, cx);
- }
- });
+ workspace
+ .with_local_workspace(window, cx, |workspace, window, cx| {
+ let existing = workspace
+ .active_pane()
+ .read(cx)
+ .items()
+ .find_map(|item| item.downcast::<KeymapEditor>());
+
+ if let Some(existing) = existing {
+ workspace.activate_item(&existing, true, true, window, cx);
+ } else {
+ let keymap_editor =
+ cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
+ workspace.add_item_to_active_pane(
+ Box::new(keymap_editor),
+ None,
+ true,
+ window,
+ cx,
+ );
+ }
+ })
+ .detach();
+ })
});
cx.observe_new(|_workspace: &mut Workspace, window, cx| {
@@ -114,17 +159,116 @@ impl KeymapEventChannel {
}
}
+#[derive(Default, PartialEq)]
+enum SearchMode {
+ #[default]
+ Normal,
+ KeyStroke,
+}
+
+impl SearchMode {
+ fn invert(&self) -> Self {
+ match self {
+ SearchMode::Normal => SearchMode::KeyStroke,
+ SearchMode::KeyStroke => SearchMode::Normal,
+ }
+ }
+}
+
+#[derive(Default, PartialEq, Copy, Clone)]
+enum FilterState {
+ #[default]
+ All,
+ Conflicts,
+}
+
+impl FilterState {
+ fn invert(&self) -> Self {
+ match self {
+ FilterState::All => FilterState::Conflicts,
+ FilterState::Conflicts => FilterState::All,
+ }
+ }
+}
+
+type ActionMapping = (SharedString, Option<SharedString>);
+
+#[derive(Default)]
+struct ConflictState {
+ conflicts: Vec<usize>,
+ action_keybind_mapping: HashMap<ActionMapping, Vec<usize>>,
+}
+
+impl ConflictState {
+ fn new(key_bindings: &[ProcessedKeybinding]) -> Self {
+ let mut action_keybind_mapping: HashMap<_, Vec<usize>> = HashMap::default();
+
+ key_bindings
+ .iter()
+ .enumerate()
+ .filter(|(_, binding)| {
+ !binding.keystroke_text.is_empty()
+ && binding
+ .source
+ .as_ref()
+ .is_some_and(|source| matches!(source.0, KeybindSource::User))
+ })
+ .for_each(|(index, binding)| {
+ action_keybind_mapping
+ .entry(binding.get_action_mapping())
+ .or_default()
+ .push(index);
+ });
+
+ Self {
+ conflicts: action_keybind_mapping
+ .values()
+ .filter(|indices| indices.len() > 1)
+ .flatten()
+ .copied()
+ .collect(),
+ action_keybind_mapping,
+ }
+ }
+
+ fn conflicting_indices_for_mapping(
+ &self,
+ action_mapping: ActionMapping,
+ keybind_idx: usize,
+ ) -> Option<Vec<usize>> {
+ self.action_keybind_mapping
+ .get(&action_mapping)
+ .and_then(|indices| {
+ let mut indices = indices.iter().filter(|&idx| *idx != keybind_idx).peekable();
+ indices.peek().is_some().then(|| indices.copied().collect())
+ })
+ }
+
+ fn has_conflict(&self, candidate_idx: &usize) -> bool {
+ self.conflicts.contains(candidate_idx)
+ }
+
+ fn any_conflicts(&self) -> bool {
+ !self.conflicts.is_empty()
+ }
+}
+
struct KeymapEditor {
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
_keymap_subscription: Subscription,
keybindings: Vec<ProcessedKeybinding>,
+ keybinding_conflict_state: ConflictState,
+ filter_state: FilterState,
+ search_mode: SearchMode,
// corresponds 1 to 1 with keybindings
string_match_candidates: Arc<Vec<StringMatchCandidate>>,
matches: Vec<StringMatch>,
table_interaction_state: Entity<TableInteractionState>,
filter_editor: Entity<Editor>,
+ keystroke_editor: Entity<KeystrokeInput>,
selected_index: Option<usize>,
+ context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
}
impl EventEmitter<()> for KeymapEditor {}
@@ -137,15 +281,19 @@ impl Focusable for KeymapEditor {
impl KeymapEditor {
fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
- let focus_handle = cx.focus_handle();
-
let _keymap_subscription =
cx.observe_global::<KeymapEventChannel>(Self::update_keybindings);
let table_interaction_state = TableInteractionState::new(window, cx);
+ let keystroke_editor = cx.new(|cx| {
+ let mut keystroke_editor = KeystrokeInput::new(None, window, cx);
+ keystroke_editor.highlight_on_focus = false;
+ keystroke_editor
+ });
+
let filter_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text("Filter action names...", cx);
+ editor.set_placeholder_text("Filter action names…", cx);
editor
});
@@ -158,16 +306,30 @@ impl KeymapEditor {
})
.detach();
+ cx.subscribe(&keystroke_editor, |this, _, _, cx| {
+ if matches!(this.search_mode, SearchMode::Normal) {
+ return;
+ }
+
+ this.update_matches(cx);
+ })
+ .detach();
+
let mut this = Self {
workspace,
keybindings: vec![],
+ keybinding_conflict_state: ConflictState::default(),
+ filter_state: FilterState::default(),
+ search_mode: SearchMode::default(),
string_match_candidates: Arc::new(vec![]),
matches: vec![],
- focus_handle: focus_handle.clone(),
+ focus_handle: cx.focus_handle(),
_keymap_subscription,
table_interaction_state,
filter_editor,
+ keystroke_editor,
selected_index: None,
+ context_menu: None,
};
this.update_keybindings(cx);
@@ -175,30 +337,47 @@ impl KeymapEditor {
this
}
- fn current_query(&self, cx: &mut Context<Self>) -> String {
+ fn current_action_query(&self, cx: &App) -> String {
self.filter_editor.read(cx).text(cx)
}
+ fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> {
+ match self.search_mode {
+ SearchMode::KeyStroke => self
+ .keystroke_editor
+ .read(cx)
+ .keystrokes()
+ .iter()
+ .cloned()
+ .collect(),
+ SearchMode::Normal => Default::default(),
+ }
+ }
+
fn update_matches(&self, cx: &mut Context<Self>) {
- let query = self.current_query(cx);
+ let action_query = self.current_action_query(cx);
+ let keystroke_query = self.current_keystroke_query(cx);
- cx.spawn(async move |this, cx| Self::process_query(this, query, cx).await)
- .detach();
+ cx.spawn(async move |this, cx| {
+ Self::process_query(this, action_query, keystroke_query, cx).await
+ })
+ .detach();
}
async fn process_query(
this: WeakEntity<Self>,
- query: String,
+ action_query: String,
+ keystroke_query: Vec<Keystroke>,
cx: &mut AsyncApp,
) -> anyhow::Result<()> {
- let query = command_palette::normalize_action_query(&query);
+ let action_query = command_palette::normalize_action_query(&action_query);
let (string_match_candidates, keybind_count) = this.read_with(cx, |this, _| {
(this.string_match_candidates.clone(), this.keybindings.len())
})?;
let executor = cx.background_executor().clone();
let mut matches = fuzzy::match_strings(
&string_match_candidates,
- &query,
+ &action_query,
true,
true,
keybind_count,
@@ -207,7 +386,35 @@ impl KeymapEditor {
)
.await;
this.update(cx, |this, cx| {
- if query.is_empty() {
+ match this.filter_state {
+ FilterState::Conflicts => {
+ matches.retain(|candidate| {
+ this.keybinding_conflict_state
+ .has_conflict(&candidate.candidate_id)
+ });
+ }
+ FilterState::All => {}
+ }
+
+ match this.search_mode {
+ SearchMode::KeyStroke => {
+ matches.retain(|item| {
+ this.keybindings[item.candidate_id]
+ .keystrokes()
+ .is_some_and(|keystrokes| {
+ keystroke_query.iter().all(|key| {
+ keystrokes.iter().any(|keystroke| {
+ keystroke.key == key.key
+ && keystroke.modifiers == key.modifiers
+ })
+ })
+ })
+ });
+ }
+ SearchMode::Normal => {}
+ }
+
+ if action_query.is_empty() {
// apply default sort
// sorts by source precedence, and alphabetically by action name within each source
matches.sort_by_key(|match_item| {
@@ -221,7 +428,7 @@ impl KeymapEditor {
Some(Default) => 3,
None => 4,
};
- return (source_precedence, keybind.action.as_ref());
+ return (source_precedence, keybind.action_name.as_ref());
});
}
this.selected_index.take();
@@ -233,12 +440,21 @@ impl KeymapEditor {
fn process_bindings(
json_language: Arc<Language>,
+ rust_language: Arc<Language>,
cx: &mut App,
) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
let key_bindings_ptr = cx.key_bindings();
let lock = key_bindings_ptr.borrow();
let key_bindings = lock.bindings();
- let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names());
+ let mut unmapped_action_names =
+ HashSet::from_iter(cx.all_action_names().into_iter().copied());
+ let action_documentation = cx.action_documentation();
+ let mut generator = KeymapFile::action_schema_generator();
+ let action_schema = HashMap::from_iter(
+ cx.action_schemas(&mut generator)
+ .into_iter()
+ .filter_map(|(name, schema)| schema.map(|schema| (name, schema))),
+ );
let mut processed_bindings = Vec::new();
let mut string_match_candidates = Vec::new();
@@ -248,13 +464,15 @@ impl KeymapEditor {
let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
let ui_key_binding = Some(
- ui::KeyBinding::new(key_binding.clone(), cx)
+ ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
.vim_mode(source == Some(settings::KeybindSource::Vim)),
);
let context = key_binding
.predicate()
- .map(|predicate| KeybindContextString::Local(predicate.to_string().into()))
+ .map(|predicate| {
+ KeybindContextString::Local(predicate.to_string().into(), rust_language.clone())
+ })
.unwrap_or(KeybindContextString::Global);
let source = source.map(|source| (source, source.name().into()));
@@ -264,14 +482,17 @@ impl KeymapEditor {
let action_input = key_binding
.action_input()
.map(|input| SyntaxHighlightedText::new(input, json_language.clone()));
+ let action_docs = action_documentation.get(action_name).copied();
let index = processed_bindings.len();
let string_match_candidate = StringMatchCandidate::new(index, &action_name);
processed_bindings.push(ProcessedKeybinding {
keystroke_text: keystroke_text.into(),
ui_key_binding,
- action: action_name.into(),
+ action_name: action_name.into(),
action_input,
+ action_docs,
+ action_schema: action_schema.get(action_name).cloned(),
context: Some(context),
source,
});
@@ -285,8 +506,10 @@ impl KeymapEditor {
processed_bindings.push(ProcessedKeybinding {
keystroke_text: empty.clone(),
ui_key_binding: None,
- action: (*action_name).into(),
+ action_name: action_name.into(),
action_input: None,
+ action_docs: action_documentation.get(action_name).copied(),
+ action_schema: action_schema.get(action_name).cloned(),
context: None,
source: None,
});
@@ -299,11 +522,19 @@ impl KeymapEditor {
fn update_keybindings(&mut self, cx: &mut Context<KeymapEditor>) {
let workspace = self.workspace.clone();
cx.spawn(async move |this, cx| {
- let json_language = Self::load_json_language(workspace, cx).await;
+ let json_language = load_json_language(workspace.clone(), cx).await;
+ let rust_language = load_rust_language(workspace.clone(), cx).await;
- let query = this.update(cx, |this, cx| {
+ let (action_query, keystroke_query) = this.update(cx, |this, cx| {
let (key_bindings, string_match_candidates) =
- Self::process_bindings(json_language.clone(), cx);
+ Self::process_bindings(json_language, rust_language, cx);
+
+ this.keybinding_conflict_state = ConflictState::new(&key_bindings);
+
+ if !this.keybinding_conflict_state.any_conflicts() {
+ this.filter_state = FilterState::All;
+ }
+
this.keybindings = key_bindings;
this.string_match_candidates = Arc::new(string_match_candidates);
this.matches = this
@@ -317,43 +548,17 @@ impl KeymapEditor {
string: candidate.string.clone(),
})
.collect();
- this.current_query(cx)
+ (
+ this.current_action_query(cx),
+ this.current_keystroke_query(cx),
+ )
})?;
// calls cx.notify
- Self::process_query(this, query, cx).await
+ Self::process_query(this, action_query, keystroke_query, cx).await
})
.detach_and_log_err(cx);
}
- async fn load_json_language(
- workspace: WeakEntity<Workspace>,
- cx: &mut AsyncApp,
- ) -> Arc<Language> {
- let json_language_task = workspace
- .read_with(cx, |workspace, cx| {
- workspace
- .project()
- .read(cx)
- .languages()
- .language_for_name("JSON")
- })
- .context("Failed to load JSON language")
- .log_err();
- let json_language = match json_language_task {
- Some(task) => task.await.context("Failed to load JSON language").log_err(),
- None => None,
- };
- return json_language.unwrap_or_else(|| {
- Arc::new(Language::new(
- LanguageConfig {
- name: "JSON".into(),
- ..Default::default()
- },
- Some(tree_sitter_json::LANGUAGE.into()),
- ))
- });
- }
-
fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
let mut dispatch_context = KeyContext::new_with_defaults();
dispatch_context.add("KeymapEditor");
@@ -389,13 +594,85 @@ impl KeymapEditor {
self.selected_index.take();
}
- fn selected_binding(&self) -> Option<&ProcessedKeybinding> {
+ fn selected_keybind_idx(&self) -> Option<usize> {
self.selected_index
.and_then(|match_index| self.matches.get(match_index))
.map(|r#match| r#match.candidate_id)
+ }
+
+ fn selected_binding(&self) -> Option<&ProcessedKeybinding> {
+ self.selected_keybind_idx()
.and_then(|keybind_index| self.keybindings.get(keybind_index))
}
+ fn select_index(&mut self, index: usize, cx: &mut Context<Self>) {
+ if self.selected_index != Some(index) {
+ self.selected_index = Some(index);
+ cx.notify();
+ }
+ }
+
+ fn create_context_menu(
+ &mut self,
+ position: Point<Pixels>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.context_menu = self.selected_binding().map(|selected_binding| {
+ let selected_binding_has_no_context = selected_binding
+ .context
+ .as_ref()
+ .and_then(KeybindContextString::local)
+ .is_none();
+
+ let selected_binding_is_unbound = selected_binding.keystrokes().is_none();
+
+ let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| {
+ menu.action_disabled_when(
+ selected_binding_is_unbound,
+ "Edit",
+ Box::new(EditBinding),
+ )
+ .action("Create", Box::new(CreateBinding))
+ .action_disabled_when(
+ selected_binding_is_unbound,
+ "Delete",
+ Box::new(DeleteBinding),
+ )
+ .separator()
+ .action("Copy Action", Box::new(CopyAction))
+ .action_disabled_when(
+ selected_binding_has_no_context,
+ "Copy Context",
+ Box::new(CopyContext),
+ )
+ });
+
+ let context_menu_handle = context_menu.focus_handle(cx);
+ window.defer(cx, move |window, _cx| window.focus(&context_menu_handle));
+ let subscription = cx.subscribe_in(
+ &context_menu,
+ window,
+ |this, _, _: &DismissEvent, window, cx| {
+ this.dismiss_context_menu(window, cx);
+ },
+ );
+ (context_menu, position, subscription)
+ });
+
+ cx.notify();
+ }
+
+ fn dismiss_context_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.context_menu.take();
+ window.focus(&self.focus_handle);
+ cx.notify();
+ }
+
+ fn context_menu_deployed(&self) -> bool {
+ self.context_menu.is_some()
+ }
+
fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
if let Some(selected) = self.selected_index {
let selected = selected + 1;
@@ -459,18 +736,37 @@ impl KeymapEditor {
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
- self.edit_selected_keybinding(window, cx);
+ self.open_edit_keybinding_modal(false, window, cx);
}
- fn edit_selected_keybinding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- let Some(keybind) = self.selected_binding() else {
+ fn open_edit_keybinding_modal(
+ &mut self,
+ create: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some((keybind_idx, keybind)) = self
+ .selected_keybind_idx()
+ .zip(self.selected_binding().cloned())
+ else {
return;
};
+ let keymap_editor = cx.entity();
self.workspace
.update(cx, |workspace, cx| {
let fs = workspace.app_state().fs.clone();
+ let workspace_weak = cx.weak_entity();
workspace.toggle_modal(window, cx, |window, cx| {
- let modal = KeybindingEditorModal::new(keybind.clone(), fs, window, cx);
+ let modal = KeybindingEditorModal::new(
+ create,
+ keybind,
+ keybind_idx,
+ keymap_editor,
+ workspace_weak,
+ fs,
+ window,
+ cx,
+ );
window.focus(&modal.focus_handle(cx));
modal
});
@@ -479,7 +775,26 @@ impl KeymapEditor {
}
fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context<Self>) {
- self.edit_selected_keybinding(window, cx);
+ self.open_edit_keybinding_modal(false, window, cx);
+ }
+
+ fn create_binding(&mut self, _: &CreateBinding, window: &mut Window, cx: &mut Context<Self>) {
+ self.open_edit_keybinding_modal(true, window, cx);
+ }
+
+ fn delete_binding(&mut self, _: &DeleteBinding, window: &mut Window, cx: &mut Context<Self>) {
+ let Some(to_remove) = self.selected_binding().cloned() else {
+ return;
+ };
+ let Ok(fs) = self
+ .workspace
+ .read_with(cx, |workspace, _| workspace.app_state().fs.clone())
+ else {
+ return;
+ };
+ let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
+ cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
+ .detach_and_notify_err(window, cx);
}
fn copy_context_to_clipboard(
@@ -507,28 +822,81 @@ impl KeymapEditor {
) {
let action = self
.selected_binding()
- .map(|binding| binding.action.to_string());
+ .map(|binding| binding.action_name.to_string());
let Some(action) = action else {
return;
};
cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone()));
}
+
+ fn toggle_conflict_filter(
+ &mut self,
+ _: &ToggleConflictFilter,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.filter_state = self.filter_state.invert();
+ self.update_matches(cx);
+ }
+
+ fn toggle_keystroke_search(
+ &mut self,
+ _: &ToggleKeystrokeSearch,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.search_mode = self.search_mode.invert();
+ self.update_matches(cx);
+
+ // Update the keystroke editor to turn the `search` bool on
+ self.keystroke_editor.update(cx, |keystroke_editor, cx| {
+ keystroke_editor.set_search_mode(self.search_mode == SearchMode::KeyStroke);
+ cx.notify();
+ });
+
+ match self.search_mode {
+ SearchMode::KeyStroke => {
+ window.focus(&self.keystroke_editor.read(cx).recording_focus_handle(cx));
+ }
+ SearchMode::Normal => {}
+ }
+ }
}
#[derive(Clone)]
struct ProcessedKeybinding {
keystroke_text: SharedString,
ui_key_binding: Option<ui::KeyBinding>,
- action: SharedString,
+ action_name: SharedString,
action_input: Option<SyntaxHighlightedText>,
+ action_docs: Option<&'static str>,
+ action_schema: Option<schemars::Schema>,
context: Option<KeybindContextString>,
source: Option<(KeybindSource, SharedString)>,
}
-#[derive(Clone, Debug, IntoElement)]
+impl ProcessedKeybinding {
+ fn get_action_mapping(&self) -> ActionMapping {
+ (
+ self.keystroke_text.clone(),
+ self.context
+ .as_ref()
+ .and_then(|context| context.local())
+ .cloned(),
+ )
+ }
+
+ fn keystrokes(&self) -> Option<&[Keystroke]> {
+ self.ui_key_binding
+ .as_ref()
+ .map(|binding| binding.keystrokes.as_slice())
+ }
+}
+
+#[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)]
enum KeybindContextString {
Global,
- Local(SharedString),
+ Local(SharedString, Arc<Language>),
}
impl KeybindContextString {
@@ -537,27 +905,39 @@ impl KeybindContextString {
pub fn local(&self) -> Option<&SharedString> {
match self {
KeybindContextString::Global => None,
- KeybindContextString::Local(name) => Some(name),
+ KeybindContextString::Local(name, _) => Some(name),
}
}
pub fn local_str(&self) -> Option<&str> {
match self {
KeybindContextString::Global => None,
- KeybindContextString::Local(name) => Some(name),
+ KeybindContextString::Local(name, _) => Some(name),
}
}
}
impl RenderOnce for KeybindContextString {
- fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+ fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
match self {
- KeybindContextString::Global => KeybindContextString::GLOBAL.clone(),
- KeybindContextString::Local(name) => name,
+ KeybindContextString::Global => {
+ muted_styled_text(KeybindContextString::GLOBAL.clone(), cx).into_any_element()
+ }
+ KeybindContextString::Local(name, language) => {
+ SyntaxHighlightedText::new(name, language).into_any_element()
+ }
}
}
}
+fn muted_styled_text(text: SharedString, cx: &App) -> StyledText {
+ let len = text.len();
+ StyledText::new(text).with_highlights([(
+ 0..len,
+ gpui::HighlightStyle::color(cx.theme().colors().text_muted),
+ )])
+}
+
impl Item for KeymapEditor {
type Event = ();
@@ -571,7 +951,9 @@ impl Render for KeymapEditor {
let row_count = self.matches.len();
let theme = cx.theme();
- div()
+ v_flex()
+ .id("keymap-editor")
+ .track_focus(&self.focus_handle)
.key_context(self.dispatch_context(window, cx))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
@@ -580,29 +962,105 @@ impl Render for KeymapEditor {
.on_action(cx.listener(Self::focus_search))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::edit_binding))
+ .on_action(cx.listener(Self::create_binding))
+ .on_action(cx.listener(Self::delete_binding))
.on_action(cx.listener(Self::copy_action_to_clipboard))
.on_action(cx.listener(Self::copy_context_to_clipboard))
+ .on_action(cx.listener(Self::toggle_conflict_filter))
+ .on_action(cx.listener(Self::toggle_keystroke_search))
.size_full()
+ .p_2()
+ .gap_1()
.bg(theme.colors().editor_background)
- .id("keymap-editor")
- .track_focus(&self.focus_handle)
- .px_4()
- .v_flex()
- .pb_4()
.child(
- h_flex()
- .key_context({
- let mut context = KeyContext::new_with_defaults();
- context.add("BufferSearchBar");
- context
- })
- .w_full()
- .h_12()
- .px_4()
- .my_4()
- .border_2()
- .border_color(theme.colors().border)
- .child(self.filter_editor.clone()),
+ v_flex()
+ .p_2()
+ .gap_2()
+ .child(
+ h_flex()
+ .gap_2()
+ .child(
+ div()
+ .key_context({
+ let mut context = KeyContext::new_with_defaults();
+ context.add("BufferSearchBar");
+ context
+ })
+ .size_full()
+ .h_8()
+ .pl_2()
+ .pr_1()
+ .py_1()
+ .border_1()
+ .border_color(theme.colors().border)
+ .rounded_lg()
+ .child(self.filter_editor.clone()),
+ )
+ .child(
+ IconButton::new(
+ "KeymapEditorToggleFiltersIcon",
+ IconName::Keyboard,
+ )
+ .shape(ui::IconButtonShape::Square)
+ .tooltip(|window, cx| {
+ Tooltip::for_action(
+ "Search by Keystroke",
+ &ToggleKeystrokeSearch,
+ window,
+ cx,
+ )
+ })
+ .toggle_state(matches!(self.search_mode, SearchMode::KeyStroke))
+ .on_click(|_, window, cx| {
+ window.dispatch_action(ToggleKeystrokeSearch.boxed_clone(), cx);
+ }),
+ )
+ .when(self.keybinding_conflict_state.any_conflicts(), |this| {
+ this.child(
+ IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
+ .shape(ui::IconButtonShape::Square)
+ .tooltip({
+ let filter_state = self.filter_state;
+
+ move |window, cx| {
+ Tooltip::for_action(
+ match filter_state {
+ FilterState::All => "Show Conflicts",
+ FilterState::Conflicts => "Hide Conflicts",
+ },
+ &ToggleConflictFilter,
+ window,
+ cx,
+ )
+ }
+ })
+ .selected_icon_color(Color::Warning)
+ .toggle_state(matches!(
+ self.filter_state,
+ FilterState::Conflicts
+ ))
+ .on_click(|_, window, cx| {
+ window.dispatch_action(
+ ToggleConflictFilter.boxed_clone(),
+ cx,
+ );
+ }),
+ )
+ }),
+ )
+ .when(matches!(self.search_mode, SearchMode::KeyStroke), |this| {
+ this.child(
+ div()
+ .map(|this| {
+ if self.keybinding_conflict_state.any_conflicts() {
+ this.pr(rems_from_px(54.))
+ } else {
+ this.pr_7()
+ }
+ })
+ .child(self.keystroke_editor.clone()),
+ )
+ }),
)
.child(
Table::new()
@@ -613,29 +1071,65 @@ impl Render for KeymapEditor {
.uniform_list(
"keymap-editor-table",
row_count,
- cx.processor(move |this, range: Range<usize>, _window, _cx| {
+ cx.processor(move |this, range: Range<usize>, _window, cx| {
+ let context_menu_deployed = this.context_menu_deployed();
range
.filter_map(|index| {
let candidate_id = this.matches.get(index)?.candidate_id;
let binding = &this.keybindings[candidate_id];
-
- let action = binding.action.clone().into_any_element();
+ let action_name = binding.action_name.clone();
+
+ let action = div()
+ .id(("keymap action", index))
+ .child(command_palette::humanize_action_name(&action_name))
+ .when(!context_menu_deployed, |this| {
+ this.tooltip({
+ let action_name = binding.action_name.clone();
+ let action_docs = binding.action_docs;
+ move |_, cx| {
+ let action_tooltip = Tooltip::new(&action_name);
+ let action_tooltip = match action_docs {
+ Some(docs) => action_tooltip.meta(docs),
+ None => action_tooltip,
+ };
+ cx.new(|_| action_tooltip).into()
+ }
+ })
+ })
+ .into_any_element();
let keystrokes = binding.ui_key_binding.clone().map_or(
binding.keystroke_text.clone().into_any_element(),
IntoElement::into_any_element,
);
- let action_input = binding
- .action_input
- .clone()
- .map_or(gpui::Empty.into_any_element(), |input| {
- input.into_any_element()
- });
- let context = binding
- .context
- .clone()
- .map_or(gpui::Empty.into_any_element(), |context| {
- context.into_any_element()
- });
+ let action_input = match binding.action_input.clone() {
+ Some(input) => input.into_any_element(),
+ None => {
+ if binding.action_schema.is_some() {
+ muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx)
+ .into_any_element()
+ } else {
+ gpui::Empty.into_any_element()
+ }
+ }
+ };
+ let context = binding.context.clone().map_or(
+ gpui::Empty.into_any_element(),
+ |context| {
+ let is_local = context.local().is_some();
+
+ div()
+ .id(("keymap context", index))
+ .child(context.clone())
+ .when(is_local && !context_menu_deployed, |this| {
+ this.tooltip(Tooltip::element({
+ move |_, _| {
+ context.clone().into_any_element()
+ }
+ }))
+ })
+ .into_any_element()
+ },
+ );
let source = binding
.source
.clone()
@@ -29,6 +29,7 @@ impl FeatureFlag for SettingsUiFeatureFlag {
const NAME: &'static str = "settings-ui";
}
+/// Imports settings from Visual Studio Code.
#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = zed)]
#[serde(deny_unknown_fields)]
@@ -37,6 +38,7 @@ pub struct ImportVsCodeSettings {
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)]
@@ -44,7 +46,13 @@ pub struct ImportCursorSettings {
#[serde(default)]
pub skip_prompt: bool,
}
-actions!(zed, [OpenSettingsEditor]);
+actions!(
+ zed,
+ [
+ /// Opens the settings editor.
+ OpenSettingsEditor
+ ]
+);
pub fn init(cx: &mut App) {
cx.on_action(|_: &OpenSettingsEditor, cx| {
@@ -2,9 +2,9 @@ use std::{ops::Range, rc::Rc, time::Duration};
use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide};
use gpui::{
- AppContext, Axis, Context, Entity, FocusHandle, FontWeight, Length,
- ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Task, UniformListScrollHandle,
- WeakEntity, transparent_black, uniform_list,
+ AppContext, Axis, Context, Entity, FocusHandle, Length, ListHorizontalSizingBehavior,
+ ListSizingBehavior, MouseButton, Task, UniformListScrollHandle, WeakEntity, transparent_black,
+ uniform_list,
};
use settings::Settings as _;
use ui::{
@@ -471,11 +471,9 @@ pub fn render_row<const COLS: usize>(
.map_or([None; COLS], |widths| widths.map(Some));
let row = div().w_full().child(
- div()
+ h_flex()
+ .id("table_row")
.w_full()
- .flex()
- .flex_row()
- .items_center()
.justify_between()
.px_1p5()
.py_1()
@@ -518,11 +516,12 @@ pub fn render_header<const COLS: usize>(
.p_2()
.border_b_1()
.border_color(cx.theme().colors().border)
- .children(headers.into_iter().zip(column_widths).map(|(h, width)| {
- base_cell_style(width, cx)
- .font_weight(FontWeight::SEMIBOLD)
- .child(h)
- }))
+ .children(
+ headers
+ .into_iter()
+ .zip(column_widths)
+ .map(|(h, width)| base_cell_style(width, cx).child(h)),
+ )
}
#[derive(Clone)]
@@ -3,6 +3,7 @@ use schemars::{JsonSchema, json_schema};
use serde::Deserialize;
use serde_json_lenient::Value;
use std::borrow::Cow;
+use util::schemars::DefaultDenyUnknownFields;
#[derive(Deserialize)]
pub struct VsSnippetsFile {
@@ -13,6 +14,7 @@ pub struct VsSnippetsFile {
impl VsSnippetsFile {
pub fn generate_json_schema() -> Value {
let schema = schemars::generate::SchemaSettings::draft2019_09()
+ .with_transform(DefaultDenyUnknownFields)
.into_generator()
.root_schema_for::<Self>();
@@ -54,7 +54,15 @@ impl From<ScopeFileName> for ScopeName {
}
}
-actions!(snippets, [ConfigureSnippets, OpenFolder]);
+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();
@@ -55,23 +55,27 @@ impl Render for IndentGuidesStory {
}),
)
.with_sizing_behavior(gpui::ListSizingBehavior::Infer)
- .with_decoration(ui::indent_guides(
- cx.entity().clone(),
- px(16.),
- ui::IndentGuideColors {
- default: Color::Info.color(cx),
- hover: Color::Accent.color(cx),
- active: Color::Accent.color(cx),
- },
- |this, range, _cx, _context| {
- this.depths
- .iter()
- .skip(range.start)
- .take(range.end - range.start)
- .cloned()
- .collect()
- },
- )),
+ .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().clone(),
+ |this, range, _cx, _context| {
+ this.depths
+ .iter()
+ .skip(range.start)
+ .take(range.end - range.start)
+ .cloned()
+ .collect()
+ },
+ ),
+ ),
),
)
}
@@ -25,7 +25,13 @@ use std::{path::PathBuf, process::Stdio, sync::Arc};
use ui::prelude::*;
use util::ResultExt;
-actions!(supermaven, [SignOut]);
+actions!(
+ supermaven,
+ [
+ /// Signs out of Supermaven.
+ SignOut
+ ]
+);
pub fn init(client: Arc<Client>, cx: &mut App) {
let supermaven = cx.new(|_| Supermaven::Starting);
@@ -5,7 +5,14 @@ pub mod svg_preview_view;
actions!(
svg,
- [OpenPreview, OpenPreviewToTheSide, OpenFollowingPreview]
+ [
+ /// 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) {
@@ -25,6 +25,7 @@ use workspace::{
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)]
@@ -32,7 +33,15 @@ pub struct Toggle {
#[serde(default)]
pub select_last: bool,
}
-actions!(tab_switcher, [CloseSelectedItem, ToggleAll]);
+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<Picker<TabSwitcherDelegate>>,
@@ -1,10 +1,8 @@
-use anyhow::Result;
use gpui::SharedString;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use serde_json::json;
-/// Represents a schema for a specific adapter
+/// JSON schema for a specific adapter
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct AdapterSchema {
/// The adapter name identifier
@@ -16,47 +14,3 @@ pub struct AdapterSchema {
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct AdapterSchemas(pub Vec<AdapterSchema>);
-
-impl AdapterSchemas {
- pub fn generate_json_schema(&self) -> Result<serde_json_lenient::Value> {
- let adapter_conditions = self
- .0
- .iter()
- .map(|adapter_schema| {
- let adapter_name = adapter_schema.adapter.to_string();
- json!({
- "if": {
- "properties": {
- "adapter": { "const": adapter_name }
- }
- },
- "then": adapter_schema.schema
- })
- })
- .collect::<Vec<_>>();
-
- let schema = serde_json_lenient::json!({
- "$schema": "http://json-schema.org/draft-07/schema#",
- "title": "Debug Adapter Configurations",
- "description": "Configuration for debug adapters. Schema changes based on the selected adapter.",
- "type": "array",
- "items": {
- "type": "object",
- "required": ["adapter", "label"],
- "properties": {
- "adapter": {
- "type": "string",
- "description": "The name of the debug adapter"
- },
- "label": {
- "type": "string",
- "description": "The name of the debug configuration"
- },
- },
- "allOf": adapter_conditions
- }
- });
-
- Ok(serde_json_lenient::to_value(schema)?)
- }
-}
@@ -6,7 +6,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::net::Ipv4Addr;
use std::path::PathBuf;
-use util::debug_panic;
+use util::{debug_panic, schemars::add_new_subschema};
use crate::{TaskTemplate, adapter_schema::AdapterSchemas};
@@ -286,11 +286,10 @@ pub struct DebugScenario {
pub struct DebugTaskFile(pub Vec<DebugScenario>);
impl DebugTaskFile {
- pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json_lenient::Value {
+ pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json::Value {
let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator();
- let build_task_schema = generator.root_schema_for::<BuildTaskDefinition>();
- let mut build_task_value =
- serde_json_lenient::to_value(&build_task_schema).unwrap_or_default();
+
+ let mut build_task_value = BuildTaskDefinition::json_schema(&mut generator).to_value();
if let Some(template_object) = build_task_value
.get_mut("anyOf")
@@ -301,7 +300,12 @@ impl DebugTaskFile {
.get_mut("properties")
.and_then(|value| value.as_object_mut())
{
- properties.remove("label");
+ if 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
@@ -311,38 +315,60 @@ impl DebugTaskFile {
arr.retain(|v| v.as_str() != Some("label"));
}
} else {
- debug_panic!("Task Template schema in debug scenario's needs to be updated");
+ debug_panic!(
+ "Generated TaskTemplate json schema did not match expectations. \
+ Schema is: {build_task_value:?}"
+ );
}
- let task_definitions = build_task_value
- .get("definitions")
- .cloned()
- .unwrap_or_default();
-
let adapter_conditions = schemas
.0
.iter()
.map(|adapter_schema| {
let adapter_name = adapter_schema.adapter.to_string();
- serde_json::json!({
- "if": {
- "properties": {
- "adapter": { "const": adapter_name }
- }
- },
- "then": adapter_schema.schema
- })
+ add_new_subschema(
+ &mut generator,
+ &format!("{adapter_name}DebugSettings"),
+ serde_json::json!({
+ "if": {
+ "properties": {
+ "adapter": { "const": adapter_name }
+ }
+ },
+ "then": adapter_schema.schema
+ }),
+ )
})
.collect::<Vec<_>>();
- serde_json_lenient::json!({
- "$schema": "http://json-schema.org/draft-07/schema#",
+ 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",
"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",
@@ -352,7 +378,7 @@ impl DebugTaskFile {
"type": "string",
"description": "The name of the debug configuration"
},
- "build": build_task_value,
+ "build": build_task_definition_ref,
"tcp_connection": {
"type": "object",
"description": "Optional TCP connection information for connecting to an already running debug adapter",
@@ -375,7 +401,7 @@ impl DebugTaskFile {
},
"allOf": adapter_conditions
},
- "definitions": task_definitions
+ "$defs": generator.take_definitions(true),
})
}
}
@@ -5,6 +5,7 @@ enum ShellKind {
#[default]
Posix,
Powershell,
+ Nushell,
Cmd,
}
@@ -18,6 +19,8 @@ impl ShellKind {
ShellKind::Powershell
} else if program == "cmd" || program.ends_with("cmd.exe") {
ShellKind::Cmd
+ } else if program == "nu" {
+ ShellKind::Nushell
} else {
// Someother shell detected, the user might install and use a
// unix-like shell.
@@ -30,6 +33,7 @@ impl ShellKind {
Self::Powershell => Self::to_powershell_variable(input),
Self::Cmd => Self::to_cmd_variable(input),
Self::Posix => input.to_owned(),
+ Self::Nushell => Self::to_nushell_variable(input),
}
}
@@ -70,11 +74,86 @@ impl ShellKind {
}
}
+ fn to_nushell_variable(input: &str) -> String {
+ let mut result = String::new();
+ let mut source = input;
+ let mut is_start = true;
+
+ loop {
+ match source.chars().next() {
+ None => return result,
+ Some('$') => {
+ source = Self::parse_nushell_var(&source[1..], &mut result, is_start);
+ is_start = false;
+ }
+ Some(_) => {
+ is_start = false;
+ let chunk_end = source.find('$').unwrap_or(source.len());
+ let (chunk, rest) = source.split_at(chunk_end);
+ result.push_str(chunk);
+ source = rest;
+ }
+ }
+ }
+ }
+
+ fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str {
+ if source.starts_with("env.") {
+ text.push('$');
+ return source;
+ }
+
+ match source.chars().next() {
+ Some('{') => {
+ let source = &source[1..];
+ if let Some(end) = source.find('}') {
+ let var_name = &source[..end];
+ if !var_name.is_empty() {
+ if !is_start {
+ text.push_str("(");
+ }
+ text.push_str("$env.");
+ text.push_str(var_name);
+ if !is_start {
+ text.push_str(")");
+ }
+ &source[end + 1..]
+ } else {
+ text.push_str("${}");
+ &source[end + 1..]
+ }
+ } else {
+ text.push_str("${");
+ source
+ }
+ }
+ Some(c) if c.is_alphabetic() || c == '_' => {
+ let end = source
+ .find(|c: char| !c.is_alphanumeric() && c != '_')
+ .unwrap_or(source.len());
+ let var_name = &source[..end];
+ if !is_start {
+ text.push_str("(");
+ }
+ text.push_str("$env.");
+ text.push_str(var_name);
+ if !is_start {
+ text.push_str(")");
+ }
+ &source[end..]
+ }
+ _ => {
+ text.push('$');
+ source
+ }
+ }
+ }
+
fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec<String> {
match self {
ShellKind::Powershell => vec!["-C".to_owned(), combined_command],
ShellKind::Cmd => vec!["/C".to_owned(), combined_command],
- ShellKind::Posix => interactive
+ ShellKind::Posix | ShellKind::Nushell => interactive
.then(|| "-i".to_owned())
.into_iter()
.chain(["-c".to_owned(), combined_command])
@@ -142,25 +221,67 @@ impl ShellBuilder {
ShellKind::Cmd => {
format!("{} /C '{}'", self.program, command_label)
}
- ShellKind::Posix => {
+ ShellKind::Posix | ShellKind::Nushell => {
let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
- format!("{} {interactivity}-c '{}'", self.program, command_label)
+ format!(
+ "{} {interactivity}-c '$\"{}\"'",
+ self.program, command_label
+ )
}
}
}
/// Returns the program and arguments to run this task in a shell.
- pub fn build(mut self, task_command: String, task_args: &Vec<String>) -> (String, Vec<String>) {
- let combined_command = task_args
- .into_iter()
- .fold(task_command, |mut command, arg| {
- command.push(' ');
- command.push_str(&self.kind.to_shell_variable(arg));
- command
- });
-
- self.args
- .extend(self.kind.args_for_shell(self.interactive, combined_command));
+ pub fn build(
+ mut self,
+ task_command: Option<String>,
+ task_args: &Vec<String>,
+ ) -> (String, Vec<String>) {
+ if let Some(task_command) = task_command {
+ let combined_command = task_args
+ .into_iter()
+ .fold(task_command, |mut command, arg| {
+ command.push(' ');
+ command.push_str(&self.kind.to_shell_variable(arg));
+ command
+ });
+
+ self.args
+ .extend(self.kind.args_for_shell(self.interactive, combined_command));
+ }
(self.program, self.args)
}
}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_nu_shell_variable_substitution() {
+ let shell = Shell::Program("nu".to_owned());
+ let shell_builder = ShellBuilder::new(true, &shell);
+
+ let (program, args) = shell_builder.build(
+ Some("echo".into()),
+ &vec![
+ "${hello}".to_string(),
+ "$world".to_string(),
+ "nothing".to_string(),
+ "--$something".to_string(),
+ "$".to_string(),
+ "${test".to_string(),
+ ],
+ );
+
+ assert_eq!(program, "nu");
+ assert_eq!(
+ args,
+ vec![
+ "-i",
+ "-c",
+ "echo $env.hello $env.world nothing --($env.something) $ ${test"
+ ]
+ );
+ }
+}
@@ -46,7 +46,7 @@ pub struct SpawnInTerminal {
/// Human readable name of the terminal tab.
pub label: String,
/// Executable command to spawn.
- pub command: String,
+ pub command: Option<String>,
/// Arguments to the command, potentially unsubstituted,
/// to let the shell that spawns the command to do the substitution, if needed.
pub args: Vec<String>,
@@ -4,6 +4,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::path::PathBuf;
+use util::schemars::DefaultDenyUnknownFields;
use util::serde::default_true;
use util::{ResultExt, truncate_and_remove_front};
@@ -116,6 +117,7 @@ impl TaskTemplates {
/// Generates JSON schema of Tasks JSON template format.
pub fn generate_json_schema() -> serde_json_lenient::Value {
let schema = schemars::generate::SchemaSettings::draft2019_09()
+ .with_transform(DefaultDenyUnknownFields)
.into_generator()
.root_schema_for::<Self>();
@@ -253,8 +255,8 @@ impl TaskTemplate {
command_label
},
),
- command,
- args: self.args.clone(),
+ command: Some(command),
+ args: args_with_substitutions,
env,
use_new_terminal: self.use_new_terminal,
allow_concurrent_runs: self.allow_concurrent_runs,
@@ -633,24 +635,24 @@ mod tests {
"Human-readable label should have long substitutions trimmed"
);
assert_eq!(
- spawn_in_terminal.command,
+ 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 $ZED_SELECTED_TEXT",
- "arg2 $ZED_COLUMN",
- "arg3 $ZED_SYMBOL",
+ "arg1 test_selected_text",
+ "arg2 5678",
+ "arg3 010101010101010101010101010101010101010101010101010101010101",
],
- "Args should not be substituted with variables"
+ "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
+ spawn_in_terminal.command.clone().unwrap()
),
"Command label args should be substituted with variables and those should not be shortened"
);
@@ -709,7 +711,7 @@ mod tests {
assert_substituted_variables(&resolved_task, Vec::new());
let resolved = resolved_task.resolved;
assert_eq!(resolved.label, task.label);
- assert_eq!(resolved.command, task.command);
+ assert_eq!(resolved.command, Some(task.command));
assert_eq!(resolved.args, task.args);
}
@@ -90,7 +90,7 @@ fn task_type_to_adapter_name(task_type: &str) -> String {
"pwa-node" | "node" | "node-terminal" | "chrome" | "pwa-chrome" | "edge" | "pwa-edge"
| "msedge" | "pwa-msedge" => "JavaScript",
"go" => "Delve",
- "php" => "PHP",
+ "php" => "Xdebug",
"cppdbg" | "lldb" => "CodeLLDB",
"debugpy" => "Debugpy",
"rdbg" => "rdbg",
@@ -47,7 +47,10 @@ impl VsCodeTaskDefinition {
replacer: &EnvVariableReplacer,
) -> anyhow::Result<Option<TaskTemplate>> {
if self.other_attributes.contains_key("dependsOn") {
- log::warn!("Skipping deserializing of a task with the unsupported `dependsOn` key");
+ 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),
@@ -56,7 +56,7 @@ pub fn to_esc_str(
("tab", AlacModifiers::None) => Some("\x09"),
("escape", AlacModifiers::None) => Some("\x1b"),
("enter", AlacModifiers::None) => Some("\x0d"),
- ("enter", AlacModifiers::Shift) => Some("\x0d"),
+ ("enter", AlacModifiers::Shift) => Some("\x0a"),
("enter", AlacModifiers::Alt) => Some("\x1b\x0d"),
("backspace", AlacModifiers::None) => Some("\x7f"),
//Interesting escape codes
@@ -406,6 +406,22 @@ mod test {
}
}
+ #[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
@@ -121,6 +121,10 @@ impl PtyProcessInfo {
}
}
+ pub(crate) fn kill_current_process(&mut self) -> bool {
+ self.refresh().map_or(false, |process| process.kill())
+ }
+
fn load(&mut self) -> Option<ProcessInfo> {
let process = self.refresh()?;
let cwd = process.cwd().map_or(PathBuf::new(), |p| p.to_owned());
@@ -58,7 +58,7 @@ use std::{
path::PathBuf,
process::ExitStatus,
sync::Arc,
- time::Duration,
+ time::{Duration, Instant},
};
use thiserror::Error;
@@ -73,18 +73,36 @@ 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,
]
);
@@ -144,7 +162,8 @@ enum InternalEvent {
UpdateSelection(Point<Pixels>),
// Adjusted mouse position, should open
FindHyperlink(Point<Pixels>, bool),
- Copy,
+ // Whether keep selection when copy
+ Copy(Option<bool>),
// Vi mode events
ToggleViMode,
ViMotion(ViMotion),
@@ -353,35 +372,48 @@ impl TerminalBuilder {
release_channel::AppVersion::global(cx).to_string(),
);
- let mut terminal_title_override = None;
+ #[derive(Default)]
+ struct ShellParams {
+ program: String,
+ args: Option<Vec<String>>,
+ title_override: Option<SharedString>,
+ }
- let pty_options = {
- let alac_shell = match shell.clone() {
- Shell::System => {
- #[cfg(target_os = "windows")]
- {
- Some(alacritty_terminal::tty::Shell::new(
- util::get_windows_system_shell(),
- Vec::new(),
- ))
- }
- #[cfg(not(target_os = "windows"))]
- {
- None
- }
- }
- Shell::Program(program) => {
- Some(alacritty_terminal::tty::Shell::new(program, Vec::new()))
- }
- Shell::WithArguments {
- program,
- args,
- title_override,
- } => {
- terminal_title_override = title_override;
- Some(alacritty_terminal::tty::Shell::new(program, args))
+ let shell_params = match shell.clone() {
+ Shell::System => {
+ #[cfg(target_os = "windows")]
+ {
+ Some(ShellParams {
+ program: util::get_windows_system_shell(),
+ ..Default::default()
+ })
}
- };
+ #[cfg(not(target_os = "windows"))]
+ None
+ }
+ Shell::Program(program) => Some(ShellParams {
+ program,
+ ..Default::default()
+ }),
+ Shell::WithArguments {
+ program,
+ args,
+ title_override,
+ } => Some(ShellParams {
+ program,
+ args: 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| params.program.clone());
+
+ let pty_options = {
+ let alac_shell = shell_params.map(|params| {
+ alacritty_terminal::tty::Shell::new(params.program, params.args.unwrap_or_default())
+ });
alacritty_terminal::tty::Options {
shell: alac_shell,
@@ -483,6 +515,10 @@ impl TerminalBuilder {
vi_mode_enabled: false,
is_ssh_terminal,
python_venv_directory,
+ last_mouse_move_time: Instant::now(),
+ last_hyperlink_search_position: None,
+ #[cfg(windows)]
+ shell_program,
};
Ok(TerminalBuilder {
@@ -641,6 +677,10 @@ pub struct Terminal {
task: Option<TaskState>,
vi_mode_enabled: bool,
is_ssh_terminal: bool,
+ last_mouse_move_time: Instant,
+ last_hyperlink_search_position: Option<Point<Pixels>>,
+ #[cfg(windows)]
+ shell_program: Option<String>,
}
pub struct TaskState {
@@ -686,6 +726,20 @@ impl Terminal {
fn process_event(&mut self, event: AlacTermEvent, cx: &mut Context<Self>) {
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);
}
@@ -878,9 +932,15 @@ impl Terminal {
}
}
- InternalEvent::Copy => {
+ InternalEvent::Copy(keep_selection) => {
if let Some(txt) = term.selection_to_string() {
- cx.write_to_clipboard(ClipboardItem::new_string(txt))
+ 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) => {
@@ -1049,8 +1109,8 @@ impl Terminal {
.push_back(InternalEvent::SetSelection(selection));
}
- pub fn copy(&mut self) {
- self.events.push_back(InternalEvent::Copy);
+ pub fn copy(&mut self, keep_selection: Option<bool>) {
+ self.events.push_back(InternalEvent::Copy(keep_selection));
}
pub fn clear(&mut self) {
@@ -1208,8 +1268,7 @@ impl Terminal {
}
"y" => {
- self.events.push_back(InternalEvent::Copy);
- self.events.push_back(InternalEvent::SetSelection(None));
+ self.copy(Some(false));
return;
}
@@ -1283,24 +1342,27 @@ impl Terminal {
fn make_content(term: &Term<ZedListener>, 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: content
- .display_iter
- //TODO: Add this once there's a way to retain empty lines
- // .filter(|ic| {
- // !ic.flags.contains(Flags::HIDDEN)
- // && !(ic.bg == Named(NamedColor::Background)
- // && ic.c == ' '
- // && !ic.flags.contains(Flags::INVERSE))
- // })
- .map(|ic| IndexedCell {
- point: ic.point,
- cell: ic.cell.clone(),
- })
- .collect::<Vec<IndexedCell>>(),
+ cells,
mode: content.mode,
display_offset: content.display_offset,
- selection_text: term.selection_to_string(),
+ selection_text,
selection: content.selection,
cursor: content.cursor,
cursor_char: term.grid()[content.cursor.point].c,
@@ -1433,10 +1495,26 @@ impl Terminal {
if self.selection_phase == SelectionPhase::Selecting {
self.last_content.last_hovered_word = None;
} else if self.last_content.terminal_bounds.bounds.contains(&position) {
- self.events.push_back(InternalEvent::FindHyperlink(
- position - self.last_content.terminal_bounds.bounds.origin,
- false,
- ));
+ // 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;
}
@@ -1575,7 +1653,7 @@ impl Terminal {
}
} else {
if e.button == MouseButton::Left && setting.copy_on_select {
- self.copy();
+ self.copy(Some(true));
}
//Hyperlinks
@@ -1746,6 +1824,14 @@ impl Terminal {
}
}
+ pub fn kill_active_task(&mut self) {
+ if let Some(task) = self.task() {
+ if task.status == TaskStatus::Running {
+ self.pty_info.kill_current_process();
+ }
+ }
+ }
+
pub fn task(&self) -> Option<&TaskState> {
self.task.as_ref()
}
@@ -40,6 +40,7 @@ pub struct TerminalSettings {
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,
@@ -48,6 +49,7 @@ pub struct TerminalSettings {
pub max_scroll_history_lines: Option<usize>,
pub toolbar: Toolbar,
pub scrollbar: ScrollbarSettings,
+ pub minimum_contrast: f32,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -93,12 +95,14 @@ pub enum VenvSettings {
/// to the current working directory. We recommend overriding this
/// in your project's settings, rather than globally.
activate_script: Option<ActivateScript>,
+ venv_name: Option<String>,
directories: Option<Vec<PathBuf>>,
},
}
pub struct VenvSettingsContent<'a> {
pub activate_script: ActivateScript,
+ pub venv_name: &'a str,
pub directories: &'a [PathBuf],
}
@@ -108,9 +112,11 @@ impl VenvSettings {
VenvSettings::Off => None,
VenvSettings::On {
activate_script,
+ venv_name,
directories,
} => Some(VenvSettingsContent {
activate_script: activate_script.unwrap_or(ActivateScript::Default),
+ venv_name: venv_name.as_deref().unwrap_or(""),
directories: directories.as_deref().unwrap_or(&[]),
}),
}
@@ -126,6 +132,7 @@ pub enum ActivateScript {
Fish,
Nushell,
PowerShell,
+ Pyenv,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -193,6 +200,10 @@ pub struct TerminalSettingsContent {
///
/// Default: false
pub copy_on_select: Option<bool>,
+ /// Whether to keep the text selection after copying it to the clipboard.
+ ///
+ /// Default: false
+ pub keep_selection_on_copy: Option<bool>,
/// Whether to show the terminal button in the status bar.
///
/// Default: true
@@ -224,6 +235,21 @@ pub struct TerminalSettingsContent {
pub toolbar: Option<ToolbarContent>,
/// Scrollbar-related settings
pub scrollbar: Option<ScrollbarSettingsContent>,
+ /// 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: 0 (no adjustment)
+ pub minimum_contrast: Option<f32>,
}
impl settings::Settings for TerminalSettings {
@@ -232,7 +258,18 @@ impl settings::Settings for TerminalSettings {
type FileContent = TerminalSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
- sources.json_merge()
+ let settings: Self = sources.json_merge()?;
+
+ // Validate minimum_contrast for APCA
+ if settings.minimum_contrast < 0.0 || settings.minimum_contrast > 106.0 {
+ anyhow::bail!(
+ "terminal.minimum_contrast must be between 0 and 106, but got {}. \
+ APCA values: 0 = no adjustment, 75 = recommended for body text, 106 = maximum contrast.",
+ settings.minimum_contrast
+ );
+ }
+
+ Ok(settings)
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
@@ -0,0 +1,474 @@
+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_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
+ );
+ }
+}
@@ -1,16 +1,18 @@
+use crate::color_contrast;
use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine};
use gpui::{
- AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, Element,
- ElementId, Entity, FocusHandle, Font, 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, WindowTextSystem, div, fill, point, px,
- relative, size,
+ 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::{
@@ -37,7 +39,7 @@ use crate::{BlockContext, BlockProperties, ContentMode, TerminalMode, TerminalVi
/// The information generated during layout that is necessary for painting.
pub struct LayoutState {
hitbox: Hitbox,
- cells: Vec<LayoutCell>,
+ batched_text_runs: Vec<BatchedTextRun>,
rects: Vec<LayoutRect>,
relative_highlighted_ranges: Vec<(RangeInclusive<AlacPoint>, Hsla)>,
cursor: Option<CursorLayout>,
@@ -75,37 +77,69 @@ impl DisplayCursor {
}
}
-#[derive(Debug, Default)]
-pub struct LayoutCell {
- pub point: AlacPoint<i32, i32>,
- text: gpui::ShapedLine,
+/// A batched text run that combines multiple adjacent cells with the same style
+#[derive(Debug)]
+pub struct BatchedTextRun {
+ pub start_point: AlacPoint<i32, i32>,
+ pub text: String,
+ pub cell_count: usize,
+ pub style: TextRun,
+ pub font_size: AbsoluteLength,
}
-impl LayoutCell {
- fn new(point: AlacPoint<i32, i32>, text: gpui::ShapedLine) -> LayoutCell {
- LayoutCell { point, text }
+impl BatchedTextRun {
+ fn new_from_char(
+ start_point: AlacPoint<i32, i32>,
+ 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.text.push(c);
+ self.cell_count += 1;
+ self.style.len += c.len_utf8();
}
pub fn paint(
&self,
origin: Point<Pixels>,
dimensions: &TerminalBounds,
- _visible_bounds: Bounds<Pixels>,
window: &mut Window,
cx: &mut App,
) {
- let pos = {
- let point = self.point;
+ 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,
+ );
- Point::new(
- (origin.x + point.column as f32 * dimensions.cell_width).floor(),
- origin.y + 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()),
+ &[self.style.clone()],
+ Some(dimensions.cell_width),
)
- };
-
- self.text
- .paint(pos, dimensions.line_height, window, cx)
- .ok();
+ .paint(pos, dimensions.line_height, window, cx);
}
}
@@ -125,14 +159,6 @@ impl LayoutRect {
}
}
- fn extend(&self) -> Self {
- LayoutRect {
- point: self.point,
- num_of_cells: self.num_of_cells + 1,
- color: self.color,
- }
- }
-
pub fn paint(&self, origin: Point<Pixels>, dimensions: &TerminalBounds, window: &mut Window) {
let position = {
let alac_point = self.point;
@@ -151,6 +177,87 @@ impl LayoutRect {
}
}
+/// 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<BackgroundRegion>) -> Vec<BackgroundRegion> {
+ 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 {
@@ -204,23 +311,37 @@ impl TerminalElement {
grid: impl Iterator<Item = IndexedCell>,
start_line_offset: i32,
text_style: &TextStyle,
- // terminal_theme: &TerminalStyle,
- text_system: &WindowTextSystem,
hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
- window: &Window,
+ minimum_contrast: f32,
cx: &App,
- ) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
+ ) -> (Vec<LayoutRect>, Vec<BatchedTextRun>) {
+ let start_time = Instant::now();
let theme = cx.theme();
- let mut cells = vec![];
- let mut rects = vec![];
- let mut cur_rect: Option<LayoutRect> = None;
- let mut cur_alac_color = None;
+ // 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<BackgroundRegion> = Vec::with_capacity(estimated_regions);
+ let mut current_batch: Option<BatchedTextRun> = 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;
@@ -228,85 +349,121 @@ impl TerminalElement {
mem::swap(&mut fg, &mut bg);
}
- //Expand background rect range
- {
- if matches!(bg, Named(NamedColor::Background)) {
- //Continue to next cell, resetting variables if necessary
- cur_alac_color = None;
- if let Some(rect) = cur_rect {
- rects.push(rect);
- cur_rect = None
+ // 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 {
- match cur_alac_color {
- Some(cur_color) => {
- if bg == cur_color {
- // `cur_rect` can be None if it was moved to the `rects` vec after wrapping around
- // from one line to the next. The variables are all set correctly but there is no current
- // rect, so we create one if necessary.
- cur_rect = cur_rect.map_or_else(
- || {
- Some(LayoutRect::new(
- AlacPoint::new(
- alac_line,
- cell.point.column.0 as i32,
- ),
- 1,
- convert_color(&bg, theme),
- ))
- },
- |rect| Some(rect.extend()),
- );
- } else {
- cur_alac_color = Some(bg);
- if cur_rect.is_some() {
- rects.push(cur_rect.take().unwrap());
- }
- cur_rect = Some(LayoutRect::new(
- AlacPoint::new(alac_line, cell.point.column.0 as i32),
- 1,
- convert_color(&bg, theme),
- ));
- }
- }
- None => {
- cur_alac_color = Some(bg);
- cur_rect = Some(LayoutRect::new(
- AlacPoint::new(alac_line, cell.point.column.0 as i32),
- 1,
- convert_color(&bg, theme),
- ));
- }
- }
+ 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 = cell.extra.is_some();
//Layout current cell text
{
if !is_blank(&cell) {
- let cell_text = cell.c.to_string();
- let cell_style =
- TerminalElement::cell_style(&cell, fg, theme, text_style, hyperlink);
-
- let layout_cell = text_system.shape_line(
- cell_text.into(),
- text_style.font_size.to_pixels(window.rem_size()),
- &[cell_style],
+ cell_count += 1;
+ let cell_style = TerminalElement::cell_style(
+ &cell,
+ fg,
+ bg,
+ theme,
+ text_style,
+ hyperlink,
+ minimum_contrast,
);
- cells.push(LayoutCell::new(
- AlacPoint::new(alac_line, cell.point.column.0 as i32),
- layout_cell,
- ))
+ let cell_point = AlacPoint::new(alac_line, cell.point.column.0 as i32);
+
+ // 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);
+ } else {
+ // Flush current batch and start new one
+ let old_batch = current_batch.take().unwrap();
+ batched_runs.push(old_batch);
+ current_batch = Some(BatchedTextRun::new_from_char(
+ cell_point,
+ cell.c,
+ cell_style,
+ text_style.font_size,
+ ));
+ }
+ } else {
+ // Start new batch
+ current_batch = Some(BatchedTextRun::new_from_char(
+ cell_point,
+ cell.c,
+ cell_style,
+ text_style.font_size,
+ ));
+ }
};
}
}
+ }
+
+ // Flush any remaining batch
+ if let Some(batch) = current_batch {
+ batched_runs.push(batch);
+ }
- if cur_rect.is_some() {
- rects.push(cur_rect.take().unwrap());
+ // 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,
+ ));
}
}
- (cells, rects)
+
+ 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
@@ -337,17 +494,48 @@ impl TerminalElement {
}
}
+ /// 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 // Additional Powerline separators: angles, flames, etc.
+ | 0xE0C0..=0xE0C8 // Powerline separators: pixelated triangles, curves
+ | 0xE0CC..=0xE0D4 // Powerline separators: rounded triangles, ice/lego style
+ )
+ }
+
/// 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::ansi::Color,
+ bg: terminal::alacritty_terminal::vte::ansi::Color,
colors: &Theme,
text_style: &TextStyle,
hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
+ 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 = color_contrast::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.
@@ -680,6 +868,7 @@ impl Element for TerminalElement {
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(),
@@ -695,7 +884,7 @@ impl Element for TerminalElement {
let font_features = terminal_settings
.font_features
.as_ref()
- .unwrap_or(&settings.buffer_font.features)
+ .unwrap_or(&FontFeatures::disable_ligatures())
.clone();
let font_weight = terminal_settings.font_weight.unwrap_or_default();
@@ -844,18 +1033,22 @@ impl Element for TerminalElement {
// 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 (cells, rects) = match content_mode {
- ContentMode::Scrollable => TerminalElement::layout_grid(
- cells.iter().cloned(),
- 0,
- &text_style,
- window.text_system(),
- last_hovered_word
- .as_ref()
- .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
- 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;
@@ -870,11 +1063,10 @@ impl Element for TerminalElement {
.cloned(),
*line_range.start(),
&text_style,
- window.text_system(),
last_hovered_word.as_ref().map(|last_hovered_word| {
(link_style, &last_hovered_word.word_match)
}),
- window,
+ minimum_contrast,
cx,
)
}
@@ -900,6 +1092,7 @@ impl Element for TerminalElement {
underline: Default::default(),
strikethrough: None,
}],
+ None,
)
};
@@ -962,7 +1155,7 @@ impl Element for TerminalElement {
LayoutState {
hitbox,
- cells,
+ batched_text_runs,
cursor,
background_color,
dimensions,
@@ -990,6 +1183,7 @@ impl Element for TerminalElement {
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;
@@ -1074,9 +1268,12 @@ impl Element for TerminalElement {
}
}
- for cell in &layout.cells {
- cell.paint(origin, &layout.dimensions, bounds, window, cx);
+ // 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 {
if !text_to_mark.is_empty() {
@@ -1100,6 +1297,7 @@ impl Element for TerminalElement {
underline: ime_style.underline,
strikethrough: None,
}],
+ None
);
shaped_line
.paint(ime_position, layout.dimensions.line_height, window, cx)
@@ -1121,6 +1319,14 @@ impl Element for TerminalElement {
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
+ );
},
);
});
@@ -1275,7 +1481,7 @@ pub fn is_blank(cell: &IndexedCell) -> bool {
return false;
}
- true
+ return true;
}
fn to_highlighted_range_lines(
@@ -1390,3 +1596,417 @@ pub fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme:
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::{AbsoluteLength, Hsla, font};
+
+ #[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
+
+ // 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 = color_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 = color_contrast::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 = color_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 = color_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 = color_contrast::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 = color_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 = color_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 = color_contrast::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 = color_contrast::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 = color_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(),
+ background_color: None,
+ underline: None,
+ strikethrough: None,
+ };
+
+ let style2 = TextRun {
+ len: 1,
+ font: font("Helvetica"),
+ color: Hsla::red(),
+ background_color: None,
+ underline: None,
+ strikethrough: None,
+ };
+
+ let style3 = TextRun {
+ len: 1,
+ font: font("Helvetica"),
+ color: Hsla::blue(), // Different color
+ background_color: None,
+ underline: None,
+ strikethrough: None,
+ };
+
+ let font_size = AbsoluteLength::Pixels(px(12.0));
+ let batch =
+ BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'a', style1.clone(), 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(),
+ background_color: None,
+ underline: None,
+ strikethrough: None,
+ };
+
+ 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(),
+ background_color: None,
+ underline: None,
+ strikethrough: None,
+ };
+
+ 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_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);
+ }
+}
@@ -46,7 +46,13 @@ use zed_actions::assistant::InlineAssist;
const TERMINAL_PANEL_KEY: &str = "TerminalPanel";
-actions!(terminal_panel, [ToggleFocus]);
+actions!(
+ terminal_panel,
+ [
+ /// Toggles focus on the terminal panel.
+ ToggleFocus
+ ]
+);
pub fn init(cx: &mut App) {
cx.observe_new(
@@ -499,7 +505,7 @@ impl TerminalPanel {
let task = SpawnInTerminal {
command_label,
- command,
+ command: Some(command),
args,
..task.clone()
};
@@ -1431,7 +1437,7 @@ impl Panel for TerminalPanel {
if (self.is_enabled(cx) || !self.has_no_terminals(cx))
&& TerminalSettings::get_global(cx).button
{
- Some(IconName::Terminal)
+ Some(IconName::TerminalAlt)
} else {
None
}
@@ -1,3 +1,4 @@
+mod color_contrast;
mod persistence;
pub mod terminal_element;
pub mod terminal_panel;
@@ -70,15 +71,23 @@ const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"];
#[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, [RerunTask]);
+actions!(
+ terminal,
+ [
+ /// Reruns the last executed task in the terminal.
+ RerunTask
+ ]
+);
pub fn init(cx: &mut App) {
assistant_slash_command::init(cx);
@@ -706,7 +715,7 @@ impl TerminalView {
///Attempt to paste the clipboard into the terminal
fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
- self.terminal.update(cx, |term, _| term.copy());
+ self.terminal.update(cx, |term, _| term.copy(None));
cx.notify();
}
@@ -815,6 +824,11 @@ impl TerminalView {
};
dispatch_context.set("mouse_format", format);
};
+
+ if self.terminal.read(cx).last_content.selection.is_some() {
+ dispatch_context.add("selection");
+ }
+
dispatch_context
}
@@ -12,9 +12,10 @@ use gpui::{
use refineable::Refineable;
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Serialize};
-use settings::{ParameterizedJsonSchema, Settings, SettingsSources, replace_subschema};
+use settings::{ParameterizedJsonSchema, Settings, SettingsSources};
use std::sync::Arc;
use util::ResultExt as _;
+use util::schemars::replace_subschema;
const MIN_FONT_SIZE: Pixels = px(6.0);
const MIN_LINE_HEIGHT: f32 = 1.0;
@@ -978,11 +979,10 @@ pub struct ThemeName(pub Arc<str>);
inventory::submit! {
ParameterizedJsonSchema {
add_and_get_ref: |generator, _params, cx| {
- let schema = json_schema!({
+ replace_subschema::<ThemeName>(generator, || json_schema!({
"type": "string",
"enum": ThemeRegistry::global(cx).list_names(),
- });
- replace_subschema::<ThemeName>(generator, schema)
+ }))
}
}
}
@@ -996,15 +996,14 @@ pub struct IconThemeName(pub Arc<str>);
inventory::submit! {
ParameterizedJsonSchema {
add_and_get_ref: |generator, _params, cx| {
- let schema = json_schema!({
+ replace_subschema::<IconThemeName>(generator, || json_schema!({
"type": "string",
"enum": ThemeRegistry::global(cx)
.list_icon_themes()
.into_iter()
.map(|icon_theme| icon_theme.name)
.collect::<Vec<SharedString>>(),
- });
- replace_subschema::<IconThemeName>(generator, schema)
+ }))
}
}
}
@@ -1018,11 +1017,12 @@ pub struct FontFamilyName(pub Arc<str>);
inventory::submit! {
ParameterizedJsonSchema {
add_and_get_ref: |generator, params, _cx| {
- let schema = json_schema!({
- "type": "string",
- "enum": params.font_names,
- });
- replace_subschema::<FontFamilyName>(generator, schema)
+ replace_subschema::<FontFamilyName>(generator, || {
+ json_schema!({
+ "type": "string",
+ "enum": params.font_names,
+ })
+ })
}
}
}
@@ -17,7 +17,13 @@ use zed_actions::{ExtensionCategoryFilter, Extensions};
use crate::icon_theme_selector::{IconThemeSelector, IconThemeSelectorDelegate};
-actions!(theme_selector, [Reload]);
+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| {
@@ -32,7 +32,7 @@ call.workspace = true
chrono.workspace = true
client.workspace = true
db.workspace = true
-gpui.workspace = true
+gpui = { workspace = true, features = ["screen-capture"] }
notifications.workspace = true
project.workspace = true
remote.workspace = true
@@ -1,4 +1,5 @@
use gpui::{Entity, OwnedMenu, OwnedMenuItem};
+use settings::Settings;
#[cfg(not(target_os = "macos"))]
use gpui::{Action, actions};
@@ -11,8 +12,18 @@ 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, [ActivateMenuRight, ActivateMenuLeft]);
+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)]
@@ -234,15 +245,21 @@ impl ApplicationMenu {
cx.defer_in(window, move |_, window, cx| next_handle.show(window, cx));
}
- pub fn all_menus_shown(&self) -> bool {
- self.entries.iter().any(|entry| entry.handle.is_deployed())
+ pub fn all_menus_shown(&self, cx: &mut Context<Self>) -> 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<Self>) -> impl IntoElement {
- let all_menus_shown = self.all_menus_shown();
+ let all_menus_shown = self.all_menus_shown(cx);
if let Some(pending_menu_open) = self.pending_menu_open.take() {
if let Some(entry) = self
@@ -11,7 +11,17 @@ use workspace::notifications::DetachAndPromptErr;
use crate::TitleBar;
-actions!(collab, [ToggleScreenSharing, ToggleMute, ToggleDeafen]);
+actions!(
+ collab,
+ [
+ /// Toggles screen sharing on or off.
+ ToggleScreenSharing,
+ /// Toggles microphone mute.
+ ToggleMute,
+ /// Toggles deafen mode (mute both microphone and speakers).
+ ToggleDeafen
+ ]
+);
fn toggle_screen_sharing(_: &ToggleScreenSharing, window: &mut Window, cx: &mut App) {
let call = ActiveCall::global(cx).read(cx);
@@ -51,7 +51,6 @@ impl OnboardingBanner {
}
fn dismiss(&mut self, cx: &mut Context<Self>) {
- telemetry::event!("Banner Dismissed", source = self.source);
persist_dismissed(&self.source, cx);
self.dismissed = true;
cx.notify();
@@ -144,7 +143,10 @@ impl Render for OnboardingBanner {
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| this.dismiss(cx)))
+ .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",
@@ -1,6 +1,6 @@
use gpui::{
- AnyElement, Context, Decorations, InteractiveElement, IntoElement, MouseButton, ParentElement,
- Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px,
+ AnyElement, Context, Decorations, Hsla, InteractiveElement, IntoElement, MouseButton,
+ ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px,
};
use smallvec::SmallVec;
use std::mem;
@@ -37,6 +37,18 @@ impl PlatformTitleBar {
px(32.)
}
+ pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> 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<T>(&mut self, children: T)
where
T: IntoIterator<Item = AnyElement>,
@@ -50,15 +62,7 @@ impl Render for PlatformTitleBar {
let supported_controls = window.window_controls();
let decorations = window.window_decorations();
let height = Self::height(window);
- let titlebar_color = 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
- };
+ let titlebar_color = self.title_bar_color(window, cx);
let close_action = Box::new(workspace::CloseWindow);
let children = mem::take(&mut self.children);
@@ -8,7 +8,10 @@ mod title_bar_settings;
#[cfg(feature = "stories")]
mod stories;
-use crate::{application_menu::ApplicationMenu, platform_title_bar::PlatformTitleBar};
+use crate::{
+ application_menu::{ApplicationMenu, show_menus},
+ platform_title_bar::PlatformTitleBar,
+};
#[cfg(not(target_os = "macos"))]
use crate::application_menu::{
@@ -47,7 +50,17 @@ const MAX_PROJECT_NAME_LENGTH: usize = 40;
const MAX_BRANCH_NAME_LENGTH: usize = 40;
const MAX_SHORT_SHA_LENGTH: usize = 8;
-actions!(collab, [ToggleUserMenu, ToggleProjectMenu, SwitchBranch]);
+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) {
TitleBarSettings::register(cx);
@@ -123,6 +136,8 @@ impl Render for TitleBar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let title_bar_settings = *TitleBarSettings::get_global(cx);
+ let show_menus = show_menus(cx);
+
let mut children = Vec::new();
children.push(
@@ -132,10 +147,14 @@ impl Render for TitleBar {
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(), |title_bar, menu| {
- render_project_items &= !menu.read(cx).all_menus_shown();
- title_bar.child(menu)
- })
+ .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| {
@@ -180,11 +199,39 @@ impl Render for TitleBar {
.into_any_element(),
);
- self.platform_titlebar.update(cx, |this, _| {
- this.set_children(children);
- });
+ if show_menus {
+ self.platform_titlebar.update(cx, |this, _| {
+ this.set_children(
+ self.application_menu
+ .clone()
+ .map(|menu| menu.into_any_element()),
+ );
+ });
- self.platform_titlebar.clone().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()
+ }
}
}
@@ -11,6 +11,7 @@ pub struct TitleBarSettings {
pub show_branch_name: bool,
pub show_project_items: bool,
pub show_sign_in: bool,
+ pub show_menus: bool,
}
#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
@@ -39,6 +40,10 @@ pub struct TitleBarSettingsContent {
///
/// Default: true
pub show_sign_in: Option<bool>,
+ /// Whether to show the menus in the title bar.
+ ///
+ /// Default: false
+ pub show_menus: Option<bool>,
}
impl Settings for TitleBarSettings {
@@ -15,7 +15,13 @@ use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
use util::ResultExt;
use workspace::{ModalView, Workspace};
-actions!(toolchain, [Select]);
+actions!(
+ toolchain,
+ [
+ /// Selects a toolchain for the current project.
+ Select
+ ]
+);
pub fn init(cx: &mut App) {
cx.observe_new(ToolchainSelector::register).detach();
@@ -34,6 +34,9 @@ workspace-hack.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true
+[dev-dependencies]
+gpui = { workspace = true, features = ["test-support"] }
+
[features]
default = []
stories = ["dep:story"]
@@ -30,6 +30,7 @@ mod scrollbar;
mod settings_container;
mod settings_group;
mod stack;
+mod sticky_items;
mod tab;
mod tab_bar;
mod toggle;
@@ -70,6 +71,7 @@ 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 toggle::*;
@@ -24,6 +24,7 @@ pub enum ContextMenuItem {
entry_render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>,
selectable: bool,
+ documentation_aside: Option<DocumentationAside>,
},
}
@@ -31,11 +32,13 @@ 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<DocumentationAside>,
) -> Self {
Self::CustomEntry {
entry_render: Box::new(entry_render),
handler: Rc::new(move |_, window, cx| handler(window, cx)),
selectable: true,
+ documentation_aside,
}
}
}
@@ -156,6 +159,7 @@ pub struct ContextMenu {
keep_open_on_confirm: bool,
documentation_aside: Option<(usize, DocumentationAside)>,
fixed_width: Option<DefiniteLength>,
+ align_popover_top: bool,
}
#[derive(Copy, Clone, PartialEq, Eq)]
@@ -170,6 +174,12 @@ pub struct DocumentationAside {
render: Rc<dyn Fn(&mut App) -> AnyElement>,
}
+impl DocumentationAside {
+ pub fn new(side: DocumentationSide, render: Rc<dyn Fn(&mut App) -> AnyElement>) -> Self {
+ Self { side, render }
+ }
+}
+
impl Focusable for ContextMenu {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
@@ -206,6 +216,7 @@ impl ContextMenu {
key_context: "menu".into(),
_on_blur_subscription,
keep_open_on_confirm: false,
+ align_popover_top: true,
documentation_aside: None,
fixed_width: None,
end_slot_action: None,
@@ -248,6 +259,7 @@ impl ContextMenu {
key_context: "menu".into(),
_on_blur_subscription,
keep_open_on_confirm: true,
+ align_popover_top: true,
documentation_aside: None,
fixed_width: None,
end_slot_action: None,
@@ -288,6 +300,7 @@ impl ContextMenu {
|this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
),
keep_open_on_confirm: false,
+ align_popover_top: true,
documentation_aside: None,
fixed_width: None,
end_slot_action: None,
@@ -456,6 +469,7 @@ impl ContextMenu {
entry_render: Box::new(entry_render),
handler: Rc::new(|_, _, _| {}),
selectable: false,
+ documentation_aside: None,
});
self
}
@@ -469,6 +483,7 @@ impl ContextMenu {
entry_render: Box::new(entry_render),
handler: Rc::new(move |_, window, cx| handler(window, cx)),
selectable: true,
+ documentation_aside: None,
});
self
}
@@ -653,7 +668,7 @@ impl ContextMenu {
}
}
- fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
+ pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
if let Some(ix) = self.selected_index {
let next_index = ix + 1;
if self.items.len() <= next_index {
@@ -679,20 +694,15 @@ impl ContextMenu {
cx: &mut Context<Self>,
) {
if let Some(ix) = self.selected_index {
- if ix == 0 {
- self.handle_select_last(&SelectLast, window, cx);
- } else {
- for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
- if item.is_selectable() {
- self.select_index(ix, window, cx);
- cx.notify();
- break;
- }
+ for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
+ if item.is_selectable() {
+ self.select_index(ix, window, cx);
+ cx.notify();
+ return;
}
}
- } else {
- self.handle_select_last(&SelectLast, window, cx);
}
+ self.handle_select_last(&SelectLast, window, cx);
}
fn select_index(
@@ -705,10 +715,19 @@ impl ContextMenu {
let item = self.items.get(ix)?;
if item.is_selectable() {
self.selected_index = Some(ix);
- if let ContextMenuItem::Entry(entry) = item {
- if let Some(callback) = &entry.documentation_aside {
+ 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)
@@ -763,6 +782,11 @@ impl ContextMenu {
self
}
+ pub fn align_popover_bottom(mut self) -> Self {
+ self.align_popover_top = false;
+ self
+ }
+
fn render_menu_item(
&self,
ix: usize,
@@ -806,6 +830,7 @@ impl ContextMenu {
entry_render,
handler,
selectable,
+ ..
} => {
let handler = handler.clone();
let menu = cx.entity().downgrade();
@@ -1084,7 +1109,13 @@ impl Render for ContextMenu {
.when(is_wide_window, |this| this.flex_row())
.when(!is_wide_window, |this| this.flex_col())
.w_full()
- .items_start()
+ .map(|div| {
+ if self.align_popover_top {
+ div.items_start()
+ } else {
+ div.items_end()
+ }
+ })
.gap_1()
.child(div().children(aside.clone().and_then(|(_, aside)| {
(aside.side == DocumentationSide::Left).then(|| render_aside(aside, cx))
@@ -1150,3 +1181,75 @@ impl Render for ContextMenu {
})))
}
}
+
+#[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, |_, _| {})
+ })
+ });
+
+ 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)"
+ );
+ });
+ }
+}
@@ -1,8 +1,7 @@
use std::{cmp::Ordering, ops::Range, rc::Rc};
-use gpui::{
- AnyElement, App, Bounds, Entity, Hsla, Point, UniformListDecoration, fill, point, size,
-};
+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::*;
@@ -32,7 +31,8 @@ impl IndentGuideColors {
pub struct IndentGuides {
colors: IndentGuideColors,
indent_size: Pixels,
- compute_indents_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> SmallVec<[usize; 64]>>,
+ compute_indents_fn:
+ Option<Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> SmallVec<[usize; 64]>>>,
render_fn: Option<
Box<
dyn Fn(
@@ -45,25 +45,11 @@ pub struct IndentGuides {
on_click: Option<Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>>,
}
-pub fn indent_guides<V: Render>(
- entity: Entity<V>,
- indent_size: Pixels,
- colors: IndentGuideColors,
- compute_indents_fn: impl Fn(
- &mut V,
- Range<usize>,
- &mut Window,
- &mut Context<V>,
- ) -> SmallVec<[usize; 64]>
- + 'static,
-) -> IndentGuides {
- 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))
- });
+pub fn indent_guides(indent_size: Pixels, colors: IndentGuideColors) -> IndentGuides {
IndentGuides {
colors,
indent_size,
- compute_indents_fn,
+ compute_indents_fn: None,
render_fn: None,
on_click: None,
}
@@ -79,6 +65,25 @@ impl IndentGuides {
self
}
+ /// Sets the function that computes indents for uniform list decoration.
+ pub fn with_compute_indents_fn<V: Render>(
+ mut self,
+ entity: Entity<V>,
+ compute_indents_fn: impl Fn(
+ &mut V,
+ Range<usize>,
+ &mut Window,
+ &mut Context<V>,
+ ) -> 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<V: Render>(
mut self,
@@ -97,6 +102,53 @@ impl IndentGuides {
self.render_fn = Some(Box::new(render_fn));
self
}
+
+ fn render_from_layout(
+ &self,
+ indent_guides: SmallVec<[IndentGuideLayout; 12]>,
+ bounds: Bounds<Pixels>,
+ 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.
@@ -136,9 +188,7 @@ pub struct IndentGuideLayout {
/// Implements the necessary functionality for rendering indent guides inside a uniform list.
mod uniform_list {
- use gpui::{
- DispatchPhase, Hitbox, HitboxBehavior, MouseButton, MouseDownEvent, MouseMoveEvent,
- };
+ use gpui::UniformListDecoration;
use super::*;
@@ -147,6 +197,7 @@ mod uniform_list {
&self,
visible_range: Range<usize>,
bounds: Bounds<Pixels>,
+ _scroll_offset: Point<Pixels>,
item_height: Pixels,
item_count: usize,
window: &mut Window,
@@ -160,227 +211,212 @@ mod uniform_list {
if includes_trailing_indent {
visible_range.end += 1;
}
- let visible_entries = &(self.compute_indents_fn)(visible_range.clone(), window, cx);
+ 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,
);
- 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()
+ self.render_from_layout(indent_guides, bounds, item_height, window, cx)
}
}
+}
- struct IndentGuidesElement {
- colors: IndentGuideColors,
- indent_guides: Rc<SmallVec<[RenderedIndentGuide; 12]>>,
- on_hovered_indent_guide_click:
- Option<Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>>,
- }
+/// Implements the necessary functionality for rendering indent guides inside a sticky items.
+mod sticky_items {
+ use crate::StickyItemsDecoration;
- enum IndentGuidesElementPrepaintState {
- Static,
- Interactive {
- hitboxes: Rc<SmallVec<[Hitbox; 12]>>,
- on_hovered_indent_guide_click: Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>,
- },
+ use super::*;
+
+ impl StickyItemsDecoration for IndentGuides {
+ fn compute(
+ &self,
+ indents: &SmallVec<[usize; 8]>,
+ bounds: Bounds<Pixels>,
+ _scroll_offset: Point<Pixels>,
+ 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)
+ }
}
+}
- impl Element for IndentGuidesElement {
- type RequestLayoutState = ();
- type PrepaintState = IndentGuidesElementPrepaintState;
+struct IndentGuidesElement {
+ colors: IndentGuideColors,
+ indent_guides: Rc<SmallVec<[RenderedIndentGuide; 12]>>,
+ on_hovered_indent_guide_click: Option<Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>>,
+}
- fn id(&self) -> Option<ElementId> {
- None
- }
+enum IndentGuidesElementPrepaintState {
+ Static,
+ Interactive {
+ hitboxes: Rc<SmallVec<[Hitbox; 12]>>,
+ on_hovered_indent_guide_click: Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>,
+ },
+}
- fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
- None
- }
+impl Element for IndentGuidesElement {
+ type RequestLayoutState = ();
+ type PrepaintState = IndentGuidesElementPrepaintState;
- 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 id(&self) -> Option<ElementId> {
+ None
+ }
- fn prepaint(
- &mut self,
- _id: Option<&gpui::GlobalElementId>,
- _inspector_id: Option<&gpui::InspectorElementId>,
- _bounds: Bounds<Pixels>,
- _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 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<Pixels>,
+ _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<Pixels>,
- _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));
- }
+ fn paint(
+ &mut self,
+ _id: Option<&gpui::GlobalElementId>,
+ _inspector_id: Option<&gpui::InspectorElementId>,
+ _bounds: Bounds<Pixels>,
+ _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;
- }
+ }
+ 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 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);
+ 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();
- }
+ 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));
}
+ });
+ 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;
- }
+ 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)
- }
+ }
+ 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) => {}
}
+ (None, Some(_)) => cx.notify(current_view),
+ (Some(_), None) => cx.notify(current_view),
+ (None, None) => {}
}
}
- });
- }
+ }
+ });
}
}
}
+}
- impl IntoElement for IndentGuidesElement {
- type Element = Self;
+impl IntoElement for IndentGuidesElement {
+ type Element = Self;
- fn into_element(self) -> Self::Element {
- self
- }
+ fn into_element(self) -> Self::Element {
+ self
}
}
@@ -13,7 +13,7 @@ pub struct KeyBinding {
/// More than one keystroke produces a chord.
///
/// This should always contain at least one keystroke.
- pub key_binding: gpui::KeyBinding,
+ pub keystrokes: Vec<Keystroke>,
/// The [`PlatformStyle`] to use when displaying this keybinding.
platform_style: PlatformStyle,
@@ -37,7 +37,7 @@ impl KeyBinding {
return Self::for_action_in(action, &focused, window, cx);
}
let key_binding = window.highest_precedence_binding_for_action(action)?;
- Some(Self::new(key_binding, cx))
+ Some(Self::new_from_gpui(key_binding, cx))
}
/// Like `for_action`, but lets you specify the context from which keybindings are matched.
@@ -48,7 +48,7 @@ impl KeyBinding {
cx: &App,
) -> Option<Self> {
let key_binding = window.highest_precedence_binding_for_action_in(action, focus)?;
- Some(Self::new(key_binding, cx))
+ Some(Self::new_from_gpui(key_binding, cx))
}
pub fn set_vim_mode(cx: &mut App, enabled: bool) {
@@ -59,9 +59,9 @@ impl KeyBinding {
cx.try_global::<VimStyle>().is_some_and(|g| g.0)
}
- pub fn new(key_binding: gpui::KeyBinding, cx: &App) -> Self {
+ pub fn new(keystrokes: Vec<Keystroke>, cx: &App) -> Self {
Self {
- key_binding,
+ keystrokes,
platform_style: PlatformStyle::platform(),
size: None,
vim_mode: KeyBinding::is_vim_mode(cx),
@@ -69,6 +69,10 @@ impl KeyBinding {
}
}
+ pub fn new_from_gpui(key_binding: gpui::KeyBinding, cx: &App) -> Self {
+ Self::new(key_binding.keystrokes().to_vec(), cx)
+ }
+
/// Sets the [`PlatformStyle`] for this [`KeyBinding`].
pub fn platform_style(mut self, platform_style: PlatformStyle) -> Self {
self.platform_style = platform_style;
@@ -92,15 +96,20 @@ impl KeyBinding {
self.vim_mode = enabled;
self
}
+}
- fn render_key(&self, keystroke: &Keystroke, color: Option<Color>) -> AnyElement {
- let key_icon = icon_for_key(keystroke, self.platform_style);
- match key_icon {
- Some(icon) => KeyIcon::new(icon, color).size(self.size).into_any_element(),
- None => {
- let key = util::capitalize(&keystroke.key);
- Key::new(&key, color).size(self.size).into_any_element()
- }
+fn render_key(
+ keystroke: &Keystroke,
+ color: Option<Color>,
+ platform_style: PlatformStyle,
+ size: impl Into<Option<AbsoluteLength>>,
+) -> AnyElement {
+ let key_icon = icon_for_key(keystroke, platform_style);
+ match key_icon {
+ Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
+ None => {
+ let key = util::capitalize(&keystroke.key);
+ Key::new(&key, color).size(size).into_any_element()
}
}
}
@@ -108,17 +117,12 @@ impl KeyBinding {
impl RenderOnce for KeyBinding {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let color = self.disabled.then_some(Color::Disabled);
- let use_text = self.vim_mode
- || matches!(
- self.platform_style,
- PlatformStyle::Linux | PlatformStyle::Windows
- );
+
h_flex()
.debug_selector(|| {
format!(
"KEY_BINDING-{}",
- self.key_binding
- .keystrokes()
+ self.keystrokes
.iter()
.map(|k| k.key.to_string())
.collect::<Vec<_>>()
@@ -127,35 +131,56 @@ impl RenderOnce for KeyBinding {
})
.gap(DynamicSpacing::Base04.rems(cx))
.flex_none()
- .children(self.key_binding.keystrokes().iter().map(|keystroke| {
+ .children(self.keystrokes.iter().map(|keystroke| {
h_flex()
.flex_none()
.py_0p5()
.rounded_xs()
.text_color(cx.theme().colors().text_muted)
- .when(use_text, |el| {
- el.child(
- Key::new(
- keystroke_text(&keystroke, self.platform_style, self.vim_mode),
- color,
- )
- .size(self.size),
- )
- })
- .when(!use_text, |el| {
- el.children(render_modifiers(
- &keystroke.modifiers,
- self.platform_style,
- color,
- self.size,
- true,
- ))
- .map(|el| el.child(self.render_key(&keystroke, color)))
- })
+ .children(render_keystroke(
+ keystroke,
+ color,
+ self.size,
+ self.platform_style,
+ self.vim_mode,
+ ))
}))
}
}
+pub fn render_keystroke(
+ keystroke: &Keystroke,
+ color: Option<Color>,
+ size: impl Into<Option<AbsoluteLength>>,
+ platform_style: PlatformStyle,
+ vim_mode: bool,
+) -> Vec<AnyElement> {
+ 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, 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, color, platform_style, size));
+ elements
+ }
+}
+
fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> {
match keystroke.key.as_str() {
"left" => Some(IconName::ArrowLeft),
@@ -466,7 +491,7 @@ impl Component for KeyBinding {
vec![
single_example(
"Default",
- KeyBinding::new(
+ KeyBinding::new_from_gpui(
gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None),
cx,
)
@@ -474,7 +499,7 @@ impl Component for KeyBinding {
),
single_example(
"Mac Style",
- KeyBinding::new(
+ KeyBinding::new_from_gpui(
gpui::KeyBinding::new("cmd-s", gpui::NoAction, None),
cx,
)
@@ -483,7 +508,7 @@ impl Component for KeyBinding {
),
single_example(
"Windows Style",
- KeyBinding::new(
+ KeyBinding::new_from_gpui(
gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None),
cx,
)
@@ -496,9 +521,12 @@ impl Component for KeyBinding {
"Vim Mode",
vec![single_example(
"Vim Mode Enabled",
- KeyBinding::new(gpui::KeyBinding::new("dd", gpui::NoAction, None), cx)
- .vim_mode(true)
- .into_any_element(),
+ KeyBinding::new_from_gpui(
+ gpui::KeyBinding::new("dd", gpui::NoAction, None),
+ cx,
+ )
+ .vim_mode(true)
+ .into_any_element(),
)],
),
example_group_with_title(
@@ -506,7 +534,7 @@ impl Component for KeyBinding {
vec![
single_example(
"Multiple Keys",
- KeyBinding::new(
+ KeyBinding::new_from_gpui(
gpui::KeyBinding::new("ctrl-k ctrl-b", gpui::NoAction, None),
cx,
)
@@ -514,7 +542,7 @@ impl Component for KeyBinding {
),
single_example(
"With Shift",
- KeyBinding::new(
+ KeyBinding::new_from_gpui(
gpui::KeyBinding::new("shift-cmd-p", gpui::NoAction, None),
cx,
)
@@ -216,7 +216,7 @@ impl Component for KeybindingHint {
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
let enter = KeyBinding::for_action(&menu::Confirm, window, cx)
- .unwrap_or(KeyBinding::new(enter_fallback, cx));
+ .unwrap_or(KeyBinding::new_from_gpui(enter_fallback, cx));
let bg_color = cx.theme().colors().surface_background;
@@ -1,7 +1,9 @@
mod highlighted_label;
mod label;
mod label_like;
+mod loading_label;
pub use highlighted_label::*;
pub use label::*;
pub use label_like::*;
+pub use loading_label::*;
@@ -1,24 +1,24 @@
+use crate::prelude::*;
use gpui::{Animation, AnimationExt, FontWeight, pulsating_between};
use std::time::Duration;
-use ui::prelude::*;
#[derive(IntoElement)]
-pub struct AnimatedLabel {
+pub struct LoadingLabel {
base: Label,
text: SharedString,
}
-impl AnimatedLabel {
+impl LoadingLabel {
pub fn new(text: impl Into<SharedString>) -> Self {
let text = text.into();
- AnimatedLabel {
+ LoadingLabel {
base: Label::new(text.clone()),
text,
}
}
}
-impl LabelCommon for AnimatedLabel {
+impl LabelCommon for LoadingLabel {
fn size(mut self, size: LabelSize) -> Self {
self.base = self.base.size(size);
self
@@ -80,14 +80,14 @@ impl LabelCommon for AnimatedLabel {
}
}
-impl RenderOnce for AnimatedLabel {
+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(
- "animated-label",
+ "loading_label",
vec![
Animation::new(Duration::from_secs(1)),
Animation::new(Duration::from_secs(1)).repeat(),
@@ -105,6 +105,24 @@ impl<M: ManagedView> PopoverMenuHandle<M> {
.map_or(false, |model| model.focus_handle(cx).is_focused(window))
})
}
+
+ pub fn refresh_menu(
+ &self,
+ window: &mut Window,
+ cx: &mut App,
+ new_menu_builder: Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>>>,
+ ) {
+ 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<M: ManagedView> {
@@ -69,8 +69,7 @@ impl RenderOnce for ProgressBar {
.w_full()
.h(px(8.0))
.rounded_full()
- .py(px(2.0))
- .px(px(4.0))
+ .p(px(2.0))
.bg(self.bg_color)
.shadow(vec![gpui::BoxShadow {
color: gpui::black().opacity(0.08),
@@ -0,0 +1,336 @@
+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<T> {
+ compute_fn: Rc<dyn Fn(Range<usize>, &mut Window, &mut App) -> SmallVec<[T; 8]>>,
+ render_fn: Rc<dyn Fn(T, &mut Window, &mut App) -> SmallVec<[AnyElement; 8]>>,
+ decorations: Vec<Box<dyn StickyItemsDecoration>>,
+}
+
+pub fn sticky_items<V, T>(
+ entity: Entity<V>,
+ compute_fn: impl Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> SmallVec<[T; 8]>
+ + 'static,
+ render_fn: impl Fn(&mut V, T, &mut Window, &mut Context<V>) -> SmallVec<[AnyElement; 8]> + 'static,
+) -> StickyItems<T>
+where
+ V: Render,
+ T: StickyCandidate + Clone + 'static,
+{
+ let entity_compute = entity.clone();
+ let entity_render = entity.clone();
+
+ let compute_fn = Rc::new(
+ move |range: Range<usize>, 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<T> StickyItems<T>
+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<AnyElement>,
+ drifting_decoration: Option<AnyElement>,
+ 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<ElementId> {
+ 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<Pixels>,
+ _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<Pixels>,
+ _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<T> UniformListDecoration for StickyItems<T>
+where
+ T: StickyCandidate + Clone + 'static,
+{
+ fn compute(
+ &self,
+ visible_range: Range<usize>,
+ bounds: Bounds<Pixels>,
+ scroll_offset: Point<Pixels>,
+ 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<T> {
+ entry: T,
+ index: usize,
+ drifting: bool,
+}
+
+fn find_sticky_anchor<T: StickyCandidate + Clone>(
+ entries: &SmallVec<[T; 8]>,
+ visible_range_start: usize,
+) -> Option<StickyAnchor<T>> {
+ 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<Pixels>,
+ scroll_offset: Point<Pixels>,
+ item_height: Pixels,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyElement;
+}
@@ -18,16 +18,16 @@ impl Render for KeybindingStory {
Story::container(cx)
.child(Story::title_for::<KeyBinding>(cx))
.child(Story::label("Single Key", cx))
- .child(KeyBinding::new(binding("Z"), cx))
+ .child(KeyBinding::new_from_gpui(binding("Z"), cx))
.child(Story::label("Single Key with Modifier", cx))
.child(
div()
.flex()
.gap_3()
- .child(KeyBinding::new(binding("ctrl-c"), cx))
- .child(KeyBinding::new(binding("alt-c"), cx))
- .child(KeyBinding::new(binding("cmd-c"), cx))
- .child(KeyBinding::new(binding("shift-c"), cx)),
+ .child(KeyBinding::new_from_gpui(binding("ctrl-c"), cx))
+ .child(KeyBinding::new_from_gpui(binding("alt-c"), cx))
+ .child(KeyBinding::new_from_gpui(binding("cmd-c"), cx))
+ .child(KeyBinding::new_from_gpui(binding("shift-c"), cx)),
)
.child(Story::label("Single Key with Modifier (Permuted)", cx))
.child(
@@ -41,42 +41,59 @@ impl Render for KeybindingStory {
.gap_4()
.py_3()
.children(chunk.map(|permutation| {
- KeyBinding::new(binding(&(permutation.join("-") + "-x")), cx)
+ KeyBinding::new_from_gpui(
+ binding(&(permutation.join("-") + "-x")),
+ cx,
+ )
}))
}),
),
)
.child(Story::label("Single Key with All Modifiers", cx))
- .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z"), cx))
+ .child(KeyBinding::new_from_gpui(
+ binding("ctrl-alt-cmd-shift-z"),
+ cx,
+ ))
.child(Story::label("Chord", cx))
- .child(KeyBinding::new(binding("a z"), cx))
+ .child(KeyBinding::new_from_gpui(binding("a z"), cx))
.child(Story::label("Chord with Modifier", cx))
- .child(KeyBinding::new(binding("ctrl-a shift-z"), cx))
- .child(KeyBinding::new(binding("fn-s"), cx))
+ .child(KeyBinding::new_from_gpui(binding("ctrl-a shift-z"), cx))
+ .child(KeyBinding::new_from_gpui(binding("fn-s"), cx))
.child(Story::label("Single Key with All Modifiers (Linux)", cx))
.child(
- KeyBinding::new(binding("ctrl-alt-cmd-shift-z"), cx)
+ KeyBinding::new_from_gpui(binding("ctrl-alt-cmd-shift-z"), cx)
.platform_style(PlatformStyle::Linux),
)
.child(Story::label("Chord (Linux)", cx))
- .child(KeyBinding::new(binding("a z"), cx).platform_style(PlatformStyle::Linux))
+ .child(
+ KeyBinding::new_from_gpui(binding("a z"), cx).platform_style(PlatformStyle::Linux),
+ )
.child(Story::label("Chord with Modifier (Linux)", cx))
.child(
- KeyBinding::new(binding("ctrl-a shift-z"), cx).platform_style(PlatformStyle::Linux),
+ KeyBinding::new_from_gpui(binding("ctrl-a shift-z"), cx)
+ .platform_style(PlatformStyle::Linux),
+ )
+ .child(
+ KeyBinding::new_from_gpui(binding("fn-s"), cx).platform_style(PlatformStyle::Linux),
)
- .child(KeyBinding::new(binding("fn-s"), cx).platform_style(PlatformStyle::Linux))
.child(Story::label("Single Key with All Modifiers (Windows)", cx))
.child(
- KeyBinding::new(binding("ctrl-alt-cmd-shift-z"), cx)
+ KeyBinding::new_from_gpui(binding("ctrl-alt-cmd-shift-z"), cx)
.platform_style(PlatformStyle::Windows),
)
.child(Story::label("Chord (Windows)", cx))
- .child(KeyBinding::new(binding("a z"), cx).platform_style(PlatformStyle::Windows))
+ .child(
+ KeyBinding::new_from_gpui(binding("a z"), cx)
+ .platform_style(PlatformStyle::Windows),
+ )
.child(Story::label("Chord with Modifier (Windows)", cx))
.child(
- KeyBinding::new(binding("ctrl-a shift-z"), cx)
+ KeyBinding::new_from_gpui(binding("ctrl-a shift-z"), cx)
+ .platform_style(PlatformStyle::Windows),
+ )
+ .child(
+ KeyBinding::new_from_gpui(binding("fn-s"), cx)
.platform_style(PlatformStyle::Windows),
)
- .child(KeyBinding::new(binding("fn-s"), cx).platform_style(PlatformStyle::Windows))
}
}
@@ -1,5 +1,6 @@
use gpui::{
- AnyElement, AnyView, ElementId, Hsla, IntoElement, Styled, Window, div, hsla, prelude::*,
+ AnyElement, AnyView, ClickEvent, ElementId, Hsla, IntoElement, Styled, Window, div, hsla,
+ prelude::*,
};
use std::sync::Arc;
@@ -44,7 +45,7 @@ pub struct Checkbox {
toggle_state: ToggleState,
disabled: bool,
placeholder: bool,
- on_click: Option<Box<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>>,
+ on_click: Option<Box<dyn Fn(&ToggleState, &ClickEvent, &mut Window, &mut App) + 'static>>,
filled: bool,
style: ToggleStyle,
tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
@@ -83,6 +84,16 @@ impl Checkbox {
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
@@ -226,8 +237,8 @@ impl RenderOnce for 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)
+ this.on_click(move |click, window, cx| {
+ on_click(&self.toggle_state.inverse(), click, window, cx)
})
},
)
@@ -1,3 +1,5 @@
+use std::rc::Rc;
+
use gpui::{Action, AnyElement, AnyView, AppContext as _, FocusHandle, IntoElement, Render};
use settings::Settings;
use theme::ThemeSettings;
@@ -7,15 +9,36 @@ use crate::{Color, KeyBinding, Label, LabelSize, StyledExt, h_flex, v_flex};
#[derive(RegisterComponent)]
pub struct Tooltip {
- title: SharedString,
+ title: Title,
meta: Option<SharedString>,
key_binding: Option<KeyBinding>,
}
+#[derive(Clone, IntoElement)]
+enum Title {
+ Str(SharedString),
+ Callback(Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>),
+}
+
+impl From<SharedString> 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<SharedString>, cx: &mut App) -> AnyView {
cx.new(|_| Self {
- title: title.into(),
+ title: Title::Str(title.into()),
meta: None,
key_binding: None,
})
@@ -26,7 +49,7 @@ impl Tooltip {
let title = title.into();
move |_, cx| {
cx.new(|_| Self {
- title: title.clone(),
+ title: title.clone().into(),
meta: None,
key_binding: None,
})
@@ -34,15 +57,15 @@ impl Tooltip {
}
}
- pub fn for_action_title<Title: Into<SharedString>>(
- title: Title,
+ pub fn for_action_title<T: Into<SharedString>>(
+ title: T,
action: &dyn Action,
- ) -> impl Fn(&mut Window, &mut App) -> AnyView + use<Title> {
+ ) -> impl Fn(&mut Window, &mut App) -> AnyView + use<T> {
let title = title.into();
let action = action.boxed_clone();
move |window, cx| {
cx.new(|cx| Self {
- title: title.clone(),
+ title: Title::Str(title.clone()),
meta: None,
key_binding: KeyBinding::for_action(action.as_ref(), window, cx),
})
@@ -60,7 +83,7 @@ impl Tooltip {
let focus_handle = focus_handle.clone();
move |window, cx| {
cx.new(|cx| Self {
- title: title.clone(),
+ title: Title::Str(title.clone()),
meta: None,
key_binding: KeyBinding::for_action_in(action.as_ref(), &focus_handle, window, cx),
})
@@ -75,7 +98,7 @@ impl Tooltip {
cx: &mut App,
) -> AnyView {
cx.new(|cx| Self {
- title: title.into(),
+ title: Title::Str(title.into()),
meta: None,
key_binding: KeyBinding::for_action(action, window, cx),
})
@@ -90,7 +113,7 @@ impl Tooltip {
cx: &mut App,
) -> AnyView {
cx.new(|cx| Self {
- title: title.into(),
+ title: title.into().into(),
meta: None,
key_binding: KeyBinding::for_action_in(action, focus_handle, window, cx),
})
@@ -105,7 +128,7 @@ impl Tooltip {
cx: &mut App,
) -> AnyView {
cx.new(|cx| Self {
- title: title.into(),
+ title: title.into().into(),
meta: Some(meta.into()),
key_binding: action.and_then(|action| KeyBinding::for_action(action, window, cx)),
})
@@ -121,7 +144,7 @@ impl Tooltip {
cx: &mut App,
) -> AnyView {
cx.new(|cx| Self {
- title: title.into(),
+ title: title.into().into(),
meta: Some(meta.into()),
key_binding: action
.and_then(|action| KeyBinding::for_action_in(action, focus_handle, window, cx)),
@@ -131,12 +154,35 @@ impl Tooltip {
pub fn new(title: impl Into<SharedString>) -> Self {
Self {
- title: title.into(),
+ 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: title,
+ meta: None,
+ key_binding: None,
+ })
+ .into()
+ }
+ }
+
pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
self.meta = Some(meta.into());
self
@@ -25,7 +25,7 @@ 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};
+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,
@@ -39,7 +39,7 @@ pub trait StyledExt: Styled + Sized {
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
///
/// Example Elements: Title Bar, Panel, Tab Bar, Editor
- fn elevation_1(self, cx: &mut App) -> Self {
+ fn elevation_1(self, cx: &App) -> Self {
elevated(self, cx, ElevationIndex::Surface)
}
@@ -27,6 +27,8 @@ pub struct SingleLineInput {
///
/// Its position is determined by the [`FieldLabelLayout`].
label: Option<SharedString>,
+ /// The size of the label text.
+ label_size: LabelSize,
/// The placeholder text for the text field.
placeholder: SharedString,
/// Exposes the underlying [`Entity<Editor>`] to allow for customizing the editor beyond the provided API.
@@ -59,6 +61,7 @@ impl SingleLineInput {
Self {
label: None,
+ label_size: LabelSize::Small,
placeholder: placeholder_text,
editor,
start_icon: None,
@@ -76,6 +79,11 @@ impl SingleLineInput {
self
}
+ pub fn label_size(mut self, size: LabelSize) -> Self {
+ self.label_size = size;
+ self
+ }
+
pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
self.disabled = disabled;
self.editor
@@ -138,7 +146,7 @@ impl Render for SingleLineInput {
.when_some(self.label.clone(), |this, label| {
this.child(
Label::new(label)
- .size(LabelSize::Small)
+ .size(self.label_size)
.color(if self.disabled {
Color::Disabled
} else {
@@ -148,16 +156,17 @@ impl Render for SingleLineInput {
})
.child(
h_flex()
+ .min_w_48()
+ .min_h_8()
+ .w_full()
.px_2()
.py_1p5()
- .bg(style.background_color)
+ .flex_grow()
.text_color(style.text_color)
- .rounded_md()
+ .rounded_lg()
+ .bg(style.background_color)
.border_1()
.border_color(style.border_color)
- .min_w_48()
- .w_full()
- .flex_grow()
.when_some(self.start_icon, |this, icon| {
this.gap_1()
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
@@ -173,16 +182,28 @@ impl Component for SingleLineInput {
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
- let input_1 =
- cx.new(|cx| SingleLineInput::new(window, cx, "placeholder").label("Some Label"));
+ let input_small =
+ cx.new(|cx| SingleLineInput::new(window, cx, "placeholder").label("Small Label"));
+
+ let input_regular = cx.new(|cx| {
+ SingleLineInput::new(window, cx, "placeholder")
+ .label("Regular Label")
+ .label_size(LabelSize::Default)
+ });
Some(
v_flex()
.gap_6()
- .children(vec![example_group(vec![single_example(
- "Default",
- div().child(input_1.clone()).into_any_element(),
- )])])
+ .children(vec![example_group(vec![
+ single_example(
+ "Small Label (Default)",
+ div().child(input_small.clone()).into_any_element(),
+ ),
+ single_example(
+ "Regular Label",
+ div().child(input_regular.clone()).into_any_element(),
+ ),
+ ])])
.into_any_element(),
)
}
@@ -30,6 +30,7 @@ log.workspace = true
rand = { workspace = true, optional = true }
regex.workspace = true
rust-embed.workspace = true
+schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
@@ -166,10 +166,108 @@ impl<T: AsRef<Path>> From<T> for SanitizedPath {
}
}
+#[derive(Debug, Clone, Copy)]
+pub enum PathStyle {
+ Posix,
+ Windows,
+}
+
+impl PathStyle {
+ #[cfg(target_os = "windows")]
+ pub const fn current() -> Self {
+ PathStyle::Windows
+ }
+
+ #[cfg(not(target_os = "windows"))]
+ pub const fn current() -> Self {
+ PathStyle::Posix
+ }
+
+ #[inline]
+ pub fn separator(&self) -> &str {
+ match self {
+ PathStyle::Posix => "/",
+ PathStyle::Windows => "\\",
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct RemotePathBuf {
+ inner: PathBuf,
+ style: PathStyle,
+ string: String, // Cached string representation
+}
+
+impl RemotePathBuf {
+ pub fn new(path: PathBuf, style: PathStyle) -> Self {
+ #[cfg(target_os = "windows")]
+ let string = match style {
+ PathStyle::Posix => path.to_string_lossy().replace('\\', "/"),
+ PathStyle::Windows => path.to_string_lossy().into(),
+ };
+ #[cfg(not(target_os = "windows"))]
+ let string = match style {
+ PathStyle::Posix => path.to_string_lossy().to_string(),
+ PathStyle::Windows => path.to_string_lossy().replace('/', "\\"),
+ };
+ Self {
+ inner: path,
+ style,
+ string,
+ }
+ }
+
+ pub fn from_str(path: &str, style: PathStyle) -> Self {
+ let path_buf = PathBuf::from(path);
+ Self::new(path_buf, style)
+ }
+
+ pub fn to_string(&self) -> String {
+ self.string.clone()
+ }
+
+ #[cfg(target_os = "windows")]
+ pub fn to_proto(self) -> String {
+ match self.path_style() {
+ PathStyle::Posix => self.to_string(),
+ PathStyle::Windows => self.inner.to_string_lossy().replace('\\', "/"),
+ }
+ }
+
+ #[cfg(not(target_os = "windows"))]
+ pub fn to_proto(self) -> String {
+ match self.path_style() {
+ PathStyle::Posix => self.inner.to_string_lossy().to_string(),
+ PathStyle::Windows => self.to_string(),
+ }
+ }
+
+ pub fn as_path(&self) -> &Path {
+ &self.inner
+ }
+
+ pub fn path_style(&self) -> PathStyle {
+ self.style
+ }
+
+ pub fn parent(&self) -> Option<RemotePathBuf> {
+ self.inner
+ .parent()
+ .map(|p| RemotePathBuf::new(p.to_path_buf(), self.style))
+ }
+}
+
/// A delimiter to use in `path_query:row_number:column_number` strings parsing.
pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
const ROW_COL_CAPTURE_REGEX: &str = r"(?xs)
+ ([^\(]+)\:(?:
+ \((\d+)[,:](\d+)\) # filename:(row,column), filename:(row:column)
+ |
+ \((\d+)\)() # filename:(row)
+ )
+ |
([^\(]+)(?:
\((\d+)[,:](\d+)\) # filename(row,column), filename(row:column)
|
@@ -674,6 +772,15 @@ mod tests {
column: None
}
);
+
+ assert_eq!(
+ PathWithPosition::parse_str("Types.hs:(617,9)-(670,28):"),
+ PathWithPosition {
+ path: PathBuf::from("Types.hs"),
+ row: Some(617),
+ column: Some(9),
+ }
+ );
}
#[test]
@@ -0,0 +1,58 @@
+use schemars::{JsonSchema, transform::transform_subschemas};
+
+const DEFS_PATH: &str = "#/$defs/";
+
+/// Replaces the JSON schema definition for some type if it is in use (in the definitions list), and
+/// returns a reference to it.
+///
+/// This asserts that JsonSchema::schema_name() + "2" does not exist because this indicates that
+/// there are multiple types that use this name, and unfortunately schemars APIs do not support
+/// resolving this ambiguity - see https://github.com/GREsau/schemars/issues/449
+///
+/// This takes a closure for `schema` because some settings types are not available on the remote
+/// server, and so will crash when attempting to access e.g. GlobalThemeRegistry.
+pub fn replace_subschema<T: JsonSchema>(
+ generator: &mut schemars::SchemaGenerator,
+ schema: impl Fn() -> schemars::Schema,
+) -> schemars::Schema {
+ // fallback on just using the schema name, which could collide.
+ let schema_name = T::schema_name();
+ let definitions = generator.definitions_mut();
+ assert!(!definitions.contains_key(&format!("{schema_name}2")));
+ if definitions.contains_key(schema_name.as_ref()) {
+ definitions.insert(schema_name.to_string(), schema().to_value());
+ }
+ schemars::Schema::new_ref(format!("{DEFS_PATH}{schema_name}"))
+}
+
+/// Adds a new JSON schema definition and returns a reference to it. **Panics** if the name is
+/// already in use.
+pub fn add_new_subschema(
+ generator: &mut schemars::SchemaGenerator,
+ name: &str,
+ schema: serde_json::Value,
+) -> schemars::Schema {
+ let old_definition = generator.definitions_mut().insert(name.to_string(), schema);
+ assert_eq!(old_definition, None);
+ schemars::Schema::new_ref(format!("{DEFS_PATH}{name}"))
+}
+
+/// Defaults `additionalProperties` to `true`, as if `#[schemars(deny_unknown_fields)]` was on every
+/// struct. Skips structs that have `additionalProperties` set (such as if #[serde(flatten)] is used
+/// on a map).
+#[derive(Clone)]
+pub struct DefaultDenyUnknownFields;
+
+impl schemars::transform::Transform for DefaultDenyUnknownFields {
+ fn transform(&mut self, schema: &mut schemars::Schema) {
+ if let Some(object) = schema.as_object_mut() {
+ if object.contains_key("properties")
+ && !object.contains_key("additionalProperties")
+ && !object.contains_key("unevaluatedProperties")
+ {
+ object.insert("additionalProperties".to_string(), false.into());
+ }
+ }
+ transform_subschemas(self, schema);
+ }
+}
@@ -16,8 +16,13 @@ pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<Strin
let mut command_string = String::new();
let mut command = std::process::Command::new(&shell_path);
// In some shells, file descriptors greater than 2 cannot be used in interactive mode,
- // so file descriptor 0 (stdin) is used instead. [Citation Needed]
+ // 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 ENV_OUTPUT_FD: std::os::fd::RawFd = 0;
+ let redir = match shell_name {
+ Some("rc") => format!(">[1={}]", ENV_OUTPUT_FD), // `[1=0]`
+ _ => format!(">&{}", ENV_OUTPUT_FD), // `>&0`
+ };
command.stdin(Stdio::null());
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
@@ -38,10 +43,7 @@ pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<Strin
}
// cd into the directory, triggering directory specific side-effects (asdf, direnv, etc)
command_string.push_str(&format!("cd '{}';", directory.display()));
- command_string.push_str(&format!(
- "sh -c \"{} --printenv >&{}\";",
- zed_path, ENV_OUTPUT_FD
- ));
+ 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);
@@ -5,6 +5,7 @@ pub mod fs;
pub mod markdown;
pub mod paths;
pub mod redact;
+pub mod schemars;
pub mod serde;
pub mod shell_env;
pub mod size;
@@ -1096,52 +1097,6 @@ mod tests {
assert_eq!(vec, &[1000, 101, 21, 19, 17, 13, 9, 8]);
}
- #[test]
- fn test_get_shell_safe_zed_path_with_spaces() {
- // Test that shlex::try_quote handles paths with spaces correctly
- let path_with_spaces = "/Applications/Zed Nightly.app/Contents/MacOS/zed";
- let quoted = shlex::try_quote(path_with_spaces).unwrap();
-
- // The quoted path should be properly escaped for shell use
- assert!(quoted.contains(path_with_spaces));
-
- // When used in a shell command, it should not be split at spaces
- let command = format!("sh -c '{} --printenv'", quoted);
- println!("Command would be: {}", command);
-
- // Test that shlex can parse it back correctly
- let parsed = shlex::split(&format!("{} --printenv", quoted)).unwrap();
- assert_eq!(parsed.len(), 2);
- assert_eq!(parsed[0], path_with_spaces);
- assert_eq!(parsed[1], "--printenv");
- }
-
- #[test]
- fn test_shell_command_construction_with_quoted_path() {
- // Test the specific pattern used in shell_env.rs to ensure proper quoting
- let path_with_spaces = "/Applications/Zed Nightly.app/Contents/MacOS/zed";
- let quoted_path = shlex::try_quote(path_with_spaces).unwrap();
-
- // This should be: '/Applications/Zed Nightly.app/Contents/MacOS/zed'
- assert_eq!(
- quoted_path,
- "'/Applications/Zed Nightly.app/Contents/MacOS/zed'"
- );
-
- // Test the command construction pattern from shell_env.rs
- // The fixed version should use double quotes around the entire sh -c argument
- let env_fd = 0;
- let command = format!("sh -c \"{} --printenv >&{}\";", quoted_path, env_fd);
-
- // This should produce: sh -c "'/Applications/Zed Nightly.app/Contents/MacOS/zed' --printenv >&0";
- let expected =
- "sh -c \"'/Applications/Zed Nightly.app/Contents/MacOS/zed' --printenv >&0\";";
- assert_eq!(command, expected);
-
- // The command should not contain the problematic double single-quote pattern
- assert!(!command.contains("''"));
- }
-
#[test]
fn test_truncate_to_bottom_n_sorted_by() {
let mut vec: Vec<u32> = vec![5, 2, 3, 4, 1];
@@ -3,7 +3,15 @@ use gpui::{Context, Window, actions};
use crate::{Vim, state::Mode};
-actions!(vim, [ChangeListOlder, ChangeListNewer]);
+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>) {
Vim::action(editor, cx, |vim, _: &ChangeListOlder, window, cx| {
@@ -28,8 +28,8 @@ use std::{
use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
use ui::ActiveTheme;
use util::ResultExt;
-use workspace::notifications::DetachAndPromptErr;
use workspace::{Item, SaveIntent, notifications::NotifyResultExt};
+use workspace::{SplitDirection, notifications::DetachAndPromptErr};
use zed_actions::{OpenDocs, RevealTarget};
use crate::{
@@ -44,18 +44,21 @@ use crate::{
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 {
@@ -64,6 +67,7 @@ pub struct WithRange {
action: WrappedAction,
}
+/// Executes a command with the specified count.
#[derive(Clone, Debug, PartialEq, Action)]
#[action(namespace = vim, no_json, no_register)]
pub struct WithCount {
@@ -155,12 +159,14 @@ impl VimOption {
}
}
+/// Sets vim options and configuration values.
#[derive(Clone, PartialEq, Action)]
#[action(namespace = vim, no_json, no_register)]
pub struct VimSet {
options: Vec<VimOption>,
}
+/// Saves the current file with optional save intent.
#[derive(Clone, PartialEq, Action)]
#[action(namespace = vim, no_json, no_register)]
struct VimSave {
@@ -168,6 +174,14 @@ struct VimSave {
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 {
@@ -177,8 +191,18 @@ enum DeleteMarks {
actions!(
vim,
- [VisualCommand, CountCommand, ShellCommand, ArgumentRequired]
+ [
+ /// 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 {
@@ -306,6 +330,33 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
});
+ 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 project_path = ProjectPath {
+ worktree_id: worktree.read(cx).id(),
+ path: Arc::from(Path::new(&action.filename)),
+ };
+
+ 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<Editor>) {
let _ = window.prompt(
@@ -981,8 +1032,24 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
save_intent: Some(SaveIntent::Overwrite),
}),
VimCommand::new(("cq", "uit"), zed_actions::Quit),
- VimCommand::new(("sp", "lit"), workspace::SplitHorizontal),
- VimCommand::new(("vs", "plit"), workspace::SplitVertical),
+ VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).args(|_, args| {
+ Some(
+ VimSplit {
+ vertical: false,
+ filename: args,
+ }
+ .boxed_clone(),
+ )
+ }),
+ VimCommand::new(("vs", "plit"), workspace::SplitVertical).args(|_, args| {
+ Some(
+ VimSplit {
+ vertical: true,
+ filename: args,
+ }
+ .boxed_clone(),
+ )
+ }),
VimCommand::new(
("bd", "elete"),
workspace::CloseActiveItem {
@@ -1039,13 +1106,28 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
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).range(wrap_count),
- VimCommand::new(("cp", "revious"), editor::actions::GoToPreviousDiagnostic)
- .range(wrap_count),
- VimCommand::new(("cN", "ext"), editor::actions::GoToPreviousDiagnostic).range(wrap_count),
- VimCommand::new(("lp", "revious"), editor::actions::GoToPreviousDiagnostic)
+ VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic::default())
.range(wrap_count),
- VimCommand::new(("lN", "ext"), editor::actions::GoToPreviousDiagnostic).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)
@@ -1282,6 +1364,7 @@ fn generate_positions(string: &str, query: &str) -> Vec<usize> {
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 {
@@ -1480,6 +1563,7 @@ impl OnMatchingLines {
}
}
+/// Executes a shell command and returns the output.
#[derive(Clone, Debug, PartialEq, Action)]
#[action(namespace = vim, no_json, no_register)]
pub struct ShellExec {
@@ -1669,7 +1753,7 @@ impl ShellExec {
id: TaskId("vim".to_string()),
full_label: command.clone(),
label: command.clone(),
- command: command.clone(),
+ command: Some(command.clone()),
args: Vec::new(),
command_label: command.clone(),
cwd,
@@ -6,7 +6,13 @@ use text::SelectionGoal;
use crate::{Vim, motion::Motion, state::Mode};
-actions!(vim, [HelixNormalAfter]);
+actions!(
+ vim,
+ [
+ /// Switches to normal mode after the cursor (Helix-style).
+ HelixNormalAfter
+ ]
+);
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, Vim::helix_normal_after);
@@ -362,40 +368,40 @@ mod test {
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;
+ #[gpui::test]
+ async fn test_delete(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
- // // test delete a selection
- // cx.set_state(
- // indoc! {"
- // The qu«ick ˇ»brown
- // fox jumps over
- // the lazy dog."},
- // Mode::HelixNormal,
- // );
+ // 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.simulate_keystrokes("d");
- // cx.assert_state(
- // indoc! {"
- // The quˇbrown
- // fox jumps over
- // the lazy dog."},
- // Mode::HelixNormal,
- // );
+ cx.assert_state(
+ indoc! {"
+ The quˇbrown
+ fox jumps over
+ the lazy dog."},
+ Mode::HelixNormal,
+ );
- // // test deleting a single character
- // cx.simulate_keystrokes("d");
+ // test deleting a single character
+ cx.simulate_keystrokes("d");
- // cx.assert_state(
- // indoc! {"
- // The quˇrown
- // fox jumps over
- // the lazy dog."},
- // Mode::HelixNormal,
- // );
- // }
+ 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) {
@@ -13,7 +13,17 @@ pub(crate) enum IndentDirection {
Auto,
}
-actions!(vim, [Indent, Outdent, AutoIndent]);
+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>) {
Vim::action(editor, cx, |vim, _: &Indent, window, cx| {
@@ -5,7 +5,15 @@ use language::SelectionGoal;
use settings::Settings;
use vim_mode_setting::HelixModeSetting;
-actions!(vim, [NormalBefore, TemporaryNormal]);
+actions!(
+ vim,
+ [
+ /// Switches to normal mode with cursor positioned before the current character.
+ NormalBefore,
+ /// Temporarily switches to normal mode for one command.
+ TemporaryNormal
+ ]
+);
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, Vim::normal_before);
@@ -176,6 +176,7 @@ enum IndentType {
Same,
}
+/// Moves to the start of the next word.
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
@@ -184,6 +185,7 @@ struct NextWordStart {
ignore_punctuation: bool,
}
+/// Moves to the end of the next word.
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
@@ -192,6 +194,7 @@ struct NextWordEnd {
ignore_punctuation: bool,
}
+/// Moves to the start of the previous word.
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
@@ -200,6 +203,7 @@ struct PreviousWordStart {
ignore_punctuation: bool,
}
+/// Moves to the end of the previous word.
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
@@ -208,6 +212,7 @@ struct PreviousWordEnd {
ignore_punctuation: bool,
}
+/// Moves to the start of the next subword.
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
@@ -216,6 +221,7 @@ pub(crate) struct NextSubwordStart {
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)]
@@ -224,6 +230,7 @@ pub(crate) struct NextSubwordEnd {
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)]
@@ -232,6 +239,7 @@ pub(crate) struct PreviousSubwordStart {
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)]
@@ -240,6 +248,7 @@ pub(crate) struct PreviousSubwordEnd {
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)]
@@ -248,6 +257,7 @@ pub(crate) struct Up {
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)]
@@ -256,6 +266,7 @@ pub(crate) struct Down {
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)]
@@ -264,6 +275,7 @@ struct FirstNonWhitespace {
display_lines: bool,
}
+/// Moves to the end of the current line.
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
@@ -272,6 +284,7 @@ struct EndOfLine {
display_lines: bool,
}
+/// Moves to the start of the current line.
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
@@ -280,6 +293,7 @@ pub struct StartOfLine {
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)]
@@ -288,6 +302,7 @@ struct MiddleOfLine {
display_lines: bool,
}
+/// Finds the next unmatched bracket or delimiter.
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
@@ -296,6 +311,7 @@ struct UnmatchedForward {
char: char,
}
+/// Finds the previous unmatched bracket or delimiter.
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
@@ -307,46 +323,85 @@ struct UnmatchedBackward {
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,
]
);
@@ -24,9 +24,9 @@ use crate::{
};
use collections::BTreeSet;
use convert::ConvertTarget;
-use editor::Bias;
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};
@@ -36,33 +36,62 @@ 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,
+ /// 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,
+ /// 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,
]
);
@@ -111,6 +140,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
})
});
vim.visual_delete(false, window, cx);
+ vim.switch_mode(Mode::HelixNormal, true, window, cx);
});
Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, window, cx| {
@@ -167,6 +197,120 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
}
});
});
+ Vim::action(editor, cx, |vim, _: &UndoLastLine, window, cx| {
+ Vim::take_forced_motion(cx);
+ vim.update_editor(window, cx, |vim, editor, window, 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.iter().cloned().collect::<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::<String>()
+ })
+ .collect::<Vec<_>>();
+ 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::<String>();
+
+ 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::<Vec<_>>();
+
+ 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);
@@ -1849,4 +1993,102 @@ mod test {
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,
+ );
+ }
}
@@ -212,7 +212,19 @@ impl Vim {
}
}
- Mode::HelixNormal => {}
+ Mode::HelixNormal => {
+ if selection.is_empty() {
+ // Handle empty selection by operating on the whole word
+ let (word_range, _) = snapshot.surrounding_word(selection.start, false);
+ let word_start = snapshot.offset_to_point(word_range.start);
+ let word_end = snapshot.offset_to_point(word_range.end);
+ ranges.push(word_start..word_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;
@@ -245,12 +257,16 @@ impl Vim {
})
});
});
- self.switch_mode(Mode::Normal, true, window, cx)
+ 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]
@@ -419,4 +435,25 @@ mod test {
.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
+ cx.set_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);
+ }
}
@@ -9,6 +9,7 @@ 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)]
@@ -17,6 +18,7 @@ struct Increment {
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)]
@@ -14,6 +14,7 @@ use crate::{
state::{Mode, Register},
};
+/// Pastes text from the specified register at the cursor position.
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
@@ -11,7 +11,19 @@ use editor::Editor;
use gpui::{Action, App, Context, Window, actions};
use workspace::Workspace;
-actions!(vim, [Repeat, EndRepeat, ToggleRecord, ReplayLastRecording]);
+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
@@ -7,18 +7,31 @@ use editor::{
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,
- PageDown
+ /// Scrolls down by one page.
+ PageDown,
+ /// Scrolls right by half a page's width.
+ HalfPageRight,
+ /// Scrolls left by half a page's width.
+ HalfPageLeft,
]
);
@@ -43,6 +56,16 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
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 {
@@ -115,6 +138,10 @@ fn scroll_editor(
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;
@@ -124,8 +151,14 @@ fn scroll_editor(
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 =
@@ -155,9 +188,8 @@ fn scroll_editor(
(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 >= map.max_point().row().0
- {
- map.max_point().row()
+ 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)
@@ -177,13 +209,56 @@ fn scroll_editor(
} else {
head.row()
};
- let new_head =
- map.clip_point(DisplayPoint::new(new_row, starting_column), Bias::Left);
+
+ // 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, selection.goal)
+ selection.collapse_to(new_head, goal)
} else {
- selection.set_head(new_head, selection.goal)
+ selection.set_head(new_head, goal)
};
})
},
@@ -464,4 +539,30 @@ mod test {
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");
+ }
}
@@ -16,6 +16,7 @@ use crate::{
state::{Mode, SearchState},
};
+/// Moves to the next search match.
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
@@ -28,6 +29,7 @@ pub(crate) struct MoveToNext {
regex: bool,
}
+/// Moves to the previous search match.
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
@@ -40,6 +42,7 @@ pub(crate) struct MoveToPrevious {
regex: bool,
}
+/// Initiates a search operation with the specified parameters.
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
@@ -50,6 +53,7 @@ pub(crate) struct Search {
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)]
@@ -58,6 +62,7 @@ pub struct FindCommand {
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 {
@@ -66,14 +71,26 @@ pub struct ReplaceCommand {
}
#[derive(Clone, Debug, PartialEq)]
-pub(crate) struct Replacement {
+pub struct Replacement {
search: String,
replacement: String,
- should_replace_all: bool,
- is_case_sensitive: bool,
+ case_sensitive: Option<bool>,
+ flag_n: bool,
+ flag_g: bool,
+ flag_c: bool,
}
-actions!(vim, [SearchSubmit, MoveToNextMatch, MoveToPreviousMatch]);
+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>) {
Vim::action(editor, cx, Vim::move_to_next);
@@ -453,71 +470,89 @@ impl Vim {
result.notify_err(workspace, cx);
})
}
- let vim = cx.entity().clone();
- pane.update(cx, |pane, cx| {
- let mut options = SearchOptions::REGEX;
+ let Some(search_bar) = pane.update(cx, |pane, cx| {
+ pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
+ }) 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 Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
- return;
+ let search = if replacement.search.is_empty() {
+ search_bar.query(cx)
+ } else {
+ replacement.search
};
- let search = search_bar.update(cx, |search_bar, cx| {
- if !search_bar.show(window, cx) {
- return None;
- }
-
- if replacement.is_case_sensitive {
- options.set(SearchOptions::CASE_SENSITIVE, true)
- }
- let search = if replacement.search.is_empty() {
- search_bar.query(cx)
- } else {
- replacement.search
- };
- if search_bar.should_use_smartcase_search(cx) {
- options.set(
- SearchOptions::CASE_SENSITIVE,
- search_bar.is_contains_uppercase(&search),
- );
- }
- if !replacement.should_replace_all {
- options.set(SearchOptions::ONE_MATCH_PER_LINE, true);
- }
+ 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 {
+ options.set(SearchOptions::CASE_SENSITIVE, false)
+ }
- search_bar.set_replacement(Some(&replacement.replacement), cx);
- Some(search_bar.search(&search, Some(options), window, cx))
- });
- let Some(search) = search else { return };
- 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_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, window, cx);
- vim.update(cx, |vim, cx| {
- vim.move_cursor(
- Motion::StartOfLine {
- display_lines: false,
- },
- None,
- window,
- cx,
- )
- });
+ if !replacement.flag_g {
+ options.set(SearchOptions::ONE_MATCH_PER_LINE, true);
+ }
- // 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);
- })?;
- anyhow::Ok(())
+ 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), 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, 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);
})
+ .detach_and_log_err(cx);
}
}
@@ -578,16 +613,19 @@ impl Replacement {
let mut replacement = Replacement {
search,
replacement,
- should_replace_all: false,
- is_case_sensitive: true,
+ case_sensitive: None,
+ flag_g: false,
+ flag_n: false,
+ flag_c: false,
};
for c in flags.chars() {
match c {
- 'g' => replacement.should_replace_all = true,
- 'c' | 'n' => replacement.should_replace_all = false,
- 'i' => replacement.is_case_sensitive = false,
- 'I' => replacement.is_case_sensitive = true,
+ '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),
_ => {}
}
}
@@ -898,7 +936,6 @@ mod test {
});
}
- // cargo test -p vim --features neovim test_replace_with_range_at_start
#[gpui::test]
async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
@@ -964,6 +1001,121 @@ mod test {
});
}
+ #[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::<BufferSearchBar>()
+ .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;
@@ -7,7 +7,15 @@ use crate::{
motion::{Motion, MotionKind},
};
-actions!(vim, [Substitute, SubstituteLine]);
+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>) {
Vim::action(editor, cx, |vim, _: &Substitute, window, cx| {
@@ -46,6 +46,7 @@ pub enum Object {
EntireFile,
}
+/// Selects a word text object.
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
@@ -54,6 +55,7 @@ struct Word {
ignore_punctuation: bool,
}
+/// Selects a subword text object.
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
#[action(namespace = vim)]
#[serde(deny_unknown_fields)]
@@ -61,6 +63,7 @@ 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)]
@@ -258,25 +261,45 @@ fn find_mini_brackets(
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 parentheses.
Parentheses,
+ /// Selects text within the nearest brackets.
MiniBrackets,
+ /// Selects text within any type of brackets.
AnyBrackets,
+ /// Selects text within square brackets.
SquareBrackets,
+ /// Selects text within curly brackets.
CurlyBrackets,
+ /// Selects text within angle brackets.
AngleBrackets,
+ /// 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
]
);
@@ -13,7 +13,15 @@ use language::{Point, SelectionGoal};
use std::ops::Range;
use std::sync::Arc;
-actions!(vim, [ToggleReplace, UndoReplace]);
+actions!(
+ vim,
+ [
+ /// Toggles replace mode.
+ ToggleReplace,
+ /// Undoes the last replacement.
+ UndoReplace
+ ]
+);
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, |vim, _: &ToggleReplace, window, cx| {
@@ -4,7 +4,13 @@ use editor::{Bias, Editor, RewrapOptions, SelectionEffects, display_map::ToDispl
use gpui::{Context, Window, actions};
use language::SelectionGoal;
-actions!(vim, [Rewrap]);
+actions!(
+ vim,
+ [
+ /// Rewraps the selected text to fit within the line width.
+ Rewrap
+ ]
+);
pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, |vim, _: &Rewrap, window, cx| {
@@ -1,13 +1,11 @@
use std::ops::{Deref, DerefMut};
use editor::test::editor_lsp_test_context::EditorLspTestContext;
-use gpui::{Context, Entity, SemanticVersion, UpdateGlobal, actions};
+use gpui::{Context, Entity, SemanticVersion, UpdateGlobal};
use search::{BufferSearchBar, project_search::ProjectSearchBar};
use crate::{state::Operator, *};
-actions!(agent, [Chat]);
-
pub struct VimTestContext {
cx: EditorLspTestContext,
}
@@ -134,55 +134,105 @@ struct PushLiteral {
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,
+ /// Opens the default keymap file.
OpenDefaultKeymap,
+ /// 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,
]
);
// in the workspace namespace so it's not filtered out when vim is disabled.
-actions!(workspace, [ToggleVimMode,]);
+actions!(
+ workspace,
+ [
+ /// Toggles Vim mode on or off.
+ ToggleVimMode,
+ ]
+);
/// Initializes the `vim` crate.
pub fn init(cx: &mut App) {
@@ -325,6 +375,7 @@ pub(crate) struct Vim {
pub(crate) current_tx: Option<TransactionId>,
pub(crate) current_anchor: Option<Selection<Anchor>>,
pub(crate) undo_modes: HashMap<TransactionId, Mode>,
+ pub(crate) undo_last_line_tx: Option<TransactionId>,
selected_register: Option<char>,
pub search: SearchState,
@@ -372,6 +423,7 @@ impl Vim {
stored_visual_mode: None,
current_tx: None,
+ undo_last_line_tx: None,
current_anchor: None,
undo_modes: HashMap::default(),
@@ -23,23 +23,41 @@ use crate::{
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,
+ /// 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,
]
);
@@ -0,0 +1,16 @@
+{"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"}}
@@ -0,0 +1,23 @@
+{"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"}}
@@ -0,0 +1,13 @@
+{"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"}}
@@ -0,0 +1,14 @@
+{"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"}}
@@ -0,0 +1,15 @@
+{"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"}
@@ -0,0 +1,21 @@
+{"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"}}
@@ -26,7 +26,6 @@ install_cli.workspace = true
language.workspace = true
picker.workspace = true
project.workspace = true
-schemars.workspace = true
serde.workspace = true
settings.workspace = true
telemetry.workspace = true
@@ -1,4 +1,3 @@
-use super::base_keymap_setting::BaseKeymap;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{
App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, Task, WeakEntity, Window,
@@ -6,13 +5,19 @@ use gpui::{
};
use picker::{Picker, PickerDelegate};
use project::Fs;
-use settings::{Settings, update_settings_file};
+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!(welcome, [ToggleBaseKeymapSelector]);
+actions!(
+ welcome,
+ [
+ /// Toggles the base keymap selector modal.
+ ToggleBaseKeymapSelector
+ ]
+);
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
@@ -17,22 +17,24 @@ use workspace::{
open_new,
};
-pub use base_keymap_setting::BaseKeymap;
pub use multibuffer_hint::*;
mod base_keymap_picker;
-mod base_keymap_setting;
mod multibuffer_hint;
mod welcome_ui;
-actions!(welcome, [ResetHints]);
+actions!(
+ welcome,
+ [
+ /// Resets the welcome screen hints to their initial state.
+ ResetHints
+ ]
+);
pub const FIRST_OPEN: &str = "first_open";
pub const DOCS_URL: &str = "https://zed.dev/docs/";
pub fn init(cx: &mut App) {
- BaseKeymap::register(cx);
-
cx.observe_new(|workspace: &mut Workspace, _, _cx| {
workspace.register_action(|workspace, _: &Welcome, window, cx| {
let welcome_page = WelcomePage::new(workspace, cx);
@@ -221,9 +221,9 @@ pub enum DockPosition {
impl DockPosition {
fn label(&self) -> &'static str {
match self {
- Self::Left => "left",
- Self::Bottom => "bottom",
- Self::Right => "right",
+ Self::Left => "Left",
+ Self::Bottom => "Bottom",
+ Self::Right => "Right",
}
}
@@ -864,7 +864,7 @@ impl Render for PanelButtons {
let action = dock.toggle_action();
let tooltip: SharedString =
- format!("Close {} dock", dock.position.label()).into();
+ format!("Close {} Dock", dock.position.label()).into();
(action, tooltip)
} else {
@@ -923,6 +923,7 @@ impl Render for PanelButtons {
.collect();
let has_buttons = !buttons.is_empty();
+
h_flex()
.gap_1()
.children(buttons)
@@ -95,37 +95,45 @@ pub enum SaveIntent {
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<SaveIntent>,
#[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)]
pub struct CloseInactiveItems {
+ #[serde(default)]
pub save_intent: Option<SaveIntent>,
#[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<SaveIntent>,
#[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)]
@@ -134,6 +142,7 @@ pub struct CloseCleanItems {
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)]
@@ -142,6 +151,7 @@ pub struct CloseItemsToTheRight {
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)]
@@ -150,6 +160,7 @@ pub struct CloseItemsToTheLeft {
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)]
@@ -158,6 +169,7 @@ pub struct RevealInProjectPanel {
pub entry_id: Option<u64>,
}
+/// Opens the search interface with the specified configuration.
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)]
#[action(namespace = pane)]
#[serde(deny_unknown_fields)]
@@ -173,25 +185,45 @@ pub struct DeploySearch {
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.
SplitLeft,
+ /// Splits the pane upward.
SplitUp,
+ /// Splits the pane to the right.
SplitRight,
+ /// Splits the pane downward.
SplitDown,
+ /// 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,
]
);
@@ -1314,6 +1346,7 @@ impl Pane {
pub fn close_inactive_items(
&mut self,
action: &CloseInactiveItems,
+ target_item_id: Option<EntityId>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
@@ -1321,7 +1354,11 @@ impl Pane {
return Task::ready(Ok(()));
}
- let active_item_id = self.active_item_id();
+ 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(
@@ -2564,6 +2601,7 @@ impl Pane {
.handler(window.handler_for(&pane, move |pane, window, cx| {
pane.close_inactive_items(
&close_inactive_items_action,
+ Some(item_id),
window,
cx,
)
@@ -2703,9 +2741,7 @@ impl Pane {
.when(visible_in_project_panel, |menu| {
menu.entry(
"Reveal In Project Panel",
- Some(Box::new(RevealInProjectPanel {
- entry_id: Some(entry_id),
- })),
+ Some(Box::new(RevealInProjectPanel::default())),
window.handler_for(&pane, move |pane, _, cx| {
pane.project
.update(cx, |_, cx| {
@@ -3475,7 +3511,7 @@ impl Render for Pane {
)
.on_action(
cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| {
- pane.close_inactive_items(action, window, cx)
+ pane.close_inactive_items(action, None, window, cx)
.detach_and_log_err(cx);
}),
)
@@ -5811,6 +5847,7 @@ mod tests {
save_intent: None,
close_pinned: false,
},
+ None,
window,
cx,
)
@@ -5820,6 +5857,43 @@ mod tests {
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_inactive_items(
+ &CloseInactiveItems {
+ 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);
@@ -6176,6 +6250,7 @@ mod tests {
save_intent: None,
close_pinned: false,
},
+ None,
window,
cx,
)
@@ -42,7 +42,7 @@ impl Render for StatusBar {
.justify_between()
.gap(DynamicSpacing::Base08.rems(cx))
.py(DynamicSpacing::Base04.rems(cx))
- .px(DynamicSpacing::Base08.rems(cx))
+ .px(DynamicSpacing::Base06.rems(cx))
.bg(cx.theme().colors().status_bar_background)
.map(|el| match window.window_decorations() {
Decorations::Server => el,
@@ -58,22 +58,23 @@ impl Render for StatusBar {
.border_b(px(1.0))
.border_color(cx.theme().colors().status_bar_background),
})
- .child(self.render_left_tools(cx))
- .child(self.render_right_tools(cx))
+ .child(self.render_left_tools())
+ .child(self.render_right_tools())
}
}
impl StatusBar {
- fn render_left_tools(&self, cx: &mut Context<Self>) -> impl IntoElement {
+ fn render_left_tools(&self) -> impl IntoElement {
h_flex()
- .gap(DynamicSpacing::Base04.rems(cx))
+ .gap_1()
.overflow_x_hidden()
.children(self.left_items.iter().map(|item| item.to_any()))
}
- fn render_right_tools(&self, cx: &mut Context<Self>) -> impl IntoElement {
+ fn render_right_tools(&self) -> impl IntoElement {
h_flex()
- .gap(DynamicSpacing::Base04.rems(cx))
+ .gap_1()
+ .overflow_x_hidden()
.children(self.right_items.iter().rev().map(|item| item.to_any()))
}
}
@@ -11,7 +11,13 @@ use ui::{
use crate::{Item, Workspace};
-actions!(dev, [OpenThemePreview]);
+actions!(
+ dev,
+ [
+ /// Opens the theme preview window.
+ OpenThemePreview
+ ]
+);
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _, _| {
@@ -169,44 +169,83 @@ pub trait DebuggerProvider {
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,
+ /// Closes the active dock.
CloseActiveDock,
+ /// Closes all docks.
CloseAllDocks,
+ /// 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,
+ /// 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 the left dock.
ToggleLeftDock,
+ /// Toggles the right dock.
ToggleRightDock,
+ /// Toggles zoom on the active pane.
ToggleZoom,
+ /// Stops following a collaborator.
Unfollow,
+ /// Shows the welcome screen.
Welcome,
+ /// Restores the banner.
RestoreBanner,
+ /// Toggles expansion of the selected item.
ToggleExpandItem,
]
);
@@ -216,14 +255,17 @@ pub struct OpenPaths {
pub paths: Vec<PathBuf>,
}
+/// 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,
@@ -231,10 +273,16 @@ pub struct MoveItemToPane {
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,
@@ -242,38 +290,52 @@ pub struct MoveItemToPaneInDirection {
pub clone: bool,
}
+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<SaveIntent>,
}
+/// 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<SaveIntent>,
}
+/// 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<SaveIntent>,
}
+/// 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<SaveIntent>,
}
+/// Sends a sequence of keystrokes to the active element.
#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
#[action(namespace = workspace)]
pub struct SendKeystrokes(pub String);
+/// Reloads the active item or workspace.
#[derive(Clone, Deserialize, PartialEq, Default, JsonSchema, Action)]
#[action(namespace = workspace)]
#[serde(deny_unknown_fields)]
@@ -284,11 +346,13 @@ pub struct Reload {
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)]
@@ -340,13 +404,21 @@ pub struct DecreaseOpenDocksSize {
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,
]
);
@@ -402,6 +474,7 @@ impl PartialEq for Toast {
}
}
+/// Opens a new terminal with the specified working directory.
#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema, Action)]
#[action(namespace = workspace)]
#[serde(deny_unknown_fields)]
@@ -2704,6 +2777,7 @@ impl Workspace {
save_intent: None,
close_pinned: false,
},
+ None,
window,
cx,
)
@@ -2806,12 +2880,14 @@ impl Workspace {
})
}
- fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
if let Some(dock) = self.active_dock(window, 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>) {
@@ -3766,11 +3842,13 @@ impl Workspace {
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);
}
- serialize_workspace = *focus_changed || pane != self.active_pane();
}
pane::Event::UserSavedItem { item, save_intent } => {
cx.emit(Event::UserSavedItem {
@@ -5450,7 +5528,9 @@ impl Workspace {
))
.on_action(cx.listener(
|workspace: &mut Workspace, _: &CloseActiveDock, window, cx| {
- workspace.close_active_dock(window, cx);
+ if !workspace.close_active_dock(window, cx) {
+ cx.propagate();
+ }
},
))
.on_action(
@@ -6580,6 +6660,10 @@ impl WorkspaceStore {
Ok(())
})?
}
+
+ pub fn workspaces(&self) -> &HashSet<WindowHandle<Workspace>> {
+ &self.workspaces
+ }
}
impl ViewId {
@@ -6659,14 +6743,25 @@ actions!(
/// 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
]
);
-actions!(zed, [OpenLog]);
+actions!(
+ zed,
+ [
+ /// Opens the Zed log file.
+ OpenLog
+ ]
+);
async fn join_channel_internal(
channel_id: ChannelId,
@@ -7473,6 +7568,7 @@ fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
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,
@@ -7481,8 +7577,9 @@ pub fn client_side_decorations(
const BORDER_SIZE: Pixels = px(1.0);
let decorations = window.window_decorations();
- if matches!(decorations, Decorations::Client { .. }) {
- window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
+ 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);
@@ -9358,6 +9455,7 @@ mod tests {
save_intent: Some(SaveIntent::Save),
close_pinned: true,
},
+ None,
window,
cx,
)
@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
-version = "0.195.0"
+version = "0.196.0"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]
@@ -23,6 +23,7 @@ activity_indicator.workspace = true
agent.workspace = true
agent_ui.workspace = true
agent_settings.workspace = true
+agent_servers.workspace = true
anyhow.workspace = true
askpass.workspace = true
assets.workspace = true
@@ -50,7 +50,17 @@ fn main() {
println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024);
}
- let icon = std::path::Path::new("resources/windows/app-icon.ico");
+ 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();
@@ -0,0 +1,403 @@
+; *** 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您要继续吗?
@@ -0,0 +1,15 @@
+[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
@@ -0,0 +1,9 @@
+[CustomMessages]
+AddContextMenuFiles=将“通过 %1 打开”操作添加到 Windows 资源管理器文件上下文菜单
+AddContextMenuFolders=将“通过 %1 打开”操作添加到 Windows 资源管理器目录上下文菜单
+AssociateWithFiles=将 %1 注册为受支持的文件类型的编辑器
+AddToPath=添加到 PATH (重启后生效)
+RunAfter=安装后运行 %1
+Other=其他:
+SourceFile=%1 源文件
+OpenWithContextMenu=通过 %1 打开
@@ -0,0 +1,53 @@
+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
@@ -0,0 +1,1414 @@
+[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
+
+SignTool=Defaultsign
+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"
+
+[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
+
+[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: string; ValueName: ""; ValueData: "{app}\resources\xml.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\bower.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\c.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\config.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\javascript.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\config.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\csharp.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\xml.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\css.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\csharp.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\xml.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\config.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\ruby.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\yaml.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\yaml.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\ruby.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\config.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\config.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\config.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\go.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\c.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\config.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\jade.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\java.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\java.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\javascript.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\react.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\javascript.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\javascript.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\json.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\less.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\javascript.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\php.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\powershell.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\powershell.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\powershell.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\python.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\python.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\ruby.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\sass.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\sass.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\sql.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\typescript.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\react.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\vue.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\xml.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\xml.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\yaml.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\yaml.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; 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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"
+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: string; ValueName: ""; ValueData: "{app}\resources\default.ico"
+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 InitializeSetup(): Boolean;
+begin
+ Result := True;
+
+ if not WizardSilent() and IsAdmin() then begin
+ MsgBox('This User Installer is not meant to be run as an Administrator.', mbError, MB_OK);
+ Result := False;
+ end;
+end;
+
+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;
@@ -29,7 +29,7 @@ use project::project_settings::ProjectSettings;
use recent_projects::{SshSettings, open_ssh_project};
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use session::{AppSession, Session};
-use settings::{Settings, SettingsStore, watch_config_file};
+use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
use std::{
env,
io::{self, IsTerminal},
@@ -43,7 +43,7 @@ use theme::{
};
use util::{ConnectionResult, ResultExt, TryFutureExt, maybe};
use uuid::Uuid;
-use welcome::{BaseKeymap, FIRST_OPEN, show_welcome_view};
+use welcome::{FIRST_OPEN, show_welcome_view};
use workspace::{
AppState, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, WorkspaceStore,
notifications::NotificationId,
@@ -167,16 +167,6 @@ pub fn main() {
#[cfg(unix)]
util::prevent_root_execution();
- // Check if there is a pending installer
- // If there is, run the installer and exit
- // And we don't want to run the installer if we are not the first instance
- #[cfg(target_os = "windows")]
- let is_first_instance = crate::zed::windows_only_instance::is_first_instance();
- #[cfg(target_os = "windows")]
- if is_first_instance && auto_update::check_pending_installation() {
- return;
- }
-
let args = Args::parse();
// `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass
@@ -191,6 +181,16 @@ pub fn main() {
return;
}
+ // Check if there is a pending installer
+ // If there is, run the installer and exit
+ // And we don't want to run the installer if we are not the first instance
+ #[cfg(target_os = "windows")]
+ let is_first_instance = crate::zed::windows_only_instance::is_first_instance();
+ #[cfg(target_os = "windows")]
+ if is_first_instance && auto_update::check_pending_installation() {
+ return;
+ }
+
if args.dump_all_actions {
dump_all_gpui_actions();
return;
@@ -441,11 +441,31 @@ pub fn main() {
debug_adapter_extension::init(extension_host_proxy.clone(), cx);
language::init(cx);
- language_extension::init(extension_host_proxy.clone(), languages.clone());
languages::init(languages.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);
@@ -520,6 +540,7 @@ pub fn main() {
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);
+ agent_servers::init(cx);
web_search::init(cx);
web_search_providers::init(app_state.client.clone(), cx);
snippet_provider::init(cx);
@@ -727,11 +748,10 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
if let Some(connection_options) = request.ssh_connection {
cx.spawn(async move |mut cx| {
- let paths_with_position =
- derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await;
+ let paths: Vec<PathBuf> = request.open_paths.into_iter().map(PathBuf::from).collect();
open_ssh_project(
connection_options,
- paths_with_position.into_iter().map(|p| p.path).collect(),
+ paths,
app_state,
workspace::OpenOptions::default(),
&mut cx,
@@ -1368,13 +1388,14 @@ fn dump_all_gpui_actions() {
name: &'static str,
human_name: String,
aliases: &'static [&'static str],
+ documentation: Option<&'static str>,
}
let mut actions = gpui::generate_list_of_all_registered_actions()
- .into_iter()
.map(|action| ActionDef {
name: action.name,
human_name: command_palette::humanize_action_name(action.name),
aliases: action.deprecated_aliases,
+ documentation: action.documentation,
})
.collect::<Vec<ActionDef>>();
@@ -48,9 +48,10 @@ use release_channel::{AppCommitSha, ReleaseChannel};
use rope::Rope;
use search::project_search::ProjectSearchBar;
use settings::{
- DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile, KeymapFileLoadResult,
- Settings, SettingsStore, VIM_KEYMAP_PATH, initial_local_debug_tasks_content,
- initial_project_settings_content, initial_tasks_content, update_settings_file,
+ BaseKeymap, DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile,
+ KeymapFileLoadResult, Settings, SettingsStore, VIM_KEYMAP_PATH,
+ initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content,
+ update_settings_file,
};
use std::path::PathBuf;
use std::sync::atomic::{self, AtomicBool};
@@ -62,7 +63,7 @@ use util::markdown::MarkdownString;
use util::{ResultExt, asset_str};
use uuid::Uuid;
use vim_mode_setting::VimModeSetting;
-use welcome::{BaseKeymap, DOCS_URL, MultibufferHint};
+use welcome::{DOCS_URL, MultibufferHint};
use workspace::notifications::{NotificationId, dismiss_app_notification, show_app_notification};
use workspace::{
AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings,
@@ -78,19 +79,33 @@ use zed_actions::{
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.
OpenProjectSettings,
+ /// 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,
]
);
@@ -199,8 +199,11 @@ pub fn app_menus() -> Vec<Menu> {
MenuItem::action("Go to Type Definition", editor::actions::GoToTypeDefinition),
MenuItem::action("Find All References", editor::actions::FindAllReferences),
MenuItem::separator(),
- MenuItem::action("Next Problem", editor::actions::GoToDiagnostic),
- MenuItem::action("Previous Problem", editor::actions::GoToPreviousDiagnostic),
+ MenuItem::action("Next Problem", editor::actions::GoToDiagnostic::default()),
+ MenuItem::action(
+ "Previous Problem",
+ editor::actions::GoToPreviousDiagnostic::default(),
+ ),
],
},
Menu {
@@ -255,8 +255,11 @@ impl Render for QuickActionBar {
.action("Go to Symbol", Box::new(ToggleOutline))
.action("Go to Line/Column", Box::new(ToggleGoToLine))
.separator()
- .action("Next Problem", Box::new(GoToDiagnostic))
- .action("Previous Problem", Box::new(GoToPreviousDiagnostic))
+ .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(
@@ -338,52 +341,6 @@ impl Render for QuickActionBar {
);
}
- 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 {
- menu = menu.toggleable_entry(
- "Inline Diagnostics",
- inline_diagnostics_enabled,
- IconPosition::Start,
- Some(ToggleInlineDiagnostics.boxed_clone()),
- {
- let editor = editor.clone();
- move |window, cx| {
- editor
- .update(cx, |editor, cx| {
- editor.toggle_inline_diagnostics(
- &ToggleInlineDiagnostics,
- 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();
@@ -432,6 +389,55 @@ impl Render for QuickActionBar {
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, |_| 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,
@@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};
// 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)]
@@ -18,6 +19,7 @@ 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)]
@@ -28,15 +30,25 @@ pub struct OpenZedUrl {
actions!(
zed,
[
+ /// Opens the settings editor.
OpenSettings,
+ /// Opens the default keymap file.
OpenDefaultKeymap,
+ /// Opens account settings.
OpenAccountSettings,
+ /// Opens server settings.
OpenServerSettings,
+ /// Quits the application.
Quit,
+ /// Opens the user keymap file.
OpenKeymap,
+ /// Shows information about Zed.
About,
+ /// Opens the documentation website.
OpenDocs,
+ /// Views open source licenses.
OpenLicenses,
+ /// Opens the telemetry log.
OpenTelemetryLog,
]
);
@@ -56,6 +68,7 @@ pub enum ExtensionCategoryFilter {
DebugAdapters,
}
+/// Opens the extensions management interface.
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = zed)]
#[serde(deny_unknown_fields)]
@@ -65,6 +78,7 @@ pub struct Extensions {
pub category_filter: Option<ExtensionCategoryFilter>,
}
+/// Decreases the font size in the editor buffer.
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = zed)]
#[serde(deny_unknown_fields)]
@@ -73,6 +87,7 @@ pub struct DecreaseBufferFontSize {
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)]
@@ -81,6 +96,7 @@ pub struct IncreaseBufferFontSize {
pub persist: bool,
}
+/// Resets the buffer font size to the default value.
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = zed)]
#[serde(deny_unknown_fields)]
@@ -89,6 +105,7 @@ pub struct ResetBufferFontSize {
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)]
@@ -97,6 +114,7 @@ pub struct DecreaseUiFontSize {
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)]
@@ -105,6 +123,7 @@ pub struct IncreaseUiFontSize {
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)]
@@ -116,7 +135,13 @@ pub struct ResetUiFontSize {
pub mod dev {
use gpui::actions;
- actions!(dev, [ToggleInspector]);
+ actions!(
+ dev,
+ [
+ /// Toggles the developer inspector for debugging UI elements.
+ ToggleInspector
+ ]
+ );
}
pub mod workspace {
@@ -139,9 +164,13 @@ pub mod git {
actions!(
git,
[
+ /// Checks out a different git branch.
CheckoutBranch,
+ /// Switches to a different git branch.
Switch,
+ /// Selects a different repository.
SelectRepo,
+ /// Opens the git branch selector.
#[action(deprecated_aliases = ["branches::OpenRecent"])]
Branch
]
@@ -151,25 +180,51 @@ pub mod git {
pub mod jj {
use gpui::actions;
- actions!(jj, [BookmarkList]);
+ actions!(
+ jj,
+ [
+ /// Opens the Jujutsu bookmark list.
+ BookmarkList
+ ]
+ );
}
pub mod toast {
use gpui::actions;
- actions!(toast, [RunAction]);
+ actions!(
+ toast,
+ [
+ /// Runs the action associated with a toast notification.
+ RunAction
+ ]
+ );
}
pub mod command_palette {
use gpui::actions;
- actions!(command_palette, [Toggle]);
+ actions!(
+ command_palette,
+ [
+ /// Toggles the command palette.
+ Toggle
+ ]
+ );
}
pub mod feedback {
use gpui::actions;
- actions!(feedback, [FileBugReport, GiveFeedback]);
+ actions!(
+ feedback,
+ [
+ /// Opens the bug report form.
+ FileBugReport,
+ /// Opens the feedback form.
+ GiveFeedback
+ ]
+ );
}
pub mod theme_selector {
@@ -177,6 +232,7 @@ pub mod theme_selector {
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)]
@@ -191,6 +247,7 @@ pub mod icon_theme_selector {
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)]
@@ -205,7 +262,20 @@ pub mod agent {
actions!(
agent,
- [OpenConfiguration, OpenOnboardingModal, ResetOnboarding]
+ [
+ /// Opens the agent configuration panel.
+ OpenConfiguration,
+ /// Opens the agent onboarding modal.
+ OpenOnboardingModal,
+ /// Resets the agent onboarding state.
+ ResetOnboarding,
+ /// Starts a chat conversation with the agent.
+ Chat,
+ /// Displays the previous message in the history.
+ PreviousHistoryMessage,
+ /// Displays the next message in the history.
+ NextHistoryMessage
+ ]
);
}
@@ -223,8 +293,15 @@ pub mod assistant {
]
);
- actions!(assistant, [ShowConfiguration]);
+ 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)]
@@ -233,6 +310,7 @@ pub mod assistant {
pub prompt_to_select: Option<Uuid>,
}
+ /// Deploys the assistant interface with the specified configuration.
#[derive(Clone, Default, Deserialize, PartialEq, JsonSchema, Action)]
#[action(namespace = assistant)]
#[serde(deny_unknown_fields)]
@@ -244,9 +322,18 @@ pub mod assistant {
pub mod debugger {
use gpui::actions;
- actions!(debugger, [OpenOnboardingModal, ResetOnboarding]);
+ 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)]
@@ -255,6 +342,7 @@ pub struct OpenRecent {
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)]
@@ -276,7 +364,7 @@ pub enum RevealTarget {
Dock,
}
-/// Spawn a task with name or open tasks modal.
+/// Spawns a task with name or opens tasks modal.
#[derive(Debug, PartialEq, Clone, Deserialize, JsonSchema, Action)]
#[action(namespace = task)]
#[serde(untagged)]
@@ -309,7 +397,7 @@ impl Spawn {
}
}
-/// Rerun the last task.
+/// Reruns the last task.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = task)]
#[serde(deny_unknown_fields)]
@@ -350,15 +438,36 @@ pub mod outline {
pub static TOGGLE_OUTLINE: OnceLock<fn(AnyView, &mut Window, &mut App)> = OnceLock::new();
}
-actions!(zed_predict_onboarding, [OpenZedPredictOnboarding]);
-actions!(git_onboarding, [OpenGitIntegrationOnboarding]);
+actions!(
+ zed_predict_onboarding,
+ [
+ /// Opens the Zed Predict onboarding modal.
+ OpenZedPredictOnboarding
+ ]
+);
+actions!(
+ git_onboarding,
+ [
+ /// Opens the git integration onboarding modal.
+ OpenGitIntegrationOnboarding
+ ]
+);
-actions!(debug_panel, [ToggleFocus]);
+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,
]
);
@@ -10,7 +10,15 @@ use workspace::Workspace;
use crate::{RateCompletionModal, onboarding_modal::ZedPredictModal};
-actions!(edit_prediction, [ResetOnboarding, RateCompletions]);
+actions!(
+ edit_prediction,
+ [
+ /// Resets the edit prediction onboarding state.
+ ResetOnboarding,
+ /// Opens the rate completions modal.
+ RateCompletions
+ ]
+);
pub fn init(cx: &mut App) {
cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
@@ -9,11 +9,17 @@ use workspace::{ModalView, Workspace};
actions!(
zeta,
[
+ /// Rates the active completion with a thumbs up.
ThumbsUpActiveCompletion,
+ /// Rates the active completion with a thumbs down.
ThumbsDownActiveCompletion,
+ /// Navigates to the next edit in the completion history.
NextEdit,
+ /// Navigates to the previous edit in the completion history.
PreviousEdit,
+ /// Focuses on the completions list.
FocusCompletions,
+ /// Previews the selected completion.
PreviewCompletion,
]
);
@@ -72,7 +72,13 @@ const MAX_EVENT_TOKENS: usize = 500;
/// Maximum number of events to track.
const MAX_EVENT_COUNT: usize = 16;
-actions!(edit_prediction, [ClearHistory]);
+actions!(
+ edit_prediction,
+ [
+ /// Clears the edit prediction history.
+ ClearHistory
+ ]
+);
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
pub struct InlineCompletionId(Uuid);
@@ -221,6 +221,8 @@ Alternatively, you can provide an OAuth token via the `GH_COPILOT_TOKEN` environ
> **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 inline completions), you must configure your enterprise endpoint as described in [Configuring GitHub Copilot Enterprise](./edit-prediction.md#github-copilot-enterprise).
+
### Google AI {#google-ai}
> ✅ Supports tool use
@@ -646,3 +648,35 @@ You can choose between `thread` (the default) and `text_thread`:
}
}
```
+
+### 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
+{
+ "agent": {
+ "expand_edit_card": "false"
+ }
+}
+```
+
+This setting is currently only available in Preview.
+It should be up in Stable by the next release.
+
+### 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
+{
+ "agent": {
+ "expand_terminal_card": "false"
+ }
+}
+```
+
+This setting is currently only available in Preview.
+It should be up in Stable by the next release.
@@ -267,6 +267,24 @@ To use GitHub Copilot as your provider, set this within `settings.json`:
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 {#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
+{
+ "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
@@ -221,11 +221,11 @@ Most of the servers would rely on this way of configuring only.
Apart of the LSP-related server configuration options, certain servers in Zed allow configuring the way binary is launched by Zed.
-Languages mention in the documentation, whether they support it or not and their defaults for the configuration values:
+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
- "languages": {
- "Markdown": {
+ "lsp": {
+ "rust-analyzer": {
"binary": {
// Whether to fetch the binary from the internet, or attempt to find locally.
"ignore_system_version": false,
@@ -1218,13 +1218,16 @@ or
### Drag And Drop Selection
-- Description: Whether to allow drag and drop text selection in buffer.
+- 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: `true`
-
-**Options**
+- Default:
-`boolean` values
+```json
+"drag_and_drop_selection": {
+ "enabled": true,
+ "delay": 300
+}
+```
## Editor Toolbar
@@ -2564,6 +2567,7 @@ List of `integer` column numbers
"alternate_scroll": "off",
"blinking": "terminal_controlled",
"copy_on_select": false,
+ "keep_selection_on_copy": false,
"dock": "bottom",
"default_width": 640,
"default_height": 320,
@@ -2688,6 +2692,26 @@ List of `integer` column numbers
}
```
+### 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: `false`
+
+**Options**
+
+`boolean` values
+
+**Example**
+
+```json
+{
+ "terminal": {
+ "keep_selection_on_copy": true
+ }
+}
+```
+
### 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
@@ -8,9 +8,6 @@ Zed implements the client side of the protocol, and various _debug adapters_ imp
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.
-> We currently offer onboarding support for users. We are eager to hear from you if you encounter any issues or have suggestions for improvement for our debugging experience.
-> You can schedule a call via [Cal.com](https://cal.com/team/zed-research/debugger)
-
## 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:
@@ -37,6 +37,48 @@ 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.
+
## Contributor links
- [CONTRIBUTING.md](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md)
@@ -136,6 +136,21 @@ This error seems to be caused by OS resource constraints. Installing and running
## 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.
@@ -112,29 +112,42 @@ 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::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 editor::ToggleGitBlameInline} | {#kb editor::ToggleGitBlameInline} |
+| 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::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 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).
@@ -136,17 +136,17 @@ When this happens, and both bindings are active in the current context, Zed will
### Non-QWERTY keyboards
-As of Zed 0.162.0, Zed has some support for non-QWERTY keyboards on macOS. Better support for non-QWERTY keyboards on Linux is planned.
+Zed's support for non-QWERTY keyboards is still a work in progress.
-There are roughly three categories of keyboard to consider:
+If your keyboard can type the full ASCII ranges (DVORAK, COLEMAK, etc.) then shortcuts should work as you expect.
-Keyboards that support full ASCII (QWERTY, DVORAK, COLEMAK, etc.). On these keyboards bindings are resolved based on the character that would be generated by the key. So to type `cmd-[`, find the key labeled `[` and press it with command.
+Otherwise, read on...
-Keyboards that are mostly non-ASCII, but support full ASCII when the command key is pressed. For example Cyrillic keyboards, Armenian, Hebrew, etc. On these keyboards bindings are resolved based on the character that would be generated by typing the key with command pressed. So to type `ctrl-a`, find the key that generates `cmd-a`. For these keyboards, keyboard shortcuts are displayed in the app using their ASCII equivalents. If the ASCII-equivalents are not printed on your keyboard, you can use the macOS keyboard viewer and holding down the `cmd` key to find things (though often the ASCII equivalents are in a QWERTY layout).
+#### macOS
-Finally keyboards that support extended Latin alphabets (usually ISO keyboards) require the most support. For example French AZERTY, German QWERTZ, etc. On these keyboards it is often not possible to type the entire ASCII range without option. To ensure that shortcuts _can_ be typed without option, keyboard shortcuts are mapped to "key equivalents" in the same way as [macOS](). This mapping is defined per layout, and is a compromise between leaving keyboard shortcuts triggered by the same character they are defined with, keeping shortcuts in the same place as a QWERTY layout, and moving shortcuts out of the way of system shortcuts.
+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 regardless of the `use_key_equivalents` setting. For example in Thai, pressing `ctrl-ๆ` will match bindings associated with `ctrl-q` or `ctrl-ๆ`
-For example on a German QWERTZ keyboard, the `cmd->` shortcut is moved to `cmd-:` because `cmd->` is the system window switcher and this is where that shortcut is typed on a QWERTY keyboard. `cmd-+` stays the same because + is still typeable without option, and as a result, `cmd-[` and `cmd-]` become `cmd-ö` and `cmd-ä`, moving out of the way of the `+` key.
+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 builtin 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:
@@ -161,6 +161,12 @@ If you are defining shortcuts in your personal keymap, you can opt into the key
]
```
+### 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 builtin 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 wanted!
+
## Tips and tricks
### Disabling a binding
@@ -1,12 +1,8 @@
# Java
-There are two extensions that provide Java language support for Zed:
-
-- Zed Java: [zed-extensions/java](https://github.com/zed-extensions/java) and
-- Java with Eclipse JDTLS: [zed-java-eclipse-jdtls](https://github.com/ABckh/zed-java-eclipse-jdtls).
-
-Both use:
+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)
@@ -25,11 +21,9 @@ Or manually download and install [OpenJDK 23](https://jdk.java.net/23/).
You can install either by opening {#action zed::Extensions}({#kb zed::Extensions}) and searching for `java`.
-We recommend you install one or the other and not both.
-
## Settings / Initialization Options
-Both extensions will automatically download the language server, see: [Manual JDTLS Install](#manual-jdts-install) below if you'd prefer to manage that yourself.
+The extension will automatically download the language server, see: [Manual JDTLS Install](#manual-jdts-install) below if you'd prefer to manage that yourself.
For available `initialization_options` please see the [Initialize Request section of the Eclipse.jdt.ls Wiki](https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request).
@@ -47,21 +41,25 @@ You can add these customizations to your Zed Settings by launching {#action zed:
}
```
-### Java with Eclipse JDTLS settings
+## Example Configs
+
+### JDTLS Binary
+
+By default, zed will look in your `PATH` for a `jdtls` binary, if you wish to specify an explicit binary you can do so via settings:
```json
-{
"lsp": {
- "java": {
- "settings": {},
- "initialization_options": {}
+ "jdtls": {
+ "binary": {
+ "path": "/path/to/java/bin/jdtls",
+ // "arguments": [],
+ // "env": {},
+ "ignore_system_version": true
+ }
}
}
-}
```
-## Example Configs
-
### Zed Java Initialization Options
There are also many more options you can pass directly to the language server, for example:
@@ -152,27 +150,9 @@ There are also many more options you can pass directly to the language server, f
}
```
-### Java with Eclipse JTDLS Configuration {#zed-java-eclipse-configuration}
-
-Configuration options match those provided in the [redhat-developer/vscode-java extension](https://github.com/redhat-developer/vscode-java#supported-vs-code-settings).
-
-For example, to enable [Lombok Support](https://github.com/redhat-developer/vscode-java/wiki/Lombok-support):
-
-```json
-{
- "lsp": {
- "java": {
- "settings": {
- "java.jdt.ls.lombokSupport.enabled:": true
- }
- }
- }
-}
-```
-
## Manual JDTLS Install
-If you prefer, you can install JDTLS yourself and both extensions can be configured to use that instead.
+If you prefer, you can install JDTLS yourself and the extension can be configured to use that instead.
- MacOS: `brew install jdtls`
- Arch: [`jdtls` from AUR](https://aur.archlinux.org/packages/jdtls)
@@ -184,12 +164,5 @@ Or manually download install:
## See also
-- [Zed Java Readme](https://github.com/zed-extensions/java)
-- [Java with Eclipse JDTLS Readme](https://github.com/ABckh/zed-java-eclipse-jdtls)
-
-## Support
-
-If you have issues with either of these plugins, please open issues on their respective repositories:
-
+- [Zed Java Repo](https://github.com/zed-extensions/java)
- [Zed Java Issues](https://github.com/zed-extensions/java/issues)
-- [Java with Eclipse JDTLS Issues](https://github.com/ABckh/zed-java-eclipse-jdtls/issues)
@@ -256,7 +256,7 @@ In order to do that, you need to configure the language server so that it knows
"tailwindcss-language-server": {
"settings": {
"includeLanguages": {
- "erb": "html",
+ "html/erb": "html",
"ruby": "html"
},
"experimental": {
@@ -379,3 +379,22 @@ The Ruby extension provides a debug adapter for debugging Ruby code. Zed's name
}
]
```
+
+## 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.
+
+```jsonc
+{
+ "HTML/ERB": {
+ "formatter": {
+ "external": {
+ "command": "erb-formatter",
+ "arguments": ["--stdin-filename", "{buffer_path}"],
+ },
+ },
+ },
+}
+```
@@ -112,7 +112,8 @@ To disable this behavior use:
"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_sign_in": true, // Show/hide sign-in button
+ "show_menus": false // Show/hide menus
},
```
@@ -9,12 +9,13 @@ repository = "https://github.com/zed-industries/zed"
[language_servers.emmet-language-server]
name = "Emmet Language Server"
language = "HTML"
-languages = ["HTML", "PHP", "ERB", "JavaScript", "TSX", "CSS", "HEEX", "Elixir"]
+languages = ["HTML", "PHP", "ERB", "HTML/ERB", "JavaScript", "TSX", "CSS", "HEEX", "Elixir"]
[language_servers.emmet-language-server.language_ids]
"HTML" = "html"
"PHP" = "php"
"ERB" = "eruby"
+"HTML/ERB" = "eruby"
"JavaScript" = "javascriptreact"
"TSX" = "typescriptreact"
"CSS" = "css"
@@ -1,17 +0,0 @@
-[package]
-name = "perplexity"
-version = "0.1.0"
-edition.workspace = true
-publish.workspace = true
-license = "Apache-2.0"
-
-[lib]
-path = "src/perplexity.rs"
-crate-type = ["cdylib"]
-
-[lints]
-workspace = true
-
-[dependencies]
-serde = "1"
-zed_extension_api = { path = "../../crates/extension_api" }
@@ -1 +0,0 @@
-../../LICENSE-APACHE
@@ -1,43 +0,0 @@
-# Zed Perplexity Extension
-
-This example extension adds the `/perplexity` [slash command](https://zed.dev/docs/assistant/commands) to the Zed AI assistant.
-
-## Usage
-
-Open the AI Assistant panel (`cmd-r` or `ctrl-r`) and enter:
-
-```
-/perplexity What's the weather in Boulder, CO tomorrow evening?
-```
-
-## Development Setup
-
-1. Install the Rust toolchain and clone the zed repo:
-
- ```
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
-
- mkdir -p ~/code
- cd ~/code
- git clone https://github.com/zed-industries/zed
- ```
-
-1. Open Zed
-1. Open Zed Extensions (`cmd-shift-x` / `ctrl-shift-x`)
-1. Click "Install Dev Extension"
-1. Navigate to the "extensions/perplexity" folder inside the zed git repo.
-1. Ensure your `PERPLEXITY_API_KEY` environment variable is set (instructions below)
-
- ```sh
- env | grep PERPLEXITY_API_KEY
- ```
-
-1. Quit and relaunch Zed
-
-## PERPLEXITY_API_KEY
-
-This extension requires a Perplexity API key to be available via the `PERPLEXITY_API_KEY` environment variable.
-
-To obtain a Perplexity.ai API token, login to your Perplexity.ai account and go [Settings->API](https://www.perplexity.ai/settings/api) and under "API Keys" click "Generate". This will require you to have [Perplexity Pro](https://www.perplexity.ai/pro) or to buy API credits. By default the extension uses `llama-3.1-sonar-small-128k-online`, currently cheapest model available which is roughly half a penny per request + a penny per 50,000 tokens. So most requests will cost less than $0.01 USD.
-
-Take your API key and add it to your environment by adding `export PERPLEXITY_API_KEY="pplx-0123456789abcdef..."` to your `~/.zshrc` or `~/.bashrc`. Reload close and reopen your terminal session. Check with `env |grep PERPLEXITY_API_KEY`.
@@ -1,12 +0,0 @@
-id = "perplexity"
-name = "Perplexity"
-version = "0.1.0"
-description = "Ask questions to Perplexity AI directly from Zed"
-authors = ["Zed Industries <support@zed.dev>"]
-repository = "https://github.com/zed-industries/zed"
-schema_version = 1
-
-[slash_commands.perplexity]
-description = "Ask a question to Perplexity AI"
-requires_argument = true
-tooltip_text = "Ask Perplexity"
@@ -1,158 +0,0 @@
-use zed::{
- http_client::HttpMethod,
- http_client::HttpRequest,
- serde_json::{self, json},
-};
-use zed_extension_api::{self as zed, Result, http_client::RedirectPolicy};
-
-struct Perplexity;
-
-impl zed::Extension for Perplexity {
- fn new() -> Self {
- Self
- }
-
- fn run_slash_command(
- &self,
- command: zed::SlashCommand,
- argument: Vec<String>,
- worktree: Option<&zed::Worktree>,
- ) -> zed::Result<zed::SlashCommandOutput> {
- // Check if the command is 'perplexity'
- if command.name != "perplexity" {
- return Err("Invalid command. Expected 'perplexity'.".into());
- }
-
- let worktree = worktree.ok_or("Worktree is required")?;
- // Join arguments with space as the query
- let query = argument.join(" ");
- if query.is_empty() {
- return Ok(zed::SlashCommandOutput {
- text: "Error: Query not provided. Please enter a question or topic.".to_string(),
- sections: vec![],
- });
- }
-
- // Get the API key from the environment
- let env_vars = worktree.shell_env();
- let api_key = env_vars
- .iter()
- .find(|(key, _)| key == "PERPLEXITY_API_KEY")
- .map(|(_, value)| value.clone())
- .ok_or("PERPLEXITY_API_KEY not found in environment")?;
-
- // Prepare the request
- let request = HttpRequest {
- method: HttpMethod::Post,
- url: "https://api.perplexity.ai/chat/completions".to_string(),
- headers: vec![
- ("Authorization".to_string(), format!("Bearer {}", api_key)),
- ("Content-Type".to_string(), "application/json".to_string()),
- ],
- body: Some(
- serde_json::to_vec(&json!({
- "model": "llama-3.1-sonar-small-128k-online",
- "messages": [{"role": "user", "content": query}],
- "stream": true,
- }))
- .unwrap(),
- ),
- redirect_policy: RedirectPolicy::FollowAll,
- };
-
- // Make the HTTP request
- match zed::http_client::fetch_stream(&request) {
- Ok(stream) => {
- let mut full_content = String::new();
- let mut buffer = String::new();
- while let Ok(Some(chunk)) = stream.next_chunk() {
- buffer.push_str(&String::from_utf8_lossy(&chunk));
- for line in buffer.lines() {
- if let Some(json) = line.strip_prefix("data: ") {
- if let Ok(event) = serde_json::from_str::<StreamEvent>(json) {
- if let Some(choice) = event.choices.first() {
- full_content.push_str(&choice.delta.content);
- }
- }
- }
- }
- buffer.clear();
- }
- Ok(zed::SlashCommandOutput {
- text: full_content,
- sections: vec![],
- })
- }
- Err(e) => Ok(zed::SlashCommandOutput {
- text: format!("API request failed. Error: {}. API Key: {}", e, api_key),
- sections: vec![],
- }),
- }
- }
-
- fn complete_slash_command_argument(
- &self,
- _command: zed::SlashCommand,
- query: Vec<String>,
- ) -> zed::Result<Vec<zed::SlashCommandArgumentCompletion>> {
- let suggestions = vec!["How do I develop a Zed extension?"];
- let query = query.join(" ").to_lowercase();
-
- Ok(suggestions
- .into_iter()
- .filter(|suggestion| suggestion.to_lowercase().contains(&query))
- .map(|suggestion| zed::SlashCommandArgumentCompletion {
- label: suggestion.to_string(),
- new_text: suggestion.to_string(),
- run_command: true,
- })
- .collect())
- }
-
- fn language_server_command(
- &mut self,
- _language_server_id: &zed_extension_api::LanguageServerId,
- _worktree: &zed_extension_api::Worktree,
- ) -> Result<zed_extension_api::Command> {
- Err("Not implemented".into())
- }
-}
-
-#[derive(serde::Deserialize)]
-struct StreamEvent {
- id: String,
- model: String,
- created: u64,
- usage: Usage,
- object: String,
- choices: Vec<Choice>,
-}
-
-#[derive(serde::Deserialize)]
-struct Usage {
- prompt_tokens: u32,
- completion_tokens: u32,
- total_tokens: u32,
-}
-
-#[derive(serde::Deserialize)]
-struct Choice {
- index: u32,
- finish_reason: Option<String>,
- message: Message,
- delta: Delta,
-}
-
-#[derive(serde::Deserialize)]
-struct Message {
- role: String,
- content: String,
-}
-
-#[derive(serde::Deserialize)]
-struct Delta {
- role: String,
- content: String,
-}
-
-zed::register_extension!(Perplexity);
@@ -0,0 +1,271 @@
+[CmdletBinding()]
+Param(
+ [Parameter()][Alias('i')][switch]$Install,
+ [Parameter()][Alias('h')][switch]$Help,
+ [Parameter()][string]$Name
+)
+
+. "$PSScriptRoot/lib/blob-store.ps1"
+. "$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
+
+if ($Help) {
+ Write-Output "Usage: test.ps1 [-Install] [-Help]"
+ Write-Output "Build the installer for Windows.\n"
+ Write-Output "Options:"
+ 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
+Pop-Location
+
+function CheckEnvironmentVariables {
+ $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
+}
+
+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
+ Copy-Item -Path ".\target\release\zed.exe" -Destination "$innoDir\Zed.exe" -Force
+ Copy-Item -Path ".\target\release\cli.exe" -Destination "$innoDir\cli.exe" -Force
+ Copy-Item -Path ".\target\release\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
+ }
+ "preview" {
+ cargo build --release --features preview --no-default-features --package explorer_command_injector
+ }
+ default {
+ cargo build --release --package explorer_command_injector
+ }
+ }
+ Copy-Item -Path ".\target\release\explorer_command_injector.dll" -Destination "$innoDir\zed_explorer_command_injector.dll" -Force
+}
+
+function ZipZedAndItsFriendsDebug {
+ $items = @(
+ ".\target\release\zed.pdb",
+ ".\target\release\cli.pdb",
+ ".\target\release\auto_update_helper.pdb",
+ ".\target\release\explorer_command_injector.pdb"
+ )
+
+ Compress-Archive -Path $items -DestinationPath ".\target\release\zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" -Force
+}
+
+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 {
+ $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 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\auto_update_helper.exe" -Destination "$innoDir\tools\auto_update_helper.exe" -Force
+}
+
+function BuildInstaller {
+ $issFilePath = "$innoDir\zed.iss"
+ switch ($channel) {
+ "stable" {
+ $appId = "{{2DB0DA96-CA55-49BB-AF4F-64AF36A86712}"
+ $appIconName = "app-icon"
+ $appName = "Zed"
+ $appDisplayName = "Zed"
+ $appSetupName = "ZedEditorUserSetup-x64-$env:RELEASE_VERSION"
+ # 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 = "ZedEditorUserSetup-x64-$env:RELEASE_VERSION-preview"
+ # 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 = "ZedEditorUserSetup-x64-$env:RELEASE_VERSION-nightly"
+ # 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 = "ZedEditorUserSetup-x64-$env:RELEASE_VERSION-dev"
+ # 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 = "iscc.exe"
+ $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
+ }
+
+ $signTool = "powershell.exe -ExecutionPolicy Bypass -File $innoDir\sign.ps1 `$f"
+
+ $defs = @()
+ foreach ($key in $definitions.Keys) {
+ $defs += "/d$key=`"$($definitions[$key])`""
+ }
+
+ $innoArgs = @($issFilePath) + $defs + "/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"
+
+CheckEnvironmentVariables
+PrepareForBundle
+GenerateLicenses
+BuildZedAndItsFriends
+MakeAppx
+SignZedAndItsFriends
+ZipZedAndItsFriendsDebug
+CollectFiles
+BuildInstaller
+
+$debugArchive = ".\target\release\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"
+UploadToBlobStorePublic -BucketName "zed-debug-symbols" -FileToUpload $debugArchive -BlobStoreKey $debugStoreKey
+
+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
+}
@@ -18,5 +18,5 @@ Write-Host "target directory size: ${current_size_gb}GB. max size: ${MAX_SIZE_IN
if ($current_size_gb -gt $MAX_SIZE_IN_GB) {
Write-Host "clearing target directory"
- Remove-Item -Recurse -Force -Path "target\*"
+ Remove-Item -Recurse -Force -Path "target\*" -ErrorAction SilentlyContinue
}
@@ -94,3 +94,24 @@ for (const promptPath of modifiedPrompts) {
);
}
}
+
+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"),
+ );
+ }
+}
@@ -0,0 +1,37 @@
+$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
+}
@@ -6,6 +6,15 @@ CARGO_ABOUT_VERSION="0.7"
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"
{
@@ -27,8 +36,9 @@ 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
-cargo about generate \
+$WRAPPER cargo about generate \
$FAIL_FLAG \
-c script/licenses/zed-licenses.toml \
"$TEMPLATE_FILE" >>"$OUTPUT_FILE"
@@ -0,0 +1,44 @@
+$CARGO_ABOUT_VERSION="0.7"
+$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"
@@ -0,0 +1,16 @@
+if ($args.Length -ne 1) {
+ Write-Error "Usage: $($MyInvocation.MyCommand.Name) <crate_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."
+}
@@ -0,0 +1,68 @@
+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"
+}
@@ -0,0 +1,6 @@
+
+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
+}
@@ -177,9 +177,3 @@ license = "MIT"
[[pet-windows-store.clarify.files]]
path = '../../LICENSE'
checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[ring.clarify]
-license = "ISC AND OpenSSL"
-[[ring.clarify.files]]
-path = 'LICENSE'
-checksum = '76b39f9b371688eac9d8323f96ee80b3aef5ecbc2217f25377bd4e4a615296a9'
@@ -11,24 +11,29 @@ fi
input_file=$1;
if [[ "$input_file" == *.json ]]; then
- version=$(cat $input_file | jq -r .app_version)
- channel=$(cat $input_file | jq -r .release_channel)
- target_triple=$(cat $input_file | jq -r .target)
+ version=$(cat $input_file | jq -r .panic.app_version)
+ channel=$(cat $input_file | jq -r .panic.release_channel)
+ target_triple=$(cat $input_file | jq -r .panic.target)
- which llvm-symbolizer rustfilt >dev/null || echo Need to install llvm-symbolizer and rustfilt
+ which llvm-symbolizer rustfilt >/dev/null || (echo Need to install llvm-symbolizer and rustfilt && exit 1)
echo $channel;
mkdir -p target/dsyms/$channel
- dsym="$channel/zed-$version-$target_triple.dbg"
+ if [[ "$version" == "remote-server-"* ]]; then
+ version="${version#remote-server-}"
+ dsym="$channel/remote_server-$version-$target_triple.dbg"
+ else
+ dsym="$channel/zed-$version-$target_triple.dbg"
+ fi
if [[ ! -f target/dsyms/$dsym ]]; then
echo "Downloading $dsym..."
curl -o target/dsyms/$dsym.gz "https://zed-debug-symbols.nyc3.digitaloceanspaces.com/$dsym.gz"
gunzip target/dsyms/$dsym.gz
fi
- cat $input_file | jq -r .backtrace[] | sed s'/.*+//' | llvm-symbolizer --no-demangle --obj=target/dsyms/$dsym | rustfilt
+ cat $input_file | jq -r .panic.backtrace[] | sed s'/.*+//' | llvm-symbolizer --no-demangle --obj=target/dsyms/$dsym | rustfilt
else # ips file
@@ -0,0 +1,60 @@
+# Based on the template in: https://docs.digitalocean.com/reference/api/spaces-api/
+$ErrorActionPreference = "Stop"
+. "$PSScriptRoot\lib\blob-store.ps1"
+. "$PSScriptRoot\lib\workspace.ps1"
+
+$allowedTargets = @("windows")
+
+function Test-AllowedTarget {
+ param (
+ [string]$Target
+ )
+
+ return $allowedTargets -contains $Target
+}
+
+# Process arguments
+if ($args.Count -gt 0) {
+ $target = $args[0]
+ if (Test-AllowedTarget $target) {
+ # Valid target
+ } else {
+ Write-Error "Error: Target '$target' is not allowed.`nUsage: $($MyInvocation.MyCommand.Name) [$($allowedTargets -join ', ')]"
+ exit 1
+ }
+} else {
+ Write-Error "Error: Target is not specified.`nUsage: $($MyInvocation.MyCommand.Name) [$($allowedTargets -join ', ')]"
+ exit 1
+}
+
+ParseZedWorkspace
+Write-Host "Uploading nightly for target: $target"
+
+$bucketName = "zed-nightly-host"
+
+# Get current git SHA
+$sha = git rev-parse HEAD
+$sha | Out-File -FilePath "target/latest-sha" -NoNewline
+
+# 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
+# }
+
+switch ($target) {
+ "windows" {
+ UploadToBlobStore -BucketName $bucketName -FileToUpload $env:SETUP_PATH -BlobStoreKey "nightly/zed_editor_installer_x86_64.exe"
+ UploadToBlobStore -BucketName $bucketName -FileToUpload "target/latest-sha" -BlobStoreKey "nightly/latest-sha-windows"
+
+ Remove-Item -Path $env:SETUP_PATH -ErrorAction SilentlyContinue
+ Remove-Item -Path "target/latest-sha" -ErrorAction SilentlyContinue
+ }
+
+ default {
+ Write-Error "Error: Unknown target '$target'"
+ exit 1
+ }
+}
@@ -107,6 +107,7 @@ rustc-hash = { version = "1" }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] }
rustls = { version = "0.23", features = ["ring"] }
rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] }
+schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] }
sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] }
sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] }
semver = { version = "1", features = ["serde"] }
@@ -239,6 +240,7 @@ rustc-hash = { version = "1" }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] }
rustls = { version = "0.23", features = ["ring"] }
rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] }
+schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] }
sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] }
sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] }
semver = { version = "1", features = ["serde"] }
@@ -427,6 +429,7 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
+scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] }
scopeguard = { version = "1" }
syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
@@ -466,6 +469,7 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
+scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] }
scopeguard = { version = "1" }
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
@@ -505,6 +509,7 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
+scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] }
scopeguard = { version = "1" }
syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
@@ -544,6 +549,7 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
+scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] }
scopeguard = { version = "1" }
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
@@ -565,6 +571,7 @@ itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
naga = { version = "25", features = ["spv-out", "wgsl-in"] }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] }
+scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] }
scopeguard = { version = "1" }
sync_wrapper = { version = "1", default-features = false, features = ["futures"] }
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
@@ -572,7 +579,9 @@ tokio-socks = { version = "0.5", features = ["futures-io"] }
tokio-stream = { version = "0.1", features = ["fs"] }
tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "errhandlingapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] }
@@ -46,6 +46,8 @@ extend-exclude = [
"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