Merge branch 'main' into helix-match-fix to fix the tests
|
@ -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",
|
||||
|
|
64
.github/actions/install_trusted_signing/action.yml
vendored
Normal file
|
@ -0,0 +1,64 @@
|
|||
name: "Trusted Signing on Windows"
|
||||
description: "Install trusted signing on Windows."
|
||||
|
||||
# Modified from https://github.com/Azure/trusted-signing-action
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set variables
|
||||
id: set-variables
|
||||
shell: "pwsh"
|
||||
run: |
|
||||
$defaultPath = $env:PSModulePath -split ';' | Select-Object -First 1
|
||||
"PSMODULEPATH=$defaultPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
|
||||
"TRUSTED_SIGNING_MODULE_VERSION=0.5.3" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
"BUILD_TOOLS_NUGET_VERSION=10.0.22621.3233" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
"TRUSTED_SIGNING_NUGET_VERSION=1.0.53" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
"DOTNET_SIGNCLI_NUGET_VERSION=0.9.1-beta.24469.1" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
|
||||
- name: Cache TrustedSigning PowerShell module
|
||||
id: cache-module
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-module
|
||||
with:
|
||||
path: ${{ steps.set-variables.outputs.PSMODULEPATH }}\TrustedSigning\${{ steps.set-variables.outputs.TRUSTED_SIGNING_MODULE_VERSION }}
|
||||
key: TrustedSigning-${{ steps.set-variables.outputs.TRUSTED_SIGNING_MODULE_VERSION }}
|
||||
if: ${{ inputs.cache-dependencies == 'true' }}
|
||||
|
||||
- name: Cache Microsoft.Windows.SDK.BuildTools NuGet package
|
||||
id: cache-buildtools
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-buildtools
|
||||
with:
|
||||
path: ~\AppData\Local\TrustedSigning\Microsoft.Windows.SDK.BuildTools\Microsoft.Windows.SDK.BuildTools.${{ steps.set-variables.outputs.BUILD_TOOLS_NUGET_VERSION }}
|
||||
key: Microsoft.Windows.SDK.BuildTools-${{ steps.set-variables.outputs.BUILD_TOOLS_NUGET_VERSION }}
|
||||
if: ${{ inputs.cache-dependencies == 'true' }}
|
||||
|
||||
- name: Cache Microsoft.Trusted.Signing.Client NuGet package
|
||||
id: cache-tsclient
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-tsclient
|
||||
with:
|
||||
path: ~\AppData\Local\TrustedSigning\Microsoft.Trusted.Signing.Client\Microsoft.Trusted.Signing.Client.${{ steps.set-variables.outputs.TRUSTED_SIGNING_NUGET_VERSION }}
|
||||
key: Microsoft.Trusted.Signing.Client-${{ steps.set-variables.outputs.TRUSTED_SIGNING_NUGET_VERSION }}
|
||||
if: ${{ inputs.cache-dependencies == 'true' }}
|
||||
|
||||
- name: Cache SignCli NuGet package
|
||||
id: cache-signcli
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-signcli
|
||||
with:
|
||||
path: ~\AppData\Local\TrustedSigning\sign\sign.${{ steps.set-variables.outputs.DOTNET_SIGNCLI_NUGET_VERSION }}
|
||||
key: SignCli-${{ steps.set-variables.outputs.DOTNET_SIGNCLI_NUGET_VERSION }}
|
||||
if: ${{ inputs.cache-dependencies == 'true' }}
|
||||
|
||||
- name: Install Trusted Signing module
|
||||
shell: "pwsh"
|
||||
run: |
|
||||
Install-Module -Name TrustedSigning -RequiredVersion ${{ steps.set-variables.outputs.TRUSTED_SIGNING_MODULE_VERSION }} -Force -Repository PSGallery
|
||||
if: ${{ inputs.cache-dependencies != 'true' || steps.cache-module.outputs.cache-hit != 'true' }}
|
75
.github/workflows/ci.yml
vendored
|
@ -411,11 +411,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 +429,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
|
||||
|
@ -763,12 +753,67 @@ 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 }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
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: Install trusted signing
|
||||
uses: ./.github/actions/install_trusted_signing
|
||||
|
||||
- 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
|
||||
if: ${{ !(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
|
||||
|
|
71
.github/workflows/release_nightly.yml
vendored
|
@ -51,6 +51,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
|
||||
|
@ -213,10 +239,54 @@ 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 }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
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: Install trusted signing
|
||||
uses: ./.github/actions/install_trusted_signing
|
||||
|
||||
- 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 +295,7 @@ jobs:
|
|||
- bundle-mac
|
||||
- bundle-linux-x86
|
||||
- bundle-linux-arm
|
||||
- bundle-windows-x64
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
|
112
Cargo.lock
generated
|
@ -2,6 +2,33 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "acp"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agent_servers",
|
||||
"agentic-coding-protocol",
|
||||
"anyhow",
|
||||
"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 +134,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 +175,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 +239,7 @@ dependencies = [
|
|||
"settings",
|
||||
"smol",
|
||||
"streaming_diff",
|
||||
"task",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"terminal",
|
||||
|
@ -212,6 +261,22 @@ dependencies = [
|
|||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agentic-coding-protocol"
|
||||
version = "0.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b962eee17ee3924870d9b9d28cc8b6dcb5421e4d4e81cd864226374a122ceed1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"futures 0.3.31",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.8"
|
||||
|
@ -538,6 +603,8 @@ dependencies = [
|
|||
"anyhow",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"net",
|
||||
"parking_lot",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"util",
|
||||
|
@ -5189,6 +5256,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"
|
||||
|
@ -8953,6 +9030,7 @@ dependencies = [
|
|||
"credentials_provider",
|
||||
"deepseek",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"google_ai",
|
||||
|
@ -10231,6 +10309,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"
|
||||
|
@ -11336,14 +11426,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"
|
||||
|
@ -12535,6 +12617,7 @@ dependencies = [
|
|||
"prost 0.9.0",
|
||||
"prost-build 0.9.0",
|
||||
"serde",
|
||||
"typed-path",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
|
@ -13198,6 +13281,7 @@ dependencies = [
|
|||
"fs",
|
||||
"futures 0.3.31",
|
||||
"git",
|
||||
"git2",
|
||||
"git_hosting_providers",
|
||||
"gpui",
|
||||
"gpui_tokio",
|
||||
|
@ -14059,6 +14143,7 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dyn-clone",
|
||||
"indexmap",
|
||||
"ref-cast",
|
||||
|
@ -17036,6 +17121,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"
|
||||
|
@ -18282,6 +18373,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
|
@ -19553,6 +19645,7 @@ dependencies = [
|
|||
"rustix 1.0.7",
|
||||
"rustls 0.23.26",
|
||||
"rustls-webpki 0.103.1",
|
||||
"schemars",
|
||||
"scopeguard",
|
||||
"sea-orm",
|
||||
"sea-query-binder",
|
||||
|
@ -19946,10 +20039,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.195.0"
|
||||
version = "0.196.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
"agent_servers",
|
||||
"agent_settings",
|
||||
"agent_ui",
|
||||
"anyhow",
|
||||
|
|
14
Cargo.toml
|
@ -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" }
|
||||
|
@ -311,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" }
|
||||
|
@ -398,6 +404,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
|||
# External crates
|
||||
#
|
||||
|
||||
agentic-coding-protocol = "0.0.5"
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
|
@ -624,6 +631,8 @@ 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.6"
|
||||
zstd = "0.11"
|
||||
|
@ -660,6 +669,7 @@ features = [
|
|||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Imaging",
|
||||
"Win32_Graphics_Imaging_D2D",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_Security",
|
||||
"Win32_Security_Credentials",
|
||||
"Win32_Storage_FileSystem",
|
||||
|
|
1
assets/icons/ai_gemini.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 402 B |
|
@ -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>
|
||||
|
|
Before Width: | Height: | Size: 601 B After Width: | Height: | Size: 1 KiB |
3
assets/icons/tool_bulb.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 776 B |
3
assets/icons/tool_folder.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 778 B |
5
assets/icons/tool_hammer.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 988 B |
4
assets/icons/tool_pencil.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 835 B |
4
assets/icons/tool_regex.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 631 B |
4
assets/icons/tool_search.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 421 B |
5
assets/icons/tool_terminal.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 625 B |
17
assets/icons/tool_web.svg
Normal file
|
@ -0,0 +1,17 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2663_433)">
|
||||
<mask id="mask0_2663_433" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
|
||||
<path d="M16 0H0V16H16V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2663_433)">
|
||||
<path d="M8 13C10.7614 13 13 10.7614 13 7.99999C13 5.23857 10.7614 3 8 3C5.23857 3 3 5.23857 3 7.99999C3 10.7614 5.23857 13 8 13Z" 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 8H13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2663_433">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1,001 B |
|
@ -306,6 +306,15 @@
|
|||
"enter": "agent::AcceptSuggestedContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"up": "agent::PreviousHistoryMessage",
|
||||
"down": "agent::NextHistoryMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"bindings": {
|
||||
|
|
|
@ -357,6 +357,15 @@
|
|||
"ctrl--": "pane::GoBack"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"up": "agent::PreviousHistoryMessage",
|
||||
"down": "agent::NextHistoryMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"bindings": {
|
||||
|
|
|
@ -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",
|
||||
|
@ -364,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",
|
||||
|
@ -390,27 +365,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",
|
||||
|
@ -430,7 +418,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",
|
||||
|
|
|
@ -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": {
|
||||
|
@ -861,7 +868,11 @@
|
|||
/// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
|
||||
///
|
||||
/// Default: true
|
||||
"expand_edit_card": 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": {
|
||||
|
@ -1599,6 +1610,9 @@
|
|||
"use_on_type_format": false,
|
||||
"allow_rewrap": "anywhere",
|
||||
"soft_wrap": "editor_width",
|
||||
"completions": {
|
||||
"words": "disabled"
|
||||
},
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
|
@ -1612,6 +1626,9 @@
|
|||
}
|
||||
},
|
||||
"Plain Text": {
|
||||
"completions": {
|
||||
"words": "disabled"
|
||||
},
|
||||
"allow_rewrap": "anywhere"
|
||||
},
|
||||
"Python": {
|
||||
|
@ -1840,6 +1857,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,
|
||||
|
|
46
crates/acp/Cargo.toml
Normal file
|
@ -0,0 +1,46 @@
|
|||
[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
|
||||
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
|
1
crates/acp/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE-GPL
|
1625
crates/acp/src/acp.rs
Normal file
27
crates/agent_servers/Cargo.toml
Normal file
|
@ -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
|
1
crates/agent_servers/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE-GPL
|
231
crates/agent_servers/src/agent_servers.rs
Normal file
|
@ -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) {}
|
||||
}
|
|
@ -68,6 +68,7 @@ pub struct AgentSettings {
|
|||
pub preferred_completion_mode: CompletionMode,
|
||||
pub enable_feedback: bool,
|
||||
pub expand_edit_card: bool,
|
||||
pub expand_terminal_card: bool,
|
||||
}
|
||||
|
||||
impl AgentSettings {
|
||||
|
@ -296,6 +297,10 @@ pub struct AgentSettingsContent {
|
|||
///
|
||||
/// 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)]
|
||||
|
@ -447,6 +452,10 @@ impl Settings for AgentSettings {
|
|||
);
|
||||
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
|
||||
|
|
5
crates/agent_ui/src/acp.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod completion_provider;
|
||||
mod message_history;
|
||||
mod thread_view;
|
||||
|
||||
pub use thread_view::AcpThreadView;
|
574
crates/agent_ui/src/acp/completion_provider.rs
Normal file
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
81
crates/agent_ui/src/acp/message_history.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
pub struct MessageHistory<T> {
|
||||
items: Vec<T>,
|
||||
current: Option<usize>,
|
||||
}
|
||||
|
||||
impl<T> MessageHistory<T> {
|
||||
pub fn new() -> Self {
|
||||
MessageHistory {
|
||||
items: Vec::new(),
|
||||
current: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, message: T) {
|
||||
self.current.take();
|
||||
self.items.push(message);
|
||||
}
|
||||
|
||||
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::new();
|
||||
|
||||
// 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"));
|
||||
}
|
||||
}
|
1972
crates/agent_ui/src/acp/thread_view.rs
Normal file
|
@ -740,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
|
||||
})
|
||||
|
|
|
@ -7,12 +7,14 @@ use std::time::Duration;
|
|||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::NewGeminiThread;
|
||||
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 +40,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 +112,12 @@ pub fn init(cx: &mut App) {
|
|||
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &NewGeminiThread, 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 +134,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 +198,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 +220,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 +253,7 @@ impl ActiveView {
|
|||
thread.scroll_to_bottom(cx);
|
||||
});
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {}
|
||||
ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => {}
|
||||
|
@ -653,7 +669,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 => {}
|
||||
},
|
||||
|
@ -733,6 +750,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,7 +760,10 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -862,6 +885,21 @@ 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();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let thread_view = cx.new_window_entity(|window, cx| {
|
||||
crate::acp::AcpThreadView::new(workspace, project, window, cx)
|
||||
})?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_active_view(ActiveView::AcpThread { thread_view }, window, cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn deploy_rules_library(
|
||||
&mut self,
|
||||
action: &OpenRulesLibrary,
|
||||
|
@ -994,6 +1032,7 @@ impl AgentPanel {
|
|||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
self.fs.clone(),
|
||||
|
@ -1025,6 +1064,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);
|
||||
}
|
||||
|
@ -1144,7 +1186,10 @@ impl AgentPanel {
|
|||
})
|
||||
.log_err();
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
||||
ActiveView::AcpThread { .. }
|
||||
| ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1197,6 +1242,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 +1403,8 @@ impl AgentPanel {
|
|||
}
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
ActiveView::AcpThread { .. } => {}
|
||||
ActiveView::History | ActiveView::Configuration => {}
|
||||
}
|
||||
|
||||
if current_is_special && !new_is_special {
|
||||
|
@ -1437,6 +1490,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 +1647,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,7 +1784,10 @@ 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")
|
||||
|
@ -1755,6 +1815,9 @@ impl AgentPanel {
|
|||
menu = menu
|
||||
.action("New Thread", NewThread::default().boxed_clone())
|
||||
.action("New Text Thread", NewTextThread.boxed_clone())
|
||||
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
|
||||
this.action("New Gemini Thread", NewGeminiThread.boxed_clone())
|
||||
})
|
||||
.when_some(active_thread, |this, active_thread| {
|
||||
let thread = active_thread.read(cx);
|
||||
if !thread.is_empty() {
|
||||
|
@ -1893,6 +1956,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 +2097,9 @@ impl AgentPanel {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {
|
||||
return false;
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
|
||||
return false;
|
||||
}
|
||||
|
@ -2615,6 +2684,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 +3033,9 @@ impl AgentPanel {
|
|||
.detach();
|
||||
});
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {
|
||||
unimplemented!()
|
||||
}
|
||||
ActiveView::TextThread { context_editor, .. } => {
|
||||
context_editor.update(cx, |context_editor, cx| {
|
||||
TextThreadEditor::insert_dragged_files(
|
||||
|
@ -3034,6 +3109,7 @@ impl Render for AgentPanel {
|
|||
});
|
||||
this.continue_conversation(window, cx);
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {}
|
||||
ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => {}
|
||||
|
@ -3075,6 +3151,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;
|
||||
|
@ -56,6 +57,8 @@ actions!(
|
|||
[
|
||||
/// Creates a new text-based conversation thread.
|
||||
NewTextThread,
|
||||
/// Creates a new Gemini CLI-based conversation thread.
|
||||
NewGeminiThread,
|
||||
/// Toggles the context picker interface for adding files, symbols, or other context.
|
||||
ToggleContextPicker,
|
||||
/// Toggles the navigation menu for switching between threads and views.
|
||||
|
@ -76,8 +79,6 @@ actions!(
|
|||
AddContextServer,
|
||||
/// Removes the currently selected thread.
|
||||
RemoveSelectedThread,
|
||||
/// Starts a chat conversation with the agent.
|
||||
Chat,
|
||||
/// Starts a chat conversation with follow-up enabled.
|
||||
ChatWithFollow,
|
||||
/// Cycles to the next inline assist suggestion.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -47,13 +47,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,
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
@ -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()
|
||||
|
|
|
@ -638,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}"),
|
||||
}?;
|
||||
|
||||
|
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 577 KiB |
|
@ -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();
|
||||
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -1066,7 +1066,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(
|
||||
|
|
|
@ -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<()>,
|
||||
|
@ -2174,7 +2201,6 @@ 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 let Some(breakpoints) = editor.breakpoint_store.as_ref() {
|
||||
|
@ -19871,7 +19897,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.
|
||||
///
|
||||
|
|
|
@ -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)]
|
||||
|
@ -644,7 +643,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));
|
||||
|
@ -1022,7 +1025,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,
|
||||
|
@ -1611,6 +1617,7 @@ impl EditorElement {
|
|||
strikethrough: None,
|
||||
underline: None,
|
||||
}],
|
||||
None,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
|
@ -3263,10 +3270,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,
|
||||
|
@ -5707,6 +5716,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 { .. }
|
||||
|
@ -6888,6 +6910,7 @@ impl EditorElement {
|
|||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
None,
|
||||
);
|
||||
|
||||
layout.width
|
||||
|
@ -6916,6 +6939,7 @@ impl EditorElement {
|
|||
text,
|
||||
self.style.text.font_size.to_pixels(window.rem_size()),
|
||||
&[run],
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -7184,10 +7208,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));
|
||||
|
@ -7207,6 +7233,7 @@ impl LineWithInvisibles {
|
|||
chunk,
|
||||
font_size,
|
||||
&[text_style.to_run(highlighted_chunk.text.len())],
|
||||
None,
|
||||
);
|
||||
AvailableSpace::Definite(shaped_line.width)
|
||||
} else {
|
||||
|
@ -7251,7 +7278,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;
|
||||
|
@ -7266,6 +7293,7 @@ impl LineWithInvisibles {
|
|||
line.clone().into(),
|
||||
font_size,
|
||||
&styles,
|
||||
None,
|
||||
);
|
||||
width += shaped_line.width;
|
||||
len += shaped_line.len;
|
||||
|
@ -7935,6 +7963,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,
|
||||
|
@ -8440,6 +8469,7 @@ impl Element for EditorElement {
|
|||
scroll_width,
|
||||
em_advance,
|
||||
&line_layouts,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
|
@ -8594,6 +8624,7 @@ impl Element for EditorElement {
|
|||
scroll_width,
|
||||
em_advance,
|
||||
&line_layouts,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
|
@ -8831,6 +8862,7 @@ impl Element for EditorElement {
|
|||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
None
|
||||
);
|
||||
let space_invisible = window.text_system().shape_line(
|
||||
"•".into(),
|
||||
|
@ -8843,6 +8875,7 @@ impl Element for EditorElement {
|
|||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
None
|
||||
);
|
||||
|
||||
let mode = snapshot.mode.clone();
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -1607,24 +1607,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 {
|
||||
|
|
|
@ -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;
|
||||
|
@ -151,12 +152,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 +178,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,
|
||||
}
|
||||
|
@ -210,7 +216,7 @@ impl ScrollManager {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let (new_anchor, top_row) = if scroll_position.y <= 0. {
|
||||
let (new_anchor, top_row) = if scroll_position.y <= 0. && scroll_position.x <= 0. {
|
||||
(
|
||||
ScrollAnchor {
|
||||
anchor: Anchor::min(),
|
||||
|
@ -218,6 +224,22 @@ impl ScrollManager {
|
|||
},
|
||||
0,
|
||||
)
|
||||
} else if scroll_position.y <= 0. {
|
||||
let buffer_point = map
|
||||
.clip_point(
|
||||
DisplayPoint::new(DisplayRow(0), scroll_position.x as u32),
|
||||
Bias::Left,
|
||||
)
|
||||
.to_point(map);
|
||||
let anchor = map.buffer_snapshot.anchor_at(buffer_point, Bias::Right);
|
||||
|
||||
(
|
||||
ScrollAnchor {
|
||||
anchor: anchor,
|
||||
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 {
|
||||
|
@ -242,8 +264,13 @@ impl ScrollManager {
|
|||
}
|
||||
};
|
||||
|
||||
let scroll_top_buffer_point =
|
||||
DisplayPoint::new(DisplayRow(scroll_top as u32), 0).to_point(map);
|
||||
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);
|
||||
|
@ -476,6 +503,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 +528,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>,
|
||||
|
@ -675,25 +710,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 +769,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -274,12 +274,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 {
|
||||
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 +297,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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -319,14 +322,16 @@ impl Editor {
|
|||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
scroll_position.x = target_left / em_advance;
|
||||
self.set_scroll_position_internal(scroll_position, true, true, window, cx);
|
||||
true
|
||||
} else if target_right > scroll_right {
|
||||
self.scroll_manager.anchor.offset.x = (target_right - viewport_width) / max_glyph_width;
|
||||
scroll_position.x = (target_right - viewport_width) / em_advance;
|
||||
self.set_scroll_position_internal(scroll_position, true, true, window, cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
78
crates/explorer_command_injector/AppxManifest-Nightly.xml
Normal file
|
@ -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 Editor 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 Editor Nightly"
|
||||
Description="Zed Editor 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 Editor 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>
|
78
crates/explorer_command_injector/AppxManifest-Preview.xml
Normal file
|
@ -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 Editor 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 Editor Preview"
|
||||
Description="Zed Editor 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 Editor 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>
|
79
crates/explorer_command_injector/AppxManifest.xml
Normal file
|
@ -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 Editor</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 Editor"
|
||||
Description="Zed Editor 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 Editor">
|
||||
<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>
|
28
crates/explorer_command_injector/Cargo.toml
Normal file
|
@ -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
|
1
crates/explorer_command_injector/LICENSE-GPL
Symbolic link
|
@ -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")
|
||||
}
|
|
@ -54,7 +54,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},
|
||||
|
@ -1689,6 +1689,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()?;
|
||||
|
@ -1701,7 +1702,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
|
||||
|
@ -1718,7 +1722,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),
|
||||
})
|
||||
})?
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
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, ExtensionDebugAdapterProviderProxy, ExtensionHostProxy, ExtensionLanguageProxy,
|
||||
|
@ -328,7 +331,7 @@ impl HeadlessExtensionStore {
|
|||
version: extension.version,
|
||||
dev: extension.dev,
|
||||
},
|
||||
PathBuf::from(envelope.payload.tmp_dir),
|
||||
PathBuf::from_proto(envelope.payload.tmp_dir),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
|
|
|
@ -92,6 +92,23 @@ impl FeatureFlag for JjUiFeatureFlag {
|
|||
const NAME: &'static str = "jj-ui";
|
||||
}
|
||||
|
||||
pub struct AcpFeatureFlag;
|
||||
|
||||
impl FeatureFlag for AcpFeatureFlag {
|
||||
const NAME: &'static str = "acp";
|
||||
}
|
||||
|
||||
pub struct ZedCloudFeatureFlag {}
|
||||
|
||||
impl FeatureFlag for ZedCloudFeatureFlag {
|
||||
const NAME: &'static str = "zed-cloud";
|
||||
|
||||
fn enabled_for_staff() -> bool {
|
||||
// Require individual opt-in, for now.
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FeatureFlagViewExt<V: 'static> {
|
||||
fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
|
||||
where
|
||||
|
|
|
@ -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));
|
||||
(
|
||||
|
|
|
@ -2844,7 +2844,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),
|
||||
)
|
||||
|
@ -2965,15 +2965,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)),
|
||||
),
|
||||
)
|
||||
|
@ -3066,6 +3071,7 @@ impl GitPanel {
|
|||
Some(
|
||||
self.panel_header_container(window, cx)
|
||||
.px_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
panel_button(change_string)
|
||||
.color(Color::Muted)
|
||||
|
@ -3080,23 +3086,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());
|
||||
})
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -3174,7 +3182,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));
|
||||
|
@ -3259,6 +3267,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| {
|
||||
|
@ -3371,6 +3380,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(),
|
||||
))
|
||||
|
@ -3415,8 +3425,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(
|
||||
|
@ -3431,22 +3441,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)
|
||||
|
@ -3480,12 +3489,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(
|
||||
|
@ -3507,43 +3515,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(
|
||||
|
@ -4621,7 +4624,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();
|
||||
|
|
|
@ -220,7 +220,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() {
|
||||
|
|
|
@ -506,35 +506,6 @@ pub trait UniformListDecoration {
|
|||
) -> AnyElement;
|
||||
}
|
||||
|
||||
/// A trait for implementing top slots in a [`UniformList`].
|
||||
/// Top slots are elements that appear at the top of the list and can adjust
|
||||
/// the visible range of list items.
|
||||
pub trait UniformListTopSlot {
|
||||
/// Returns elements to render at the top slot for the given visible range.
|
||||
fn compute(
|
||||
&mut self,
|
||||
visible_range: Range<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> SmallVec<[AnyElement; 8]>;
|
||||
|
||||
/// Layout and prepaint the top slot elements.
|
||||
fn prepaint(
|
||||
&self,
|
||||
elements: &mut SmallVec<[AnyElement; 8]>,
|
||||
bounds: Bounds<Pixels>,
|
||||
item_height: Pixels,
|
||||
scroll_offset: Point<Pixels>,
|
||||
padding: crate::Edges<Pixels>,
|
||||
can_scroll_horizontally: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
);
|
||||
|
||||
/// Paint the top slot elements.
|
||||
fn paint(&self, elements: &mut SmallVec<[AnyElement; 8]>, window: &mut Window, cx: &mut App);
|
||||
}
|
||||
|
||||
impl UniformList {
|
||||
/// Selects a specific list item for measurement.
|
||||
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -226,10 +226,21 @@ impl HttpClientWithUrl {
|
|||
}
|
||||
|
||||
/// Builds a Zed LLM URL using the given path.
|
||||
pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||
pub fn build_zed_llm_url(
|
||||
&self,
|
||||
path: &str,
|
||||
query: &[(&str, &str)],
|
||||
use_cloud: bool,
|
||||
) -> 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" => {
|
||||
if use_cloud {
|
||||
"https://cloud.zed.dev"
|
||||
} else {
|
||||
"https://llm.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,
|
||||
|
@ -252,6 +253,14 @@ pub enum IconName {
|
|||
TextSnippet,
|
||||
ThumbsDown,
|
||||
ThumbsUp,
|
||||
ToolBulb,
|
||||
ToolFolder,
|
||||
ToolHammer,
|
||||
ToolPencil,
|
||||
ToolRegex,
|
||||
ToolSearch,
|
||||
ToolTerminal,
|
||||
ToolWeb,
|
||||
Trash,
|
||||
TrashAlt,
|
||||
Triangle,
|
||||
|
|
|
@ -28,6 +28,7 @@ credentials_provider.workspace = true
|
|||
copilot.workspace = true
|
||||
deepseek = { workspace = true, features = ["schemars"] }
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
google_ai = { workspace = true, features = ["schemars"] }
|
||||
|
|
|
@ -2,6 +2,7 @@ use anthropic::AnthropicModelMode;
|
|||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use client::{Client, ModelRequestUsage, UserStore, zed_urls};
|
||||
use feature_flags::{FeatureFlagAppExt as _, ZedCloudFeatureFlag};
|
||||
use futures::{
|
||||
AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream,
|
||||
};
|
||||
|
@ -136,6 +137,7 @@ impl State {
|
|||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx);
|
||||
let use_cloud = cx.has_flag::<ZedCloudFeatureFlag>();
|
||||
|
||||
Self {
|
||||
client: client.clone(),
|
||||
|
@ -163,7 +165,7 @@ impl State {
|
|||
.await;
|
||||
}
|
||||
|
||||
let response = Self::fetch_models(client, llm_api_token).await?;
|
||||
let response = Self::fetch_models(client, llm_api_token, use_cloud).await?;
|
||||
cx.update(|cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
let mut models = Vec::new();
|
||||
|
@ -265,13 +267,18 @@ impl State {
|
|||
async fn fetch_models(
|
||||
client: Arc<Client>,
|
||||
llm_api_token: LlmApiToken,
|
||||
use_cloud: bool,
|
||||
) -> Result<ListModelsResponse> {
|
||||
let http_client = &client.http_client();
|
||||
let token = llm_api_token.acquire(&client).await?;
|
||||
|
||||
let request = http_client::Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri(http_client.build_zed_llm_url("/models", &[])?.as_ref())
|
||||
.uri(
|
||||
http_client
|
||||
.build_zed_llm_url("/models", &[], use_cloud)?
|
||||
.as_ref(),
|
||||
)
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.body(AsyncBody::empty())?;
|
||||
let mut response = http_client
|
||||
|
@ -535,6 +542,7 @@ impl CloudLanguageModel {
|
|||
llm_api_token: LlmApiToken,
|
||||
app_version: Option<SemanticVersion>,
|
||||
body: CompletionBody,
|
||||
use_cloud: bool,
|
||||
) -> Result<PerformLlmCompletionResponse> {
|
||||
let http_client = &client.http_client();
|
||||
|
||||
|
@ -542,9 +550,11 @@ impl CloudLanguageModel {
|
|||
let mut refreshed_token = false;
|
||||
|
||||
loop {
|
||||
let request_builder = http_client::Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(http_client.build_zed_llm_url("/completions", &[])?.as_ref());
|
||||
let request_builder = http_client::Request::builder().method(Method::POST).uri(
|
||||
http_client
|
||||
.build_zed_llm_url("/completions", &[], use_cloud)?
|
||||
.as_ref(),
|
||||
);
|
||||
let request_builder = if let Some(app_version) = app_version {
|
||||
request_builder.header(ZED_VERSION_HEADER_NAME, app_version.to_string())
|
||||
} else {
|
||||
|
@ -771,6 +781,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
let model_id = self.model.id.to_string();
|
||||
let generate_content_request =
|
||||
into_google(request, model_id.clone(), GoogleModelMode::Default);
|
||||
let use_cloud = cx.has_flag::<ZedCloudFeatureFlag>();
|
||||
async move {
|
||||
let http_client = &client.http_client();
|
||||
let token = llm_api_token.acquire(&client).await?;
|
||||
|
@ -786,7 +797,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
.method(Method::POST)
|
||||
.uri(
|
||||
http_client
|
||||
.build_zed_llm_url("/count_tokens", &[])?
|
||||
.build_zed_llm_url("/count_tokens", &[], use_cloud)?
|
||||
.as_ref(),
|
||||
)
|
||||
.header("Content-Type", "application/json")
|
||||
|
@ -835,6 +846,9 @@ impl LanguageModel for CloudLanguageModel {
|
|||
let intent = request.intent;
|
||||
let mode = request.mode;
|
||||
let app_version = cx.update(|cx| AppVersion::global(cx)).ok();
|
||||
let use_cloud = cx
|
||||
.update(|cx| cx.has_flag::<ZedCloudFeatureFlag>())
|
||||
.unwrap_or(false);
|
||||
match self.model.provider {
|
||||
zed_llm_client::LanguageModelProvider::Anthropic => {
|
||||
let request = into_anthropic(
|
||||
|
@ -872,6 +886,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
provider_request: serde_json::to_value(&request)
|
||||
.map_err(|e| anyhow!(e))?,
|
||||
},
|
||||
use_cloud,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| match err.downcast::<ApiError>() {
|
||||
|
@ -924,6 +939,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
provider_request: serde_json::to_value(&request)
|
||||
.map_err(|e| anyhow!(e))?,
|
||||
},
|
||||
use_cloud,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -964,6 +980,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
provider_request: serde_json::to_value(&request)
|
||||
.map_err(|e| anyhow!(e))?,
|
||||
},
|
||||
use_cloud,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
|
|
@ -185,15 +185,18 @@ impl LanguageServerState {
|
|||
menu = menu.separator().item(button);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(server_info) = item.server_info() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
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 {
|
||||
|
@ -218,16 +221,40 @@ impl LanguageServerState {
|
|||
.other_servers_start_index
|
||||
.is_some_and(|index| index == i)
|
||||
{
|
||||
menu = menu.separator();
|
||||
menu = menu.separator().header("Other Buffers");
|
||||
}
|
||||
|
||||
if i == 0 && self.other_servers_start_index.is_some() {
|
||||
menu = menu.header("Current Buffer");
|
||||
}
|
||||
|
||||
menu = menu.item(ContextMenuItem::custom_entry(
|
||||
move |_, _| {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.group("menu_item")
|
||||
.w_full()
|
||||
.child(Indicator::dot().color(status_color))
|
||||
.child(Label::new(server_info.name.0.clone()))
|
||||
.when(!has_logs, |div| div.cursor_default())
|
||||
.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()
|
||||
},
|
||||
{
|
||||
|
@ -708,8 +735,6 @@ impl LspTool {
|
|||
state.update(cx, |state, cx| state.fill_menu(menu, cx))
|
||||
});
|
||||
lsp_tool.lsp_menu = Some(menu.clone());
|
||||
// TODO kb will this work?
|
||||
// what about the selections?
|
||||
lsp_tool.popover_menu_handle.refresh_menu(
|
||||
window,
|
||||
cx,
|
||||
|
@ -836,17 +861,27 @@ impl Render for LspTool {
|
|||
}
|
||||
}
|
||||
|
||||
let indicator = if has_errors {
|
||||
Some(Indicator::dot().color(Color::Error))
|
||||
let (indicator, description) = if has_errors {
|
||||
(
|
||||
Some(Indicator::dot().color(Color::Error)),
|
||||
"Server with errors",
|
||||
)
|
||||
} else if has_warnings {
|
||||
Some(Indicator::dot().color(Color::Warning))
|
||||
(
|
||||
Some(Indicator::dot().color(Color::Warning)),
|
||||
"Server with warnings",
|
||||
)
|
||||
} else if has_other_notifications {
|
||||
Some(Indicator::dot().color(Color::Modified))
|
||||
(
|
||||
Some(Indicator::dot().color(Color::Modified)),
|
||||
"Server with notifications",
|
||||
)
|
||||
} else {
|
||||
None
|
||||
(None, "All Servers Operational")
|
||||
};
|
||||
|
||||
let lsp_tool = cx.entity().clone();
|
||||
|
||||
div().child(
|
||||
PopoverMenu::new("lsp-tool")
|
||||
.menu(move |_, cx| lsp_tool.read(cx).lsp_menu.clone())
|
||||
|
@ -858,7 +893,13 @@ impl Render for LspTool {
|
|||
.icon_size(IconSize::Small)
|
||||
.indicator_border_color(Some(cx.theme().colors().status_bar_background)),
|
||||
move |window, cx| {
|
||||
Tooltip::for_action("Language Servers", &ToggleMenu, window, cx)
|
||||
Tooltip::with_meta(
|
||||
"Language Servers",
|
||||
Some(&ToggleMenu),
|
||||
description,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
37
crates/migrator/src/migrations/m_2025_07_08/settings.rs
Normal file
|
@ -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(|| {
|
||||
|
|
25
crates/net/Cargo.toml
Normal file
|
@ -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
|
1
crates/net/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE-GPL
|
69
crates/net/src/async_net.rs
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
45
crates/net/src/listener.rs
Normal file
|
@ -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 _) }
|
||||
}
|
||||
}
|
107
crates/net/src/net.rs
Normal file
|
@ -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(())
|
||||
})
|
||||
}
|
||||
}
|
59
crates/net/src/socket.rs
Normal file
|
@ -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) };
|
||||
}
|
||||
}
|
60
crates/net/src/stream.rs
Normal file
|
@ -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 _) }
|
||||
}
|
||||
}
|
76
crates/net/src/util.rs
Normal file
|
@ -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
|
||||
}
|
|
@ -4584,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.);
|
||||
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,
|
||||
);
|
||||
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()
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
};
|
||||
|
|
|
@ -67,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();
|
||||
|
|
|
@ -33,7 +33,7 @@ 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},
|
||||
|
@ -253,11 +253,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 {
|
||||
|
@ -282,12 +287,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,
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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::{
|
||||
|
@ -1159,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();
|
||||
|
||||
|
@ -1410,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)
|
||||
|
@ -4039,7 +4048,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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,17 +171,26 @@ impl Project {
|
|||
.unwrap_or_default();
|
||||
env.extend(settings.env.clone());
|
||||
|
||||
match &self.ssh_details(cx) {
|
||||
Some((_, ssh_command)) => {
|
||||
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() {
|
||||
|
@ -226,11 +249,7 @@ impl Project {
|
|||
// precedence.
|
||||
env.extend(settings.env.clone());
|
||||
|
||||
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,8 +323,13 @@ 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());
|
||||
|
@ -304,8 +342,12 @@ impl Project {
|
|||
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 {
|
||||
|
@ -343,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,
|
||||
|
@ -533,6 +575,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
|
||||
|
@ -555,24 +598,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,
|
||||
|
|