Compare commits
95 Commits
linux/keys
...
improve-gi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ae31d0dc7 | ||
|
|
31a4f6d411 | ||
|
|
52f2b32557 | ||
|
|
8dca4d150e | ||
|
|
440beb8a90 | ||
|
|
ce63a6ddd8 | ||
|
|
26ba6e7e00 | ||
|
|
363a265051 | ||
|
|
37e73e3277 | ||
|
|
32f5132bde | ||
|
|
fd5650d4ed | ||
|
|
8b6b039b63 | ||
|
|
4848bd705e | ||
|
|
45d0686129 | ||
|
|
eca36c502e | ||
|
|
6673c7cd4c | ||
|
|
a2f5c47e2d | ||
|
|
c6a6db9754 | ||
|
|
6f9e052edb | ||
|
|
2edf85f054 | ||
|
|
00ec243771 | ||
|
|
84124c60db | ||
|
|
cf1ce1beed | ||
|
|
e4effa5e01 | ||
|
|
f50041779d | ||
|
|
51df8a17ef | ||
|
|
85d12548a1 | ||
|
|
0af7d32b7d | ||
|
|
1cadff9311 | ||
|
|
8f6b9f0d65 | ||
|
|
970a1066f5 | ||
|
|
833bc6979a | ||
|
|
a8cc927303 | ||
|
|
13ddd5e4cb | ||
|
|
1b6e212eba | ||
|
|
46834d31f1 | ||
|
|
e070c81687 | ||
|
|
5b61b8c8ed | ||
|
|
625ce12a3e | ||
|
|
12bc8907d9 | ||
|
|
67c765a99a | ||
|
|
206cce6783 | ||
|
|
c3edc2cfc1 | ||
|
|
625a4b90a5 | ||
|
|
fbead09c30 | ||
|
|
0797f7b66e | ||
|
|
6f6c2915b2 | ||
|
|
0bd65829f7 | ||
|
|
90bf602ceb | ||
|
|
cd024b8870 | ||
|
|
af71e15ea0 | ||
|
|
d0e01dbd8f | ||
|
|
d65855c4a1 | ||
|
|
70351360d7 | ||
|
|
993e0f55ec | ||
|
|
496bf0ec43 | ||
|
|
c09f484ec4 | ||
|
|
a58a75c0f6 | ||
|
|
d1a6c5d494 | ||
|
|
10028aaae8 | ||
|
|
3b9bb521f4 | ||
|
|
7eb739d489 | ||
|
|
b4cbea50bb | ||
|
|
153840199e | ||
|
|
8812e7cd14 | ||
|
|
56d0ae6782 | ||
|
|
d52f07b77c | ||
|
|
089ce8f6aa | ||
|
|
842ac984d5 | ||
|
|
87362c602f | ||
|
|
94916cd3b6 | ||
|
|
7915b9f93f | ||
|
|
33f1ac8b34 | ||
|
|
a1188848ef | ||
|
|
7588280915 | ||
|
|
f82fdaa0a4 | ||
|
|
41085f8f55 | ||
|
|
8e1d341d09 | ||
|
|
9d2b7c8033 | ||
|
|
c6603e4fba | ||
|
|
c549b712fd | ||
|
|
d6bff274eb | ||
|
|
cfc9cfa4ab | ||
|
|
e2e529bd94 | ||
|
|
e6c41b577b | ||
|
|
f8f827583d | ||
|
|
36c325bc60 | ||
|
|
f4106ad404 | ||
|
|
1583dd2d6f | ||
|
|
d7fd9245cd | ||
|
|
5f21a9bd32 | ||
|
|
c30e28179a | ||
|
|
8bc1396a55 | ||
|
|
51c24e2010 | ||
|
|
3169f06404 |
@@ -1,64 +0,0 @@
|
||||
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' }}
|
||||
13
.github/actions/run_tests/action.yml
vendored
@@ -1,12 +1,6 @@
|
||||
name: "Run tests"
|
||||
description: "Runs the tests"
|
||||
|
||||
inputs:
|
||||
use-xvfb:
|
||||
description: "Whether to run tests with xvfb"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -26,9 +20,4 @@ runs:
|
||||
|
||||
- name: Run tests
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
if [ "${{ inputs.use-xvfb }}" == "true" ]; then
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24 -nolisten tcp" cargo nextest run --workspace --no-fail-fast
|
||||
else
|
||||
cargo nextest run --workspace --no-fail-fast
|
||||
fi
|
||||
run: cargo nextest run --workspace --no-fail-fast
|
||||
|
||||
30
.github/workflows/ci.yml
vendored
@@ -21,6 +21,9 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
|
||||
jobs:
|
||||
job_spec:
|
||||
@@ -52,9 +55,10 @@ jobs:
|
||||
fi
|
||||
# Specify anything which should skip full CI in this regex:
|
||||
# - docs/
|
||||
# - script/update_top_ranking_issues/
|
||||
# - .github/ISSUE_TEMPLATE/
|
||||
# - .github/workflows/ (except .github/workflows/ci.yml)
|
||||
SKIP_REGEX='^(docs/|\.github/(ISSUE_TEMPLATE|workflows/(?!ci)))'
|
||||
SKIP_REGEX='^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!ci)))'
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -vP "$SKIP_REGEX") ]]; then
|
||||
echo "run_tests=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
@@ -71,7 +75,7 @@ jobs:
|
||||
echo "run_license=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
NIX_REGEX='^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)'
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep "$NIX_REGEX") ]]; then
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -P "$NIX_REGEX") ]]; then
|
||||
echo "run_nix=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "run_nix=false" >> $GITHUB_OUTPUT
|
||||
@@ -334,8 +338,6 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
with:
|
||||
use-xvfb: true
|
||||
|
||||
- name: Build other binaries and features
|
||||
run: |
|
||||
@@ -494,9 +496,6 @@ jobs:
|
||||
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
|
||||
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
|
||||
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
@@ -579,10 +578,6 @@ jobs:
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -636,10 +631,6 @@ jobs:
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -692,16 +683,12 @@ jobs:
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
needs: [linux_tests]
|
||||
name: Build Zed on FreeBSD
|
||||
# env:
|
||||
# MYTOKEN : ${{ secrets.MYTOKEN }}
|
||||
# MYTOKEN2: "value2"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build FreeBSD remote-server
|
||||
id: freebsd-build
|
||||
uses: vmactions/freebsd-vm@c3ae29a132c8ef1924775414107a97cac042aad5 # v1.2.0
|
||||
with:
|
||||
# envs: "MYTOKEN MYTOKEN2"
|
||||
usesh: true
|
||||
release: 13.5
|
||||
copyback: true
|
||||
@@ -768,8 +755,6 @@ jobs:
|
||||
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"
|
||||
@@ -786,9 +771,6 @@ jobs:
|
||||
# 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
|
||||
|
||||
22
.github/workflows/release_nightly.yml
vendored
@@ -12,6 +12,9 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
|
||||
jobs:
|
||||
style:
|
||||
@@ -91,9 +94,6 @@ jobs:
|
||||
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
|
||||
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
|
||||
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
@@ -125,10 +125,6 @@ jobs:
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2004
|
||||
needs: tests
|
||||
env:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -164,10 +160,6 @@ jobs:
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2204-arm
|
||||
needs: tests
|
||||
env:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -198,9 +190,6 @@ jobs:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: github-8vcpu-ubuntu-2404
|
||||
needs: tests
|
||||
env:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
name: Build Zed on FreeBSD
|
||||
# env:
|
||||
# MYTOKEN : ${{ secrets.MYTOKEN }}
|
||||
@@ -257,8 +246,6 @@ jobs:
|
||||
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"
|
||||
@@ -276,9 +263,6 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"file_types": {
|
||||
"Dockerfile": ["Dockerfile*[!dockerignore]"],
|
||||
"JSONC": ["assets/**/*.json", "renovate.json"],
|
||||
"JSONC": ["**/assets/**/*.json", "renovate.json"],
|
||||
"Git Ignore": ["dockerignore"]
|
||||
},
|
||||
"hard_tabs": false,
|
||||
|
||||
32
Cargo.lock
generated
@@ -9,6 +9,7 @@ dependencies = [
|
||||
"agent_servers",
|
||||
"agentic-coding-protocol",
|
||||
"anyhow",
|
||||
"assistant_tool",
|
||||
"async-pipe",
|
||||
"buffer_diff",
|
||||
"editor",
|
||||
@@ -263,9 +264,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agentic-coding-protocol"
|
||||
version = "0.0.6"
|
||||
version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1ac0351749af7bf53c65042ef69fefb9351aa8b7efa0a813d6281377605c37d"
|
||||
checksum = "a75f520bcc049ebe40c8c99427aa61b48ad78a01bcc96a13b350b903dcfb9438"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -3108,6 +3109,7 @@ dependencies = [
|
||||
"context_server",
|
||||
"ctor",
|
||||
"dap",
|
||||
"dap-types",
|
||||
"dap_adapters",
|
||||
"dashmap 6.1.0",
|
||||
"debugger_ui",
|
||||
@@ -4391,12 +4393,15 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"hex",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
"notifications",
|
||||
"parking_lot",
|
||||
"parse_int",
|
||||
"paths",
|
||||
"picker",
|
||||
"pretty_assertions",
|
||||
@@ -8978,6 +8983,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"language",
|
||||
"lsp",
|
||||
"project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"util",
|
||||
@@ -9030,7 +9036,6 @@ dependencies = [
|
||||
"credentials_provider",
|
||||
"deepseek",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"google_ai",
|
||||
@@ -9657,12 +9662,11 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.95.1"
|
||||
source = "git+https://github.com/zed-industries/lsp-types?rev=c9c189f1c5dd53c624a419ce35bc77ad6a908d18#c9c189f1c5dd53c624a419ce35bc77ad6a908d18"
|
||||
source = "git+https://github.com/zed-industries/lsp-types?rev=6add7052b598ea1f40f7e8913622c3958b009b60#6add7052b598ea1f40f7e8913622c3958b009b60"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -11274,6 +11278,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse_int"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c464266693329dd5a8715098c7f86e6c5fd5d985018b8318f53d9c6c2b21a31"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "partial-json-fixer"
|
||||
version = "0.5.3"
|
||||
@@ -12317,6 +12330,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"askpass",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"buffer_diff",
|
||||
"circular-buffer",
|
||||
"client",
|
||||
@@ -12362,6 +12376,7 @@ dependencies = [
|
||||
"sha2",
|
||||
"shellexpand 2.1.2",
|
||||
"shlex",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"snippet",
|
||||
"snippet_provider",
|
||||
@@ -14096,7 +14111,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "scap"
|
||||
version = "0.0.8"
|
||||
source = "git+https://github.com/zed-industries/scap?rev=08f0a01417505cc0990b9931a37e5120db92e0d0#08f0a01417505cc0990b9931a37e5120db92e0d0"
|
||||
source = "git+https://github.com/zed-industries/scap?rev=270538dc780f5240723233ff901e1054641ed318#270538dc780f5240723233ff901e1054641ed318"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cocoa 0.25.0",
|
||||
@@ -14674,6 +14689,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"theme",
|
||||
"tree-sitter-json",
|
||||
@@ -18373,7 +18389,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
@@ -19645,6 +19660,7 @@ dependencies = [
|
||||
"rustix 1.0.7",
|
||||
"rustls 0.23.26",
|
||||
"rustls-webpki 0.103.1",
|
||||
"scap",
|
||||
"schemars",
|
||||
"scopeguard",
|
||||
"sea-orm",
|
||||
@@ -19692,7 +19708,9 @@ dependencies = [
|
||||
"wasmtime-cranelift",
|
||||
"wasmtime-environ",
|
||||
"winapi",
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.0",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
|
||||
@@ -404,7 +404,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agentic-coding-protocol = "0.0.6"
|
||||
agentic-coding-protocol = "0.0.7"
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
@@ -492,7 +492,7 @@ libc = "0.2"
|
||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189f1c5dd53c624a419ce35bc77ad6a908d18" }
|
||||
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "6add7052b598ea1f40f7e8913622c3958b009b60" }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
metal = "0.29"
|
||||
moka = { version = "0.12.10", features = ["sync"] }
|
||||
@@ -507,6 +507,7 @@ ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
parking_lot = "0.12.1"
|
||||
partial-json-fixer = "0.5.3"
|
||||
parse_int = "0.9"
|
||||
pathdiff = "0.2"
|
||||
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
@@ -546,7 +547,7 @@ rustc-demangle = "0.1.23"
|
||||
rustc-hash = "2.1.0"
|
||||
rustls = { version = "0.23.26" }
|
||||
rustls-platform-verifier = "0.5.0"
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "08f0a01417505cc0990b9931a37e5120db92e0d0", default-features = false }
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false }
|
||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
|
||||
@@ -1 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bug"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.49219 2.29071L6.41455 3.1933" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.61816 3.1933L10.508 2.29071" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.7042 5.89221V5.15749C5.69033 4.85975 5.73943 4.56239 5.84856 4.28336C5.95768 4.00434 6.12456 3.74943 6.33913 3.53402C6.55369 3.31862 6.81149 3.14718 7.09697 3.03005C7.38245 2.91292 7.68969 2.85254 8.00014 2.85254C8.3106 2.85254 8.61784 2.91292 8.90332 3.03005C9.18879 3.14718 9.44659 3.31862 9.66116 3.53402C9.87572 3.74943 10.0426 4.00434 10.1517 4.28336C10.2609 4.56239 10.31 4.85975 10.2961 5.15749V5.89221" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.00006 13.0426C6.13263 13.0426 4.60474 11.6005 4.60474 9.83792V8.23558C4.60474 7.66895 4.84322 7.12554 5.26772 6.72487C5.69221 6.32421 6.26796 6.09912 6.86829 6.09912H9.13184C9.73217 6.09912 10.3079 6.32421 10.7324 6.72487C11.1569 7.12554 11.3954 7.66895 11.3954 8.23558V9.83792C11.3954 11.6005 9.86749 13.0426 8.00006 13.0426Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.60452 6.25196C3.51235 6.13878 2.60693 5.17677 2.60693 3.9884" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.60462 8.81659H2.34106" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.4541 13.3186C2.4541 12.1302 3.41611 11.1116 4.60448 11.0551" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.0761 3.9884C13.0761 5.17677 12.1706 6.13878 11.0955 6.25196" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.6591 8.81659H11.3955" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.3955 11.0551C12.5839 11.1116 13.5459 12.1302 13.5459 13.3186" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 615 B After Width: | Height: | Size: 2.1 KiB |
@@ -1,5 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.03125 3V3.03125M3.03125 3.03125V9M3.03125 3.03125C3.03125 5 6 5 6 5M3.03125 9C3.03125 11 6 11 6 11M3.03125 9V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="8" y="2.5" width="6" height="5" rx="1.5" fill="black"/>
|
||||
<rect x="8" y="8.46875" width="6" height="5.0625" rx="1.5" fill="black"/>
|
||||
<path d="M3 3V3.03125M3 3.03125V9M3 3.03125C3 5 5.96875 5 5.96875 5M3 9C3 11 5.96875 11 5.96875 11M3 9V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="8" y="3" width="5.5" height="4" rx="1.5" fill="black"/>
|
||||
<rect x="8" y="9" width="5.5" height="4" rx="1.5" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 423 B |
@@ -1,6 +1,7 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 3.25C4.02614 3.25 4.25 3.02614 4.25 2.75C4.25 2.47386 4.02614 2.25 3.75 2.25C3.47386 2.25 3.25 2.47386 3.25 2.75C3.25 3.02614 3.47386 3.25 3.75 3.25ZM3.75 4.25C4.57843 4.25 5.25 3.57843 5.25 2.75C5.25 1.92157 4.57843 1.25 3.75 1.25C2.92157 1.25 2.25 1.92157 2.25 2.75C2.25 3.57843 2.92157 4.25 3.75 4.25Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.25 3.25C8.52614 3.25 8.75 3.02614 8.75 2.75C8.75 2.47386 8.52614 2.25 8.25 2.25C7.97386 2.25 7.75 2.47386 7.75 2.75C7.75 3.02614 7.97386 3.25 8.25 3.25ZM8.25 4.25C9.07843 4.25 9.75 3.57843 9.75 2.75C9.75 1.92157 9.07843 1.25 8.25 1.25C7.42157 1.25 6.75 1.92157 6.75 2.75C6.75 3.57843 7.42157 4.25 8.25 4.25Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 9.75C4.02614 9.75 4.25 9.52614 4.25 9.25C4.25 8.97386 4.02614 8.75 3.75 8.75C3.47386 8.75 3.25 8.97386 3.25 9.25C3.25 9.52614 3.47386 9.75 3.75 9.75ZM3.75 10.75C4.57843 10.75 5.25 10.0784 5.25 9.25C5.25 8.42157 4.57843 7.75 3.75 7.75C2.92157 7.75 2.25 8.42157 2.25 9.25C2.25 10.0784 2.92157 10.75 3.75 10.75Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.25 3.75H4.25V5.59609C4.67823 5.35824 5.24991 5.25 6 5.25H7.25017C7.5262 5.25 7.75 5.02625 7.75 4.75V3.75H8.75V4.75C8.75 5.57832 8.07871 6.25 7.25017 6.25H6C5.14559 6.25 4.77639 6.41132 4.59684 6.56615C4.42571 6.71373 4.33877 6.92604 4.25 7.30651V8.25H3.25V3.75Z" fill="black"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="5" cy="12" r="1.25" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M5 11V5" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M5 10C5 10 5.5 8 7 8C7.73103 8 8.69957 8 9.50049 8C10.3289 8 11 7.32843 11 6.5V5" stroke="black" stroke-width="1.5"/>
|
||||
<circle cx="5" cy="4" r="1.25" stroke="black" stroke-width="1.5"/>
|
||||
<circle cx="11" cy="4" r="1.25" stroke="black" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 487 B |
@@ -1 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-tree"><path d="M21 12h-8"/><path d="M21 6H8"/><path d="M21 18h-8"/><path d="M3 6v4c0 1.1.9 2 2 2h3"/><path d="M3 10v6c0 1.1.9 2 2 2h3"/></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.5 8H9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.5 4L6.5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.5 12H9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 3.5V6.33333C3 7.25 3.72 8 4.6 8H7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 6V10.5C3 11.325 3.72 12 4.6 12H7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 349 B After Width: | Height: | Size: 680 B |
1
assets/icons/location_edit.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-location-edit-icon lucide-location-edit"><path d="M17.97 9.304A8 8 0 0 0 2 10c0 4.69 4.887 9.562 7.022 11.468"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/><circle cx="10" cy="10" r="3"/></svg>
|
||||
|
After Width: | Height: | Size: 491 B |
5
assets/icons/terminal_alt.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="M11.8889 3H4.11111C3.49746 3 3 3.49746 3 4.11111V11.8889C3 12.5025 3.49746 13 4.11111 13H11.8889C12.5025 13 13 12.5025 13 11.8889V4.11111C13 3.49746 12.5025 3 11.8889 3Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.37939 10.3243H10.3794" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.64966 9.32837L7.64966 7.32837L5.64966 5.32837" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 659 B |
@@ -1,3 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.9 8.00002C7.44656 8.00002 8.7 6.74637 8.7 5.20002C8.7 3.65368 7.44656 2.40002 5.9 2.40002C4.35344 2.40002 3.1 3.65368 3.1 5.20002C3.1 6.74637 4.35344 8.00002 5.9 8.00002ZM7.00906 9.05002H4.79094C2.69684 9.05002 1 10.7475 1 12.841C1 13.261 1.3395 13.6 1.75819 13.6H10.0409C10.4609 13.6 10.8 13.261 10.8 12.841C10.8 10.7475 9.1025 9.05002 7.00906 9.05002ZM11.4803 9.40002H9.86484C10.87 10.2247 11.5 11.4585 11.5 12.841C11.5 13.121 11.4169 13.3791 11.2812 13.6H14.3C14.6872 13.6 15 13.285 15 12.8803C15 10.9663 13.4338 9.40002 11.4803 9.40002ZM10.45 8.00002C11.8041 8.00002 12.9 6.90409 12.9 5.55002C12.9 4.19596 11.8041 3.10002 10.45 3.10002C9.90072 3.10002 9.39913 3.28716 8.9905 3.59243C9.2425 4.07631 9.4 4.61815 9.4 5.20002C9.4 5.97702 9.13903 6.69059 8.70897 7.27181C9.15281 7.72002 9.7675 8.00002 10.45 8.00002Z" fill="white"/>
|
||||
<path d="M6.79118 8.27005C8.27568 8.27005 9.4791 7.06663 9.4791 5.58214C9.4791 4.09765 8.27568 2.89423 6.79118 2.89423C5.30669 2.89423 4.10327 4.09765 4.10327 5.58214C4.10327 7.06663 5.30669 8.27005 6.79118 8.27005Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.79112 8.60443C4.19441 8.60443 2.08936 10.7095 2.08936 13.3062H11.4929C11.4929 10.7095 9.38784 8.60443 6.79112 8.60443Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14.6984 12.9263C14.6984 10.8893 13.4895 8.99736 12.2806 8.09067C12.6779 7.79254 12.9957 7.40104 13.2057 6.95083C13.4157 6.50062 13.5115 6.00558 13.4846 5.50952C13.4577 5.01346 13.309 4.53168 13.0515 4.10681C12.7941 3.68194 12.4358 3.3271 12.0085 3.07367" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 947 B After Width: | Height: | Size: 999 B |
@@ -1,5 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 2L6.72534 5.87534C6.6601 6.07367 6.5492 6.25392 6.40155 6.40155C6.25392 6.5492 6.07367 6.6601 5.87534 6.72534L2 8L5.87534 9.27466C6.07367 9.3399 6.25392 9.4508 6.40155 9.59845C6.5492 9.74608 6.6601 9.92633 6.72534 10.1247L8 14L9.27466 10.1247C9.3399 9.92633 9.4508 9.74608 9.59845 9.59845C9.74608 9.4508 9.92633 9.3399 10.1247 9.27466L14 8L10.1247 6.72534C9.92633 6.6601 9.74608 6.5492 9.59845 6.40155C9.4508 6.25392 9.3399 6.07367 9.27466 5.87534L8 2Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 2.93652L6.9243 6.20697C6.86924 6.37435 6.77565 6.52646 6.65105 6.65105C6.52646 6.77565 6.37435 6.86924 6.20697 6.9243L2.93652 8L6.20697 9.0757C6.37435 9.13076 6.52646 9.22435 6.65105 9.34895C6.77565 9.47354 6.86924 9.62565 6.9243 9.79306L8 13.0635L9.0757 9.79306C9.13076 9.62565 9.22435 9.47354 9.34895 9.34895C9.47354 9.22435 9.62565 9.13076 9.79306 9.0757L13.0635 8L9.79306 6.9243C9.62565 6.86924 9.47354 6.77565 9.34895 6.65105C9.22435 6.52646 9.13076 6.37435 9.0757 6.20697L8 2.93652Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 998 B After Width: | Height: | Size: 1.0 KiB |
@@ -320,7 +320,8 @@
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"up": "agent::PreviousHistoryMessage",
|
||||
"down": "agent::NextHistoryMessage"
|
||||
"down": "agent::NextHistoryMessage",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -855,6 +856,7 @@
|
||||
"alt-shift-y": "git::UnstageFile",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"space": "git::ToggleStaged",
|
||||
"shift-space": "git::StageRange",
|
||||
"tab": "git_panel::FocusEditor",
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
@@ -997,6 +999,7 @@
|
||||
{
|
||||
"context": "FileFinder || (FileFinder > Picker > Editor)",
|
||||
"bindings": {
|
||||
"ctrl-p": "file_finder::Toggle",
|
||||
"ctrl-shift-a": "file_finder::ToggleSplitMenu",
|
||||
"ctrl-shift-i": "file_finder::ToggleFilterMenu"
|
||||
}
|
||||
@@ -1112,7 +1115,10 @@
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-f": "search::FocusSearch"
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"alt-find": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"alt-ctrl-f": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"alt-c": "keymap_editor::ToggleConflictFilter"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -371,7 +371,8 @@
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"up": "agent::PreviousHistoryMessage",
|
||||
"down": "agent::NextHistoryMessage"
|
||||
"down": "agent::NextHistoryMessage",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -929,6 +930,7 @@
|
||||
"enter": "menu::Confirm",
|
||||
"cmd-alt-y": "git::ToggleStaged",
|
||||
"space": "git::ToggleStaged",
|
||||
"shift-space": "git::StageRange",
|
||||
"cmd-y": "git::StageFile",
|
||||
"cmd-shift-y": "git::UnstageFile",
|
||||
"alt-down": "git_panel::FocusEditor",
|
||||
@@ -1096,6 +1098,7 @@
|
||||
"ctrl-cmd-space": "terminal::ShowCharacterPalette",
|
||||
"cmd-c": "terminal::Copy",
|
||||
"cmd-v": "terminal::Paste",
|
||||
"cmd-f": "buffer_search::Deploy",
|
||||
"cmd-a": "editor::SelectAll",
|
||||
"cmd-k": "terminal::Clear",
|
||||
"cmd-n": "workspace::NewTerminal",
|
||||
@@ -1211,7 +1214,8 @@
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-f": "search::FocusSearch"
|
||||
"cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"cmd-alt-c": "keymap_editor::ToggleConflictFilter"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -466,7 +466,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == insert && showing_signature_help && !showing_completions",
|
||||
"context": "(vim_mode == insert || vim_mode == normal) && showing_signature_help && !showing_completions",
|
||||
"bindings": {
|
||||
"ctrl-p": "editor::SignatureHelpPrevious",
|
||||
"ctrl-n": "editor::SignatureHelpNext"
|
||||
@@ -841,6 +841,7 @@
|
||||
"i": "git_panel::FocusEditor",
|
||||
"x": "git::ToggleStaged",
|
||||
"shift-x": "git::StageAll",
|
||||
"g x": "git::StageRange",
|
||||
"shift-u": "git::UnstageAll"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1135,6 +1135,7 @@
|
||||
"**/.svn",
|
||||
"**/.hg",
|
||||
"**/.jj",
|
||||
"**/.repo",
|
||||
"**/CVS",
|
||||
"**/.DS_Store",
|
||||
"**/Thumbs.db",
|
||||
@@ -1157,16 +1158,14 @@
|
||||
// Control whether the git blame information is shown inline,
|
||||
// in the currently focused line.
|
||||
"inline_blame": {
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
// Sets a delay after which the inline blame information is shown.
|
||||
// Delay is restarted with every cursor movement.
|
||||
// "delay_ms": 600
|
||||
//
|
||||
"delay_ms": 0,
|
||||
// Whether or not to display the git commit summary on the same line.
|
||||
// "show_commit_summary": false
|
||||
//
|
||||
"show_commit_summary": false,
|
||||
// The minimum column number to show the inline blame information at
|
||||
// "min_column": 0
|
||||
"min_column": 0
|
||||
},
|
||||
// How git hunks are displayed visually in the editor.
|
||||
// This setting can take two values:
|
||||
@@ -1379,11 +1378,11 @@
|
||||
// This will be merged with the platform's default font fallbacks
|
||||
// "font_fallbacks": ["FiraCode Nerd Fonts"],
|
||||
// The weight of the editor font in standard CSS units from 100 to 900.
|
||||
// "font_weight": 400
|
||||
"font_weight": 400,
|
||||
// Sets the maximum number of lines in the terminal's scrollback buffer.
|
||||
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
|
||||
// Existing terminals will not pick up this change until they are recreated.
|
||||
// "max_scroll_history_lines": 10000,
|
||||
"max_scroll_history_lines": 10000,
|
||||
// The minimum APCA perceptual contrast between foreground and background colors.
|
||||
// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
|
||||
// especially for dark mode. Values range from 0 to 106.
|
||||
|
||||
@@ -20,6 +20,7 @@ gemini = []
|
||||
agent_servers.workspace = true
|
||||
agentic-coding-protocol.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
@@ -2,14 +2,19 @@ pub use acp::ToolCallId;
|
||||
use agent_servers::AgentServer;
|
||||
use agentic_coding_protocol::{self as acp, UserMessageChunk};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::ActionLog;
|
||||
use buffer_diff::BufferDiff;
|
||||
use editor::{MultiBuffer, PathKey};
|
||||
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
|
||||
use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
|
||||
use itertools::Itertools;
|
||||
use language::{Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _};
|
||||
use language::{
|
||||
Anchor, Buffer, BufferSnapshot, Capability, LanguageRegistry, OffsetRangeExt as _, Point,
|
||||
text_diff,
|
||||
};
|
||||
use markdown::Markdown;
|
||||
use project::Project;
|
||||
use project::{AgentLocation, Project};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Formatter, Write};
|
||||
use std::{
|
||||
@@ -159,6 +164,26 @@ impl AgentThreadEntry {
|
||||
Self::ToolCall(too_call) => too_call.to_markdown(cx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diff(&self) -> Option<&Diff> {
|
||||
if let AgentThreadEntry::ToolCall(ToolCall {
|
||||
content: Some(ToolCallContent::Diff { diff }),
|
||||
..
|
||||
}) = self
|
||||
{
|
||||
Some(&diff)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn locations(&self) -> Option<&[acp::ToolCallLocation]> {
|
||||
if let AgentThreadEntry::ToolCall(ToolCall { locations, .. }) = self {
|
||||
Some(locations)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -168,6 +193,7 @@ pub struct ToolCall {
|
||||
pub icon: IconName,
|
||||
pub content: Option<ToolCallContent>,
|
||||
pub status: ToolCallStatus,
|
||||
pub locations: Vec<acp::ToolCallLocation>,
|
||||
}
|
||||
|
||||
impl ToolCall {
|
||||
@@ -328,6 +354,8 @@ impl ToolCallContent {
|
||||
pub struct Diff {
|
||||
pub multibuffer: Entity<MultiBuffer>,
|
||||
pub path: PathBuf,
|
||||
pub new_buffer: Entity<Buffer>,
|
||||
pub old_buffer: Entity<Buffer>,
|
||||
_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
@@ -362,6 +390,7 @@ impl Diff {
|
||||
let task = cx.spawn({
|
||||
let multibuffer = multibuffer.clone();
|
||||
let path = path.clone();
|
||||
let new_buffer = new_buffer.clone();
|
||||
async move |cx| {
|
||||
diff_task.await?;
|
||||
|
||||
@@ -401,6 +430,8 @@ impl Diff {
|
||||
Self {
|
||||
multibuffer,
|
||||
path,
|
||||
new_buffer,
|
||||
old_buffer,
|
||||
_task: task,
|
||||
}
|
||||
}
|
||||
@@ -421,6 +452,8 @@ pub struct AcpThread {
|
||||
entries: Vec<AgentThreadEntry>,
|
||||
title: SharedString,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>,
|
||||
send_task: Option<Task<()>>,
|
||||
connection: Arc<acp::AgentConnection>,
|
||||
child_status: Option<Task<Result<()>>>,
|
||||
@@ -522,7 +555,11 @@ impl AcpThread {
|
||||
}
|
||||
});
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
Self {
|
||||
action_log,
|
||||
shared_buffers: Default::default(),
|
||||
entries: Default::default(),
|
||||
title: "ACP Thread".into(),
|
||||
project,
|
||||
@@ -534,6 +571,14 @@ impl AcpThread {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn action_log(&self) -> &Entity<ActionLog> {
|
||||
&self.action_log
|
||||
}
|
||||
|
||||
pub fn project(&self) -> &Entity<Project> {
|
||||
&self.project
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn fake(
|
||||
stdin: async_pipe::PipeWriter,
|
||||
@@ -558,7 +603,11 @@ impl AcpThread {
|
||||
}
|
||||
});
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
Self {
|
||||
action_log,
|
||||
shared_buffers: Default::default(),
|
||||
entries: Default::default(),
|
||||
title: "ACP Thread".into(),
|
||||
project,
|
||||
@@ -589,6 +638,26 @@ impl AcpThread {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_pending_edit_tool_calls(&self) -> bool {
|
||||
for entry in self.entries.iter().rev() {
|
||||
match entry {
|
||||
AgentThreadEntry::UserMessage(_) => return false,
|
||||
AgentThreadEntry::ToolCall(ToolCall {
|
||||
status:
|
||||
ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::Running,
|
||||
..
|
||||
},
|
||||
content: Some(ToolCallContent::Diff { .. }),
|
||||
..
|
||||
}) => return true,
|
||||
AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
|
||||
self.entries.push(entry);
|
||||
cx.emit(AcpThreadEvent::NewEntry);
|
||||
@@ -644,65 +713,63 @@ impl AcpThread {
|
||||
|
||||
pub fn request_tool_call(
|
||||
&mut self,
|
||||
label: String,
|
||||
icon: acp::Icon,
|
||||
content: Option<acp::ToolCallContent>,
|
||||
confirmation: acp::ToolCallConfirmation,
|
||||
tool_call: acp::RequestToolCallConfirmationParams,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ToolCallRequest {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
let status = ToolCallStatus::WaitingForConfirmation {
|
||||
confirmation: ToolCallConfirmation::from_acp(
|
||||
confirmation,
|
||||
tool_call.confirmation,
|
||||
self.project.read(cx).languages().clone(),
|
||||
cx,
|
||||
),
|
||||
respond_tx: tx,
|
||||
};
|
||||
|
||||
let id = self.insert_tool_call(label, status, icon, content, cx);
|
||||
let id = self.insert_tool_call(tool_call.tool_call, status, cx);
|
||||
ToolCallRequest { id, outcome: rx }
|
||||
}
|
||||
|
||||
pub fn push_tool_call(
|
||||
&mut self,
|
||||
label: String,
|
||||
icon: acp::Icon,
|
||||
content: Option<acp::ToolCallContent>,
|
||||
request: acp::PushToolCallParams,
|
||||
cx: &mut Context<Self>,
|
||||
) -> acp::ToolCallId {
|
||||
let status = ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::Running,
|
||||
};
|
||||
|
||||
self.insert_tool_call(label, status, icon, content, cx)
|
||||
self.insert_tool_call(request, status, cx)
|
||||
}
|
||||
|
||||
fn insert_tool_call(
|
||||
&mut self,
|
||||
label: String,
|
||||
tool_call: acp::PushToolCallParams,
|
||||
status: ToolCallStatus,
|
||||
icon: acp::Icon,
|
||||
content: Option<acp::ToolCallContent>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> acp::ToolCallId {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
let id = acp::ToolCallId(self.entries.len() as u64);
|
||||
|
||||
self.push_entry(
|
||||
AgentThreadEntry::ToolCall(ToolCall {
|
||||
id,
|
||||
label: cx.new(|cx| {
|
||||
Markdown::new(label.into(), Some(language_registry.clone()), None, cx)
|
||||
}),
|
||||
icon: acp_icon_to_ui_icon(icon),
|
||||
content: content
|
||||
.map(|content| ToolCallContent::from_acp(content, language_registry, cx)),
|
||||
status,
|
||||
let call = ToolCall {
|
||||
id,
|
||||
label: cx.new(|cx| {
|
||||
Markdown::new(
|
||||
tool_call.label.into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
icon: acp_icon_to_ui_icon(tool_call.icon),
|
||||
content: tool_call
|
||||
.content
|
||||
.map(|content| ToolCallContent::from_acp(content, language_registry, cx)),
|
||||
locations: tool_call.locations,
|
||||
status,
|
||||
};
|
||||
|
||||
self.push_entry(AgentThreadEntry::ToolCall(call), cx);
|
||||
|
||||
id
|
||||
}
|
||||
@@ -804,14 +871,16 @@ impl AcpThread {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn initialize(&self) -> impl use<> + Future<Output = Result<acp::InitializeResponse>> {
|
||||
pub fn initialize(
|
||||
&self,
|
||||
) -> impl use<> + Future<Output = Result<acp::InitializeResponse, acp::Error>> {
|
||||
let connection = self.connection.clone();
|
||||
async move { Ok(connection.request(acp::InitializeParams).await?) }
|
||||
async move { connection.request(acp::InitializeParams).await }
|
||||
}
|
||||
|
||||
pub fn authenticate(&self) -> impl use<> + Future<Output = Result<()>> {
|
||||
pub fn authenticate(&self) -> impl use<> + Future<Output = Result<(), acp::Error>> {
|
||||
let connection = self.connection.clone();
|
||||
async move { Ok(connection.request(acp::AuthenticateParams).await?) }
|
||||
async move { connection.request(acp::AuthenticateParams).await }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -819,7 +888,7 @@ impl AcpThread {
|
||||
&mut self,
|
||||
message: &str,
|
||||
cx: &mut Context<Self>,
|
||||
) -> BoxFuture<'static, Result<()>> {
|
||||
) -> BoxFuture<'static, Result<(), acp::Error>> {
|
||||
self.send(
|
||||
acp::SendUserMessageParams {
|
||||
chunks: vec![acp::UserMessageChunk::Text {
|
||||
@@ -834,7 +903,7 @@ impl AcpThread {
|
||||
&mut self,
|
||||
message: acp::SendUserMessageParams,
|
||||
cx: &mut Context<Self>,
|
||||
) -> BoxFuture<'static, Result<()>> {
|
||||
) -> BoxFuture<'static, Result<(), acp::Error>> {
|
||||
let agent = self.connection.clone();
|
||||
self.push_entry(
|
||||
AgentThreadEntry::UserMessage(UserMessage::from_acp(
|
||||
@@ -865,7 +934,7 @@ impl AcpThread {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<(), acp::Error>> {
|
||||
let agent = self.connection.clone();
|
||||
|
||||
if self.send_task.take().is_some() {
|
||||
@@ -898,13 +967,123 @@ impl AcpThread {
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_text_file(
|
||||
&self,
|
||||
request: acp::ReadTextFileParams,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<String>> {
|
||||
let project = self.project.clone();
|
||||
let action_log = self.action_log.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
let load = project.update(cx, |project, cx| {
|
||||
let path = project
|
||||
.project_path_for_absolute_path(&request.path, cx)
|
||||
.context("invalid path")?;
|
||||
anyhow::Ok(project.open_buffer(path, cx))
|
||||
});
|
||||
let buffer = load??.await?;
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
})?;
|
||||
project.update(cx, |project, cx| {
|
||||
let position = buffer
|
||||
.read(cx)
|
||||
.snapshot()
|
||||
.anchor_before(Point::new(request.line.unwrap_or_default(), 0));
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
|
||||
this.update(cx, |this, _| {
|
||||
let text = snapshot.text();
|
||||
this.shared_buffers.insert(buffer.clone(), snapshot);
|
||||
text
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_text_file(
|
||||
&self,
|
||||
path: PathBuf,
|
||||
content: String,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let project = self.project.clone();
|
||||
let action_log = self.action_log.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
let load = project.update(cx, |project, cx| {
|
||||
let path = project
|
||||
.project_path_for_absolute_path(&path, cx)
|
||||
.context("invalid path")?;
|
||||
anyhow::Ok(project.open_buffer(path, cx))
|
||||
});
|
||||
let buffer = load??.await?;
|
||||
let snapshot = this.update(cx, |this, cx| {
|
||||
this.shared_buffers
|
||||
.get(&buffer)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| buffer.read(cx).snapshot())
|
||||
})?;
|
||||
let edits = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let old_text = snapshot.text();
|
||||
text_diff(old_text.as_str(), &content)
|
||||
.into_iter()
|
||||
.map(|(range, replacement)| {
|
||||
(
|
||||
snapshot.anchor_after(range.start)
|
||||
..snapshot.anchor_before(range.end),
|
||||
replacement,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.await;
|
||||
cx.update(|cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: edits
|
||||
.last()
|
||||
.map(|(range, _)| range.end)
|
||||
.unwrap_or(Anchor::MIN),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_read(buffer.clone(), cx);
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits, None, cx);
|
||||
});
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_edited(buffer.clone(), cx);
|
||||
});
|
||||
})?;
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
pub fn child_status(&mut self) -> Option<Task<Result<()>>> {
|
||||
self.child_status.take()
|
||||
}
|
||||
@@ -930,7 +1109,7 @@ impl acp::Client for AcpClientDelegate {
|
||||
async fn stream_assistant_message_chunk(
|
||||
&self,
|
||||
params: acp::StreamAssistantMessageChunkParams,
|
||||
) -> Result<()> {
|
||||
) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
@@ -947,45 +1126,37 @@ impl acp::Client for AcpClientDelegate {
|
||||
async fn request_tool_call_confirmation(
|
||||
&self,
|
||||
request: acp::RequestToolCallConfirmationParams,
|
||||
) -> Result<acp::RequestToolCallConfirmationResponse> {
|
||||
) -> Result<acp::RequestToolCallConfirmationResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let ToolCallRequest { id, outcome } = cx
|
||||
.update(|cx| {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.request_tool_call(
|
||||
request.label,
|
||||
request.icon,
|
||||
request.content,
|
||||
request.confirmation,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.request_tool_call(request, cx))
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(acp::RequestToolCallConfirmationResponse {
|
||||
id,
|
||||
outcome: outcome.await?,
|
||||
outcome: outcome.await.map_err(acp::Error::into_internal_error)?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn push_tool_call(
|
||||
&self,
|
||||
request: acp::PushToolCallParams,
|
||||
) -> Result<acp::PushToolCallResponse> {
|
||||
) -> Result<acp::PushToolCallResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let id = cx
|
||||
.update(|cx| {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.push_tool_call(request.label, request.icon, request.content, cx)
|
||||
})
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.push_tool_call(request, cx))
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(acp::PushToolCallResponse { id })
|
||||
}
|
||||
|
||||
async fn update_tool_call(&self, request: acp::UpdateToolCallParams) -> Result<()> {
|
||||
async fn update_tool_call(&self, request: acp::UpdateToolCallParams) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
@@ -997,6 +1168,34 @@ impl acp::Client for AcpClientDelegate {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
&self,
|
||||
request: acp::ReadTextFileParams,
|
||||
) -> Result<acp::ReadTextFileResponse, acp::Error> {
|
||||
let content = self
|
||||
.cx
|
||||
.update(|cx| {
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.read_text_file(request, cx))
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
Ok(acp::ReadTextFileResponse { content })
|
||||
}
|
||||
|
||||
async fn write_text_file(&self, request: acp::WriteTextFileParams) -> Result<(), acp::Error> {
|
||||
self.cx
|
||||
.update(|cx| {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.write_text_file(request.path, request.content, cx)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn acp_icon_to_ui_icon(icon: acp::Icon) -> IconName {
|
||||
@@ -1100,6 +1299,80 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edits_concurrently_to_user(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/tmp"), json!({"foo": "one\ntwo\nthree\n"}))
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [], cx).await;
|
||||
let (thread, fake_server) = fake_acp_thread(project.clone(), cx);
|
||||
let (worktree, pathbuf) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree(path!("/tmp/foo"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree.read(cx).id(), pathbuf), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (read_file_tx, read_file_rx) = oneshot::channel::<()>();
|
||||
let read_file_tx = Rc::new(RefCell::new(Some(read_file_tx)));
|
||||
|
||||
fake_server.update(cx, |fake_server, _| {
|
||||
fake_server.on_user_message(move |_, server, mut cx| {
|
||||
let read_file_tx = read_file_tx.clone();
|
||||
async move {
|
||||
let content = server
|
||||
.update(&mut cx, |server, _| {
|
||||
server.send_to_zed(acp::ReadTextFileParams {
|
||||
path: path!("/tmp/foo").into(),
|
||||
line: None,
|
||||
limit: None,
|
||||
})
|
||||
})?
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(content.content, "one\ntwo\nthree\n");
|
||||
read_file_tx.take().unwrap().send(()).unwrap();
|
||||
server
|
||||
.update(&mut cx, |server, _| {
|
||||
server.send_to_zed(acp::WriteTextFileParams {
|
||||
path: path!("/tmp/foo").into(),
|
||||
content: "one\ntwo\nthree\nfour\nfive\n".to_string(),
|
||||
})
|
||||
})?
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.send_raw("Extend the count in /tmp/foo", cx)
|
||||
});
|
||||
read_file_rx.await.ok();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "zero\n".to_string())], None, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||
"zero\none\ntwo\nthree\nfour\nfive\n"
|
||||
);
|
||||
assert_eq!(
|
||||
String::from_utf8(fs.read_file_sync(path!("/tmp/foo")).unwrap()).unwrap(),
|
||||
"zero\none\ntwo\nthree\nfour\nfive\n"
|
||||
);
|
||||
request.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -1124,6 +1397,7 @@ mod tests {
|
||||
label: "Fetch".to_string(),
|
||||
icon: acp::Icon::Globe,
|
||||
content: None,
|
||||
locations: vec![],
|
||||
})
|
||||
})?
|
||||
.await
|
||||
@@ -1553,7 +1827,7 @@ mod tests {
|
||||
acp::SendUserMessageParams,
|
||||
Entity<FakeAcpServer>,
|
||||
AsyncApp,
|
||||
) -> LocalBoxFuture<'static, Result<()>>,
|
||||
) -> LocalBoxFuture<'static, Result<(), acp::Error>>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
@@ -1565,21 +1839,24 @@ mod tests {
|
||||
}
|
||||
|
||||
impl acp::Agent for FakeAgent {
|
||||
async fn initialize(&self) -> Result<acp::InitializeResponse> {
|
||||
async fn initialize(&self) -> Result<acp::InitializeResponse, acp::Error> {
|
||||
Ok(acp::InitializeResponse {
|
||||
is_authenticated: true,
|
||||
})
|
||||
}
|
||||
|
||||
async fn authenticate(&self) -> Result<()> {
|
||||
async fn authenticate(&self) -> Result<(), acp::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cancel_send_message(&self) -> Result<()> {
|
||||
async fn cancel_send_message(&self) -> Result<(), acp::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_user_message(&self, request: acp::SendUserMessageParams) -> Result<()> {
|
||||
async fn send_user_message(
|
||||
&self,
|
||||
request: acp::SendUserMessageParams,
|
||||
) -> Result<(), acp::Error> {
|
||||
let mut cx = self.cx.clone();
|
||||
let handler = self
|
||||
.server
|
||||
@@ -1589,7 +1866,7 @@ mod tests {
|
||||
if let Some(handler) = handler {
|
||||
handler(request, self.server.clone(), self.cx.clone()).await
|
||||
} else {
|
||||
anyhow::bail!("No handler for on_user_message")
|
||||
Err(anyhow::anyhow!("No handler for on_user_message").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1624,7 +1901,7 @@ mod tests {
|
||||
handler: impl for<'a> Fn(acp::SendUserMessageParams, Entity<FakeAcpServer>, AsyncApp) -> F
|
||||
+ 'static,
|
||||
) where
|
||||
F: Future<Output = Result<()>> + 'static,
|
||||
F: Future<Output = Result<(), acp::Error>> + 'static,
|
||||
{
|
||||
self.on_user_message
|
||||
.replace(Rc::new(move |request, server, cx| {
|
||||
|
||||
@@ -448,7 +448,7 @@ impl ActivityIndicator {
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Debug: {}", session.read(cx).adapter()),
|
||||
tooltip_message: Some(session.read(cx).label().to_string()),
|
||||
tooltip_message: session.read(cx).label().map(|label| label.to_string()),
|
||||
on_click: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@ mod completion_provider;
|
||||
mod message_history;
|
||||
mod thread_view;
|
||||
|
||||
pub use message_history::MessageHistory;
|
||||
pub use thread_view::AcpThreadView;
|
||||
|
||||
@@ -3,19 +3,25 @@ pub struct MessageHistory<T> {
|
||||
current: Option<usize>,
|
||||
}
|
||||
|
||||
impl<T> MessageHistory<T> {
|
||||
pub fn new() -> Self {
|
||||
impl<T> Default for MessageHistory<T> {
|
||||
fn default() -> Self {
|
||||
MessageHistory {
|
||||
items: Vec::new(),
|
||||
current: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MessageHistory<T> {
|
||||
pub fn push(&mut self, message: T) {
|
||||
self.current.take();
|
||||
self.items.push(message);
|
||||
}
|
||||
|
||||
pub fn reset_position(&mut self) {
|
||||
self.current.take();
|
||||
}
|
||||
|
||||
pub fn prev(&mut self) -> Option<&T> {
|
||||
if self.items.is_empty() {
|
||||
return None;
|
||||
@@ -46,7 +52,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_prev_next() {
|
||||
let mut history = MessageHistory::new();
|
||||
let mut history = MessageHistory::default();
|
||||
|
||||
// Test empty history
|
||||
assert_eq!(history.prev(), None);
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use agentic_coding_protocol::{self as acp};
|
||||
use assistant_tool::ActionLog;
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
|
||||
EditorStyle, MinimapVisibility, MultiBuffer,
|
||||
EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
|
||||
};
|
||||
use file_icons::FileIcons;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId, Focusable,
|
||||
Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement, Subscription, TextStyle,
|
||||
TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, div, list, percentage,
|
||||
prelude::*, pulsating_between,
|
||||
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
|
||||
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
|
||||
Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
|
||||
Window, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*,
|
||||
pulsating_between,
|
||||
};
|
||||
use gpui::{FocusHandle, Task};
|
||||
use language::language_settings::SoftWrap;
|
||||
use language::{Buffer, Language};
|
||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||
use parking_lot::Mutex;
|
||||
use project::Project;
|
||||
use settings::Settings as _;
|
||||
use text::Anchor;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{Disclosure, Tooltip, prelude::*};
|
||||
use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
|
||||
|
||||
use ::acp::{
|
||||
@@ -38,6 +43,8 @@ use ::acp::{
|
||||
|
||||
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
|
||||
use crate::acp::message_history::MessageHistory;
|
||||
use crate::agent_diff::AgentDiff;
|
||||
use crate::{AgentDiffPane, Follow, KeepAll, OpenAgentDiff, RejectAll};
|
||||
|
||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
||||
|
||||
@@ -47,13 +54,16 @@ pub struct AcpThreadView {
|
||||
thread_state: ThreadState,
|
||||
diff_editors: HashMap<EntityId, Entity<Editor>>,
|
||||
message_editor: Entity<Editor>,
|
||||
message_set_from_history: bool,
|
||||
_message_editor_subscription: Subscription,
|
||||
mention_set: Arc<Mutex<MentionSet>>,
|
||||
last_error: Option<Entity<Markdown>>,
|
||||
list_state: ListState,
|
||||
auth_task: Option<Task<()>>,
|
||||
expanded_tool_calls: HashSet<ToolCallId>,
|
||||
expanded_thinking_blocks: HashSet<(usize, usize)>,
|
||||
message_history: MessageHistory<acp::SendUserMessageParams>,
|
||||
edits_expanded: bool,
|
||||
message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
|
||||
}
|
||||
|
||||
enum ThreadState {
|
||||
@@ -62,7 +72,7 @@ enum ThreadState {
|
||||
},
|
||||
Ready {
|
||||
thread: Entity<AcpThread>,
|
||||
_subscription: Subscription,
|
||||
_subscription: [Subscription; 2],
|
||||
},
|
||||
LoadError(LoadError),
|
||||
Unauthenticated {
|
||||
@@ -74,6 +84,7 @@ impl AcpThreadView {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -118,6 +129,17 @@ impl AcpThreadView {
|
||||
editor
|
||||
});
|
||||
|
||||
let message_editor_subscription = cx.subscribe(&message_editor, |this, _, event, _| {
|
||||
if let editor::EditorEvent::BufferEdited = &event {
|
||||
if !this.message_set_from_history {
|
||||
this.message_history.borrow_mut().reset_position();
|
||||
}
|
||||
this.message_set_from_history = false;
|
||||
}
|
||||
});
|
||||
|
||||
let mention_set = mention_set.clone();
|
||||
|
||||
let list_state = ListState::new(
|
||||
0,
|
||||
gpui::ListAlignment::Bottom,
|
||||
@@ -136,10 +158,12 @@ impl AcpThreadView {
|
||||
);
|
||||
|
||||
Self {
|
||||
workspace,
|
||||
workspace: workspace.clone(),
|
||||
project: project.clone(),
|
||||
thread_state: Self::initial_state(project, window, cx),
|
||||
thread_state: Self::initial_state(workspace, project, window, cx),
|
||||
message_editor,
|
||||
message_set_from_history: false,
|
||||
_message_editor_subscription: message_editor_subscription,
|
||||
mention_set,
|
||||
diff_editors: Default::default(),
|
||||
list_state: list_state,
|
||||
@@ -147,11 +171,13 @@ impl AcpThreadView {
|
||||
auth_task: None,
|
||||
expanded_tool_calls: HashSet::default(),
|
||||
expanded_thinking_blocks: HashSet::default(),
|
||||
message_history: MessageHistory::new(),
|
||||
edits_expanded: false,
|
||||
message_history,
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_state(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -219,15 +245,23 @@ impl AcpThreadView {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
let subscription =
|
||||
let thread_subscription =
|
||||
cx.subscribe_in(&thread, window, Self::handle_thread_event);
|
||||
|
||||
let action_log = thread.read(cx).action_log().clone();
|
||||
let action_log_subscription =
|
||||
cx.observe(&action_log, |_, _, cx| cx.notify());
|
||||
|
||||
this.list_state
|
||||
.splice(0..0, thread.read(cx).entries().len());
|
||||
|
||||
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
|
||||
|
||||
this.thread_state = ThreadState::Ready {
|
||||
thread,
|
||||
_subscription: subscription,
|
||||
_subscription: [thread_subscription, action_log_subscription],
|
||||
};
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -250,7 +284,7 @@ impl AcpThreadView {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn thread(&self) -> Option<&Entity<AcpThread>> {
|
||||
pub fn thread(&self) -> Option<&Entity<AcpThread>> {
|
||||
match &self.thread_state {
|
||||
ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
|
||||
Some(thread)
|
||||
@@ -281,7 +315,6 @@ impl AcpThreadView {
|
||||
|
||||
let mut ix = 0;
|
||||
let mut chunks: Vec<acp::UserMessageChunk> = Vec::new();
|
||||
|
||||
let project = self.project.clone();
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
let text = editor.text(cx);
|
||||
@@ -342,7 +375,7 @@ impl AcpThreadView {
|
||||
editor.remove_creases(mention_set.lock().drain(), cx)
|
||||
});
|
||||
|
||||
self.message_history.push(message);
|
||||
self.message_history.borrow_mut().push(message);
|
||||
}
|
||||
|
||||
fn previous_history_message(
|
||||
@@ -351,11 +384,11 @@ impl AcpThreadView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
Self::set_draft_message(
|
||||
self.message_set_from_history = Self::set_draft_message(
|
||||
self.message_editor.clone(),
|
||||
self.mention_set.clone(),
|
||||
self.project.clone(),
|
||||
self.message_history.prev(),
|
||||
self.message_history.borrow_mut().prev(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -367,11 +400,11 @@ impl AcpThreadView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
Self::set_draft_message(
|
||||
self.message_set_from_history = Self::set_draft_message(
|
||||
self.message_editor.clone(),
|
||||
self.mention_set.clone(),
|
||||
self.project.clone(),
|
||||
self.message_history.next(),
|
||||
self.message_history.borrow_mut().next(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -384,15 +417,11 @@ impl AcpThreadView {
|
||||
message: Option<&acp::SendUserMessageParams>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
) -> bool {
|
||||
cx.notify();
|
||||
|
||||
let Some(message) = message else {
|
||||
message_editor.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
editor.remove_creases(mention_set.lock().drain(), cx)
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut text = String::new();
|
||||
@@ -452,6 +481,35 @@ impl AcpThreadView {
|
||||
mention_set.lock().insert(crease_id, project_path);
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(thread) = self.thread() {
|
||||
AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn open_edited_buffer(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(thread) = self.thread() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(diff) =
|
||||
AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
@@ -464,7 +522,8 @@ impl AcpThreadView {
|
||||
let count = self.list_state.item_count();
|
||||
match event {
|
||||
AcpThreadEvent::NewEntry => {
|
||||
self.sync_thread_entry_view(thread.read(cx).entries().len() - 1, window, cx);
|
||||
let index = thread.read(cx).entries().len() - 1;
|
||||
self.sync_thread_entry_view(index, window, cx);
|
||||
self.list_state.splice(count..count, 1);
|
||||
}
|
||||
AcpThreadEvent::EntryUpdated(index) => {
|
||||
@@ -537,15 +596,7 @@ impl AcpThreadView {
|
||||
|
||||
fn entry_diff_multibuffer(&self, entry_ix: usize, cx: &App) -> Option<Entity<MultiBuffer>> {
|
||||
let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
|
||||
if let AgentThreadEntry::ToolCall(ToolCall {
|
||||
content: Some(ToolCallContent::Diff { diff }),
|
||||
..
|
||||
}) = &entry
|
||||
{
|
||||
Some(diff.multibuffer.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
entry.diff().map(|diff| diff.multibuffer.clone())
|
||||
}
|
||||
|
||||
fn authenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -566,7 +617,8 @@ impl AcpThreadView {
|
||||
Markdown::new(format!("Error: {err}").into(), None, None, cx)
|
||||
}))
|
||||
} else {
|
||||
this.thread_state = Self::initial_state(project.clone(), window, cx)
|
||||
this.thread_state =
|
||||
Self::initial_state(this.workspace.clone(), project.clone(), window, cx)
|
||||
}
|
||||
this.auth_task.take()
|
||||
})
|
||||
@@ -873,10 +925,43 @@ impl AcpThreadView {
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(self.render_markdown(
|
||||
tool_call.label.clone(),
|
||||
default_markdown_style(needs_confirmation, window, cx),
|
||||
)),
|
||||
.child(if tool_call.locations.len() == 1 {
|
||||
let name = tool_call.locations[0]
|
||||
.path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.display()
|
||||
.to_string();
|
||||
|
||||
h_flex()
|
||||
.id(("open-tool-call-location", entry_ix))
|
||||
.child(name)
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.pr_1()
|
||||
.gap_0p5()
|
||||
.cursor_pointer()
|
||||
.rounded_sm()
|
||||
.opacity(0.8)
|
||||
.hover(|label| {
|
||||
label.opacity(1.).bg(cx
|
||||
.theme()
|
||||
.colors()
|
||||
.element_hover
|
||||
.opacity(0.5))
|
||||
})
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.open_tool_call_location(entry_ix, 0, window, cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
} else {
|
||||
self.render_markdown(
|
||||
tool_call.label.clone(),
|
||||
default_markdown_style(needs_confirmation, window, cx),
|
||||
)
|
||||
.into_any()
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -936,15 +1021,19 @@ impl AcpThreadView {
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
match content {
|
||||
ToolCallContent::Markdown { markdown } => self
|
||||
.render_markdown(markdown.clone(), default_markdown_style(false, window, cx))
|
||||
.into_any_element(),
|
||||
ToolCallContent::Markdown { markdown } => {
|
||||
div()
|
||||
.p_2()
|
||||
.child(self.render_markdown(
|
||||
markdown.clone(),
|
||||
default_markdown_style(false, window, cx),
|
||||
))
|
||||
.into_any_element()
|
||||
}
|
||||
ToolCallContent::Diff {
|
||||
diff: Diff {
|
||||
path, multibuffer, ..
|
||||
},
|
||||
diff: Diff { multibuffer, .. },
|
||||
..
|
||||
} => self.render_diff_editor(multibuffer, path),
|
||||
} => self.render_diff_editor(multibuffer),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1364,10 +1453,9 @@ impl AcpThreadView {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>, path: &Path) -> AnyElement {
|
||||
fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
|
||||
v_flex()
|
||||
.h_full()
|
||||
.child(path.to_string_lossy().to_string())
|
||||
.child(
|
||||
if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) {
|
||||
editor.clone().into_any_element()
|
||||
@@ -1529,6 +1617,357 @@ impl AcpThreadView {
|
||||
container.into_any()
|
||||
}
|
||||
|
||||
fn render_edits_bar(
|
||||
&self,
|
||||
thread_entity: &Entity<AcpThread>,
|
||||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
let thread = thread_entity.read(cx);
|
||||
let action_log = thread.action_log();
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
|
||||
if changed_buffers.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
let active_color = cx.theme().colors().element_selected;
|
||||
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
|
||||
|
||||
let pending_edits = thread.has_pending_edit_tool_calls();
|
||||
let expanded = self.edits_expanded;
|
||||
|
||||
v_flex()
|
||||
.mt_1()
|
||||
.mx_2()
|
||||
.bg(bg_edit_files_disclosure)
|
||||
.border_1()
|
||||
.border_b_0()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_t_md()
|
||||
.shadow(vec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.15),
|
||||
offset: point(px(1.), px(-1.)),
|
||||
blur_radius: px(3.),
|
||||
spread_radius: px(0.),
|
||||
}])
|
||||
.child(self.render_edits_bar_summary(
|
||||
action_log,
|
||||
&changed_buffers,
|
||||
expanded,
|
||||
pending_edits,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.when(expanded, |parent| {
|
||||
parent.child(self.render_edits_bar_files(
|
||||
action_log,
|
||||
&changed_buffers,
|
||||
pending_edits,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.into_any()
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_edits_bar_summary(
|
||||
&self,
|
||||
action_log: &Entity<ActionLog>,
|
||||
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
|
||||
expanded: bool,
|
||||
pending_edits: bool,
|
||||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
|
||||
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
h_flex()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.when(expanded, |this| {
|
||||
this.border_b_1().border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.id("edits-container")
|
||||
.cursor_pointer()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(Disclosure::new("edits-disclosure", expanded))
|
||||
.map(|this| {
|
||||
if pending_edits {
|
||||
this.child(
|
||||
Label::new(format!(
|
||||
"Editing {} {}…",
|
||||
changed_buffers.len(),
|
||||
if changed_buffers.len() == 1 {
|
||||
"file"
|
||||
} else {
|
||||
"files"
|
||||
}
|
||||
))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.with_animation(
|
||||
"edit-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.3, 0.7)),
|
||||
|label, delta| label.alpha(delta),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
Label::new("Edits")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} {}",
|
||||
changed_buffers.len(),
|
||||
if changed_buffers.len() == 1 {
|
||||
"file"
|
||||
} else {
|
||||
"files"
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.edits_expanded = !this.edits_expanded;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new("review-changes", IconName::ListTodo)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Review Changes",
|
||||
&OpenAgentDiff,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
|
||||
})),
|
||||
)
|
||||
.child(Divider::vertical().color(DividerColor::Border))
|
||||
.child(
|
||||
Button::new("reject-all-changes", "Reject All")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.when(pending_edits, |this| {
|
||||
this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
|
||||
})
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&RejectAll,
|
||||
&focus_handle.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
.on_click({
|
||||
let action_log = action_log.clone();
|
||||
cx.listener(move |_, _, _, cx| {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.reject_all_edits(cx).detach();
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("keep-all-changes", "Keep All")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.when(pending_edits, |this| {
|
||||
this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
|
||||
})
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
.on_click({
|
||||
let action_log = action_log.clone();
|
||||
cx.listener(move |_, _, _, cx| {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.keep_all_edits(cx);
|
||||
})
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_edits_bar_files(
|
||||
&self,
|
||||
action_log: &Entity<ActionLog>,
|
||||
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
|
||||
pending_edits: bool,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
|
||||
v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
|
||||
|(index, (buffer, _diff))| {
|
||||
let file = buffer.read(cx).file()?;
|
||||
let path = file.path();
|
||||
|
||||
let file_path = path.parent().and_then(|parent| {
|
||||
let parent_str = parent.to_string_lossy();
|
||||
|
||||
if parent_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
let file_name = path.file_name().map(|name| {
|
||||
Label::new(name.to_string_lossy().to_string())
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx)
|
||||
});
|
||||
|
||||
let file_icon = FileIcons::get_icon(&path, cx)
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::Small))
|
||||
.unwrap_or_else(|| {
|
||||
Icon::new(IconName::File)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
});
|
||||
|
||||
let overlay_gradient = linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(editor_bg_color, 1.),
|
||||
linear_color_stop(editor_bg_color.opacity(0.2), 0.),
|
||||
);
|
||||
|
||||
let element = h_flex()
|
||||
.group("edited-code")
|
||||
.id(("file-container", index))
|
||||
.relative()
|
||||
.py_1()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.bg(editor_bg_color)
|
||||
.when(index < changed_buffers.len() - 1, |parent| {
|
||||
parent.border_color(cx.theme().colors().border).border_b_1()
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.id(("file-name", index))
|
||||
.pr_8()
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.overflow_x_scroll()
|
||||
.child(file_icon)
|
||||
.child(h_flex().gap_0p5().children(file_name).children(file_path))
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.open_edited_buffer(&buffer, window, cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.visible_on_hover("edited-code")
|
||||
.child(
|
||||
Button::new("review", "Review")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.open_edited_buffer(&buffer, window, cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(Divider::vertical().color(DividerColor::BorderVariant))
|
||||
.child(
|
||||
Button::new("reject-file", "Reject")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
let action_log = action_log.clone();
|
||||
move |_, _, cx| {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log
|
||||
.reject_edits_in_ranges(
|
||||
buffer.clone(),
|
||||
vec![Anchor::MIN..Anchor::MAX],
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("keep-file", "Keep")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
let action_log = action_log.clone();
|
||||
move |_, _, cx| {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.keep_edits_in_range(
|
||||
buffer.clone(),
|
||||
Anchor::MIN..Anchor::MAX,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("gradient-overlay")
|
||||
.absolute()
|
||||
.h_full()
|
||||
.w_12()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.right(px(152.))
|
||||
.bg(overlay_gradient),
|
||||
);
|
||||
|
||||
Some(element)
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn render_message_editor(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = TextSize::Small
|
||||
@@ -1559,6 +1998,76 @@ impl AcpThreadView {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
if self.thread().map_or(true, |thread| {
|
||||
thread.read(cx).status() == ThreadStatus::Idle
|
||||
}) {
|
||||
let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
|
||||
IconButton::new("send-message", IconName::Send)
|
||||
.icon_color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(self.thread().is_none() || is_editor_empty)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.chat(&Chat, window, cx);
|
||||
}))
|
||||
.when(!is_editor_empty, |button| {
|
||||
button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx))
|
||||
})
|
||||
.when(is_editor_empty, |button| {
|
||||
button.tooltip(Tooltip::text("Type a message to submit"))
|
||||
})
|
||||
.into_any_element()
|
||||
} else {
|
||||
IconButton::new("stop-generation", IconName::StopFilled)
|
||||
.icon_color(Color::Error)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Error))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
|
||||
})
|
||||
.on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let following = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| {
|
||||
workspace.is_being_followed(CollaboratorId::Agent)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
IconButton::new("follow-agent", IconName::Crosshair)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.toggle_state(following)
|
||||
.selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
|
||||
.tooltip(move |window, cx| {
|
||||
if following {
|
||||
Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
|
||||
} else {
|
||||
Tooltip::with_meta(
|
||||
"Follow Agent",
|
||||
Some(&Follow),
|
||||
"Track the agent's location as it reads and edits files.",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
if following {
|
||||
workspace.unfollow(CollaboratorId::Agent, window, cx);
|
||||
} else {
|
||||
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
|
||||
let workspace = self.workspace.clone();
|
||||
MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
|
||||
@@ -1603,6 +2112,64 @@ impl AcpThreadView {
|
||||
}
|
||||
}
|
||||
|
||||
fn open_tool_call_location(
|
||||
&self,
|
||||
entry_ix: usize,
|
||||
location_ix: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
let location = self
|
||||
.thread()?
|
||||
.read(cx)
|
||||
.entries()
|
||||
.get(entry_ix)?
|
||||
.locations()?
|
||||
.get(location_ix)?;
|
||||
|
||||
let project_path = self
|
||||
.project
|
||||
.read(cx)
|
||||
.find_project_path(&location.path, cx)?;
|
||||
|
||||
let open_task = self
|
||||
.workspace
|
||||
.update(cx, |worskpace, cx| {
|
||||
worskpace.open_path(project_path, None, true, window, cx)
|
||||
})
|
||||
.log_err()?;
|
||||
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let item = open_task.await?;
|
||||
|
||||
let Some(active_editor) = item.downcast::<Editor>() else {
|
||||
return anyhow::Ok(());
|
||||
};
|
||||
|
||||
active_editor.update_in(cx, |editor, window, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let first_hunk = editor
|
||||
.diff_hunks_in_ranges(
|
||||
&[editor::Anchor::min()..editor::Anchor::max()],
|
||||
&snapshot,
|
||||
)
|
||||
.next();
|
||||
if let Some(first_hunk) = first_hunk {
|
||||
let first_hunk_start = first_hunk.multi_buffer_range().start;
|
||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
||||
selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
|
||||
})
|
||||
}
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn open_thread_as_markdown(
|
||||
&self,
|
||||
workspace: Entity<Workspace>,
|
||||
@@ -1673,10 +2240,6 @@ impl Focusable for AcpThreadView {
|
||||
|
||||
impl Render for AcpThreadView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let text = self.message_editor.read(cx).text(cx);
|
||||
let is_editor_empty = text.is_empty();
|
||||
let focus_handle = self.message_editor.focus_handle(cx);
|
||||
|
||||
let open_as_markdown = IconButton::new("open-as-markdown", IconName::DocumentText)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
@@ -1702,6 +2265,7 @@ impl Render for AcpThreadView {
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(Self::previous_history_message))
|
||||
.on_action(cx.listener(Self::next_history_message))
|
||||
.on_action(cx.listener(Self::open_agent_diff))
|
||||
.child(match &self.thread_state {
|
||||
ThreadState::Unauthenticated { .. } => v_flex()
|
||||
.p_2()
|
||||
@@ -1755,6 +2319,7 @@ impl Render for AcpThreadView {
|
||||
.child(LoadingLabel::new("").size(LabelSize::Small))
|
||||
.into(),
|
||||
})
|
||||
.children(self.render_edits_bar(&thread, window, cx))
|
||||
} else {
|
||||
this.child(self.render_empty_state(false, cx))
|
||||
}
|
||||
@@ -1782,47 +2347,12 @@ impl Render for AcpThreadView {
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(self.render_message_editor(cx))
|
||||
.child({
|
||||
let thread = self.thread();
|
||||
|
||||
h_flex().justify_end().child(
|
||||
if thread.map_or(true, |thread| {
|
||||
thread.read(cx).status() == ThreadStatus::Idle
|
||||
}) {
|
||||
IconButton::new("send-message", IconName::Send)
|
||||
.icon_color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(thread.is_none() || is_editor_empty)
|
||||
.on_click({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(&Chat, window, cx);
|
||||
}
|
||||
})
|
||||
.when(!is_editor_empty, |button| {
|
||||
button.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("Send", &Chat, window, cx)
|
||||
})
|
||||
})
|
||||
.when(is_editor_empty, |button| {
|
||||
button.tooltip(Tooltip::text("Type a message to submit"))
|
||||
})
|
||||
} else {
|
||||
IconButton::new("stop-generation", IconName::StopFilled)
|
||||
.icon_color(Color::Error)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Error))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Stop Generation",
|
||||
&editor::actions::Cancel,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
|
||||
},
|
||||
)
|
||||
}),
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(self.render_follow_toggle(cx))
|
||||
.child(self.render_send_button(cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -787,6 +787,15 @@ impl ActiveThread {
|
||||
.unwrap()
|
||||
}
|
||||
});
|
||||
|
||||
let workspace_subscription = if let Some(workspace) = workspace.upgrade() {
|
||||
Some(cx.observe_release(&workspace, |this, _, cx| {
|
||||
this.dismiss_notifications(cx);
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut this = Self {
|
||||
language_registry,
|
||||
thread_store,
|
||||
@@ -834,6 +843,10 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(subscription) = workspace_subscription {
|
||||
this._subscriptions.push(subscription);
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
|
||||
use agent::{Thread, ThreadEvent};
|
||||
use acp::{AcpThread, AcpThreadEvent};
|
||||
use agent::{Thread, ThreadEvent, ThreadSummary};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::ActionLog;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
@@ -41,16 +43,108 @@ use zed_actions::assistant::ToggleFocus;
|
||||
pub struct AgentDiffPane {
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
editor: Entity<Editor>,
|
||||
thread: Entity<Thread>,
|
||||
thread: AgentDiffThread,
|
||||
focus_handle: FocusHandle,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
title: SharedString,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
pub enum AgentDiffThread {
|
||||
Native(Entity<Thread>),
|
||||
AcpThread(Entity<AcpThread>),
|
||||
}
|
||||
|
||||
impl AgentDiffThread {
|
||||
fn project(&self, cx: &App) -> Entity<Project> {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).project().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).project().clone(),
|
||||
}
|
||||
}
|
||||
fn action_log(&self, cx: &App) -> Entity<ActionLog> {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).action_log().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).action_log().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn summary(&self, cx: &App) -> ThreadSummary {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).summary().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => ThreadSummary::Ready(thread.read(cx).title()),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_generating(&self, cx: &App) -> bool {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).is_generating(),
|
||||
AgentDiffThread::AcpThread(thread) => {
|
||||
thread.read(cx).status() == acp::ThreadStatus::Generating
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_pending_edit_tool_uses(&self, cx: &App) -> bool {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).has_pending_edit_tool_uses(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(),
|
||||
}
|
||||
}
|
||||
|
||||
fn downgrade(&self) -> WeakAgentDiffThread {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => WeakAgentDiffThread::Native(thread.downgrade()),
|
||||
AgentDiffThread::AcpThread(thread) => {
|
||||
WeakAgentDiffThread::AcpThread(thread.downgrade())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Entity<Thread>> for AgentDiffThread {
|
||||
fn from(entity: Entity<Thread>) -> Self {
|
||||
AgentDiffThread::Native(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Entity<AcpThread>> for AgentDiffThread {
|
||||
fn from(entity: Entity<AcpThread>) -> Self {
|
||||
AgentDiffThread::AcpThread(entity)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
pub enum WeakAgentDiffThread {
|
||||
Native(WeakEntity<Thread>),
|
||||
AcpThread(WeakEntity<AcpThread>),
|
||||
}
|
||||
|
||||
impl WeakAgentDiffThread {
|
||||
pub fn upgrade(&self) -> Option<AgentDiffThread> {
|
||||
match self {
|
||||
WeakAgentDiffThread::Native(weak) => weak.upgrade().map(AgentDiffThread::Native),
|
||||
WeakAgentDiffThread::AcpThread(weak) => weak.upgrade().map(AgentDiffThread::AcpThread),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WeakEntity<Thread>> for WeakAgentDiffThread {
|
||||
fn from(entity: WeakEntity<Thread>) -> Self {
|
||||
WeakAgentDiffThread::Native(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WeakEntity<AcpThread>> for WeakAgentDiffThread {
|
||||
fn from(entity: WeakEntity<AcpThread>) -> Self {
|
||||
WeakAgentDiffThread::AcpThread(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentDiffPane {
|
||||
pub fn deploy(
|
||||
thread: Entity<Thread>,
|
||||
thread: impl Into<AgentDiffThread>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -61,14 +155,16 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
pub fn deploy_in_workspace(
|
||||
thread: Entity<Thread>,
|
||||
thread: impl Into<AgentDiffThread>,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Entity<Self> {
|
||||
let thread = thread.into();
|
||||
let existing_diff = workspace
|
||||
.items_of_type::<AgentDiffPane>(cx)
|
||||
.find(|diff| diff.read(cx).thread == thread);
|
||||
|
||||
if let Some(existing_diff) = existing_diff {
|
||||
workspace.activate_item(&existing_diff, true, true, window, cx);
|
||||
existing_diff
|
||||
@@ -81,7 +177,7 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
thread: Entity<Thread>,
|
||||
thread: AgentDiffThread,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -89,7 +185,7 @@ impl AgentDiffPane {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||||
|
||||
let project = thread.read(cx).project().clone();
|
||||
let project = thread.project(cx).clone();
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
|
||||
@@ -100,16 +196,27 @@ impl AgentDiffPane {
|
||||
editor
|
||||
});
|
||||
|
||||
let action_log = thread.read(cx).action_log().clone();
|
||||
let action_log = thread.action_log(cx).clone();
|
||||
|
||||
let mut this = Self {
|
||||
_subscriptions: vec![
|
||||
cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
|
||||
this.update_excerpts(window, cx)
|
||||
}),
|
||||
cx.subscribe(&thread, |this, _thread, event, cx| {
|
||||
this.handle_thread_event(event, cx)
|
||||
}),
|
||||
],
|
||||
_subscriptions: [
|
||||
Some(
|
||||
cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
|
||||
this.update_excerpts(window, cx)
|
||||
}),
|
||||
),
|
||||
match &thread {
|
||||
AgentDiffThread::Native(thread) => {
|
||||
Some(cx.subscribe(&thread, |this, _thread, event, cx| {
|
||||
this.handle_thread_event(event, cx)
|
||||
}))
|
||||
}
|
||||
AgentDiffThread::AcpThread(_) => None,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
title: SharedString::default(),
|
||||
multibuffer,
|
||||
editor,
|
||||
@@ -123,8 +230,7 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let thread = self.thread.read(cx);
|
||||
let changed_buffers = thread.action_log().read(cx).changed_buffers(cx);
|
||||
let changed_buffers = self.thread.action_log(cx).read(cx).changed_buffers(cx);
|
||||
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
|
||||
|
||||
for (buffer, diff_handle) in changed_buffers {
|
||||
@@ -211,7 +317,7 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
fn update_title(&mut self, cx: &mut Context<Self>) {
|
||||
let new_title = self.thread.read(cx).summary().unwrap_or("Agent Changes");
|
||||
let new_title = self.thread.summary(cx).unwrap_or("Agent Changes");
|
||||
if new_title != self.title {
|
||||
self.title = new_title;
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
@@ -275,14 +381,15 @@ impl AgentDiffPane {
|
||||
|
||||
fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.keep_all_edits(cx));
|
||||
.action_log(cx)
|
||||
.update(cx, |action_log, cx| action_log.keep_all_edits(cx))
|
||||
}
|
||||
}
|
||||
|
||||
fn keep_edits_in_selection(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
@@ -297,7 +404,7 @@ fn keep_edits_in_selection(
|
||||
fn reject_edits_in_selection(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
@@ -311,7 +418,7 @@ fn reject_edits_in_selection(
|
||||
fn keep_edits_in_ranges(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
ranges: Vec<Range<editor::Anchor>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
@@ -326,8 +433,8 @@ fn keep_edits_in_ranges(
|
||||
for hunk in &diff_hunks_in_ranges {
|
||||
let buffer = multibuffer.read(cx).buffer(hunk.buffer_id);
|
||||
if let Some(buffer) = buffer {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
|
||||
thread.action_log(cx).update(cx, |action_log, cx| {
|
||||
action_log.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -336,7 +443,7 @@ fn keep_edits_in_ranges(
|
||||
fn reject_edits_in_ranges(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
ranges: Vec<Range<editor::Anchor>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
@@ -362,8 +469,9 @@ fn reject_edits_in_ranges(
|
||||
|
||||
for (buffer, ranges) in ranges_by_buffer {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.reject_edits_in_ranges(buffer, ranges, cx)
|
||||
.action_log(cx)
|
||||
.update(cx, |action_log, cx| {
|
||||
action_log.reject_edits_in_ranges(buffer, ranges, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
@@ -461,7 +569,7 @@ impl Item for AgentDiffPane {
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
|
||||
let summary = self.thread.read(cx).summary().unwrap_or("Agent Changes");
|
||||
let summary = self.thread.summary(cx).unwrap_or("Agent Changes");
|
||||
Label::new(format!("Review: {}", summary))
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
@@ -641,7 +749,7 @@ impl Render for AgentDiffPane {
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_hunk_controls(thread: &Entity<Thread>) -> editor::RenderDiffHunkControlsFn {
|
||||
fn diff_hunk_controls(thread: &AgentDiffThread) -> editor::RenderDiffHunkControlsFn {
|
||||
let thread = thread.clone();
|
||||
|
||||
Arc::new(
|
||||
@@ -676,7 +784,7 @@ fn render_diff_hunk_controls(
|
||||
hunk_range: Range<editor::Anchor>,
|
||||
is_created_file: bool,
|
||||
line_height: Pixels,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
editor: &Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -1112,11 +1220,8 @@ impl Render for AgentDiffToolbar {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let has_pending_edit_tool_use = agent_diff
|
||||
.read(cx)
|
||||
.thread
|
||||
.read(cx)
|
||||
.has_pending_edit_tool_uses();
|
||||
let has_pending_edit_tool_use =
|
||||
agent_diff.read(cx).thread.has_pending_edit_tool_uses(cx);
|
||||
|
||||
if has_pending_edit_tool_use {
|
||||
return div().px_2().child(spinner_icon).into_any();
|
||||
@@ -1187,8 +1292,8 @@ pub enum EditorState {
|
||||
}
|
||||
|
||||
struct WorkspaceThread {
|
||||
thread: WeakEntity<Thread>,
|
||||
_thread_subscriptions: [Subscription; 2],
|
||||
thread: WeakAgentDiffThread,
|
||||
_thread_subscriptions: (Subscription, Subscription),
|
||||
singleton_editors: HashMap<WeakEntity<Buffer>, HashMap<WeakEntity<Editor>, Subscription>>,
|
||||
_settings_subscription: Subscription,
|
||||
_workspace_subscription: Option<Subscription>,
|
||||
@@ -1212,23 +1317,23 @@ impl AgentDiff {
|
||||
|
||||
pub fn set_active_thread(
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: impl Into<AgentDiffThread>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
Self::global(cx).update(cx, |this, cx| {
|
||||
this.register_active_thread_impl(workspace, thread, window, cx);
|
||||
this.register_active_thread_impl(workspace, thread.into(), window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn register_active_thread_impl(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let action_log = thread.read(cx).action_log().clone();
|
||||
let action_log = thread.action_log(cx).clone();
|
||||
|
||||
let action_log_subscription = cx.observe_in(&action_log, window, {
|
||||
let workspace = workspace.clone();
|
||||
@@ -1237,17 +1342,25 @@ impl AgentDiff {
|
||||
}
|
||||
});
|
||||
|
||||
let thread_subscription = cx.subscribe_in(&thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, _thread, event, window, cx| {
|
||||
this.handle_thread_event(&workspace, event, window, cx)
|
||||
}
|
||||
});
|
||||
let thread_subscription = match &thread {
|
||||
AgentDiffThread::Native(thread) => cx.subscribe_in(&thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, _thread, event, window, cx| {
|
||||
this.handle_native_thread_event(&workspace, event, window, cx)
|
||||
}
|
||||
}),
|
||||
AgentDiffThread::AcpThread(thread) => cx.subscribe_in(&thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, thread, event, window, cx| {
|
||||
this.handle_acp_thread_event(&workspace, thread, event, window, cx)
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
if let Some(workspace_thread) = self.workspace_threads.get_mut(&workspace) {
|
||||
// replace thread and action log subscription, but keep editors
|
||||
workspace_thread.thread = thread.downgrade();
|
||||
workspace_thread._thread_subscriptions = [action_log_subscription, thread_subscription];
|
||||
workspace_thread._thread_subscriptions = (action_log_subscription, thread_subscription);
|
||||
self.update_reviewing_editors(&workspace, window, cx);
|
||||
return;
|
||||
}
|
||||
@@ -1272,7 +1385,7 @@ impl AgentDiff {
|
||||
workspace.clone(),
|
||||
WorkspaceThread {
|
||||
thread: thread.downgrade(),
|
||||
_thread_subscriptions: [action_log_subscription, thread_subscription],
|
||||
_thread_subscriptions: (action_log_subscription, thread_subscription),
|
||||
singleton_editors: HashMap::default(),
|
||||
_settings_subscription: settings_subscription,
|
||||
_workspace_subscription: workspace_subscription,
|
||||
@@ -1319,7 +1432,7 @@ impl AgentDiff {
|
||||
|
||||
fn register_review_action<T: Action>(
|
||||
workspace: &mut Workspace,
|
||||
review: impl Fn(&Entity<Editor>, &Entity<Thread>, &mut Window, &mut App) -> PostReviewState
|
||||
review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState
|
||||
+ 'static,
|
||||
this: &Entity<AgentDiff>,
|
||||
) {
|
||||
@@ -1338,7 +1451,7 @@ impl AgentDiff {
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
fn handle_native_thread_event(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
event: &ThreadEvent,
|
||||
@@ -1380,6 +1493,40 @@ impl AgentDiff {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_acp_thread_event(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
thread: &Entity<AcpThread>,
|
||||
event: &AcpThreadEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
AcpThreadEvent::NewEntry => {
|
||||
if thread
|
||||
.read(cx)
|
||||
.entries()
|
||||
.last()
|
||||
.and_then(|entry| entry.diff())
|
||||
.is_some()
|
||||
{
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
}
|
||||
AcpThreadEvent::EntryUpdated(ix) => {
|
||||
if thread
|
||||
.read(cx)
|
||||
.entries()
|
||||
.get(*ix)
|
||||
.and_then(|entry| entry.diff())
|
||||
.is_some()
|
||||
{
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_workspace_event(
|
||||
&mut self,
|
||||
workspace: &Entity<Workspace>,
|
||||
@@ -1485,7 +1632,7 @@ impl AgentDiff {
|
||||
return;
|
||||
};
|
||||
|
||||
let action_log = thread.read(cx).action_log();
|
||||
let action_log = thread.action_log(cx);
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
|
||||
let mut unaffected = self.reviewing_editors.clone();
|
||||
@@ -1510,7 +1657,7 @@ impl AgentDiff {
|
||||
multibuffer.add_diff(diff_handle.clone(), cx);
|
||||
});
|
||||
|
||||
let new_state = if thread.read(cx).is_generating() {
|
||||
let new_state = if thread.is_generating(cx) {
|
||||
EditorState::Generating
|
||||
} else {
|
||||
EditorState::Reviewing
|
||||
@@ -1606,7 +1753,7 @@ impl AgentDiff {
|
||||
|
||||
fn keep_all(
|
||||
editor: &Entity<Editor>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
@@ -1626,7 +1773,7 @@ impl AgentDiff {
|
||||
|
||||
fn reject_all(
|
||||
editor: &Entity<Editor>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
@@ -1646,7 +1793,7 @@ impl AgentDiff {
|
||||
|
||||
fn keep(
|
||||
editor: &Entity<Editor>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
@@ -1659,7 +1806,7 @@ impl AgentDiff {
|
||||
|
||||
fn reject(
|
||||
editor: &Entity<Editor>,
|
||||
thread: &Entity<Thread>,
|
||||
thread: &AgentDiffThread,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
@@ -1682,7 +1829,7 @@ impl AgentDiff {
|
||||
fn review_in_active_editor(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
review: impl Fn(&Entity<Editor>, &Entity<Thread>, &mut Window, &mut App) -> PostReviewState,
|
||||
review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
@@ -1703,7 +1850,7 @@ impl AgentDiff {
|
||||
|
||||
if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx) {
|
||||
if let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
|
||||
let changed_buffers = thread.read(cx).action_log().read(cx).changed_buffers(cx);
|
||||
let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx);
|
||||
|
||||
let mut keys = changed_buffers.keys().cycle();
|
||||
keys.find(|k| *k == &curr_buffer);
|
||||
@@ -1801,8 +1948,9 @@ mod tests {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
let thread =
|
||||
AgentDiffThread::Native(thread_store.update(cx, |store, cx| store.create_thread(cx)));
|
||||
let action_log = cx.read(|cx| thread.action_log(cx));
|
||||
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
@@ -1988,8 +2136,9 @@ mod tests {
|
||||
});
|
||||
|
||||
// Set the active thread
|
||||
let thread = AgentDiffThread::Native(thread);
|
||||
cx.update(|window, cx| {
|
||||
AgentDiff::set_active_thread(&workspace.downgrade(), &thread, window, cx)
|
||||
AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx)
|
||||
});
|
||||
|
||||
let buffer1 = project
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
@@ -8,6 +9,7 @@ use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::NewAcpThread;
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::language_model_selector::ToggleModelSelector;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||
@@ -432,6 +434,8 @@ pub struct AgentPanel {
|
||||
configuration_subscription: Option<Subscription>,
|
||||
local_timezone: UtcOffset,
|
||||
active_view: ActiveView,
|
||||
acp_message_history:
|
||||
Rc<RefCell<crate::acp::MessageHistory<agentic_coding_protocol::SendUserMessageParams>>>,
|
||||
previous_view: Option<ActiveView>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
history: Entity<ThreadHistory>,
|
||||
@@ -624,7 +628,7 @@ impl AgentPanel {
|
||||
}
|
||||
};
|
||||
|
||||
AgentDiff::set_active_thread(&workspace, &thread, window, cx);
|
||||
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
|
||||
|
||||
let weak_panel = weak_self.clone();
|
||||
|
||||
@@ -698,6 +702,7 @@ impl AgentPanel {
|
||||
.unwrap(),
|
||||
inline_assist_context_store,
|
||||
previous_view: None,
|
||||
acp_message_history: Default::default(),
|
||||
history_store: history_store.clone(),
|
||||
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
|
||||
hovered_recent_history_item: None,
|
||||
@@ -770,13 +775,10 @@ impl AgentPanel {
|
||||
}
|
||||
|
||||
fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Preserve chat box text when using creating new thread from summary'
|
||||
let preserved_text = if action.from_thread_id.is_some() {
|
||||
self.active_message_editor()
|
||||
.map(|editor| editor.read(cx).get_text(cx).trim().to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Preserve chat box text when using creating new thread
|
||||
let preserved_text = self
|
||||
.active_message_editor()
|
||||
.map(|editor| editor.read(cx).get_text(cx).trim().to_string());
|
||||
|
||||
let thread = self
|
||||
.thread_store
|
||||
@@ -848,7 +850,7 @@ impl AgentPanel {
|
||||
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
|
||||
self.set_active_view(thread_view, window, cx);
|
||||
|
||||
AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
|
||||
AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
|
||||
}
|
||||
|
||||
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -890,14 +892,30 @@ impl AgentPanel {
|
||||
fn new_gemini_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let workspace = self.workspace.clone();
|
||||
let project = self.project.clone();
|
||||
let message_history = self.acp_message_history.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let thread_view = cx.new_window_entity(|window, cx| {
|
||||
crate::acp::AcpThreadView::new(workspace, project, window, cx)
|
||||
crate::acp::AcpThreadView::new(
|
||||
workspace.clone(),
|
||||
project,
|
||||
message_history,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_active_view(ActiveView::AcpThread { thread_view }, window, cx);
|
||||
this.set_active_view(
|
||||
ActiveView::AcpThread {
|
||||
thread_view: thread_view.clone(),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -1053,7 +1071,7 @@ impl AgentPanel {
|
||||
|
||||
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
|
||||
self.set_active_view(thread_view, window, cx);
|
||||
AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
|
||||
AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
|
||||
}
|
||||
|
||||
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -1184,7 +1202,12 @@ impl AgentPanel {
|
||||
let thread = thread.read(cx).thread().clone();
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
|
||||
AgentDiffPane::deploy_in_workspace(
|
||||
AgentDiffThread::Native(thread),
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
@@ -1420,6 +1443,8 @@ impl AgentPanel {
|
||||
self.active_view = new_view;
|
||||
}
|
||||
|
||||
self.acp_message_history.borrow_mut().reset_position();
|
||||
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
|
||||
@@ -660,7 +660,6 @@ impl InlineAssistant {
|
||||
height: Some(prompt_editor_height),
|
||||
render: build_assist_editor_renderer(prompt_editor),
|
||||
priority: 0,
|
||||
render_in_minimap: false,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Sticky,
|
||||
@@ -675,7 +674,6 @@ impl InlineAssistant {
|
||||
.into_any_element()
|
||||
}),
|
||||
priority: 0,
|
||||
render_in_minimap: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1451,7 +1449,6 @@ impl InlineAssistant {
|
||||
.into_any_element()
|
||||
}),
|
||||
priority: 0,
|
||||
render_in_minimap: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::agent_model_selector::AgentModelSelector;
|
||||
use crate::language_model_selector::ToggleModelSelector;
|
||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||
@@ -475,9 +476,12 @@ impl MessageEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Ok(diff) =
|
||||
AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx)
|
||||
{
|
||||
if let Ok(diff) = AgentDiffPane::deploy(
|
||||
AgentDiffThread::Native(self.thread.clone()),
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);
|
||||
diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));
|
||||
}
|
||||
|
||||
@@ -1256,7 +1256,6 @@ impl TextThreadEditor {
|
||||
),
|
||||
priority: usize::MAX,
|
||||
render: render_block(MessageMetadata::from(message)),
|
||||
render_in_minimap: false,
|
||||
};
|
||||
let mut new_blocks = vec![];
|
||||
let mut block_index_to_message = vec![];
|
||||
@@ -1858,7 +1857,6 @@ impl TextThreadEditor {
|
||||
.into_any_element()
|
||||
}),
|
||||
priority: 0,
|
||||
render_in_minimap: false,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -34,6 +34,11 @@ impl ExtensionSlashCommandProxy for SlashCommandRegistryProxy {
|
||||
self.slash_command_registry
|
||||
.register_command(ExtensionSlashCommand::new(extension, command), false)
|
||||
}
|
||||
|
||||
fn unregister_slash_command(&self, command_name: Arc<str>) {
|
||||
self.slash_command_registry
|
||||
.unregister_command_by_name(&command_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
|
||||
|
||||
@@ -8,7 +8,7 @@ use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
|
||||
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
|
||||
use std::{cmp, ops::Range, sync::Arc};
|
||||
use text::{Edit, Patch, Rope};
|
||||
use util::RangeExt;
|
||||
use util::{RangeExt, ResultExt as _};
|
||||
|
||||
/// Tracks actions performed by tools in a thread
|
||||
pub struct ActionLog {
|
||||
@@ -47,6 +47,10 @@ impl ActionLog {
|
||||
self.edited_since_project_diagnostics_check
|
||||
}
|
||||
|
||||
pub fn latest_snapshot(&self, buffer: &Entity<Buffer>) -> Option<text::BufferSnapshot> {
|
||||
Some(self.tracked_buffers.get(buffer)?.snapshot.clone())
|
||||
}
|
||||
|
||||
fn track_buffer_internal(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
@@ -715,6 +719,22 @@ impl ActionLog {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reject_all_edits(&mut self, cx: &mut Context<Self>) -> Task<()> {
|
||||
let futures = self.changed_buffers(cx).into_keys().map(|buffer| {
|
||||
let reject = self.reject_edits_in_ranges(buffer, vec![Anchor::MIN..Anchor::MAX], cx);
|
||||
|
||||
async move {
|
||||
reject.await.log_err();
|
||||
}
|
||||
});
|
||||
|
||||
let task = futures::future::join_all(futures);
|
||||
|
||||
cx.spawn(async move |_, _| {
|
||||
task.await;
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the set of buffers that contain edits that haven't been reviewed by the user.
|
||||
pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> {
|
||||
self.tracked_buffers
|
||||
|
||||
@@ -365,17 +365,23 @@ fn eval_disable_cursor_blinking() {
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet | 0.99 (2025-06-14)
|
||||
// claude-sonnet-4 | 0.85 (2025-06-14)
|
||||
// gemini-2.5-pro-preview-latest | 0.97 (2025-06-16)
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
// claude-3.7-sonnet | 0.59 (2025-07-14)
|
||||
// claude-sonnet-4 | 0.81 (2025-07-14)
|
||||
// gemini-2.5-pro | 0.95 (2025-07-14)
|
||||
// gemini-2.5-flash-preview-04-17 | 0.78 (2025-07-14)
|
||||
// gpt-4.1 | 0.00 (2025-07-14) (follows edit_description too literally)
|
||||
let input_file_path = "root/editor.rs";
|
||||
let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs");
|
||||
let edit_description = "Comment out the call to `BlinkManager::enable`";
|
||||
let possible_diffs = vec![
|
||||
include_str!("evals/fixtures/disable_cursor_blinking/possible-01.diff"),
|
||||
include_str!("evals/fixtures/disable_cursor_blinking/possible-02.diff"),
|
||||
include_str!("evals/fixtures/disable_cursor_blinking/possible-03.diff"),
|
||||
include_str!("evals/fixtures/disable_cursor_blinking/possible-04.diff"),
|
||||
];
|
||||
eval(
|
||||
100,
|
||||
0.95,
|
||||
0.51,
|
||||
0.05,
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
@@ -433,11 +439,7 @@ fn eval_disable_cursor_blinking() {
|
||||
),
|
||||
],
|
||||
Some(input_file_content.into()),
|
||||
EvalAssertion::judge_diff(indoc! {"
|
||||
- Calls to BlinkManager in `observe_window_activation` were commented out
|
||||
- The call to `blink_manager.enable` above the call to show_cursor_names was commented out
|
||||
- All the edits have valid indentation
|
||||
"}),
|
||||
EvalAssertion::assert_diff_any(possible_diffs),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
--- before.rs 2025-07-07 11:37:48.434629001 +0300
|
||||
+++ expected.rs 2025-07-14 10:33:53.346906775 +0300
|
||||
@@ -1780,11 +1780,11 @@
|
||||
cx.observe_window_activation(window, |editor, window, cx| {
|
||||
let active = window.is_window_active();
|
||||
editor.blink_manager.update(cx, |blink_manager, cx| {
|
||||
- if active {
|
||||
- blink_manager.enable(cx);
|
||||
- } else {
|
||||
- blink_manager.disable(cx);
|
||||
- }
|
||||
+ // if active {
|
||||
+ // blink_manager.enable(cx);
|
||||
+ // } else {
|
||||
+ // blink_manager.disable(cx);
|
||||
+ // }
|
||||
});
|
||||
}),
|
||||
],
|
||||
@@ -18463,7 +18463,7 @@
|
||||
}
|
||||
|
||||
self.blink_manager.update(cx, |blink_manager, cx| {
|
||||
- blink_manager.enable(cx);
|
||||
+ // blink_manager.enable(cx);
|
||||
});
|
||||
self.show_cursor_names(window, cx);
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
@@ -0,0 +1,29 @@
|
||||
@@ -1778,13 +1778,13 @@
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
|
||||
observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()),
|
||||
cx.observe_window_activation(window, |editor, window, cx| {
|
||||
- let active = window.is_window_active();
|
||||
+ // let active = window.is_window_active();
|
||||
editor.blink_manager.update(cx, |blink_manager, cx| {
|
||||
- if active {
|
||||
- blink_manager.enable(cx);
|
||||
- } else {
|
||||
- blink_manager.disable(cx);
|
||||
- }
|
||||
+ // if active {
|
||||
+ // blink_manager.enable(cx);
|
||||
+ // } else {
|
||||
+ // blink_manager.disable(cx);
|
||||
+ // }
|
||||
});
|
||||
}),
|
||||
],
|
||||
@@ -18463,7 +18463,7 @@
|
||||
}
|
||||
|
||||
self.blink_manager.update(cx, |blink_manager, cx| {
|
||||
- blink_manager.enable(cx);
|
||||
+ // blink_manager.enable(cx);
|
||||
});
|
||||
self.show_cursor_names(window, cx);
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
@@ -0,0 +1,34 @@
|
||||
@@ -1774,17 +1774,17 @@
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
cx.subscribe_in(&buffer, window, Self::on_buffer_event),
|
||||
cx.observe_in(&display_map, window, Self::on_display_map_changed),
|
||||
- cx.observe(&blink_manager, |_, _, cx| cx.notify()),
|
||||
+ // cx.observe(&blink_manager, |_, _, cx| cx.notify()),
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
|
||||
observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()),
|
||||
cx.observe_window_activation(window, |editor, window, cx| {
|
||||
- let active = window.is_window_active();
|
||||
+ // let active = window.is_window_active();
|
||||
editor.blink_manager.update(cx, |blink_manager, cx| {
|
||||
- if active {
|
||||
- blink_manager.enable(cx);
|
||||
- } else {
|
||||
- blink_manager.disable(cx);
|
||||
- }
|
||||
+ // if active {
|
||||
+ // blink_manager.enable(cx);
|
||||
+ // } else {
|
||||
+ // blink_manager.disable(cx);
|
||||
+ // }
|
||||
});
|
||||
}),
|
||||
],
|
||||
@@ -18463,7 +18463,7 @@
|
||||
}
|
||||
|
||||
self.blink_manager.update(cx, |blink_manager, cx| {
|
||||
- blink_manager.enable(cx);
|
||||
+ // blink_manager.enable(cx);
|
||||
});
|
||||
self.show_cursor_names(window, cx);
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
@@ -0,0 +1,33 @@
|
||||
@@ -1774,17 +1774,17 @@
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
cx.subscribe_in(&buffer, window, Self::on_buffer_event),
|
||||
cx.observe_in(&display_map, window, Self::on_display_map_changed),
|
||||
- cx.observe(&blink_manager, |_, _, cx| cx.notify()),
|
||||
+ // cx.observe(&blink_manager, |_, _, cx| cx.notify()),
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
|
||||
observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()),
|
||||
cx.observe_window_activation(window, |editor, window, cx| {
|
||||
let active = window.is_window_active();
|
||||
editor.blink_manager.update(cx, |blink_manager, cx| {
|
||||
- if active {
|
||||
- blink_manager.enable(cx);
|
||||
- } else {
|
||||
- blink_manager.disable(cx);
|
||||
- }
|
||||
+ // if active {
|
||||
+ // blink_manager.enable(cx);
|
||||
+ // } else {
|
||||
+ // blink_manager.disable(cx);
|
||||
+ // }
|
||||
});
|
||||
}),
|
||||
],
|
||||
@@ -18463,7 +18463,7 @@
|
||||
}
|
||||
|
||||
self.blink_manager.update(cx, |blink_manager, cx| {
|
||||
- blink_manager.enable(cx);
|
||||
+ // blink_manager.enable(cx);
|
||||
});
|
||||
self.show_cursor_names(window, cx);
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
@@ -69,10 +69,9 @@ impl FetchTool {
|
||||
.to_str()
|
||||
.context("invalid Content-Type header")?;
|
||||
let content_type = match content_type {
|
||||
"text/html" => ContentType::Html,
|
||||
"text/plain" => ContentType::Plaintext,
|
||||
"text/html" | "application/xhtml+xml" => ContentType::Html,
|
||||
"application/json" => ContentType::Json,
|
||||
_ => ContentType::Html,
|
||||
_ => ContentType::Plaintext,
|
||||
};
|
||||
|
||||
match content_type {
|
||||
|
||||
@@ -18,7 +18,6 @@ use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
/// If the model requests to read a file whose size exceeds this, then
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -78,11 +77,21 @@ impl Tool for ReadFileTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<ReadFileToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let path = MarkdownInlineCode(&input.path);
|
||||
let path = &input.path;
|
||||
match (input.start_line, input.end_line) {
|
||||
(Some(start), None) => format!("Read file {path} (from line {start})"),
|
||||
(Some(start), Some(end)) => format!("Read file {path} (lines {start}-{end})"),
|
||||
_ => format!("Read file {path}"),
|
||||
(Some(start), Some(end)) => {
|
||||
format!(
|
||||
"[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))",
|
||||
path, start, end, path, start, end
|
||||
)
|
||||
}
|
||||
(Some(start), None) => {
|
||||
format!(
|
||||
"[Read file `{}` (from line {})](@selection:{}:({}-{}))",
|
||||
path, start, path, start, start
|
||||
)
|
||||
}
|
||||
_ => format!("[Read file `{}`](@file:{})", path, path),
|
||||
}
|
||||
}
|
||||
Err(_) => "Read file".to_string(),
|
||||
|
||||
@@ -1389,10 +1389,17 @@ impl Room {
|
||||
let sources = cx.screen_capture_sources();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let sources = sources.await??;
|
||||
let source = sources.first().context("no display found")?;
|
||||
let sources = sources
|
||||
.await
|
||||
.map_err(|error| error.into())
|
||||
.and_then(|sources| sources);
|
||||
let source =
|
||||
sources.and_then(|sources| sources.into_iter().next().context("no display found"));
|
||||
|
||||
let publication = participant.publish_screenshare_track(&**source, cx).await;
|
||||
let publication = match source {
|
||||
Ok(source) => participant.publish_screenshare_track(&*source, cx).await,
|
||||
Err(error) => Err(error),
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let live_kit = this
|
||||
|
||||
@@ -94,6 +94,7 @@ context_server.workspace = true
|
||||
ctor.workspace = true
|
||||
dap = { workspace = true, features = ["test-support"] }
|
||||
dap_adapters = { workspace = true, features = ["test-support"] }
|
||||
dap-types.workspace = true
|
||||
debugger_ui = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
extension.workspace = true
|
||||
|
||||
@@ -1,12 +1,33 @@
|
||||
{
|
||||
"admins": [
|
||||
"nathansobo",
|
||||
"as-cii",
|
||||
"maxbrunsfeld",
|
||||
"iamnbutler",
|
||||
"mikayla-maki",
|
||||
"as-cii",
|
||||
"JosephTLyons",
|
||||
"rgbkrk"
|
||||
"maxdeviant",
|
||||
"SomeoneToIgnore",
|
||||
"mikayla-maki",
|
||||
"agu-z",
|
||||
"osiewicz",
|
||||
"ConradIrwin",
|
||||
"benbrandt",
|
||||
"bennetbo",
|
||||
"smitbarmase",
|
||||
"notpeter",
|
||||
"rgbkrk",
|
||||
"JunkuiZhang",
|
||||
"Anthony-Eid",
|
||||
"rtfeldman",
|
||||
"danilo-leal",
|
||||
"MrSubidubi",
|
||||
"cole-miller",
|
||||
"osyvokon",
|
||||
"probably-neb",
|
||||
"mgsloan",
|
||||
"P1n3appl3",
|
||||
"mslzed",
|
||||
"franciskafyi",
|
||||
"katie-z-geer"
|
||||
],
|
||||
"channels": ["zed"]
|
||||
}
|
||||
|
||||
@@ -2836,62 +2836,117 @@ async fn make_update_user_plan_message(
|
||||
account_too_young: Some(account_too_young),
|
||||
has_overdue_invoices: billing_customer
|
||||
.map(|billing_customer| billing_customer.has_overdue_invoices),
|
||||
usage: usage.map(|usage| {
|
||||
let plan = match plan {
|
||||
proto::Plan::Free => zed_llm_client::Plan::ZedFree,
|
||||
proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
};
|
||||
|
||||
let model_requests_limit = match plan.model_requests_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
let limit = if plan == zed_llm_client::Plan::ZedProTrial
|
||||
&& feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG)
|
||||
{
|
||||
1_000
|
||||
} else {
|
||||
limit
|
||||
};
|
||||
|
||||
zed_llm_client::UsageLimit::Limited(limit)
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => zed_llm_client::UsageLimit::Unlimited,
|
||||
};
|
||||
|
||||
proto::SubscriptionUsage {
|
||||
model_requests_usage_amount: usage.model_requests as u32,
|
||||
model_requests_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match model_requests_limit {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
}),
|
||||
edit_predictions_usage_amount: usage.edit_predictions as u32,
|
||||
edit_predictions_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match plan.edit_predictions_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}),
|
||||
usage: Some(
|
||||
usage
|
||||
.map(|usage| subscription_usage_to_proto(plan, usage, &feature_flags))
|
||||
.unwrap_or_else(|| make_default_subscription_usage(plan, &feature_flags)),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn model_requests_limit(
|
||||
plan: zed_llm_client::Plan,
|
||||
feature_flags: &Vec<String>,
|
||||
) -> zed_llm_client::UsageLimit {
|
||||
match plan.model_requests_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
let limit = if plan == zed_llm_client::Plan::ZedProTrial
|
||||
&& feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG)
|
||||
{
|
||||
1_000
|
||||
} else {
|
||||
limit
|
||||
};
|
||||
|
||||
zed_llm_client::UsageLimit::Limited(limit)
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => zed_llm_client::UsageLimit::Unlimited,
|
||||
}
|
||||
}
|
||||
|
||||
fn subscription_usage_to_proto(
|
||||
plan: proto::Plan,
|
||||
usage: crate::llm::db::subscription_usage::Model,
|
||||
feature_flags: &Vec<String>,
|
||||
) -> proto::SubscriptionUsage {
|
||||
let plan = match plan {
|
||||
proto::Plan::Free => zed_llm_client::Plan::ZedFree,
|
||||
proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
};
|
||||
|
||||
proto::SubscriptionUsage {
|
||||
model_requests_usage_amount: usage.model_requests as u32,
|
||||
model_requests_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match model_requests_limit(plan, feature_flags) {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
}),
|
||||
edit_predictions_usage_amount: usage.edit_predictions as u32,
|
||||
edit_predictions_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match plan.edit_predictions_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_default_subscription_usage(
|
||||
plan: proto::Plan,
|
||||
feature_flags: &Vec<String>,
|
||||
) -> proto::SubscriptionUsage {
|
||||
let plan = match plan {
|
||||
proto::Plan::Free => zed_llm_client::Plan::ZedFree,
|
||||
proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
};
|
||||
|
||||
proto::SubscriptionUsage {
|
||||
model_requests_usage_amount: 0,
|
||||
model_requests_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match model_requests_limit(plan, feature_flags) {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
}),
|
||||
edit_predictions_usage_amount: 0,
|
||||
edit_predictions_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match plan.edit_predictions_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_user_plan(session: &Session) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ use crate::stripe_client::{
|
||||
StripeCustomerId, StripeCustomerUpdate, StripeCustomerUpdateAddress, StripeCustomerUpdateName,
|
||||
StripeMeter, StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId,
|
||||
StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
|
||||
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionItems,
|
||||
UpdateSubscriptionParams,
|
||||
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection,
|
||||
UpdateSubscriptionItems, UpdateSubscriptionParams,
|
||||
};
|
||||
|
||||
pub struct StripeBilling {
|
||||
@@ -252,6 +252,7 @@ impl StripeBilling {
|
||||
name: Some(StripeCustomerUpdateName::Auto),
|
||||
shipping: None,
|
||||
});
|
||||
params.tax_id_collection = Some(StripeTaxIdCollection { enabled: true });
|
||||
|
||||
let session = self.client.create_checkout_session(params).await?;
|
||||
Ok(session.url.context("no checkout session URL")?)
|
||||
@@ -311,6 +312,7 @@ impl StripeBilling {
|
||||
name: Some(StripeCustomerUpdateName::Auto),
|
||||
shipping: None,
|
||||
});
|
||||
params.tax_id_collection = Some(StripeTaxIdCollection { enabled: true });
|
||||
|
||||
let session = self.client.create_checkout_session(params).await?;
|
||||
Ok(session.url.context("no checkout session URL")?)
|
||||
|
||||
@@ -190,6 +190,7 @@ pub struct StripeCreateCheckoutSessionParams<'a> {
|
||||
pub success_url: Option<&'a str>,
|
||||
pub billing_address_collection: Option<StripeBillingAddressCollection>,
|
||||
pub customer_update: Option<StripeCustomerUpdate>,
|
||||
pub tax_id_collection: Option<StripeTaxIdCollection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
@@ -218,6 +219,11 @@ pub struct StripeCreateCheckoutSessionSubscriptionData {
|
||||
pub trial_settings: Option<StripeSubscriptionTrialSettings>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct StripeTaxIdCollection {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StripeCheckoutSession {
|
||||
pub url: Option<String>,
|
||||
|
||||
@@ -14,8 +14,8 @@ use crate::stripe_client::{
|
||||
StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
|
||||
StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate,
|
||||
StripeMeter, StripeMeterId, StripePrice, StripePriceId, StripeSubscription,
|
||||
StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, UpdateCustomerParams,
|
||||
UpdateSubscriptionParams,
|
||||
StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, StripeTaxIdCollection,
|
||||
UpdateCustomerParams, UpdateSubscriptionParams,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -38,6 +38,7 @@ pub struct StripeCreateCheckoutSessionCall {
|
||||
pub success_url: Option<String>,
|
||||
pub billing_address_collection: Option<StripeBillingAddressCollection>,
|
||||
pub customer_update: Option<StripeCustomerUpdate>,
|
||||
pub tax_id_collection: Option<StripeTaxIdCollection>,
|
||||
}
|
||||
|
||||
pub struct FakeStripeClient {
|
||||
@@ -236,6 +237,7 @@ impl StripeClient for FakeStripeClient {
|
||||
success_url: params.success_url.map(|url| url.to_string()),
|
||||
billing_address_collection: params.billing_address_collection,
|
||||
customer_update: params.customer_update,
|
||||
tax_id_collection: params.tax_id_collection,
|
||||
});
|
||||
|
||||
Ok(StripeCheckoutSession {
|
||||
|
||||
@@ -27,8 +27,8 @@ use crate::stripe_client::{
|
||||
StripeMeter, StripePrice, StripePriceId, StripePriceRecurring, StripeSubscription,
|
||||
StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId,
|
||||
StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
|
||||
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateCustomerParams,
|
||||
UpdateSubscriptionParams,
|
||||
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection,
|
||||
UpdateCustomerParams, UpdateSubscriptionParams,
|
||||
};
|
||||
|
||||
pub struct RealStripeClient {
|
||||
@@ -448,6 +448,7 @@ impl<'a> TryFrom<StripeCreateCheckoutSessionParams<'a>> for CreateCheckoutSessio
|
||||
success_url: value.success_url,
|
||||
billing_address_collection: value.billing_address_collection.map(Into::into),
|
||||
customer_update: value.customer_update.map(Into::into),
|
||||
tax_id_collection: value.tax_id_collection.map(Into::into),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -590,3 +591,11 @@ impl From<StripeCustomerUpdate> for stripe::CreateCheckoutSessionCustomerUpdate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StripeTaxIdCollection> for stripe::CreateCheckoutSessionTaxIdCollection {
|
||||
fn from(value: StripeTaxIdCollection) -> Self {
|
||||
stripe::CreateCheckoutSessionTaxIdCollection {
|
||||
enabled: value.enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1013,7 +1013,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
// and some of which were originally opened by client B.
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.close_inactive_items(&Default::default(), window, cx)
|
||||
pane.close_inactive_items(&Default::default(), None, window, cx)
|
||||
.detach();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::tests::TestServer;
|
||||
use call::ActiveCall;
|
||||
use collections::{HashMap, HashSet};
|
||||
|
||||
use dap::{Capabilities, adapters::DebugTaskDefinition, transport::RequestHandling};
|
||||
use debugger_ui::debugger_panel::DebugPanel;
|
||||
use extension::ExtensionHostProxy;
|
||||
use fs::{FakeFs, Fs as _, RemoveOptions};
|
||||
@@ -22,6 +23,7 @@ use language::{
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::{
|
||||
ProjectPath,
|
||||
debugger::session::ThreadId,
|
||||
lsp_store::{FormatTrigger, LspFormatTarget},
|
||||
};
|
||||
use remote::SshRemoteClient;
|
||||
@@ -29,7 +31,11 @@ use remote_server::{HeadlessAppState, HeadlessProject};
|
||||
use rpc::proto;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{Arc, atomic::AtomicUsize},
|
||||
};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -688,3 +694,162 @@ async fn test_remote_server_debugger(
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_slow_adapter_startup_retries(
|
||||
cx_a: &mut TestAppContext,
|
||||
server_cx: &mut TestAppContext,
|
||||
executor: BackgroundExecutor,
|
||||
) {
|
||||
cx_a.update(|cx| {
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
command_palette_hooks::init(cx);
|
||||
zlog::init_test();
|
||||
dap_adapters::init(cx);
|
||||
});
|
||||
server_cx.update(|cx| {
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
dap_adapters::init(cx);
|
||||
});
|
||||
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
path!("/code"),
|
||||
json!({
|
||||
"lib.rs": "fn one() -> usize { 1 }"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// User A connects to the remote project via SSH.
|
||||
server_cx.update(HeadlessProject::init);
|
||||
let remote_http_client = Arc::new(BlockedHttpClient);
|
||||
let node = NodeRuntime::unavailable();
|
||||
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
|
||||
let _headless_project = server_cx.new(|cx| {
|
||||
client::init_settings(cx);
|
||||
HeadlessProject::new(
|
||||
HeadlessAppState {
|
||||
session: server_ssh,
|
||||
fs: remote_fs.clone(),
|
||||
http_client: remote_http_client,
|
||||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
|
||||
let mut server = TestServer::start(server_cx.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
cx_a.update(|cx| {
|
||||
debugger_ui::init(cx);
|
||||
command_palette_hooks::init(cx);
|
||||
});
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
|
||||
.await;
|
||||
|
||||
let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
|
||||
let debugger_panel = workspace
|
||||
.update_in(cx_a, |_workspace, window, cx| {
|
||||
cx.spawn_in(window, DebugPanel::load)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
workspace.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.add_panel(debugger_panel, window, cx);
|
||||
});
|
||||
|
||||
cx_a.run_until_parked();
|
||||
let debug_panel = workspace
|
||||
.update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
|
||||
.unwrap();
|
||||
|
||||
let workspace_window = cx_a
|
||||
.window_handle()
|
||||
.downcast::<workspace::Workspace>()
|
||||
.unwrap();
|
||||
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let session = debugger_ui::tests::start_debug_session_with(
|
||||
&workspace_window,
|
||||
cx_a,
|
||||
DebugTaskDefinition {
|
||||
adapter: "fake-adapter".into(),
|
||||
label: "test".into(),
|
||||
config: json!({
|
||||
"request": "launch"
|
||||
}),
|
||||
tcp_connection: Some(TcpArgumentsTemplate {
|
||||
port: None,
|
||||
host: None,
|
||||
timeout: None,
|
||||
}),
|
||||
},
|
||||
move |client| {
|
||||
let count = count.clone();
|
||||
client.on_request_ext::<dap::requests::Initialize, _>(move |_seq, _request| {
|
||||
if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 {
|
||||
return RequestHandling::Exit;
|
||||
}
|
||||
RequestHandling::Respond(Ok(Capabilities::default()))
|
||||
});
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
|
||||
let client = session.update(cx_a, |session, _| session.adapter_client().unwrap());
|
||||
client
|
||||
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
|
||||
reason: dap::StoppedEventReason::Pause,
|
||||
description: None,
|
||||
thread_id: Some(1),
|
||||
preserve_focus_hint: None,
|
||||
text: None,
|
||||
all_threads_stopped: None,
|
||||
hit_breakpoint_ids: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx_a.run_until_parked();
|
||||
|
||||
let active_session = debug_panel
|
||||
.update(cx_a, |this, _| this.active_session())
|
||||
.unwrap();
|
||||
|
||||
let running_state = active_session.update(cx_a, |active_session, _| {
|
||||
active_session.running_state().clone()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
client.id(),
|
||||
running_state.read_with(cx_a, |running_state, _| running_state.session_id())
|
||||
);
|
||||
assert_eq!(
|
||||
ThreadId(1),
|
||||
running_state.read_with(cx_a, |running_state, _| running_state
|
||||
.selected_thread_id()
|
||||
.unwrap())
|
||||
);
|
||||
|
||||
let shutdown_session = workspace.update(cx_a, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(session.read(cx).session_id(), cx)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
client_ssh.update(cx_a, |a, _| {
|
||||
a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
|
||||
});
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
@@ -378,6 +378,14 @@ pub trait DebugAdapter: 'static + Send + Sync {
|
||||
fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn compact_child_session(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn prefer_thread_name(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -442,10 +450,18 @@ impl DebugAdapter for FakeAdapter {
|
||||
_: Option<Vec<String>>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let connection = task_definition
|
||||
.tcp_connection
|
||||
.as_ref()
|
||||
.map(|connection| TcpArguments {
|
||||
host: connection.host(),
|
||||
port: connection.port.unwrap_or(17),
|
||||
timeout: connection.timeout,
|
||||
});
|
||||
Ok(DebugAdapterBinary {
|
||||
command: Some("command".into()),
|
||||
arguments: vec![],
|
||||
connection: None,
|
||||
connection,
|
||||
envs: HashMap::default(),
|
||||
cwd: None,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
adapters::DebugAdapterBinary,
|
||||
transport::{IoKind, LogKind, TransportDelegate},
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::Result;
|
||||
use dap_types::{
|
||||
messages::{Message, Response},
|
||||
requests::Request,
|
||||
@@ -110,9 +110,7 @@ impl DebugAdapterClient {
|
||||
self.transport_delegate
|
||||
.pending_requests
|
||||
.lock()
|
||||
.as_mut()
|
||||
.context("client is closed")?
|
||||
.insert(sequence_id, callback_tx);
|
||||
.insert(sequence_id, callback_tx)?;
|
||||
|
||||
log::debug!(
|
||||
"Client {} send `{}` request with sequence_id: {}",
|
||||
@@ -170,6 +168,7 @@ impl DebugAdapterClient {
|
||||
pub fn kill(&self) {
|
||||
log::debug!("Killing DAP process");
|
||||
self.transport_delegate.transport.lock().kill();
|
||||
self.transport_delegate.pending_requests.lock().shutdown();
|
||||
}
|
||||
|
||||
pub fn has_adapter_logs(&self) -> bool {
|
||||
@@ -184,11 +183,34 @@ impl DebugAdapterClient {
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn on_request<R: dap_types::requests::Request, F>(&self, handler: F)
|
||||
pub fn on_request<R: dap_types::requests::Request, F>(&self, mut handler: F)
|
||||
where
|
||||
F: 'static
|
||||
+ Send
|
||||
+ FnMut(u64, R::Arguments) -> Result<R::Response, dap_types::ErrorResponse>,
|
||||
{
|
||||
use crate::transport::RequestHandling;
|
||||
|
||||
self.transport_delegate
|
||||
.transport
|
||||
.lock()
|
||||
.as_fake()
|
||||
.on_request::<R, _>(move |seq, request| {
|
||||
RequestHandling::Respond(handler(seq, request))
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn on_request_ext<R: dap_types::requests::Request, F>(&self, handler: F)
|
||||
where
|
||||
F: 'static
|
||||
+ Send
|
||||
+ FnMut(
|
||||
u64,
|
||||
R::Arguments,
|
||||
) -> crate::transport::RequestHandling<
|
||||
Result<R::Response, dap_types::ErrorResponse>,
|
||||
>,
|
||||
{
|
||||
self.transport_delegate
|
||||
.transport
|
||||
|
||||
@@ -49,6 +49,12 @@ pub enum IoKind {
|
||||
StdErr,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub enum RequestHandling<T> {
|
||||
Respond(T),
|
||||
Exit,
|
||||
}
|
||||
|
||||
type LogHandlers = Arc<Mutex<SmallVec<[(LogKind, IoHandler); 2]>>>;
|
||||
|
||||
pub trait Transport: Send + Sync {
|
||||
@@ -76,7 +82,11 @@ async fn start(
|
||||
) -> Result<Box<dyn Transport>> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if cfg!(any(test, feature = "test-support")) {
|
||||
return Ok(Box::new(FakeTransport::start(cx).await?));
|
||||
if let Some(connection) = binary.connection.clone() {
|
||||
return Ok(Box::new(FakeTransport::start_tcp(connection, cx).await?));
|
||||
} else {
|
||||
return Ok(Box::new(FakeTransport::start_stdio(cx).await?));
|
||||
}
|
||||
}
|
||||
|
||||
if binary.connection.is_some() {
|
||||
@@ -90,11 +100,57 @@ async fn start(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PendingRequests {
|
||||
inner: Option<HashMap<u64, oneshot::Sender<Result<Response>>>>,
|
||||
}
|
||||
|
||||
impl PendingRequests {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: Some(HashMap::default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self, e: anyhow::Error) {
|
||||
let Some(inner) = self.inner.as_mut() else {
|
||||
return;
|
||||
};
|
||||
for (_, sender) in inner.drain() {
|
||||
sender.send(Err(e.cloned())).ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn insert(
|
||||
&mut self,
|
||||
sequence_id: u64,
|
||||
callback_tx: oneshot::Sender<Result<Response>>,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(inner) = self.inner.as_mut() else {
|
||||
bail!("client is closed")
|
||||
};
|
||||
inner.insert(sequence_id, callback_tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn remove(
|
||||
&mut self,
|
||||
sequence_id: u64,
|
||||
) -> anyhow::Result<Option<oneshot::Sender<Result<Response>>>> {
|
||||
let Some(inner) = self.inner.as_mut() else {
|
||||
bail!("client is closed");
|
||||
};
|
||||
Ok(inner.remove(&sequence_id))
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown(&mut self) {
|
||||
self.flush(anyhow!("transport shutdown"));
|
||||
self.inner = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TransportDelegate {
|
||||
log_handlers: LogHandlers,
|
||||
// TODO this should really be some kind of associative channel
|
||||
pub(crate) pending_requests:
|
||||
Arc<Mutex<Option<HashMap<u64, oneshot::Sender<Result<Response>>>>>>,
|
||||
pub(crate) pending_requests: Arc<Mutex<PendingRequests>>,
|
||||
pub(crate) transport: Mutex<Box<dyn Transport>>,
|
||||
pub(crate) server_tx: smol::lock::Mutex<Option<Sender<Message>>>,
|
||||
tasks: Mutex<Vec<Task<()>>>,
|
||||
@@ -108,7 +164,7 @@ impl TransportDelegate {
|
||||
transport: Mutex::new(transport),
|
||||
log_handlers,
|
||||
server_tx: Default::default(),
|
||||
pending_requests: Arc::new(Mutex::new(Some(HashMap::default()))),
|
||||
pending_requests: Arc::new(Mutex::new(PendingRequests::new())),
|
||||
tasks: Default::default(),
|
||||
})
|
||||
}
|
||||
@@ -151,24 +207,10 @@ impl TransportDelegate {
|
||||
Ok(()) => {
|
||||
pending_requests
|
||||
.lock()
|
||||
.take()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.for_each(|(_, request)| {
|
||||
request
|
||||
.send(Err(anyhow!("debugger shutdown unexpectedly")))
|
||||
.ok();
|
||||
});
|
||||
.flush(anyhow!("debugger shutdown unexpectedly"));
|
||||
}
|
||||
Err(e) => {
|
||||
pending_requests
|
||||
.lock()
|
||||
.take()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.for_each(|(_, request)| {
|
||||
request.send(Err(e.cloned())).ok();
|
||||
});
|
||||
pending_requests.lock().flush(e);
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -286,7 +328,7 @@ impl TransportDelegate {
|
||||
async fn recv_from_server<Stdout>(
|
||||
server_stdout: Stdout,
|
||||
mut message_handler: DapMessageHandler,
|
||||
pending_requests: Arc<Mutex<Option<HashMap<u64, oneshot::Sender<Result<Response>>>>>>,
|
||||
pending_requests: Arc<Mutex<PendingRequests>>,
|
||||
log_handlers: Option<LogHandlers>,
|
||||
) -> Result<()>
|
||||
where
|
||||
@@ -303,14 +345,10 @@ impl TransportDelegate {
|
||||
ConnectionResult::Timeout => anyhow::bail!("Timed out when connecting to debugger"),
|
||||
ConnectionResult::ConnectionReset => {
|
||||
log::info!("Debugger closed the connection");
|
||||
break Ok(());
|
||||
return Ok(());
|
||||
}
|
||||
ConnectionResult::Result(Ok(Message::Response(res))) => {
|
||||
let tx = pending_requests
|
||||
.lock()
|
||||
.as_mut()
|
||||
.context("client is closed")?
|
||||
.remove(&res.request_seq);
|
||||
let tx = pending_requests.lock().remove(res.request_seq)?;
|
||||
if let Some(tx) = tx {
|
||||
if let Err(e) = tx.send(Self::process_response(res)) {
|
||||
log::trace!("Did not send response `{:?}` for a cancelled", e);
|
||||
@@ -704,8 +742,7 @@ impl Drop for StdioTransport {
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
type RequestHandler =
|
||||
Box<dyn Send + FnMut(u64, serde_json::Value) -> dap_types::messages::Response>;
|
||||
type RequestHandler = Box<dyn Send + FnMut(u64, serde_json::Value) -> RequestHandling<Response>>;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
type ResponseHandler = Box<dyn Send + Fn(Response)>;
|
||||
@@ -716,23 +753,38 @@ pub struct FakeTransport {
|
||||
request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
|
||||
// for reverse request responses
|
||||
response_handlers: Arc<Mutex<HashMap<&'static str, ResponseHandler>>>,
|
||||
|
||||
stdin_writer: Option<PipeWriter>,
|
||||
stdout_reader: Option<PipeReader>,
|
||||
message_handler: Option<Task<Result<()>>>,
|
||||
kind: FakeTransportKind,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub enum FakeTransportKind {
|
||||
Stdio {
|
||||
stdin_writer: Option<PipeWriter>,
|
||||
stdout_reader: Option<PipeReader>,
|
||||
},
|
||||
Tcp {
|
||||
connection: TcpArguments,
|
||||
executor: BackgroundExecutor,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeTransport {
|
||||
pub fn on_request<R: dap_types::requests::Request, F>(&self, mut handler: F)
|
||||
where
|
||||
F: 'static + Send + FnMut(u64, R::Arguments) -> Result<R::Response, ErrorResponse>,
|
||||
F: 'static
|
||||
+ Send
|
||||
+ FnMut(u64, R::Arguments) -> RequestHandling<Result<R::Response, ErrorResponse>>,
|
||||
{
|
||||
self.request_handlers.lock().insert(
|
||||
R::COMMAND,
|
||||
Box::new(move |seq, args| {
|
||||
let result = handler(seq, serde_json::from_value(args).unwrap());
|
||||
let response = match result {
|
||||
let RequestHandling::Respond(response) = result else {
|
||||
return RequestHandling::Exit;
|
||||
};
|
||||
let response = match response {
|
||||
Ok(response) => Response {
|
||||
seq: seq + 1,
|
||||
request_seq: seq,
|
||||
@@ -750,7 +802,7 @@ impl FakeTransport {
|
||||
message: None,
|
||||
},
|
||||
};
|
||||
response
|
||||
RequestHandling::Respond(response)
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -764,86 +816,75 @@ impl FakeTransport {
|
||||
.insert(R::COMMAND, Box::new(handler));
|
||||
}
|
||||
|
||||
async fn start(cx: &mut AsyncApp) -> Result<Self> {
|
||||
async fn start_tcp(connection: TcpArguments, cx: &mut AsyncApp) -> Result<Self> {
|
||||
Ok(Self {
|
||||
request_handlers: Arc::new(Mutex::new(HashMap::default())),
|
||||
response_handlers: Arc::new(Mutex::new(HashMap::default())),
|
||||
message_handler: None,
|
||||
kind: FakeTransportKind::Tcp {
|
||||
connection,
|
||||
executor: cx.background_executor().clone(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_messages(
|
||||
request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
|
||||
response_handlers: Arc<Mutex<HashMap<&'static str, ResponseHandler>>>,
|
||||
stdin_reader: PipeReader,
|
||||
stdout_writer: PipeWriter,
|
||||
) -> Result<()> {
|
||||
use dap_types::requests::{Request, RunInTerminal, StartDebugging};
|
||||
use serde_json::json;
|
||||
|
||||
let (stdin_writer, stdin_reader) = async_pipe::pipe();
|
||||
let (stdout_writer, stdout_reader) = async_pipe::pipe();
|
||||
|
||||
let mut this = Self {
|
||||
request_handlers: Arc::new(Mutex::new(HashMap::default())),
|
||||
response_handlers: Arc::new(Mutex::new(HashMap::default())),
|
||||
stdin_writer: Some(stdin_writer),
|
||||
stdout_reader: Some(stdout_reader),
|
||||
message_handler: None,
|
||||
};
|
||||
|
||||
let request_handlers = this.request_handlers.clone();
|
||||
let response_handlers = this.response_handlers.clone();
|
||||
let mut reader = BufReader::new(stdin_reader);
|
||||
let stdout_writer = Arc::new(smol::lock::Mutex::new(stdout_writer));
|
||||
let mut buffer = String::new();
|
||||
|
||||
this.message_handler = Some(cx.background_spawn(async move {
|
||||
let mut reader = BufReader::new(stdin_reader);
|
||||
let mut buffer = String::new();
|
||||
|
||||
loop {
|
||||
match TransportDelegate::receive_server_message(&mut reader, &mut buffer, None)
|
||||
.await
|
||||
{
|
||||
ConnectionResult::Timeout => {
|
||||
anyhow::bail!("Timed out when connecting to debugger");
|
||||
}
|
||||
ConnectionResult::ConnectionReset => {
|
||||
log::info!("Debugger closed the connection");
|
||||
break Ok(());
|
||||
}
|
||||
ConnectionResult::Result(Err(e)) => break Err(e),
|
||||
ConnectionResult::Result(Ok(message)) => {
|
||||
match message {
|
||||
Message::Request(request) => {
|
||||
// redirect reverse requests to stdout writer/reader
|
||||
if request.command == RunInTerminal::COMMAND
|
||||
|| request.command == StartDebugging::COMMAND
|
||||
{
|
||||
let message =
|
||||
serde_json::to_string(&Message::Request(request)).unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
writer
|
||||
.write_all(
|
||||
TransportDelegate::build_rpc_message(message)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
} else {
|
||||
let response = if let Some(handle) =
|
||||
request_handlers.lock().get_mut(request.command.as_str())
|
||||
{
|
||||
handle(request.seq, request.arguments.unwrap_or(json!({})))
|
||||
} else {
|
||||
panic!("No request handler for {}", request.command);
|
||||
};
|
||||
let message =
|
||||
serde_json::to_string(&Message::Response(response))
|
||||
.unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
writer
|
||||
.write_all(
|
||||
TransportDelegate::build_rpc_message(message)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
}
|
||||
}
|
||||
Message::Event(event) => {
|
||||
loop {
|
||||
match TransportDelegate::receive_server_message(&mut reader, &mut buffer, None).await {
|
||||
ConnectionResult::Timeout => {
|
||||
anyhow::bail!("Timed out when connecting to debugger");
|
||||
}
|
||||
ConnectionResult::ConnectionReset => {
|
||||
log::info!("Debugger closed the connection");
|
||||
break Ok(());
|
||||
}
|
||||
ConnectionResult::Result(Err(e)) => break Err(e),
|
||||
ConnectionResult::Result(Ok(message)) => {
|
||||
match message {
|
||||
Message::Request(request) => {
|
||||
// redirect reverse requests to stdout writer/reader
|
||||
if request.command == RunInTerminal::COMMAND
|
||||
|| request.command == StartDebugging::COMMAND
|
||||
{
|
||||
let message =
|
||||
serde_json::to_string(&Message::Event(event)).unwrap();
|
||||
serde_json::to_string(&Message::Request(request)).unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
writer
|
||||
.write_all(
|
||||
TransportDelegate::build_rpc_message(message).as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
} else {
|
||||
let response = if let Some(handle) =
|
||||
request_handlers.lock().get_mut(request.command.as_str())
|
||||
{
|
||||
handle(request.seq, request.arguments.unwrap_or(json!({})))
|
||||
} else {
|
||||
panic!("No request handler for {}", request.command);
|
||||
};
|
||||
let response = match response {
|
||||
RequestHandling::Respond(response) => response,
|
||||
RequestHandling::Exit => {
|
||||
break Err(anyhow!("exit in response to request"));
|
||||
}
|
||||
};
|
||||
let message =
|
||||
serde_json::to_string(&Message::Response(response)).unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
writer
|
||||
@@ -854,20 +895,56 @@ impl FakeTransport {
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
}
|
||||
Message::Response(response) => {
|
||||
if let Some(handle) =
|
||||
response_handlers.lock().get(response.command.as_str())
|
||||
{
|
||||
handle(response);
|
||||
} else {
|
||||
log::error!("No response handler for {}", response.command);
|
||||
}
|
||||
}
|
||||
Message::Event(event) => {
|
||||
let message = serde_json::to_string(&Message::Event(event)).unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
writer
|
||||
.write_all(TransportDelegate::build_rpc_message(message).as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
}
|
||||
Message::Response(response) => {
|
||||
if let Some(handle) =
|
||||
response_handlers.lock().get(response.command.as_str())
|
||||
{
|
||||
handle(response);
|
||||
} else {
|
||||
log::error!("No response handler for {}", response.command);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_stdio(cx: &mut AsyncApp) -> Result<Self> {
|
||||
let (stdin_writer, stdin_reader) = async_pipe::pipe();
|
||||
let (stdout_writer, stdout_reader) = async_pipe::pipe();
|
||||
let kind = FakeTransportKind::Stdio {
|
||||
stdin_writer: Some(stdin_writer),
|
||||
stdout_reader: Some(stdout_reader),
|
||||
};
|
||||
|
||||
let mut this = Self {
|
||||
request_handlers: Arc::new(Mutex::new(HashMap::default())),
|
||||
response_handlers: Arc::new(Mutex::new(HashMap::default())),
|
||||
message_handler: None,
|
||||
kind,
|
||||
};
|
||||
|
||||
let request_handlers = this.request_handlers.clone();
|
||||
let response_handlers = this.response_handlers.clone();
|
||||
|
||||
this.message_handler = Some(cx.background_spawn(Self::handle_messages(
|
||||
request_handlers,
|
||||
response_handlers,
|
||||
stdin_reader,
|
||||
stdout_writer,
|
||||
)));
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
@@ -876,7 +953,10 @@ impl FakeTransport {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl Transport for FakeTransport {
|
||||
fn tcp_arguments(&self) -> Option<TcpArguments> {
|
||||
None
|
||||
match &self.kind {
|
||||
FakeTransportKind::Stdio { .. } => None,
|
||||
FakeTransportKind::Tcp { connection, .. } => Some(connection.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(
|
||||
@@ -887,12 +967,33 @@ impl Transport for FakeTransport {
|
||||
Box<dyn AsyncRead + Unpin + Send + 'static>,
|
||||
)>,
|
||||
> {
|
||||
let result = util::maybe!({
|
||||
Ok((
|
||||
Box::new(self.stdin_writer.take().context("Cannot reconnect")?) as _,
|
||||
Box::new(self.stdout_reader.take().context("Cannot reconnect")?) as _,
|
||||
))
|
||||
});
|
||||
let result = match &mut self.kind {
|
||||
FakeTransportKind::Stdio {
|
||||
stdin_writer,
|
||||
stdout_reader,
|
||||
} => util::maybe!({
|
||||
Ok((
|
||||
Box::new(stdin_writer.take().context("Cannot reconnect")?) as _,
|
||||
Box::new(stdout_reader.take().context("Cannot reconnect")?) as _,
|
||||
))
|
||||
}),
|
||||
FakeTransportKind::Tcp { executor, .. } => {
|
||||
let (stdin_writer, stdin_reader) = async_pipe::pipe();
|
||||
let (stdout_writer, stdout_reader) = async_pipe::pipe();
|
||||
|
||||
let request_handlers = self.request_handlers.clone();
|
||||
let response_handlers = self.response_handlers.clone();
|
||||
|
||||
self.message_handler = Some(executor.spawn(Self::handle_messages(
|
||||
request_handlers,
|
||||
response_handlers,
|
||||
stdin_reader,
|
||||
stdout_writer,
|
||||
)));
|
||||
|
||||
Ok((Box::new(stdin_writer) as _, Box::new(stdout_reader) as _))
|
||||
}
|
||||
};
|
||||
Task::ready(result)
|
||||
}
|
||||
|
||||
|
||||
@@ -547,6 +547,7 @@ async fn handle_envs(
|
||||
}
|
||||
};
|
||||
|
||||
let mut env_vars = HashMap::default();
|
||||
for path in env_files {
|
||||
let Some(path) = path
|
||||
.and_then(|s| PathBuf::from_str(s).ok())
|
||||
@@ -556,13 +557,33 @@ async fn handle_envs(
|
||||
};
|
||||
|
||||
if let Ok(file) = fs.open_sync(&path).await {
|
||||
envs.extend(dotenvy::from_read_iter(file).filter_map(Result::ok))
|
||||
let file_envs: HashMap<String, String> = dotenvy::from_read_iter(file)
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
envs.extend(file_envs.iter().map(|(k, v)| (k.clone(), v.clone())));
|
||||
env_vars.extend(file_envs);
|
||||
} else {
|
||||
warn!("While starting Go debug session: failed to read env file {path:?}");
|
||||
};
|
||||
}
|
||||
|
||||
let mut env_obj: serde_json::Map<String, Value> = serde_json::Map::new();
|
||||
|
||||
for (k, v) in env_vars {
|
||||
env_obj.insert(k, Value::String(v));
|
||||
}
|
||||
|
||||
if let Some(existing_env) = config.get("env").and_then(|v| v.as_object()) {
|
||||
for (k, v) in existing_env {
|
||||
env_obj.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if !env_obj.is_empty() {
|
||||
config.insert("env".to_string(), Value::Object(env_obj));
|
||||
}
|
||||
|
||||
// remove envFile now that it's been handled
|
||||
config.remove("entry");
|
||||
config.remove("envFile");
|
||||
Some(())
|
||||
}
|
||||
|
||||
@@ -534,6 +534,14 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
.filter(|name| !name.is_empty())?;
|
||||
Some(label.to_owned())
|
||||
}
|
||||
|
||||
fn compact_child_session(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn prefer_thread_name(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_task_type(task_type: &mut Value) {
|
||||
|
||||
@@ -32,12 +32,19 @@ use workspace::{
|
||||
ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum View {
|
||||
AdapterLogs,
|
||||
RpcMessages,
|
||||
InitializationSequence,
|
||||
}
|
||||
|
||||
struct DapLogView {
|
||||
editor: Entity<Editor>,
|
||||
focus_handle: FocusHandle,
|
||||
log_store: Entity<LogStore>,
|
||||
editor_subscriptions: Vec<Subscription>,
|
||||
current_view: Option<(SessionId, LogKind)>,
|
||||
current_view: Option<(SessionId, View)>,
|
||||
project: Entity<Project>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
@@ -77,6 +84,7 @@ struct DebugAdapterState {
|
||||
id: SessionId,
|
||||
log_messages: VecDeque<SharedString>,
|
||||
rpc_messages: RpcMessages,
|
||||
session_label: SharedString,
|
||||
adapter_name: DebugAdapterName,
|
||||
has_adapter_logs: bool,
|
||||
is_terminated: bool,
|
||||
@@ -121,12 +129,18 @@ impl MessageKind {
|
||||
}
|
||||
|
||||
impl DebugAdapterState {
|
||||
fn new(id: SessionId, adapter_name: DebugAdapterName, has_adapter_logs: bool) -> Self {
|
||||
fn new(
|
||||
id: SessionId,
|
||||
adapter_name: DebugAdapterName,
|
||||
session_label: SharedString,
|
||||
has_adapter_logs: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
log_messages: VecDeque::new(),
|
||||
rpc_messages: RpcMessages::new(),
|
||||
adapter_name,
|
||||
session_label,
|
||||
has_adapter_logs,
|
||||
is_terminated: false,
|
||||
}
|
||||
@@ -371,18 +385,22 @@ impl LogStore {
|
||||
return None;
|
||||
};
|
||||
|
||||
let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
|
||||
(
|
||||
session.adapter(),
|
||||
session
|
||||
.adapter_client()
|
||||
.map_or(false, |client| client.has_adapter_logs()),
|
||||
)
|
||||
});
|
||||
let (adapter_name, session_label, has_adapter_logs) =
|
||||
session.read_with(cx, |session, _| {
|
||||
(
|
||||
session.adapter(),
|
||||
session.label(),
|
||||
session
|
||||
.adapter_client()
|
||||
.map_or(false, |client| client.has_adapter_logs()),
|
||||
)
|
||||
});
|
||||
|
||||
state.insert(DebugAdapterState::new(
|
||||
id.session_id,
|
||||
adapter_name,
|
||||
session_label
|
||||
.unwrap_or_else(|| format!("Session {} (child)", id.session_id.0).into()),
|
||||
has_adapter_logs,
|
||||
));
|
||||
|
||||
@@ -506,12 +524,13 @@ impl Render for DapLogToolbarItemView {
|
||||
current_client
|
||||
.map(|sub_item| {
|
||||
Cow::Owned(format!(
|
||||
"{} ({}) - {}",
|
||||
"{} - {} - {}",
|
||||
sub_item.adapter_name,
|
||||
sub_item.session_id.0,
|
||||
sub_item.session_label,
|
||||
match sub_item.selected_entry {
|
||||
LogKind::Adapter => ADAPTER_LOGS,
|
||||
LogKind::Rpc => RPC_MESSAGES,
|
||||
View::AdapterLogs => ADAPTER_LOGS,
|
||||
View::RpcMessages => RPC_MESSAGES,
|
||||
View::InitializationSequence => INITIALIZATION_SEQUENCE,
|
||||
}
|
||||
))
|
||||
})
|
||||
@@ -529,8 +548,8 @@ impl Render for DapLogToolbarItemView {
|
||||
.pl_2()
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{}. {}",
|
||||
row.session_id.0, row.adapter_name,
|
||||
"{} - {}",
|
||||
row.adapter_name, row.session_label
|
||||
))
|
||||
.color(workspace::ui::Color::Muted),
|
||||
)
|
||||
@@ -669,9 +688,16 @@ impl DapLogView {
|
||||
|
||||
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
|
||||
Event::NewLogEntry { id, entry, kind } => {
|
||||
if log_view.current_view == Some((id.session_id, *kind))
|
||||
&& log_view.project == *id.project
|
||||
{
|
||||
let is_current_view = match (log_view.current_view, *kind) {
|
||||
(Some((i, View::AdapterLogs)), LogKind::Adapter)
|
||||
| (Some((i, View::RpcMessages)), LogKind::Rpc)
|
||||
if i == id.session_id =>
|
||||
{
|
||||
log_view.project == *id.project
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if is_current_view {
|
||||
log_view.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(false);
|
||||
let last_point = editor.buffer().read(cx).len(cx);
|
||||
@@ -768,10 +794,11 @@ impl DapLogView {
|
||||
.map(|state| DapMenuItem {
|
||||
session_id: state.id,
|
||||
adapter_name: state.adapter_name.clone(),
|
||||
session_label: state.session_label.clone(),
|
||||
has_adapter_logs: state.has_adapter_logs,
|
||||
selected_entry: self
|
||||
.current_view
|
||||
.map_or(LogKind::Adapter, |(_, kind)| kind),
|
||||
.map_or(View::AdapterLogs, |(_, kind)| kind),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
@@ -789,7 +816,7 @@ impl DapLogView {
|
||||
.map(|state| log_contents(state.iter().cloned()))
|
||||
});
|
||||
if let Some(rpc_log) = rpc_log {
|
||||
self.current_view = Some((id.session_id, LogKind::Rpc));
|
||||
self.current_view = Some((id.session_id, View::RpcMessages));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
|
||||
let language = self.project.read(cx).languages().language_for_name("JSON");
|
||||
editor
|
||||
@@ -830,7 +857,7 @@ impl DapLogView {
|
||||
.map(|state| log_contents(state.iter().cloned()))
|
||||
});
|
||||
if let Some(message_log) = message_log {
|
||||
self.current_view = Some((id.session_id, LogKind::Adapter));
|
||||
self.current_view = Some((id.session_id, View::AdapterLogs));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
|
||||
editor
|
||||
.read(cx)
|
||||
@@ -859,7 +886,7 @@ impl DapLogView {
|
||||
.map(|state| log_contents(state.iter().cloned()))
|
||||
});
|
||||
if let Some(rpc_log) = rpc_log {
|
||||
self.current_view = Some((id.session_id, LogKind::Rpc));
|
||||
self.current_view = Some((id.session_id, View::InitializationSequence));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
|
||||
let language = self.project.read(cx).languages().language_for_name("JSON");
|
||||
editor
|
||||
@@ -899,11 +926,12 @@ fn log_contents(lines: impl Iterator<Item = SharedString>) -> String {
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub(crate) struct DapMenuItem {
|
||||
pub session_id: SessionId,
|
||||
pub adapter_name: DebugAdapterName,
|
||||
pub has_adapter_logs: bool,
|
||||
pub selected_entry: LogKind,
|
||||
struct DapMenuItem {
|
||||
session_id: SessionId,
|
||||
session_label: SharedString,
|
||||
adapter_name: DebugAdapterName,
|
||||
has_adapter_logs: bool,
|
||||
selected_entry: View,
|
||||
}
|
||||
|
||||
const ADAPTER_LOGS: &str = "Adapter Logs";
|
||||
|
||||
@@ -40,12 +40,15 @@ file_icons.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
hex.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
notifications.workspace = true
|
||||
parking_lot.workspace = true
|
||||
parse_int.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::persistence::DebuggerPaneItem;
|
||||
use crate::session::DebugSession;
|
||||
use crate::session::running::RunningState;
|
||||
use crate::session::running::breakpoint_list::BreakpointList;
|
||||
|
||||
use crate::{
|
||||
ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList,
|
||||
FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables,
|
||||
@@ -9,6 +10,7 @@ use crate::{
|
||||
ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::IndexMap;
|
||||
use dap::adapters::DebugAdapterName;
|
||||
use dap::debugger_settings::DebugPanelDockPosition;
|
||||
use dap::{
|
||||
@@ -26,7 +28,7 @@ use text::ToPoint as _;
|
||||
|
||||
use itertools::Itertools as _;
|
||||
use language::Buffer;
|
||||
use project::debugger::session::{Session, SessionStateEvent};
|
||||
use project::debugger::session::{Session, SessionQuirks, SessionState, SessionStateEvent};
|
||||
use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId};
|
||||
use project::{Project, debugger::session::ThreadStatus};
|
||||
use rpc::proto::{self};
|
||||
@@ -35,7 +37,7 @@ use std::sync::{Arc, LazyLock};
|
||||
use task::{DebugScenario, TaskContext};
|
||||
use tree_sitter::{Query, StreamingIterator as _};
|
||||
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use util::{ResultExt, maybe};
|
||||
use util::{ResultExt, debug_panic, maybe};
|
||||
use workspace::SplitDirection;
|
||||
use workspace::item::SaveOptions;
|
||||
use workspace::{
|
||||
@@ -63,13 +65,14 @@ pub enum DebugPanelEvent {
|
||||
|
||||
pub struct DebugPanel {
|
||||
size: Pixels,
|
||||
sessions: Vec<Entity<DebugSession>>,
|
||||
active_session: Option<Entity<DebugSession>>,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
debug_scenario_scheduled_last: bool,
|
||||
pub(crate) sessions_with_children:
|
||||
IndexMap<Entity<DebugSession>, Vec<WeakEntity<DebugSession>>>,
|
||||
pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -100,7 +103,7 @@ impl DebugPanel {
|
||||
|
||||
Self {
|
||||
size: px(300.),
|
||||
sessions: vec![],
|
||||
sessions_with_children: Default::default(),
|
||||
active_session: None,
|
||||
focus_handle,
|
||||
breakpoint_list: BreakpointList::new(
|
||||
@@ -138,8 +141,9 @@ impl DebugPanel {
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn sessions(&self) -> Vec<Entity<DebugSession>> {
|
||||
self.sessions.clone()
|
||||
#[cfg(test)]
|
||||
pub(crate) fn sessions(&self) -> impl Iterator<Item = Entity<DebugSession>> {
|
||||
self.sessions_with_children.keys().cloned()
|
||||
}
|
||||
|
||||
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
|
||||
@@ -185,12 +189,20 @@ impl DebugPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let dap_store = self.project.read(cx).dap_store();
|
||||
let Some(adapter) = DapRegistry::global(cx).adapter(&scenario.adapter) else {
|
||||
return;
|
||||
};
|
||||
let quirks = SessionQuirks {
|
||||
compact: adapter.compact_child_session(),
|
||||
prefer_thread_name: adapter.prefer_thread_name(),
|
||||
};
|
||||
let session = dap_store.update(cx, |dap_store, cx| {
|
||||
dap_store.new_session(
|
||||
scenario.label.clone(),
|
||||
Some(scenario.label.clone()),
|
||||
DebugAdapterName(scenario.adapter.clone()),
|
||||
task_context.clone(),
|
||||
None,
|
||||
quirks,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -267,22 +279,34 @@ impl DebugPanel {
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
if let Err(error) = task.await {
|
||||
log::error!("{error}");
|
||||
session
|
||||
.update(cx, |session, cx| {
|
||||
session
|
||||
.console_output(cx)
|
||||
.unbounded_send(format!("error: {}", error))
|
||||
.ok();
|
||||
session.shutdown(cx)
|
||||
})?
|
||||
.await;
|
||||
let boot_task = cx.spawn({
|
||||
let session = session.clone();
|
||||
|
||||
async move |_, cx| {
|
||||
if let Err(error) = task.await {
|
||||
log::error!("{error}");
|
||||
session
|
||||
.update(cx, |session, cx| {
|
||||
session
|
||||
.console_output(cx)
|
||||
.unbounded_send(format!("error: {}", error))
|
||||
.ok();
|
||||
session.shutdown(cx)
|
||||
})?
|
||||
.await;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
|
||||
session.update(cx, |session, _| match &mut session.mode {
|
||||
SessionState::Building(state_task) => {
|
||||
*state_task = Some(boot_task);
|
||||
}
|
||||
SessionState::Running(_) => {
|
||||
debug_panic!("Session state should be in building because we are just starting it");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn rerun_last_session(
|
||||
@@ -363,14 +387,15 @@ impl DebugPanel {
|
||||
};
|
||||
|
||||
let dap_store_handle = self.project.read(cx).dap_store().clone();
|
||||
let label = curr_session.read(cx).label().clone();
|
||||
let label = curr_session.read(cx).label();
|
||||
let quirks = curr_session.read(cx).quirks();
|
||||
let adapter = curr_session.read(cx).adapter().clone();
|
||||
let binary = curr_session.read(cx).binary().cloned().unwrap();
|
||||
let task_context = curr_session.read(cx).task_context().clone();
|
||||
|
||||
let curr_session_id = curr_session.read(cx).session_id();
|
||||
self.sessions
|
||||
.retain(|session| session.read(cx).session_id(cx) != curr_session_id);
|
||||
self.sessions_with_children
|
||||
.retain(|session, _| session.read(cx).session_id(cx) != curr_session_id);
|
||||
let task = dap_store_handle.update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(curr_session_id, cx)
|
||||
});
|
||||
@@ -379,7 +404,7 @@ impl DebugPanel {
|
||||
task.await.log_err();
|
||||
|
||||
let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
|
||||
let session = dap_store.new_session(label, adapter, task_context, None, cx);
|
||||
let session = dap_store.new_session(label, adapter, task_context, None, quirks, cx);
|
||||
|
||||
let task = session.update(cx, |session, cx| {
|
||||
session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
|
||||
@@ -425,6 +450,7 @@ impl DebugPanel {
|
||||
let dap_store_handle = self.project.read(cx).dap_store().clone();
|
||||
let label = self.label_for_child_session(&parent_session, request, cx);
|
||||
let adapter = parent_session.read(cx).adapter().clone();
|
||||
let quirks = parent_session.read(cx).quirks();
|
||||
let Some(mut binary) = parent_session.read(cx).binary().cloned() else {
|
||||
log::error!("Attempted to start a child-session without a binary");
|
||||
return;
|
||||
@@ -438,6 +464,7 @@ impl DebugPanel {
|
||||
adapter,
|
||||
task_context,
|
||||
Some(parent_session.clone()),
|
||||
quirks,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -463,8 +490,8 @@ impl DebugPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(session) = self
|
||||
.sessions
|
||||
.iter()
|
||||
.sessions_with_children
|
||||
.keys()
|
||||
.find(|other| entity_id == other.entity_id())
|
||||
.cloned()
|
||||
else {
|
||||
@@ -498,15 +525,14 @@ impl DebugPanel {
|
||||
}
|
||||
session.update(cx, |session, cx| session.shutdown(cx)).ok();
|
||||
this.update(cx, |this, cx| {
|
||||
this.sessions.retain(|other| entity_id != other.entity_id());
|
||||
|
||||
this.retain_sessions(|other| entity_id != other.entity_id());
|
||||
if let Some(active_session_id) = this
|
||||
.active_session
|
||||
.as_ref()
|
||||
.map(|session| session.entity_id())
|
||||
{
|
||||
if active_session_id == entity_id {
|
||||
this.active_session = this.sessions.first().cloned();
|
||||
this.active_session = this.sessions_with_children.keys().next().cloned();
|
||||
}
|
||||
}
|
||||
cx.notify()
|
||||
@@ -813,13 +839,24 @@ impl DebugPanel {
|
||||
.on_click(window.listener_for(
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.stop_thread(cx);
|
||||
if this.session().read(cx).is_building() {
|
||||
this.session().update(cx, |session, cx| {
|
||||
session.shutdown(cx).detach()
|
||||
});
|
||||
} else {
|
||||
this.stop_thread(cx);
|
||||
}
|
||||
},
|
||||
))
|
||||
.disabled(active_session.as_ref().is_none_or(
|
||||
|session| {
|
||||
session
|
||||
.read(cx)
|
||||
.session(cx)
|
||||
.read(cx)
|
||||
.is_terminated()
|
||||
},
|
||||
))
|
||||
.disabled(
|
||||
thread_status != ThreadStatus::Stopped
|
||||
&& thread_status != ThreadStatus::Running,
|
||||
)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
let label = if capabilities
|
||||
@@ -976,8 +1013,8 @@ impl DebugPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(session) = self
|
||||
.sessions
|
||||
.iter()
|
||||
.sessions_with_children
|
||||
.keys()
|
||||
.find(|session| session.read(cx).session_id(cx) == session_id)
|
||||
{
|
||||
self.activate_session(session.clone(), window, cx);
|
||||
@@ -990,7 +1027,7 @@ impl DebugPanel {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
debug_assert!(self.sessions.contains(&session_item));
|
||||
debug_assert!(self.sessions_with_children.contains_key(&session_item));
|
||||
session_item.focus_handle(cx).focus(window);
|
||||
session_item.update(cx, |this, cx| {
|
||||
this.running_state().update(cx, |this, cx| {
|
||||
@@ -1261,18 +1298,27 @@ impl DebugPanel {
|
||||
parent_session: &Entity<Session>,
|
||||
request: &StartDebuggingRequestArguments,
|
||||
cx: &mut Context<'_, Self>,
|
||||
) -> SharedString {
|
||||
) -> Option<SharedString> {
|
||||
let adapter = parent_session.read(cx).adapter();
|
||||
if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
|
||||
if let Some(label) = adapter.label_for_child_session(request) {
|
||||
return label.into();
|
||||
return Some(label.into());
|
||||
}
|
||||
}
|
||||
let mut label = parent_session.read(cx).label().clone();
|
||||
if !label.ends_with("(child)") {
|
||||
label = format!("{label} (child)").into();
|
||||
None
|
||||
}
|
||||
|
||||
fn retain_sessions(&mut self, keep: impl Fn(&Entity<DebugSession>) -> bool) {
|
||||
self.sessions_with_children
|
||||
.retain(|session, _| keep(session));
|
||||
for children in self.sessions_with_children.values_mut() {
|
||||
children.retain(|child| {
|
||||
let Some(child) = child.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
keep(&child)
|
||||
});
|
||||
}
|
||||
label
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1302,11 +1348,11 @@ async fn register_session_inner(
|
||||
let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
|
||||
let debug_session = this.update_in(cx, |this, window, cx| {
|
||||
let parent_session = this
|
||||
.sessions
|
||||
.iter()
|
||||
.sessions_with_children
|
||||
.keys()
|
||||
.find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx))
|
||||
.cloned();
|
||||
this.sessions.retain(|session| {
|
||||
this.retain_sessions(|session| {
|
||||
!session
|
||||
.read(cx)
|
||||
.running_state()
|
||||
@@ -1337,13 +1383,23 @@ async fn register_session_inner(
|
||||
)
|
||||
.detach();
|
||||
let insert_position = this
|
||||
.sessions
|
||||
.iter()
|
||||
.sessions_with_children
|
||||
.keys()
|
||||
.position(|session| Some(session) == parent_session.as_ref())
|
||||
.map(|position| position + 1)
|
||||
.unwrap_or(this.sessions.len());
|
||||
.unwrap_or(this.sessions_with_children.len());
|
||||
// Maintain topological sort order of sessions
|
||||
this.sessions.insert(insert_position, debug_session.clone());
|
||||
let (_, old) = this.sessions_with_children.insert_before(
|
||||
insert_position,
|
||||
debug_session.clone(),
|
||||
Default::default(),
|
||||
);
|
||||
debug_assert!(old.is_none());
|
||||
if let Some(parent_session) = parent_session {
|
||||
this.sessions_with_children
|
||||
.entry(parent_session)
|
||||
.and_modify(|children| children.push(debug_session.downgrade()));
|
||||
}
|
||||
|
||||
debug_session
|
||||
})?;
|
||||
@@ -1383,7 +1439,7 @@ impl Panel for DebugPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if position.axis() != self.position(window, cx).axis() {
|
||||
self.sessions.iter().for_each(|session_item| {
|
||||
self.sessions_with_children.keys().for_each(|session_item| {
|
||||
session_item.update(cx, |item, cx| {
|
||||
item.running_state()
|
||||
.update(cx, |state, _| state.invert_axies())
|
||||
@@ -1749,6 +1805,7 @@ impl Render for DebugPanel {
|
||||
.child(breakpoint_list)
|
||||
.child(Divider::vertical())
|
||||
.child(welcome_experience)
|
||||
.child(Divider::vertical())
|
||||
} else {
|
||||
this.items_end()
|
||||
.child(welcome_experience)
|
||||
|
||||
@@ -83,6 +83,8 @@ actions!(
|
||||
Rerun,
|
||||
/// Toggles expansion of the selected item in the debugger UI.
|
||||
ToggleExpandItem,
|
||||
/// Set a data breakpoint on the selected variable or memory region.
|
||||
ToggleDataBreakpoint,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -1,16 +1,82 @@
|
||||
use std::time::Duration;
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};
|
||||
use gpui::{Animation, AnimationExt as _, Entity, Transformation, WeakEntity, percentage};
|
||||
use project::debugger::session::{ThreadId, ThreadStatus};
|
||||
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
|
||||
use util::truncate_and_trailoff;
|
||||
use util::{maybe, truncate_and_trailoff};
|
||||
|
||||
use crate::{
|
||||
debugger_panel::DebugPanel,
|
||||
session::{DebugSession, running::RunningState},
|
||||
};
|
||||
|
||||
struct SessionListEntry {
|
||||
ancestors: Vec<Entity<DebugSession>>,
|
||||
leaf: Entity<DebugSession>,
|
||||
}
|
||||
|
||||
impl SessionListEntry {
|
||||
pub(crate) fn label_element(&self, depth: usize, cx: &mut App) -> AnyElement {
|
||||
const MAX_LABEL_CHARS: usize = 150;
|
||||
|
||||
let mut label = String::new();
|
||||
for ancestor in &self.ancestors {
|
||||
label.push_str(&ancestor.update(cx, |ancestor, cx| {
|
||||
ancestor.label(cx).unwrap_or("(child)".into())
|
||||
}));
|
||||
label.push_str(" » ");
|
||||
}
|
||||
label.push_str(
|
||||
&self
|
||||
.leaf
|
||||
.update(cx, |leaf, cx| leaf.label(cx).unwrap_or("(child)".into())),
|
||||
);
|
||||
let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS);
|
||||
|
||||
let is_terminated = self
|
||||
.leaf
|
||||
.read(cx)
|
||||
.running_state
|
||||
.read(cx)
|
||||
.session()
|
||||
.read(cx)
|
||||
.is_terminated();
|
||||
let icon = {
|
||||
if is_terminated {
|
||||
Some(Indicator::dot().color(Color::Error))
|
||||
} else {
|
||||
match self
|
||||
.leaf
|
||||
.read(cx)
|
||||
.running_state
|
||||
.read(cx)
|
||||
.thread_status(cx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
project::debugger::session::ThreadStatus::Stopped => {
|
||||
Some(Indicator::dot().color(Color::Conflict))
|
||||
}
|
||||
_ => Some(Indicator::dot().color(Color::Success)),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id("session-label")
|
||||
.ml(depth * px(16.0))
|
||||
.gap_2()
|
||||
.when_some(icon, |this, indicator| this.child(indicator))
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new(label)
|
||||
.size(LabelSize::Small)
|
||||
.when(is_terminated, |this| this.strikethrough()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl DebugPanel {
|
||||
fn dropdown_label(label: impl Into<SharedString>) -> Label {
|
||||
const MAX_LABEL_CHARS: usize = 50;
|
||||
@@ -25,145 +91,205 @@ impl DebugPanel {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<impl IntoElement> {
|
||||
if let Some(running_state) = running_state {
|
||||
let sessions = self.sessions().clone();
|
||||
let weak = cx.weak_entity();
|
||||
let running_state = running_state.read(cx);
|
||||
let label = if let Some(active_session) = active_session.clone() {
|
||||
active_session.read(cx).session(cx).read(cx).label()
|
||||
} else {
|
||||
SharedString::new_static("Unknown Session")
|
||||
};
|
||||
let running_state = running_state?;
|
||||
|
||||
let is_terminated = running_state.session().read(cx).is_terminated();
|
||||
let is_started = active_session
|
||||
.is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
|
||||
let mut session_entries = Vec::with_capacity(self.sessions_with_children.len() * 3);
|
||||
let mut sessions_with_children = self.sessions_with_children.iter().peekable();
|
||||
|
||||
let session_state_indicator = if is_terminated {
|
||||
Indicator::dot().color(Color::Error).into_any_element()
|
||||
} else if !is_started {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element()
|
||||
while let Some((root, children)) = sessions_with_children.next() {
|
||||
let root_entry = if let Ok([single_child]) = <&[_; 1]>::try_from(children.as_slice())
|
||||
&& let Some(single_child) = single_child.upgrade()
|
||||
&& single_child.read(cx).quirks.compact
|
||||
{
|
||||
sessions_with_children.next();
|
||||
SessionListEntry {
|
||||
leaf: single_child.clone(),
|
||||
ancestors: vec![root.clone()],
|
||||
}
|
||||
} else {
|
||||
match running_state.thread_status(cx).unwrap_or_default() {
|
||||
ThreadStatus::Stopped => {
|
||||
Indicator::dot().color(Color::Conflict).into_any_element()
|
||||
}
|
||||
_ => Indicator::dot().color(Color::Success).into_any_element(),
|
||||
SessionListEntry {
|
||||
leaf: root.clone(),
|
||||
ancestors: Vec::new(),
|
||||
}
|
||||
};
|
||||
session_entries.push(root_entry);
|
||||
|
||||
let trigger = h_flex()
|
||||
.gap_2()
|
||||
.child(session_state_indicator)
|
||||
.justify_between()
|
||||
.child(
|
||||
DebugPanel::dropdown_label(label)
|
||||
.when(is_terminated, |this| this.strikethrough()),
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
Some(
|
||||
DropdownMenu::new_with_element(
|
||||
"debugger-session-list",
|
||||
trigger,
|
||||
ContextMenu::build(window, cx, move |mut this, _, cx| {
|
||||
let context_menu = cx.weak_entity();
|
||||
let mut session_depths = HashMap::default();
|
||||
for session in sessions.into_iter() {
|
||||
let weak_session = session.downgrade();
|
||||
let weak_session_id = weak_session.entity_id();
|
||||
let session_id = session.read(cx).session_id(cx);
|
||||
let parent_depth = session
|
||||
.read(cx)
|
||||
.session(cx)
|
||||
.read(cx)
|
||||
.parent_id(cx)
|
||||
.and_then(|parent_id| session_depths.get(&parent_id).cloned());
|
||||
let self_depth =
|
||||
*session_depths.entry(session_id).or_insert_with(|| {
|
||||
parent_depth.map(|depth| depth + 1).unwrap_or(0usize)
|
||||
});
|
||||
this = this.custom_entry(
|
||||
{
|
||||
let weak = weak.clone();
|
||||
let context_menu = context_menu.clone();
|
||||
move |_, cx| {
|
||||
weak_session
|
||||
.read_with(cx, |session, cx| {
|
||||
let context_menu = context_menu.clone();
|
||||
|
||||
let id: SharedString =
|
||||
format!("debug-session-{}", session_id.0)
|
||||
.into();
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.group(id.clone())
|
||||
.justify_between()
|
||||
.child(session.label_element(self_depth, cx))
|
||||
.child(
|
||||
IconButton::new(
|
||||
"close-debug-session",
|
||||
IconName::Close,
|
||||
)
|
||||
.visible_on_hover(id.clone())
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
move |_, window, cx| {
|
||||
weak.update(cx, |panel, cx| {
|
||||
panel.close_session(
|
||||
weak_session_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
context_menu
|
||||
.update(cx, |this, cx| {
|
||||
this.cancel(
|
||||
&Default::default(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap_or_else(|_| div().into_any_element())
|
||||
}
|
||||
},
|
||||
{
|
||||
let weak = weak.clone();
|
||||
move |window, cx| {
|
||||
weak.update(cx, |panel, cx| {
|
||||
panel.activate_session(session.clone(), window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
this
|
||||
session_entries.extend(
|
||||
sessions_with_children
|
||||
.by_ref()
|
||||
.take_while(|(session, _)| {
|
||||
session
|
||||
.read(cx)
|
||||
.session(cx)
|
||||
.read(cx)
|
||||
.parent_id(cx)
|
||||
.is_some()
|
||||
})
|
||||
.map(|(session, _)| SessionListEntry {
|
||||
leaf: session.clone(),
|
||||
ancestors: vec![],
|
||||
}),
|
||||
)
|
||||
.style(DropdownStyle::Ghost)
|
||||
.handle(self.session_picker_menu_handle.clone()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
let weak = cx.weak_entity();
|
||||
let trigger_label = if let Some(active_session) = active_session.clone() {
|
||||
active_session.update(cx, |active_session, cx| {
|
||||
active_session.label(cx).unwrap_or("(child)".into())
|
||||
})
|
||||
} else {
|
||||
SharedString::new_static("Unknown Session")
|
||||
};
|
||||
let running_state = running_state.read(cx);
|
||||
|
||||
let is_terminated = running_state.session().read(cx).is_terminated();
|
||||
let is_started = active_session
|
||||
.is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
|
||||
|
||||
let session_state_indicator = if is_terminated {
|
||||
Indicator::dot().color(Color::Error).into_any_element()
|
||||
} else if !is_started {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
match running_state.thread_status(cx).unwrap_or_default() {
|
||||
ThreadStatus::Stopped => Indicator::dot().color(Color::Conflict).into_any_element(),
|
||||
_ => Indicator::dot().color(Color::Success).into_any_element(),
|
||||
}
|
||||
};
|
||||
|
||||
let trigger = h_flex()
|
||||
.gap_2()
|
||||
.child(session_state_indicator)
|
||||
.justify_between()
|
||||
.child(
|
||||
DebugPanel::dropdown_label(trigger_label)
|
||||
.when(is_terminated, |this| this.strikethrough()),
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
let menu = DropdownMenu::new_with_element(
|
||||
"debugger-session-list",
|
||||
trigger,
|
||||
ContextMenu::build(window, cx, move |mut this, _, cx| {
|
||||
let context_menu = cx.weak_entity();
|
||||
let mut session_depths = HashMap::default();
|
||||
for session_entry in session_entries {
|
||||
let session_id = session_entry.leaf.read(cx).session_id(cx);
|
||||
let parent_depth = session_entry
|
||||
.ancestors
|
||||
.first()
|
||||
.unwrap_or(&session_entry.leaf)
|
||||
.read(cx)
|
||||
.session(cx)
|
||||
.read(cx)
|
||||
.parent_id(cx)
|
||||
.and_then(|parent_id| session_depths.get(&parent_id).cloned());
|
||||
let self_depth = *session_depths
|
||||
.entry(session_id)
|
||||
.or_insert_with(|| parent_depth.map(|depth| depth + 1).unwrap_or(0usize));
|
||||
this = this.custom_entry(
|
||||
{
|
||||
let weak = weak.clone();
|
||||
let context_menu = context_menu.clone();
|
||||
let ancestors: Rc<[_]> = session_entry
|
||||
.ancestors
|
||||
.iter()
|
||||
.map(|session| session.downgrade())
|
||||
.collect();
|
||||
let leaf = session_entry.leaf.downgrade();
|
||||
move |window, cx| {
|
||||
Self::render_session_menu_entry(
|
||||
weak.clone(),
|
||||
context_menu.clone(),
|
||||
ancestors.clone(),
|
||||
leaf.clone(),
|
||||
self_depth,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
let weak = weak.clone();
|
||||
let leaf = session_entry.leaf.clone();
|
||||
move |window, cx| {
|
||||
weak.update(cx, |panel, cx| {
|
||||
panel.activate_session(leaf.clone(), window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
this
|
||||
}),
|
||||
)
|
||||
.style(DropdownStyle::Ghost)
|
||||
.handle(self.session_picker_menu_handle.clone());
|
||||
|
||||
Some(menu)
|
||||
}
|
||||
|
||||
fn render_session_menu_entry(
|
||||
weak: WeakEntity<DebugPanel>,
|
||||
context_menu: WeakEntity<ContextMenu>,
|
||||
ancestors: Rc<[WeakEntity<DebugSession>]>,
|
||||
leaf: WeakEntity<DebugSession>,
|
||||
self_depth: usize,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let Some(session_entry) = maybe!({
|
||||
let ancestors = ancestors
|
||||
.iter()
|
||||
.map(|ancestor| ancestor.upgrade())
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
let leaf = leaf.upgrade()?;
|
||||
Some(SessionListEntry { ancestors, leaf })
|
||||
}) else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let id: SharedString = format!(
|
||||
"debug-session-{}",
|
||||
session_entry.leaf.read(cx).session_id(cx).0
|
||||
)
|
||||
.into();
|
||||
let session_entity_id = session_entry.leaf.entity_id();
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.group(id.clone())
|
||||
.justify_between()
|
||||
.child(session_entry.label_element(self_depth, cx))
|
||||
.child(
|
||||
IconButton::new("close-debug-session", IconName::Close)
|
||||
.visible_on_hover(id.clone())
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
move |_, window, cx| {
|
||||
weak.update(cx, |panel, cx| {
|
||||
panel.close_session(session_entity_id, window, cx);
|
||||
})
|
||||
.ok();
|
||||
context_menu
|
||||
.update(cx, |this, cx| {
|
||||
this.cancel(&Default::default(), window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub(crate) fn render_thread_dropdown(
|
||||
|
||||
@@ -11,7 +11,7 @@ use workspace::{Member, Pane, PaneAxis, Workspace};
|
||||
|
||||
use crate::session::running::{
|
||||
self, DebugTerminal, RunningState, SubView, breakpoint_list::BreakpointList, console::Console,
|
||||
loaded_source_list::LoadedSourceList, module_list::ModuleList,
|
||||
loaded_source_list::LoadedSourceList, memory_view::MemoryView, module_list::ModuleList,
|
||||
stack_frame_list::StackFrameList, variable_list::VariableList,
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ pub(crate) enum DebuggerPaneItem {
|
||||
Modules,
|
||||
LoadedSources,
|
||||
Terminal,
|
||||
MemoryView,
|
||||
}
|
||||
|
||||
impl DebuggerPaneItem {
|
||||
@@ -36,6 +37,7 @@ impl DebuggerPaneItem {
|
||||
DebuggerPaneItem::Modules,
|
||||
DebuggerPaneItem::LoadedSources,
|
||||
DebuggerPaneItem::Terminal,
|
||||
DebuggerPaneItem::MemoryView,
|
||||
];
|
||||
VARIANTS
|
||||
}
|
||||
@@ -43,6 +45,9 @@ impl DebuggerPaneItem {
|
||||
pub(crate) fn is_supported(&self, capabilities: &Capabilities) -> bool {
|
||||
match self {
|
||||
DebuggerPaneItem::Modules => capabilities.supports_modules_request.unwrap_or_default(),
|
||||
DebuggerPaneItem::MemoryView => capabilities
|
||||
.supports_read_memory_request
|
||||
.unwrap_or_default(),
|
||||
DebuggerPaneItem::LoadedSources => capabilities
|
||||
.supports_loaded_sources_request
|
||||
.unwrap_or_default(),
|
||||
@@ -59,6 +64,7 @@ impl DebuggerPaneItem {
|
||||
DebuggerPaneItem::Modules => SharedString::new_static("Modules"),
|
||||
DebuggerPaneItem::LoadedSources => SharedString::new_static("Sources"),
|
||||
DebuggerPaneItem::Terminal => SharedString::new_static("Terminal"),
|
||||
DebuggerPaneItem::MemoryView => SharedString::new_static("Memory View"),
|
||||
}
|
||||
}
|
||||
pub(crate) fn tab_tooltip(self) -> SharedString {
|
||||
@@ -80,6 +86,7 @@ impl DebuggerPaneItem {
|
||||
DebuggerPaneItem::Terminal => {
|
||||
"Provides an interactive terminal session within the debugging environment."
|
||||
}
|
||||
DebuggerPaneItem::MemoryView => "Allows inspection of memory contents.",
|
||||
};
|
||||
SharedString::new_static(tooltip)
|
||||
}
|
||||
@@ -204,6 +211,7 @@ pub(crate) fn deserialize_pane_layout(
|
||||
breakpoint_list: &Entity<BreakpointList>,
|
||||
loaded_sources: &Entity<LoadedSourceList>,
|
||||
terminal: &Entity<DebugTerminal>,
|
||||
memory_view: &Entity<MemoryView>,
|
||||
subscriptions: &mut HashMap<EntityId, Subscription>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<RunningState>,
|
||||
@@ -228,6 +236,7 @@ pub(crate) fn deserialize_pane_layout(
|
||||
breakpoint_list,
|
||||
loaded_sources,
|
||||
terminal,
|
||||
memory_view,
|
||||
subscriptions,
|
||||
window,
|
||||
cx,
|
||||
@@ -298,6 +307,12 @@ pub(crate) fn deserialize_pane_layout(
|
||||
DebuggerPaneItem::Terminal,
|
||||
cx,
|
||||
)),
|
||||
DebuggerPaneItem::MemoryView => Box::new(SubView::new(
|
||||
memory_view.focus_handle(cx),
|
||||
memory_view.clone().into(),
|
||||
DebuggerPaneItem::MemoryView,
|
||||
cx,
|
||||
)),
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -5,14 +5,13 @@ use dap::client::SessionId;
|
||||
use gpui::{
|
||||
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use project::Project;
|
||||
use project::debugger::session::Session;
|
||||
use project::worktree_store::WorktreeStore;
|
||||
use project::{Project, debugger::session::SessionQuirks};
|
||||
use rpc::proto;
|
||||
use running::RunningState;
|
||||
use std::{cell::OnceCell, sync::OnceLock};
|
||||
use ui::{Indicator, Tooltip, prelude::*};
|
||||
use util::truncate_and_trailoff;
|
||||
use std::cell::OnceCell;
|
||||
use ui::prelude::*;
|
||||
use workspace::{
|
||||
CollaboratorId, FollowableItem, ViewId, Workspace,
|
||||
item::{self, Item},
|
||||
@@ -20,8 +19,8 @@ use workspace::{
|
||||
|
||||
pub struct DebugSession {
|
||||
remote_id: Option<workspace::ViewId>,
|
||||
running_state: Entity<RunningState>,
|
||||
label: OnceLock<SharedString>,
|
||||
pub(crate) running_state: Entity<RunningState>,
|
||||
pub(crate) quirks: SessionQuirks,
|
||||
stack_trace_view: OnceCell<Entity<StackTraceView>>,
|
||||
_worktree_store: WeakEntity<WorktreeStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
@@ -57,6 +56,7 @@ impl DebugSession {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let quirks = session.read(cx).quirks();
|
||||
|
||||
cx.new(|cx| Self {
|
||||
_subscriptions: [cx.subscribe(&running_state, |_, _, _, cx| {
|
||||
@@ -64,7 +64,7 @@ impl DebugSession {
|
||||
})],
|
||||
remote_id: None,
|
||||
running_state,
|
||||
label: OnceLock::new(),
|
||||
quirks,
|
||||
stack_trace_view: OnceCell::new(),
|
||||
_worktree_store: project.read(cx).worktree_store().downgrade(),
|
||||
workspace,
|
||||
@@ -110,65 +110,28 @@ impl DebugSession {
|
||||
.update(cx, |state, cx| state.shutdown(cx));
|
||||
}
|
||||
|
||||
pub(crate) fn label(&self, cx: &App) -> SharedString {
|
||||
if let Some(label) = self.label.get() {
|
||||
return label.clone();
|
||||
}
|
||||
|
||||
let session = self.running_state.read(cx).session();
|
||||
|
||||
self.label
|
||||
.get_or_init(|| session.read(cx).label())
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub(crate) fn running_state(&self) -> &Entity<RunningState> {
|
||||
&self.running_state
|
||||
}
|
||||
|
||||
pub(crate) fn label_element(&self, depth: usize, cx: &App) -> AnyElement {
|
||||
const MAX_LABEL_CHARS: usize = 150;
|
||||
|
||||
let label = self.label(cx);
|
||||
let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS);
|
||||
|
||||
let is_terminated = self
|
||||
.running_state
|
||||
.read(cx)
|
||||
.session()
|
||||
.read(cx)
|
||||
.is_terminated();
|
||||
let icon = {
|
||||
if is_terminated {
|
||||
Some(Indicator::dot().color(Color::Error))
|
||||
} else {
|
||||
match self
|
||||
.running_state
|
||||
.read(cx)
|
||||
.thread_status(cx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
project::debugger::session::ThreadStatus::Stopped => {
|
||||
Some(Indicator::dot().color(Color::Conflict))
|
||||
}
|
||||
_ => Some(Indicator::dot().color(Color::Success)),
|
||||
pub(crate) fn label(&self, cx: &mut App) -> Option<SharedString> {
|
||||
let session = self.running_state.read(cx).session().clone();
|
||||
session.update(cx, |session, cx| {
|
||||
let session_label = session.label();
|
||||
let quirks = session.quirks();
|
||||
let mut single_thread_name = || {
|
||||
let threads = session.threads(cx);
|
||||
match threads.as_slice() {
|
||||
[(thread, _)] => Some(SharedString::from(&thread.name)),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
if quirks.prefer_thread_name {
|
||||
single_thread_name().or(session_label)
|
||||
} else {
|
||||
session_label.or_else(single_thread_name)
|
||||
}
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
h_flex()
|
||||
.id("session-label")
|
||||
.tooltip(Tooltip::text(format!("Session {}", self.session_id(cx).0,)))
|
||||
.ml(depth * px(16.0))
|
||||
.gap_2()
|
||||
.when_some(icon, |this, indicator| this.child(indicator))
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new(label)
|
||||
.size(LabelSize::Small)
|
||||
.when(is_terminated, |this| this.strikethrough()),
|
||||
)
|
||||
.into_any_element()
|
||||
pub fn running_state(&self) -> &Entity<RunningState> {
|
||||
&self.running_state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
pub(crate) mod breakpoint_list;
|
||||
pub(crate) mod console;
|
||||
pub(crate) mod loaded_source_list;
|
||||
pub(crate) mod memory_view;
|
||||
pub(crate) mod module_list;
|
||||
pub mod stack_frame_list;
|
||||
pub mod variable_list;
|
||||
|
||||
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use crate::{
|
||||
ToggleExpandItem,
|
||||
new_process_modal::resolve_path,
|
||||
persistence::{self, DebuggerPaneItem, SerializedLayout},
|
||||
session::running::memory_view::MemoryView,
|
||||
};
|
||||
|
||||
use super::DebugPanelItemEvent;
|
||||
@@ -34,7 +35,7 @@ use loaded_source_list::LoadedSourceList;
|
||||
use module_list::ModuleList;
|
||||
use project::{
|
||||
DebugScenarioContext, Project, WorktreeId,
|
||||
debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
|
||||
debugger::session::{self, Session, SessionEvent, SessionStateEvent, ThreadId, ThreadStatus},
|
||||
terminals::TerminalKind,
|
||||
};
|
||||
use rpc::proto::ViewId;
|
||||
@@ -81,6 +82,7 @@ pub struct RunningState {
|
||||
_schedule_serialize: Option<Task<()>>,
|
||||
pub(crate) scenario: Option<DebugScenario>,
|
||||
pub(crate) scenario_context: Option<DebugScenarioContext>,
|
||||
memory_view: Entity<MemoryView>,
|
||||
}
|
||||
|
||||
impl RunningState {
|
||||
@@ -676,14 +678,36 @@ impl RunningState {
|
||||
let session_id = session.read(cx).session_id();
|
||||
let weak_state = cx.weak_entity();
|
||||
let stack_frame_list = cx.new(|cx| {
|
||||
StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
|
||||
StackFrameList::new(
|
||||
workspace.clone(),
|
||||
session.clone(),
|
||||
weak_state.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let debug_terminal =
|
||||
parent_terminal.unwrap_or_else(|| cx.new(|cx| DebugTerminal::empty(window, cx)));
|
||||
|
||||
let variable_list =
|
||||
cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
|
||||
let memory_view = cx.new(|cx| {
|
||||
MemoryView::new(
|
||||
session.clone(),
|
||||
workspace.clone(),
|
||||
stack_frame_list.downgrade(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let variable_list = cx.new(|cx| {
|
||||
VariableList::new(
|
||||
session.clone(),
|
||||
stack_frame_list.clone(),
|
||||
memory_view.clone(),
|
||||
weak_state.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
|
||||
|
||||
@@ -770,6 +794,15 @@ impl RunningState {
|
||||
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
|
||||
this.serialize_layout(window, cx);
|
||||
}),
|
||||
cx.subscribe(
|
||||
&session,
|
||||
|this, session, event: &SessionStateEvent, cx| match event {
|
||||
SessionStateEvent::Shutdown if session.read(cx).is_building() => {
|
||||
this.shutdown(cx);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
let mut pane_close_subscriptions = HashMap::default();
|
||||
@@ -786,6 +819,7 @@ impl RunningState {
|
||||
&breakpoint_list,
|
||||
&loaded_source_list,
|
||||
&debug_terminal,
|
||||
&memory_view,
|
||||
&mut pane_close_subscriptions,
|
||||
window,
|
||||
cx,
|
||||
@@ -814,6 +848,7 @@ impl RunningState {
|
||||
let active_pane = panes.first_pane();
|
||||
|
||||
Self {
|
||||
memory_view,
|
||||
session,
|
||||
workspace,
|
||||
focus_handle,
|
||||
@@ -884,6 +919,7 @@ impl RunningState {
|
||||
let weak_project = project.downgrade();
|
||||
let weak_workspace = workspace.downgrade();
|
||||
let is_local = project.read(cx).is_local();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let DebugScenario {
|
||||
adapter,
|
||||
@@ -1224,6 +1260,12 @@ impl RunningState {
|
||||
item_kind,
|
||||
cx,
|
||||
)),
|
||||
DebuggerPaneItem::MemoryView => Box::new(SubView::new(
|
||||
self.memory_view.focus_handle(cx),
|
||||
self.memory_view.clone().into(),
|
||||
item_kind,
|
||||
cx,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1408,7 +1450,14 @@ impl RunningState {
|
||||
&self.module_list
|
||||
}
|
||||
|
||||
pub(crate) fn activate_item(&self, item: DebuggerPaneItem, window: &mut Window, cx: &mut App) {
|
||||
pub(crate) fn activate_item(
|
||||
&mut self,
|
||||
item: DebuggerPaneItem,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.ensure_pane_item(item, window, cx);
|
||||
|
||||
let (variable_list_position, pane) = self
|
||||
.panes
|
||||
.panes()
|
||||
@@ -1420,9 +1469,10 @@ impl RunningState {
|
||||
.map(|view| (view, pane))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
pane.update(cx, |this, cx| {
|
||||
this.activate_item(variable_list_position, true, true, window, cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1459,7 +1509,7 @@ impl RunningState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
|
||||
pub fn selected_thread_id(&self) -> Option<ThreadId> {
|
||||
self.thread_id
|
||||
}
|
||||
|
||||
@@ -1599,9 +1649,21 @@ impl RunningState {
|
||||
})
|
||||
.log_err();
|
||||
|
||||
self.session.update(cx, |session, cx| {
|
||||
let is_building = self.session.update(cx, |session, cx| {
|
||||
session.shutdown(cx).detach();
|
||||
})
|
||||
matches!(session.mode, session::SessionState::Building(_))
|
||||
});
|
||||
|
||||
if is_building {
|
||||
self.debug_terminal.update(cx, |terminal, cx| {
|
||||
if let Some(view) = terminal.terminal.as_ref() {
|
||||
view.update(cx, |view, cx| {
|
||||
view.terminal()
|
||||
.update(cx, |terminal, _| terminal.kill_active_task())
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_thread(&self, cx: &mut Context<Self>) {
|
||||
|
||||
@@ -24,10 +24,10 @@ use project::{
|
||||
};
|
||||
use ui::{
|
||||
ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div,
|
||||
Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, Indicator,
|
||||
InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement,
|
||||
Render, RenderOnce, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
|
||||
Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
|
||||
Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, InteractiveElement,
|
||||
IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
|
||||
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Toggleable,
|
||||
Tooltip, Window, div, h_flex, px, v_flex,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
@@ -46,6 +46,7 @@ actions!(
|
||||
pub(crate) enum SelectedBreakpointKind {
|
||||
Source,
|
||||
Exception,
|
||||
Data,
|
||||
}
|
||||
pub(crate) struct BreakpointList {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
@@ -188,6 +189,9 @@ impl BreakpointList {
|
||||
BreakpointEntryKind::ExceptionBreakpoint(bp) => {
|
||||
(SelectedBreakpointKind::Exception, bp.is_enabled)
|
||||
}
|
||||
BreakpointEntryKind::DataBreakpoint(bp) => {
|
||||
(SelectedBreakpointKind::Data, bp.0.is_enabled)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -391,7 +395,8 @@ impl BreakpointList {
|
||||
let row = line_breakpoint.breakpoint.row;
|
||||
self.go_to_line_breakpoint(path, row, window, cx);
|
||||
}
|
||||
BreakpointEntryKind::ExceptionBreakpoint(_) => {}
|
||||
BreakpointEntryKind::DataBreakpoint(_)
|
||||
| BreakpointEntryKind::ExceptionBreakpoint(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,6 +426,10 @@ impl BreakpointList {
|
||||
let id = exception_breakpoint.id.clone();
|
||||
self.toggle_exception_breakpoint(&id, cx);
|
||||
}
|
||||
BreakpointEntryKind::DataBreakpoint(data_breakpoint) => {
|
||||
let id = data_breakpoint.0.dap.data_id.clone();
|
||||
self.toggle_data_breakpoint(&id, cx);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -441,7 +450,7 @@ impl BreakpointList {
|
||||
let row = line_breakpoint.breakpoint.row;
|
||||
self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
|
||||
}
|
||||
BreakpointEntryKind::ExceptionBreakpoint(_) => {}
|
||||
_ => {}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -490,6 +499,14 @@ impl BreakpointList {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
|
||||
if let Some(session) = &self.session {
|
||||
session.update(cx, |this, cx| {
|
||||
this.toggle_data_breakpoint(&id, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
|
||||
if let Some(session) = &self.session {
|
||||
session.update(cx, |this, cx| {
|
||||
@@ -642,6 +659,7 @@ impl BreakpointList {
|
||||
SelectedBreakpointKind::Exception => {
|
||||
"Exception Breakpoints cannot be removed from the breakpoint list"
|
||||
}
|
||||
SelectedBreakpointKind::Data => "Remove data breakpoint from a breakpoint list",
|
||||
});
|
||||
let toggle_label = selection_kind.map(|(_, is_enabled)| {
|
||||
if is_enabled {
|
||||
@@ -783,8 +801,20 @@ impl Render for BreakpointList {
|
||||
weak: weak.clone(),
|
||||
})
|
||||
});
|
||||
self.breakpoints
|
||||
.extend(breakpoints.chain(exception_breakpoints));
|
||||
let data_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
|
||||
session
|
||||
.read(cx)
|
||||
.data_breakpoints()
|
||||
.map(|state| BreakpointEntry {
|
||||
kind: BreakpointEntryKind::DataBreakpoint(DataBreakpoint(state.clone())),
|
||||
weak: weak.clone(),
|
||||
})
|
||||
});
|
||||
self.breakpoints.extend(
|
||||
breakpoints
|
||||
.chain(data_breakpoints)
|
||||
.chain(exception_breakpoints),
|
||||
);
|
||||
v_flex()
|
||||
.id("breakpoint-list")
|
||||
.key_context("BreakpointList")
|
||||
@@ -905,7 +935,11 @@ impl LineBreakpoint {
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
|
||||
.child(
|
||||
Icon::new(icon_name)
|
||||
.color(Color::Debugger)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.on_mouse_down(MouseButton::Left, move |_, _, _| {});
|
||||
|
||||
ListItem::new(SharedString::from(format!(
|
||||
@@ -996,6 +1030,103 @@ struct ExceptionBreakpoint {
|
||||
data: ExceptionBreakpointsFilter,
|
||||
is_enabled: bool,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
struct DataBreakpoint(project::debugger::session::DataBreakpointState);
|
||||
|
||||
impl DataBreakpoint {
|
||||
fn render(
|
||||
&self,
|
||||
props: SupportedBreakpointProperties,
|
||||
strip_mode: Option<ActiveBreakpointStripMode>,
|
||||
ix: usize,
|
||||
is_selected: bool,
|
||||
focus_handle: FocusHandle,
|
||||
list: WeakEntity<BreakpointList>,
|
||||
) -> ListItem {
|
||||
let color = if self.0.is_enabled {
|
||||
Color::Debugger
|
||||
} else {
|
||||
Color::Muted
|
||||
};
|
||||
let is_enabled = self.0.is_enabled;
|
||||
let id = self.0.dap.data_id.clone();
|
||||
ListItem::new(SharedString::from(format!(
|
||||
"data-breakpoint-ui-item-{}",
|
||||
self.0.dap.data_id
|
||||
)))
|
||||
.rounded()
|
||||
.start_slot(
|
||||
div()
|
||||
.id(SharedString::from(format!(
|
||||
"data-breakpoint-ui-item-{}-click-handler",
|
||||
self.0.dap.data_id
|
||||
)))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
if is_enabled {
|
||||
"Disable Data Breakpoint"
|
||||
} else {
|
||||
"Enable Data Breakpoint"
|
||||
},
|
||||
&ToggleEnableBreakpoint,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let list = list.clone();
|
||||
move |_, _, cx| {
|
||||
list.update(cx, |this, cx| {
|
||||
this.toggle_data_breakpoint(&id, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
Icon::new(IconName::Binary)
|
||||
.color(color)
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.mr_4()
|
||||
.py_0p5()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.py_1()
|
||||
.gap_1()
|
||||
.min_h(px(26.))
|
||||
.justify_center()
|
||||
.id(("data-breakpoint-label", ix))
|
||||
.child(
|
||||
Label::new(self.0.context.human_readable_label())
|
||||
.size(LabelSize::Small)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel),
|
||||
),
|
||||
)
|
||||
.child(BreakpointOptionsStrip {
|
||||
props,
|
||||
breakpoint: BreakpointEntry {
|
||||
kind: BreakpointEntryKind::DataBreakpoint(self.clone()),
|
||||
weak: list,
|
||||
},
|
||||
is_selected,
|
||||
focus_handle,
|
||||
strip_mode,
|
||||
index: ix,
|
||||
}),
|
||||
)
|
||||
.toggle_state(is_selected)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExceptionBreakpoint {
|
||||
fn render(
|
||||
@@ -1062,7 +1193,11 @@ impl ExceptionBreakpoint {
|
||||
}
|
||||
})
|
||||
.cursor_pointer()
|
||||
.child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
|
||||
.child(
|
||||
Icon::new(IconName::Flame)
|
||||
.color(color)
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -1105,6 +1240,7 @@ impl ExceptionBreakpoint {
|
||||
enum BreakpointEntryKind {
|
||||
LineBreakpoint(LineBreakpoint),
|
||||
ExceptionBreakpoint(ExceptionBreakpoint),
|
||||
DataBreakpoint(DataBreakpoint),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -1140,6 +1276,14 @@ impl BreakpointEntry {
|
||||
focus_handle,
|
||||
self.weak.clone(),
|
||||
),
|
||||
BreakpointEntryKind::DataBreakpoint(data_breakpoint) => data_breakpoint.render(
|
||||
props.for_data_breakpoints(),
|
||||
strip_mode,
|
||||
ix,
|
||||
is_selected,
|
||||
focus_handle,
|
||||
self.weak.clone(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1155,6 +1299,11 @@ impl BreakpointEntry {
|
||||
exception_breakpoint.id
|
||||
)
|
||||
.into(),
|
||||
BreakpointEntryKind::DataBreakpoint(data_breakpoint) => format!(
|
||||
"data-breakpoint-control-strip--{}",
|
||||
data_breakpoint.0.dap.data_id
|
||||
)
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1172,8 +1321,8 @@ impl BreakpointEntry {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
line_breakpoint.breakpoint.condition.is_some()
|
||||
}
|
||||
// We don't support conditions on exception breakpoints
|
||||
BreakpointEntryKind::ExceptionBreakpoint(_) => false,
|
||||
// We don't support conditions on exception/data breakpoints
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1225,6 +1374,10 @@ impl SupportedBreakpointProperties {
|
||||
// TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
|
||||
Self::empty()
|
||||
}
|
||||
fn for_data_breakpoints(self) -> Self {
|
||||
// TODO: we don't yet support conditions for data breakpoints at the data layer, hence all props are disabled here.
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
#[derive(IntoElement)]
|
||||
struct BreakpointOptionsStrip {
|
||||
|
||||
@@ -12,7 +12,7 @@ use gpui::{
|
||||
Action as _, AppContext, Context, Corner, Entity, FocusHandle, Focusable, HighlightStyle, Hsla,
|
||||
Render, Subscription, Task, TextStyle, WeakEntity, actions,
|
||||
};
|
||||
use language::{Buffer, CodeLabel, ToOffset};
|
||||
use language::{Anchor, Buffer, CodeLabel, TextBufferSnapshot, ToOffset};
|
||||
use menu::{Confirm, SelectNext, SelectPrevious};
|
||||
use project::{
|
||||
Completion, CompletionResponse,
|
||||
@@ -637,27 +637,13 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
});
|
||||
|
||||
let snapshot = buffer.read(cx).text_snapshot();
|
||||
let query = snapshot.text();
|
||||
let replace_range = {
|
||||
let buffer_offset = buffer_position.to_offset(&snapshot);
|
||||
let reversed_chars = snapshot.reversed_chars_for_range(0..buffer_offset);
|
||||
let mut word_len = 0;
|
||||
for ch in reversed_chars {
|
||||
if ch.is_alphanumeric() || ch == '_' {
|
||||
word_len += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let word_start_offset = buffer_offset - word_len;
|
||||
let start_anchor = snapshot.anchor_at(word_start_offset, Bias::Left);
|
||||
start_anchor..buffer_position
|
||||
};
|
||||
let buffer_text = snapshot.text();
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
const LIMIT: usize = 10;
|
||||
let matches = fuzzy::match_strings(
|
||||
&string_matches,
|
||||
&query,
|
||||
&buffer_text,
|
||||
true,
|
||||
true,
|
||||
LIMIT,
|
||||
@@ -672,7 +658,12 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
let variable_value = variables.get(&string_match.string)?;
|
||||
|
||||
Some(project::Completion {
|
||||
replace_range: replace_range.clone(),
|
||||
replace_range: Self::replace_range_for_completion(
|
||||
&buffer_text,
|
||||
buffer_position,
|
||||
string_match.string.as_bytes(),
|
||||
&snapshot,
|
||||
),
|
||||
new_text: string_match.string.clone(),
|
||||
label: CodeLabel {
|
||||
filter_range: 0..string_match.string.len(),
|
||||
@@ -697,6 +688,28 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
})
|
||||
}
|
||||
|
||||
fn replace_range_for_completion(
|
||||
buffer_text: &String,
|
||||
buffer_position: Anchor,
|
||||
new_bytes: &[u8],
|
||||
snapshot: &TextBufferSnapshot,
|
||||
) -> Range<Anchor> {
|
||||
let buffer_offset = buffer_position.to_offset(&snapshot);
|
||||
let buffer_bytes = &buffer_text.as_bytes()[0..buffer_offset];
|
||||
|
||||
let mut prefix_len = 0;
|
||||
for i in (0..new_bytes.len()).rev() {
|
||||
if buffer_bytes.ends_with(&new_bytes[0..i]) {
|
||||
prefix_len = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let start = snapshot.clip_offset(buffer_offset - prefix_len, Bias::Left);
|
||||
|
||||
snapshot.anchor_before(start)..buffer_position
|
||||
}
|
||||
|
||||
const fn completion_type_score(completion_type: CompletionItemType) -> usize {
|
||||
match completion_type {
|
||||
CompletionItemType::Field | CompletionItemType::Property => 0,
|
||||
@@ -744,6 +757,8 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
cx.background_executor().spawn(async move {
|
||||
let completions = completion_task.await?;
|
||||
|
||||
let buffer_text = snapshot.text();
|
||||
|
||||
let completions = completions
|
||||
.into_iter()
|
||||
.map(|completion| {
|
||||
@@ -753,26 +768,14 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
.as_ref()
|
||||
.unwrap_or(&completion.label)
|
||||
.to_owned();
|
||||
let buffer_text = snapshot.text();
|
||||
let buffer_bytes = buffer_text.as_bytes();
|
||||
let new_bytes = new_text.as_bytes();
|
||||
|
||||
let mut prefix_len = 0;
|
||||
for i in (0..new_bytes.len()).rev() {
|
||||
if buffer_bytes.ends_with(&new_bytes[0..i]) {
|
||||
prefix_len = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let buffer_offset = buffer_position.to_offset(&snapshot);
|
||||
let start = buffer_offset - prefix_len;
|
||||
let start = snapshot.clip_offset(start, Bias::Left);
|
||||
let start = snapshot.anchor_before(start);
|
||||
let replace_range = start..buffer_position;
|
||||
|
||||
project::Completion {
|
||||
replace_range,
|
||||
replace_range: Self::replace_range_for_completion(
|
||||
&buffer_text,
|
||||
buffer_position,
|
||||
new_text.as_bytes(),
|
||||
&snapshot,
|
||||
),
|
||||
new_text,
|
||||
label: CodeLabel {
|
||||
filter_range: 0..completion.label.len(),
|
||||
@@ -944,3 +947,64 @@ fn color_fetcher(color: ansi::Color) -> fn(&Theme) -> Hsla {
|
||||
};
|
||||
color_fetcher
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tests::init_test;
|
||||
use editor::test::editor_test_context::EditorTestContext;
|
||||
use gpui::TestAppContext;
|
||||
use language::Point;
|
||||
|
||||
#[track_caller]
|
||||
fn assert_completion_range(
|
||||
input: &str,
|
||||
expect: &str,
|
||||
replacement: &str,
|
||||
cx: &mut EditorTestContext,
|
||||
) {
|
||||
cx.set_state(input);
|
||||
|
||||
let buffer_position =
|
||||
cx.editor(|editor, _, cx| editor.selections.newest::<Point>(cx).start);
|
||||
|
||||
let snapshot = &cx.buffer_snapshot();
|
||||
|
||||
let replace_range = ConsoleQueryBarCompletionProvider::replace_range_for_completion(
|
||||
&cx.buffer_text(),
|
||||
snapshot.anchor_before(buffer_position),
|
||||
replacement.as_bytes(),
|
||||
&snapshot,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
editor.edit(
|
||||
vec![(
|
||||
snapshot.offset_for_anchor(&replace_range.start)
|
||||
..snapshot.offset_for_anchor(&replace_range.end),
|
||||
replacement,
|
||||
)],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
pretty_assertions::assert_eq!(expect, cx.display_text());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_determine_completion_replace_range(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
assert_completion_range("resˇ", "result", "result", &mut cx);
|
||||
assert_completion_range("print(resˇ)", "print(result)", "result", &mut cx);
|
||||
assert_completion_range("$author->nˇ", "$author->name", "$author->name", &mut cx);
|
||||
assert_completion_range(
|
||||
"$author->books[ˇ",
|
||||
"$author->books[0]",
|
||||
"$author->books[0]",
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
963
crates/debugger_ui/src/session/running/memory_view.rs
Normal file
@@ -0,0 +1,963 @@
|
||||
use std::{
|
||||
cell::LazyCell,
|
||||
fmt::Write,
|
||||
ops::RangeInclusive,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{
|
||||
Action, AppContext, DismissEvent, Empty, Entity, FocusHandle, Focusable, MouseButton,
|
||||
MouseMoveEvent, Point, ScrollStrategy, ScrollWheelEvent, Stateful, Subscription, Task,
|
||||
TextStyle, UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, bounds,
|
||||
deferred, point, size, uniform_list,
|
||||
};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session::Session};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, AnyElement, App, Color, Context, ContextMenu, Div, Divider, DropdownMenu, Element,
|
||||
FluentBuilder, Icon, IconName, InteractiveElement, IntoElement, Label, LabelCommon,
|
||||
ParentElement, Pixels, PopoverMenuHandle, Render, Scrollbar, ScrollbarState, SharedString,
|
||||
StatefulInteractiveElement, Styled, TextSize, Tooltip, Window, div, h_flex, px, v_flex,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{ToggleDataBreakpoint, session::running::stack_frame_list::StackFrameList};
|
||||
|
||||
actions!(debugger, [GoToSelectedAddress]);
|
||||
|
||||
pub(crate) struct MemoryView {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
scroll_state: ScrollbarState,
|
||||
show_scrollbar: bool,
|
||||
stack_frame_list: WeakEntity<StackFrameList>,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
focus_handle: FocusHandle,
|
||||
view_state: ViewState,
|
||||
query_editor: Entity<Editor>,
|
||||
session: Entity<Session>,
|
||||
width_picker_handle: PopoverMenuHandle<ContextMenu>,
|
||||
is_writing_memory: bool,
|
||||
open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
}
|
||||
|
||||
impl Focusable for MemoryView {
|
||||
fn focus_handle(&self, _: &ui::App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
struct Drag {
|
||||
start_address: u64,
|
||||
end_address: u64,
|
||||
}
|
||||
|
||||
impl Drag {
|
||||
fn contains(&self, address: u64) -> bool {
|
||||
let range = self.memory_range();
|
||||
range.contains(&address)
|
||||
}
|
||||
|
||||
fn memory_range(&self) -> RangeInclusive<u64> {
|
||||
if self.start_address < self.end_address {
|
||||
self.start_address..=self.end_address
|
||||
} else {
|
||||
self.end_address..=self.start_address
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
enum SelectedMemoryRange {
|
||||
DragUnderway(Drag),
|
||||
DragComplete(Drag),
|
||||
}
|
||||
|
||||
impl SelectedMemoryRange {
|
||||
fn contains(&self, address: u64) -> bool {
|
||||
match self {
|
||||
SelectedMemoryRange::DragUnderway(drag) => drag.contains(address),
|
||||
SelectedMemoryRange::DragComplete(drag) => drag.contains(address),
|
||||
}
|
||||
}
|
||||
fn is_dragging(&self) -> bool {
|
||||
matches!(self, SelectedMemoryRange::DragUnderway(_))
|
||||
}
|
||||
fn drag(&self) -> &Drag {
|
||||
match self {
|
||||
SelectedMemoryRange::DragUnderway(drag) => drag,
|
||||
SelectedMemoryRange::DragComplete(drag) => drag,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ViewState {
|
||||
/// Uppermost row index
|
||||
base_row: u64,
|
||||
/// How many cells per row do we have?
|
||||
line_width: ViewWidth,
|
||||
selection: Option<SelectedMemoryRange>,
|
||||
}
|
||||
|
||||
impl ViewState {
|
||||
fn new(base_row: u64, line_width: ViewWidth) -> Self {
|
||||
Self {
|
||||
base_row,
|
||||
line_width,
|
||||
selection: None,
|
||||
}
|
||||
}
|
||||
fn row_count(&self) -> u64 {
|
||||
// This was picked fully arbitrarily. There's no incentive for us to care about page sizes other than the fact that it seems to be a good
|
||||
// middle ground for data size.
|
||||
const PAGE_SIZE: u64 = 4096;
|
||||
PAGE_SIZE / self.line_width.width as u64
|
||||
}
|
||||
fn schedule_scroll_down(&mut self) {
|
||||
self.base_row = self.base_row.saturating_add(1)
|
||||
}
|
||||
fn schedule_scroll_up(&mut self) {
|
||||
self.base_row = self.base_row.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
static HEX_BYTES_MEMOIZED: LazyLock<[SharedString; 256]> =
|
||||
LazyLock::new(|| std::array::from_fn(|byte| SharedString::from(format!("{byte:02X}"))));
|
||||
static UNKNOWN_BYTE: SharedString = SharedString::new_static("??");
|
||||
impl MemoryView {
|
||||
pub(crate) fn new(
|
||||
session: Entity<Session>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
stack_frame_list: WeakEntity<StackFrameList>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let view_state = ViewState::new(0, WIDTHS[4].clone());
|
||||
let scroll_handle = UniformListScrollHandle::default();
|
||||
|
||||
let query_editor = cx.new(|cx| Editor::single_line(window, cx));
|
||||
|
||||
let scroll_state = ScrollbarState::new(scroll_handle.clone());
|
||||
let mut this = Self {
|
||||
workspace,
|
||||
scroll_state,
|
||||
scroll_handle,
|
||||
stack_frame_list,
|
||||
show_scrollbar: false,
|
||||
hide_scrollbar_task: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
view_state,
|
||||
query_editor,
|
||||
session,
|
||||
width_picker_handle: Default::default(),
|
||||
is_writing_memory: true,
|
||||
open_context_menu: None,
|
||||
};
|
||||
this.change_query_bar_mode(false, window, cx);
|
||||
this
|
||||
}
|
||||
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
|
||||
cx.background_executor()
|
||||
.timer(SCROLLBAR_SHOW_INTERVAL)
|
||||
.await;
|
||||
panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.show_scrollbar = false;
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
||||
if !(self.show_scrollbar || self.scroll_state.is_dragging()) {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
div()
|
||||
.occlude()
|
||||
.id("memory-view-vertical-scrollbar")
|
||||
.on_mouse_move(cx.listener(|this, evt, _, cx| {
|
||||
this.handle_drag(evt);
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|_, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.h_full()
|
||||
.absolute()
|
||||
.right_1()
|
||||
.top_1()
|
||||
.bottom_0()
|
||||
.w(px(12.))
|
||||
.cursor_default()
|
||||
.children(Scrollbar::vertical(self.scroll_state.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_memory(&self, cx: &mut Context<Self>) -> UniformList {
|
||||
let weak = cx.weak_entity();
|
||||
let session = self.session.clone();
|
||||
let view_state = self.view_state.clone();
|
||||
uniform_list(
|
||||
"debugger-memory-view",
|
||||
self.view_state.row_count() as usize,
|
||||
move |range, _, cx| {
|
||||
let mut line_buffer = Vec::with_capacity(view_state.line_width.width as usize);
|
||||
let memory_start =
|
||||
(view_state.base_row + range.start as u64) * view_state.line_width.width as u64;
|
||||
let memory_end = (view_state.base_row + range.end as u64)
|
||||
* view_state.line_width.width as u64
|
||||
- 1;
|
||||
let mut memory = session.update(cx, |this, cx| {
|
||||
this.read_memory(memory_start..=memory_end, cx)
|
||||
});
|
||||
let mut rows = Vec::with_capacity(range.end - range.start);
|
||||
for ix in range {
|
||||
line_buffer.extend((&mut memory).take(view_state.line_width.width as usize));
|
||||
rows.push(render_single_memory_view_line(
|
||||
&line_buffer,
|
||||
ix as u64,
|
||||
weak.clone(),
|
||||
cx,
|
||||
));
|
||||
line_buffer.clear();
|
||||
}
|
||||
rows
|
||||
},
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| {
|
||||
let delta = evt.delta.pixel_delta(window.line_height());
|
||||
let scroll_handle = this.scroll_state.scroll_handle();
|
||||
let size = scroll_handle.content_size();
|
||||
let viewport = scroll_handle.viewport();
|
||||
let current_offset = scroll_handle.offset();
|
||||
let first_entry_offset_boundary = size.height / this.view_state.row_count() as f32;
|
||||
let last_entry_offset_boundary = size.height - first_entry_offset_boundary;
|
||||
if first_entry_offset_boundary + viewport.size.height > current_offset.y.abs() {
|
||||
// The topmost entry is visible, hence if we're scrolling up, we need to load extra lines.
|
||||
this.view_state.schedule_scroll_up();
|
||||
} else if last_entry_offset_boundary < current_offset.y.abs() + viewport.size.height {
|
||||
this.view_state.schedule_scroll_down();
|
||||
}
|
||||
scroll_handle.set_offset(current_offset + point(px(0.), delta.y));
|
||||
}))
|
||||
}
|
||||
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
EditorElement::new(
|
||||
&self.query_editor,
|
||||
Self::editor_style(&self.query_editor, cx),
|
||||
)
|
||||
}
|
||||
pub(super) fn go_to_memory_reference(
|
||||
&mut self,
|
||||
memory_reference: &str,
|
||||
evaluate_name: Option<&str>,
|
||||
stack_frame_id: Option<u64>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
use parse_int::parse;
|
||||
let Ok(as_address) = parse::<u64>(&memory_reference) else {
|
||||
return;
|
||||
};
|
||||
let access_size = evaluate_name
|
||||
.map(|typ| {
|
||||
self.session.update(cx, |this, cx| {
|
||||
this.data_access_size(stack_frame_id, typ, cx)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| Task::ready(None));
|
||||
cx.spawn(async move |this, cx| {
|
||||
let access_size = access_size.await.unwrap_or(1);
|
||||
this.update(cx, |this, cx| {
|
||||
this.view_state.selection = Some(SelectedMemoryRange::DragComplete(Drag {
|
||||
start_address: as_address,
|
||||
end_address: as_address + access_size - 1,
|
||||
}));
|
||||
this.jump_to_address(as_address, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_drag(&mut self, evt: &MouseMoveEvent) {
|
||||
if !evt.dragging() {
|
||||
return;
|
||||
}
|
||||
if !self.scroll_state.is_dragging()
|
||||
&& !self
|
||||
.view_state
|
||||
.selection
|
||||
.as_ref()
|
||||
.is_some_and(|selection| selection.is_dragging())
|
||||
{
|
||||
return;
|
||||
}
|
||||
let row_count = self.view_state.row_count();
|
||||
debug_assert!(row_count > 1);
|
||||
let scroll_handle = self.scroll_state.scroll_handle();
|
||||
let viewport = scroll_handle.viewport();
|
||||
let (top_area, bottom_area) = {
|
||||
let size = size(viewport.size.width, viewport.size.height / 10.);
|
||||
(
|
||||
bounds(viewport.origin, size),
|
||||
bounds(
|
||||
point(viewport.origin.x, viewport.origin.y + size.height * 2.),
|
||||
size,
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
if bottom_area.contains(&evt.position) {
|
||||
//ix == row_count - 1 {
|
||||
self.view_state.schedule_scroll_down();
|
||||
} else if top_area.contains(&evt.position) {
|
||||
self.view_state.schedule_scroll_up();
|
||||
}
|
||||
}
|
||||
|
||||
fn editor_style(editor: &Entity<Editor>, cx: &Context<Self>) -> EditorStyle {
|
||||
let is_read_only = editor.read(cx).read_only(cx);
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let theme = cx.theme();
|
||||
let text_style = TextStyle {
|
||||
color: if is_read_only {
|
||||
theme.colors().text_muted
|
||||
} else {
|
||||
theme.colors().text
|
||||
},
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: TextSize::Small.rems(cx).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
EditorStyle {
|
||||
background: theme.colors().editor_background,
|
||||
local_player: theme.players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_width_picker(&self, window: &mut Window, cx: &mut Context<Self>) -> DropdownMenu {
|
||||
let weak = cx.weak_entity();
|
||||
let selected_width = self.view_state.line_width.clone();
|
||||
DropdownMenu::new(
|
||||
"memory-view-width-picker",
|
||||
selected_width.label.clone(),
|
||||
ContextMenu::build(window, cx, |mut this, window, cx| {
|
||||
for width in &WIDTHS {
|
||||
let weak = weak.clone();
|
||||
let width = width.clone();
|
||||
this = this.entry(width.label.clone(), None, move |_, cx| {
|
||||
_ = weak.update(cx, |this, _| {
|
||||
// Convert base ix between 2 line widths to keep the shown memory address roughly the same.
|
||||
// All widths are powers of 2, so the conversion should be lossless.
|
||||
match this.view_state.line_width.width.cmp(&width.width) {
|
||||
std::cmp::Ordering::Less => {
|
||||
// We're converting up.
|
||||
let shift = width.width.trailing_zeros()
|
||||
- this.view_state.line_width.width.trailing_zeros();
|
||||
this.view_state.base_row >>= shift;
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
// We're converting down.
|
||||
let shift = this.view_state.line_width.width.trailing_zeros()
|
||||
- width.width.trailing_zeros();
|
||||
this.view_state.base_row <<= shift;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
this.view_state.line_width = width.clone();
|
||||
});
|
||||
});
|
||||
}
|
||||
if let Some(ix) = WIDTHS
|
||||
.iter()
|
||||
.position(|width| width.width == selected_width.width)
|
||||
{
|
||||
for _ in 0..=ix {
|
||||
this.select_next(&Default::default(), window, cx);
|
||||
}
|
||||
}
|
||||
this
|
||||
}),
|
||||
)
|
||||
.handle(self.width_picker_handle.clone())
|
||||
}
|
||||
|
||||
fn page_down(&mut self, _: &menu::SelectLast, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.view_state.base_row = self
|
||||
.view_state
|
||||
.base_row
|
||||
.overflowing_add(self.view_state.row_count())
|
||||
.0;
|
||||
cx.notify();
|
||||
}
|
||||
fn page_up(&mut self, _: &menu::SelectFirst, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.view_state.base_row = self
|
||||
.view_state
|
||||
.base_row
|
||||
.overflowing_sub(self.view_state.row_count())
|
||||
.0;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn change_query_bar_mode(
|
||||
&mut self,
|
||||
is_writing_memory: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if is_writing_memory == self.is_writing_memory {
|
||||
return;
|
||||
}
|
||||
if !self.is_writing_memory {
|
||||
self.query_editor.update(cx, |this, cx| {
|
||||
this.clear(window, cx);
|
||||
this.set_placeholder_text("Write to Selected Memory Range", cx);
|
||||
});
|
||||
self.is_writing_memory = true;
|
||||
self.query_editor.focus_handle(cx).focus(window);
|
||||
} else {
|
||||
self.query_editor.update(cx, |this, cx| {
|
||||
this.clear(window, cx);
|
||||
this.set_placeholder_text("Go to Memory Address / Expression", cx);
|
||||
});
|
||||
self.is_writing_memory = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_data_breakpoint(
|
||||
&mut self,
|
||||
_: &crate::ToggleDataBreakpoint,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(SelectedMemoryRange::DragComplete(selection)) = self.view_state.selection.clone()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let range = selection.memory_range();
|
||||
let context = Arc::new(DataBreakpointContext::Address {
|
||||
address: range.start().to_string(),
|
||||
bytes: Some(*range.end() - *range.start()),
|
||||
});
|
||||
|
||||
self.session.update(cx, |this, cx| {
|
||||
let data_breakpoint_info = this.data_breakpoint_info(context.clone(), None, cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
if let Some(info) = data_breakpoint_info.await {
|
||||
let Some(data_id) = info.data_id.clone() else {
|
||||
return;
|
||||
};
|
||||
_ = this.update(cx, |this, cx| {
|
||||
this.create_data_breakpoint(
|
||||
context,
|
||||
data_id.clone(),
|
||||
dap::DataBreakpoint {
|
||||
data_id,
|
||||
access_type: None,
|
||||
condition: None,
|
||||
hit_condition: None,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(SelectedMemoryRange::DragComplete(drag)) = &self.view_state.selection {
|
||||
// Go into memory writing mode.
|
||||
if !self.is_writing_memory {
|
||||
let should_return = self.session.update(cx, |session, cx| {
|
||||
if !session
|
||||
.capabilities()
|
||||
.supports_write_memory_request
|
||||
.unwrap_or_default()
|
||||
{
|
||||
let adapter_name = session.adapter();
|
||||
// We cannot write memory with this adapter.
|
||||
_ = self.workspace.update(cx, |this, cx| {
|
||||
this.toggle_status_toast(
|
||||
StatusToast::new(format!(
|
||||
"Debug Adapter `{adapter_name}` does not support writing to memory"
|
||||
), cx, |this, cx| {
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
_ = this.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent)
|
||||
});
|
||||
}).detach();
|
||||
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if should_return {
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_query_bar_mode(true, window, cx);
|
||||
} else if self.query_editor.focus_handle(cx).is_focused(window) {
|
||||
let mut text = self.query_editor.read(cx).text(cx);
|
||||
if text.chars().any(|c| !c.is_ascii_hexdigit()) {
|
||||
// Interpret this text as a string and oh-so-conveniently convert it.
|
||||
text = text.bytes().map(|byte| format!("{:02x}", byte)).collect();
|
||||
}
|
||||
self.session.update(cx, |this, cx| {
|
||||
let range = drag.memory_range();
|
||||
|
||||
if let Ok(as_hex) = hex::decode(text) {
|
||||
this.write_memory(*range.start(), &as_hex, cx);
|
||||
}
|
||||
});
|
||||
self.change_query_bar_mode(false, window, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
// Just change the currently viewed address.
|
||||
if !self.query_editor.focus_handle(cx).is_focused(window) {
|
||||
return;
|
||||
}
|
||||
self.jump_to_query_bar_address(cx);
|
||||
}
|
||||
|
||||
fn jump_to_query_bar_address(&mut self, cx: &mut Context<Self>) {
|
||||
use parse_int::parse;
|
||||
let text = self.query_editor.read(cx).text(cx);
|
||||
|
||||
let Ok(as_address) = parse::<u64>(&text) else {
|
||||
return self.jump_to_expression(text, cx);
|
||||
};
|
||||
self.jump_to_address(as_address, cx);
|
||||
}
|
||||
|
||||
fn jump_to_address(&mut self, address: u64, cx: &mut Context<Self>) {
|
||||
self.view_state.base_row = (address & !0xfff) / self.view_state.line_width.width as u64;
|
||||
let line_ix = (address & 0xfff) / self.view_state.line_width.width as u64;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(line_ix as usize, ScrollStrategy::Center);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn jump_to_expression(&mut self, expr: String, cx: &mut Context<Self>) {
|
||||
let Ok(selected_frame) = self
|
||||
.stack_frame_list
|
||||
.update(cx, |this, _| this.opened_stack_frame_id())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let reference = self.session.update(cx, |this, cx| {
|
||||
this.memory_reference_of_expr(selected_frame, expr, cx)
|
||||
});
|
||||
cx.spawn(async move |this, cx| {
|
||||
if let Some(reference) = reference.await {
|
||||
_ = this.update(cx, |this, cx| {
|
||||
let Ok(address) = parse_int::parse::<u64>(&reference) else {
|
||||
return;
|
||||
};
|
||||
this.jump_to_address(address, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.view_state.selection = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Jump to memory pointed to by selected memory range.
|
||||
fn go_to_address(
|
||||
&mut self,
|
||||
_: &GoToSelectedAddress,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(SelectedMemoryRange::DragComplete(drag)) = self.view_state.selection.clone()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let range = drag.memory_range();
|
||||
let Some(memory): Option<Vec<u8>> = self.session.update(cx, |this, cx| {
|
||||
this.read_memory(range, cx).map(|cell| cell.0).collect()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
if memory.len() > 8 {
|
||||
return;
|
||||
}
|
||||
let zeros_to_write = 8 - memory.len();
|
||||
let mut acc = String::from("0x");
|
||||
acc.extend(std::iter::repeat("00").take(zeros_to_write));
|
||||
let as_query = memory.into_iter().rev().fold(acc, |mut acc, byte| {
|
||||
_ = write!(&mut acc, "{:02x}", byte);
|
||||
acc
|
||||
});
|
||||
self.query_editor.update(cx, |this, cx| {
|
||||
this.set_text(as_query, window, cx);
|
||||
});
|
||||
self.jump_to_query_bar_address(cx);
|
||||
}
|
||||
|
||||
fn deploy_memory_context_menu(
|
||||
&mut self,
|
||||
range: RangeInclusive<u64>,
|
||||
position: Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let session = self.session.clone();
|
||||
let context_menu = ContextMenu::build(window, cx, |menu, _, cx| {
|
||||
let range_too_large = range.end() - range.start() > std::mem::size_of::<u64>() as u64;
|
||||
let caps = session.read(cx).capabilities();
|
||||
let supports_data_breakpoints = caps.supports_data_breakpoints.unwrap_or_default()
|
||||
&& caps.supports_data_breakpoint_bytes.unwrap_or_default();
|
||||
let memory_unreadable = LazyCell::new(|| {
|
||||
session.update(cx, |this, cx| {
|
||||
this.read_memory(range.clone(), cx)
|
||||
.any(|cell| cell.0.is_none())
|
||||
})
|
||||
});
|
||||
|
||||
let mut menu = menu.action_disabled_when(
|
||||
range_too_large || *memory_unreadable,
|
||||
"Go To Selected Address",
|
||||
GoToSelectedAddress.boxed_clone(),
|
||||
);
|
||||
|
||||
if supports_data_breakpoints {
|
||||
menu = menu.action_disabled_when(
|
||||
*memory_unreadable,
|
||||
"Set Data Breakpoint",
|
||||
ToggleDataBreakpoint.boxed_clone(),
|
||||
);
|
||||
}
|
||||
menu.context(self.focus_handle.clone())
|
||||
});
|
||||
|
||||
cx.focus_view(&context_menu, window);
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
|this, _, _: &DismissEvent, window, cx| {
|
||||
if this.open_context_menu.as_ref().is_some_and(|context_menu| {
|
||||
context_menu.0.focus_handle(cx).contains_focused(window, cx)
|
||||
}) {
|
||||
cx.focus_self(window);
|
||||
}
|
||||
this.open_context_menu.take();
|
||||
cx.notify();
|
||||
},
|
||||
);
|
||||
|
||||
self.open_context_menu = Some((context_menu, position, subscription));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ViewWidth {
|
||||
width: u8,
|
||||
label: SharedString,
|
||||
}
|
||||
|
||||
impl ViewWidth {
|
||||
const fn new(width: u8, label: &'static str) -> Self {
|
||||
Self {
|
||||
width,
|
||||
label: SharedString::new_static(label),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static WIDTHS: [ViewWidth; 7] = [
|
||||
ViewWidth::new(1, "1 byte"),
|
||||
ViewWidth::new(2, "2 bytes"),
|
||||
ViewWidth::new(4, "4 bytes"),
|
||||
ViewWidth::new(8, "8 bytes"),
|
||||
ViewWidth::new(16, "16 bytes"),
|
||||
ViewWidth::new(32, "32 bytes"),
|
||||
ViewWidth::new(64, "64 bytes"),
|
||||
];
|
||||
|
||||
fn render_single_memory_view_line(
|
||||
memory: &[MemoryCell],
|
||||
ix: u64,
|
||||
weak: gpui::WeakEntity<MemoryView>,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let Ok(view_state) = weak.update(cx, |this, _| this.view_state.clone()) else {
|
||||
return div().into_any();
|
||||
};
|
||||
let base_address = (view_state.base_row + ix) * view_state.line_width.width as u64;
|
||||
|
||||
h_flex()
|
||||
.id((
|
||||
"memory-view-row-full",
|
||||
ix * view_state.line_width.width as u64,
|
||||
))
|
||||
.size_full()
|
||||
.gap_x_2()
|
||||
.child(
|
||||
div()
|
||||
.child(
|
||||
Label::new(format!("{:016X}", base_address))
|
||||
.buffer_font(cx)
|
||||
.size(ui::LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.px_1()
|
||||
.border_r_1()
|
||||
.border_color(Color::Muted.color(cx)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.id((
|
||||
"memory-view-row-raw-memory",
|
||||
ix * view_state.line_width.width as u64,
|
||||
))
|
||||
.px_1()
|
||||
.children(memory.iter().enumerate().map(|(cell_ix, cell)| {
|
||||
let weak = weak.clone();
|
||||
div()
|
||||
.id(("memory-view-row-raw-memory-cell", cell_ix as u64))
|
||||
.px_0p5()
|
||||
.when_some(view_state.selection.as_ref(), |this, selection| {
|
||||
this.when(selection.contains(base_address + cell_ix as u64), |this| {
|
||||
let weak = weak.clone();
|
||||
|
||||
this.bg(Color::Accent.color(cx)).when(
|
||||
!selection.is_dragging(),
|
||||
|this| {
|
||||
let selection = selection.drag().memory_range();
|
||||
this.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
move |click, window, cx| {
|
||||
_ = weak.update(cx, |this, cx| {
|
||||
this.deploy_memory_context_menu(
|
||||
selection.clone(),
|
||||
click.position,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.stop_propagation();
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
.child(
|
||||
Label::new(
|
||||
cell.0
|
||||
.map(|val| HEX_BYTES_MEMOIZED[val as usize].clone())
|
||||
.unwrap_or_else(|| UNKNOWN_BYTE.clone()),
|
||||
)
|
||||
.buffer_font(cx)
|
||||
.when(cell.0.is_none(), |this| this.color(Color::Muted))
|
||||
.size(ui::LabelSize::Small),
|
||||
)
|
||||
.on_drag(
|
||||
Drag {
|
||||
start_address: base_address + cell_ix as u64,
|
||||
end_address: base_address + cell_ix as u64,
|
||||
},
|
||||
{
|
||||
let weak = weak.clone();
|
||||
move |drag, _, _, cx| {
|
||||
_ = weak.update(cx, |this, _| {
|
||||
this.view_state.selection =
|
||||
Some(SelectedMemoryRange::DragUnderway(drag.clone()));
|
||||
});
|
||||
|
||||
cx.new(|_| Empty)
|
||||
}
|
||||
},
|
||||
)
|
||||
.on_drop({
|
||||
let weak = weak.clone();
|
||||
move |drag: &Drag, _, cx| {
|
||||
_ = weak.update(cx, |this, _| {
|
||||
this.view_state.selection =
|
||||
Some(SelectedMemoryRange::DragComplete(Drag {
|
||||
start_address: drag.start_address,
|
||||
end_address: base_address + cell_ix as u64,
|
||||
}));
|
||||
});
|
||||
}
|
||||
})
|
||||
.drag_over(move |style, drag: &Drag, _, cx| {
|
||||
_ = weak.update(cx, |this, _| {
|
||||
this.view_state.selection =
|
||||
Some(SelectedMemoryRange::DragUnderway(Drag {
|
||||
start_address: drag.start_address,
|
||||
end_address: base_address + cell_ix as u64,
|
||||
}));
|
||||
});
|
||||
|
||||
style
|
||||
})
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.id((
|
||||
"memory-view-row-ascii-memory",
|
||||
ix * view_state.line_width.width as u64,
|
||||
))
|
||||
.h_full()
|
||||
.px_1()
|
||||
.mr_4()
|
||||
// .gap_x_1p5()
|
||||
.border_x_1()
|
||||
.border_color(Color::Muted.color(cx))
|
||||
.children(memory.iter().enumerate().map(|(ix, cell)| {
|
||||
let as_character = char::from(cell.0.unwrap_or(0));
|
||||
let as_visible = if as_character.is_ascii_graphic() {
|
||||
as_character
|
||||
} else {
|
||||
'·'
|
||||
};
|
||||
div()
|
||||
.px_0p5()
|
||||
.when_some(view_state.selection.as_ref(), |this, selection| {
|
||||
this.when(selection.contains(base_address + ix as u64), |this| {
|
||||
this.bg(Color::Accent.color(cx))
|
||||
})
|
||||
})
|
||||
.child(
|
||||
Label::new(format!("{as_visible}"))
|
||||
.buffer_font(cx)
|
||||
.when(cell.0.is_none(), |this| this.color(Color::Muted))
|
||||
.size(ui::LabelSize::Small),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
impl Render for MemoryView {
|
||||
fn render(
|
||||
&mut self,
|
||||
window: &mut ui::Window,
|
||||
cx: &mut ui::Context<Self>,
|
||||
) -> impl ui::IntoElement {
|
||||
let (icon, tooltip_text) = if self.is_writing_memory {
|
||||
(IconName::Pencil, "Edit memory at a selected address")
|
||||
} else {
|
||||
(
|
||||
IconName::LocationEdit,
|
||||
"Change address of currently viewed memory",
|
||||
)
|
||||
};
|
||||
v_flex()
|
||||
.id("Memory-view")
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::go_to_address))
|
||||
.p_1()
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::toggle_data_breakpoint))
|
||||
.on_action(cx.listener(Self::page_down))
|
||||
.on_action(cx.listener(Self::page_up))
|
||||
.size_full()
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_hover(cx.listener(|this, hovered, window, cx| {
|
||||
if *hovered {
|
||||
this.show_scrollbar = true;
|
||||
this.hide_scrollbar_task.take();
|
||||
cx.notify();
|
||||
} else if !this.focus_handle.contains_focused(window, cx) {
|
||||
this.hide_scrollbar(window, cx);
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.mb_0p5()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.gap_x_2()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.mb_0p5()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.when_else(
|
||||
self.query_editor
|
||||
.focus_handle(cx)
|
||||
.contains_focused(window, cx),
|
||||
|this| this.border_color(cx.theme().colors().border_focused),
|
||||
|this| this.border_color(cx.theme().colors().border_transparent),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("memory-view-editor-icon")
|
||||
.child(Icon::new(icon).size(ui::IconSize::XSmall))
|
||||
.tooltip(Tooltip::text(tooltip_text)),
|
||||
)
|
||||
.child(self.render_query_bar(cx)),
|
||||
)
|
||||
.child(self.render_width_picker(window, cx)),
|
||||
)
|
||||
.child(Divider::horizontal())
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.on_mouse_move(cx.listener(|this, evt: &MouseMoveEvent, _, _| {
|
||||
this.handle_drag(evt);
|
||||
}))
|
||||
.child(self.render_memory(cx).size_full())
|
||||
.children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
|
||||
deferred(
|
||||
anchored()
|
||||
.position(*position)
|
||||
.anchor(gpui::Corner::TopLeft)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
.with_priority(1)
|
||||
}))
|
||||
.children(self.render_vertical_scrollbar(cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::session::running::{RunningState, memory_view::MemoryView};
|
||||
|
||||
use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
|
||||
use dap::{
|
||||
ScopePresentationHint, StackFrameId, VariablePresentationHint, VariablePresentationHintKind,
|
||||
@@ -7,13 +9,17 @@ use editor::Editor;
|
||||
use gpui::{
|
||||
Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity,
|
||||
FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription,
|
||||
TextStyleRefinement, UniformListScrollHandle, actions, anchored, deferred, uniform_list,
|
||||
TextStyleRefinement, UniformListScrollHandle, WeakEntity, actions, anchored, deferred,
|
||||
uniform_list,
|
||||
};
|
||||
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
|
||||
use project::debugger::session::{Session, SessionEvent, Watcher};
|
||||
use project::debugger::{
|
||||
dap_command::DataBreakpointContext,
|
||||
session::{Session, SessionEvent, Watcher},
|
||||
};
|
||||
use std::{collections::HashMap, ops::Range, sync::Arc};
|
||||
use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*};
|
||||
use util::debug_panic;
|
||||
use util::{debug_panic, maybe};
|
||||
|
||||
actions!(
|
||||
variable_list,
|
||||
@@ -32,6 +38,8 @@ actions!(
|
||||
AddWatch,
|
||||
/// Removes the selected variable from the watch list.
|
||||
RemoveWatch,
|
||||
/// Jump to variable's memory location.
|
||||
GoToMemory,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -86,30 +94,30 @@ impl EntryPath {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum EntryKind {
|
||||
enum DapEntry {
|
||||
Watcher(Watcher),
|
||||
Variable(dap::Variable),
|
||||
Scope(dap::Scope),
|
||||
}
|
||||
|
||||
impl EntryKind {
|
||||
impl DapEntry {
|
||||
fn as_watcher(&self) -> Option<&Watcher> {
|
||||
match self {
|
||||
EntryKind::Watcher(watcher) => Some(watcher),
|
||||
DapEntry::Watcher(watcher) => Some(watcher),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_variable(&self) -> Option<&dap::Variable> {
|
||||
match self {
|
||||
EntryKind::Variable(dap) => Some(dap),
|
||||
DapEntry::Variable(dap) => Some(dap),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_scope(&self) -> Option<&dap::Scope> {
|
||||
match self {
|
||||
EntryKind::Scope(dap) => Some(dap),
|
||||
DapEntry::Scope(dap) => Some(dap),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -117,38 +125,38 @@ impl EntryKind {
|
||||
#[cfg(test)]
|
||||
fn name(&self) -> &str {
|
||||
match self {
|
||||
EntryKind::Watcher(watcher) => &watcher.expression,
|
||||
EntryKind::Variable(dap) => &dap.name,
|
||||
EntryKind::Scope(dap) => &dap.name,
|
||||
DapEntry::Watcher(watcher) => &watcher.expression,
|
||||
DapEntry::Variable(dap) => &dap.name,
|
||||
DapEntry::Scope(dap) => &dap.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct ListEntry {
|
||||
dap_kind: EntryKind,
|
||||
entry: DapEntry,
|
||||
path: EntryPath,
|
||||
}
|
||||
|
||||
impl ListEntry {
|
||||
fn as_watcher(&self) -> Option<&Watcher> {
|
||||
self.dap_kind.as_watcher()
|
||||
self.entry.as_watcher()
|
||||
}
|
||||
|
||||
fn as_variable(&self) -> Option<&dap::Variable> {
|
||||
self.dap_kind.as_variable()
|
||||
self.entry.as_variable()
|
||||
}
|
||||
|
||||
fn as_scope(&self) -> Option<&dap::Scope> {
|
||||
self.dap_kind.as_scope()
|
||||
self.entry.as_scope()
|
||||
}
|
||||
|
||||
fn item_id(&self) -> ElementId {
|
||||
use std::fmt::Write;
|
||||
let mut id = match &self.dap_kind {
|
||||
EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
|
||||
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
|
||||
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
|
||||
let mut id = match &self.entry {
|
||||
DapEntry::Watcher(watcher) => format!("watcher-{}", watcher.expression),
|
||||
DapEntry::Variable(dap) => format!("variable-{}", dap.name),
|
||||
DapEntry::Scope(dap) => format!("scope-{}", dap.name),
|
||||
};
|
||||
for name in self.path.indices.iter() {
|
||||
_ = write!(id, "-{}", name);
|
||||
@@ -158,10 +166,10 @@ impl ListEntry {
|
||||
|
||||
fn item_value_id(&self) -> ElementId {
|
||||
use std::fmt::Write;
|
||||
let mut id = match &self.dap_kind {
|
||||
EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
|
||||
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
|
||||
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
|
||||
let mut id = match &self.entry {
|
||||
DapEntry::Watcher(watcher) => format!("watcher-{}", watcher.expression),
|
||||
DapEntry::Variable(dap) => format!("variable-{}", dap.name),
|
||||
DapEntry::Scope(dap) => format!("scope-{}", dap.name),
|
||||
};
|
||||
for name in self.path.indices.iter() {
|
||||
_ = write!(id, "-{}", name);
|
||||
@@ -188,13 +196,17 @@ pub struct VariableList {
|
||||
focus_handle: FocusHandle,
|
||||
edited_path: Option<(EntryPath, Entity<Editor>)>,
|
||||
disabled: bool,
|
||||
memory_view: Entity<MemoryView>,
|
||||
weak_running: WeakEntity<RunningState>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl VariableList {
|
||||
pub fn new(
|
||||
pub(crate) fn new(
|
||||
session: Entity<Session>,
|
||||
stack_frame_list: Entity<StackFrameList>,
|
||||
memory_view: Entity<MemoryView>,
|
||||
weak_running: WeakEntity<RunningState>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -211,6 +223,7 @@ impl VariableList {
|
||||
SessionEvent::Variables | SessionEvent::Watchers => {
|
||||
this.build_entries(cx);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}),
|
||||
cx.on_focus_out(&focus_handle, window, |this, _, _, cx| {
|
||||
@@ -234,6 +247,8 @@ impl VariableList {
|
||||
edited_path: None,
|
||||
entries: Default::default(),
|
||||
entry_states: Default::default(),
|
||||
weak_running,
|
||||
memory_view,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +299,7 @@ impl VariableList {
|
||||
scope.variables_reference,
|
||||
scope.variables_reference,
|
||||
EntryPath::for_scope(&scope.name),
|
||||
EntryKind::Scope(scope),
|
||||
DapEntry::Scope(scope),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -298,7 +313,7 @@ impl VariableList {
|
||||
watcher.variables_reference,
|
||||
watcher.variables_reference,
|
||||
EntryPath::for_watcher(watcher.expression.clone()),
|
||||
EntryKind::Watcher(watcher.clone()),
|
||||
DapEntry::Watcher(watcher.clone()),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -309,9 +324,9 @@ impl VariableList {
|
||||
while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop()
|
||||
{
|
||||
match &dap_kind {
|
||||
EntryKind::Watcher(watcher) => path = path.with_child(watcher.expression.clone()),
|
||||
EntryKind::Variable(dap) => path = path.with_name(dap.name.clone().into()),
|
||||
EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()),
|
||||
DapEntry::Watcher(watcher) => path = path.with_child(watcher.expression.clone()),
|
||||
DapEntry::Variable(dap) => path = path.with_name(dap.name.clone().into()),
|
||||
DapEntry::Scope(dap) => path = path.with_child(dap.name.clone().into()),
|
||||
}
|
||||
|
||||
let var_state = self
|
||||
@@ -336,7 +351,7 @@ impl VariableList {
|
||||
});
|
||||
|
||||
entries.push(ListEntry {
|
||||
dap_kind,
|
||||
entry: dap_kind,
|
||||
path: path.clone(),
|
||||
});
|
||||
|
||||
@@ -349,7 +364,7 @@ impl VariableList {
|
||||
variables_reference,
|
||||
child.variables_reference,
|
||||
path.with_child(child.name.clone().into()),
|
||||
EntryKind::Variable(child),
|
||||
DapEntry::Variable(child),
|
||||
)
|
||||
}));
|
||||
}
|
||||
@@ -380,9 +395,9 @@ impl VariableList {
|
||||
pub fn completion_variables(&self, _cx: &mut Context<Self>) -> Vec<dap::Variable> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter_map(|entry| match &entry.dap_kind {
|
||||
EntryKind::Variable(dap) => Some(dap.clone()),
|
||||
EntryKind::Scope(_) | EntryKind::Watcher { .. } => None,
|
||||
.filter_map(|entry| match &entry.entry {
|
||||
DapEntry::Variable(dap) => Some(dap.clone()),
|
||||
DapEntry::Scope(_) | DapEntry::Watcher { .. } => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -400,12 +415,12 @@ impl VariableList {
|
||||
.get(ix)
|
||||
.and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?;
|
||||
|
||||
match &entry.dap_kind {
|
||||
EntryKind::Watcher { .. } => {
|
||||
match &entry.entry {
|
||||
DapEntry::Watcher { .. } => {
|
||||
Some(self.render_watcher(entry, *state, window, cx))
|
||||
}
|
||||
EntryKind::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
|
||||
EntryKind::Scope(_) => Some(self.render_scope(entry, *state, cx)),
|
||||
DapEntry::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
|
||||
DapEntry::Scope(_) => Some(self.render_scope(entry, *state, cx)),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -562,6 +577,51 @@ impl VariableList {
|
||||
}
|
||||
}
|
||||
|
||||
fn jump_to_variable_memory(
|
||||
&mut self,
|
||||
_: &GoToMemory,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
_ = maybe!({
|
||||
let selection = self.selection.as_ref()?;
|
||||
let entry = self.entries.iter().find(|entry| &entry.path == selection)?;
|
||||
let var = entry.entry.as_variable()?;
|
||||
let memory_reference = var.memory_reference.as_deref()?;
|
||||
|
||||
let sizeof_expr = if var.type_.as_ref().is_some_and(|t| {
|
||||
t.chars()
|
||||
.all(|c| c.is_whitespace() || c.is_alphabetic() || c == '*')
|
||||
}) {
|
||||
var.type_.as_deref()
|
||||
} else {
|
||||
var.evaluate_name
|
||||
.as_deref()
|
||||
.map(|name| name.strip_prefix("/nat ").unwrap_or_else(|| name))
|
||||
};
|
||||
self.memory_view.update(cx, |this, cx| {
|
||||
this.go_to_memory_reference(
|
||||
memory_reference,
|
||||
sizeof_expr,
|
||||
self.selected_stack_frame_id,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
let weak_panel = self.weak_running.clone();
|
||||
|
||||
window.defer(cx, move |window, cx| {
|
||||
_ = weak_panel.update(cx, |this, cx| {
|
||||
this.activate_item(
|
||||
crate::persistence::DebuggerPaneItem::MemoryView,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
fn deploy_list_entry_context_menu(
|
||||
&mut self,
|
||||
entry: ListEntry,
|
||||
@@ -569,49 +629,156 @@ impl VariableList {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let supports_set_variable = self
|
||||
.session
|
||||
.read(cx)
|
||||
.capabilities()
|
||||
.supports_set_variable
|
||||
.unwrap_or_default();
|
||||
let (supports_set_variable, supports_data_breakpoints, supports_go_to_memory) =
|
||||
self.session.read_with(cx, |session, _| {
|
||||
(
|
||||
session
|
||||
.capabilities()
|
||||
.supports_set_variable
|
||||
.unwrap_or_default(),
|
||||
session
|
||||
.capabilities()
|
||||
.supports_data_breakpoints
|
||||
.unwrap_or_default(),
|
||||
session
|
||||
.capabilities()
|
||||
.supports_read_memory_request
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
});
|
||||
let can_toggle_data_breakpoint = entry
|
||||
.as_variable()
|
||||
.filter(|_| supports_data_breakpoints)
|
||||
.and_then(|variable| {
|
||||
let variables_reference = self
|
||||
.entry_states
|
||||
.get(&entry.path)
|
||||
.map(|state| state.parent_reference)?;
|
||||
Some(self.session.update(cx, |session, cx| {
|
||||
session.data_breakpoint_info(
|
||||
Arc::new(DataBreakpointContext::Variable {
|
||||
variables_reference,
|
||||
name: variable.name.clone(),
|
||||
bytes: None,
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
});
|
||||
|
||||
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
|
||||
menu.when(entry.as_variable().is_some(), |menu| {
|
||||
menu.action("Copy Name", CopyVariableName.boxed_clone())
|
||||
.action("Copy Value", CopyVariableValue.boxed_clone())
|
||||
.when(supports_set_variable, |menu| {
|
||||
menu.action("Edit Value", EditVariable.boxed_clone())
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let can_toggle_data_breakpoint = if let Some(task) = can_toggle_data_breakpoint {
|
||||
task.await.is_some()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
cx.update(|window, cx| {
|
||||
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
|
||||
menu.when_some(entry.as_variable(), |menu, _| {
|
||||
menu.action("Copy Name", CopyVariableName.boxed_clone())
|
||||
.action("Copy Value", CopyVariableValue.boxed_clone())
|
||||
.when(supports_set_variable, |menu| {
|
||||
menu.action("Edit Value", EditVariable.boxed_clone())
|
||||
})
|
||||
.when(supports_go_to_memory, |menu| {
|
||||
menu.action("Go To Memory", GoToMemory.boxed_clone())
|
||||
})
|
||||
.action("Watch Variable", AddWatch.boxed_clone())
|
||||
.when(can_toggle_data_breakpoint, |menu| {
|
||||
menu.action(
|
||||
"Toggle Data Breakpoint",
|
||||
crate::ToggleDataBreakpoint.boxed_clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.action("Watch Variable", AddWatch.boxed_clone())
|
||||
})
|
||||
.when(entry.as_watcher().is_some(), |menu| {
|
||||
menu.action("Copy Name", CopyVariableName.boxed_clone())
|
||||
.action("Copy Value", CopyVariableValue.boxed_clone())
|
||||
.when(supports_set_variable, |menu| {
|
||||
menu.action("Edit Value", EditVariable.boxed_clone())
|
||||
.when(entry.as_watcher().is_some(), |menu| {
|
||||
menu.action("Copy Name", CopyVariableName.boxed_clone())
|
||||
.action("Copy Value", CopyVariableValue.boxed_clone())
|
||||
.when(supports_set_variable, |menu| {
|
||||
menu.action("Edit Value", EditVariable.boxed_clone())
|
||||
})
|
||||
.action("Remove Watch", RemoveWatch.boxed_clone())
|
||||
})
|
||||
.action("Remove Watch", RemoveWatch.boxed_clone())
|
||||
.context(focus_handle.clone())
|
||||
});
|
||||
|
||||
_ = this.update(cx, |this, cx| {
|
||||
cx.focus_view(&context_menu, window);
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
|this, _, _: &DismissEvent, window, cx| {
|
||||
if this.open_context_menu.as_ref().is_some_and(|context_menu| {
|
||||
context_menu.0.focus_handle(cx).contains_focused(window, cx)
|
||||
}) {
|
||||
cx.focus_self(window);
|
||||
}
|
||||
this.open_context_menu.take();
|
||||
cx.notify();
|
||||
},
|
||||
);
|
||||
|
||||
this.open_context_menu = Some((context_menu, position, subscription));
|
||||
});
|
||||
})
|
||||
.context(self.focus_handle.clone())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn toggle_data_breakpoint(
|
||||
&mut self,
|
||||
_: &crate::ToggleDataBreakpoint,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(entry) = self
|
||||
.selection
|
||||
.as_ref()
|
||||
.and_then(|selection| self.entries.iter().find(|entry| &entry.path == selection))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((name, var_ref)) = entry.as_variable().map(|var| &var.name).zip(
|
||||
self.entry_states
|
||||
.get(&entry.path)
|
||||
.map(|state| state.parent_reference),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let context = Arc::new(DataBreakpointContext::Variable {
|
||||
variables_reference: var_ref,
|
||||
name: name.clone(),
|
||||
bytes: None,
|
||||
});
|
||||
let data_breakpoint = self.session.update(cx, |session, cx| {
|
||||
session.data_breakpoint_info(context.clone(), None, cx)
|
||||
});
|
||||
|
||||
cx.focus_view(&context_menu, window);
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
|this, _, _: &DismissEvent, window, cx| {
|
||||
if this.open_context_menu.as_ref().is_some_and(|context_menu| {
|
||||
context_menu.0.focus_handle(cx).contains_focused(window, cx)
|
||||
}) {
|
||||
cx.focus_self(window);
|
||||
}
|
||||
this.open_context_menu.take();
|
||||
let session = self.session.downgrade();
|
||||
cx.spawn(async move |_, cx| {
|
||||
let Some(data_id) = data_breakpoint.await.and_then(|info| info.data_id) else {
|
||||
return;
|
||||
};
|
||||
_ = session.update(cx, |session, cx| {
|
||||
session.create_data_breakpoint(
|
||||
context,
|
||||
data_id.clone(),
|
||||
dap::DataBreakpoint {
|
||||
data_id,
|
||||
access_type: None,
|
||||
condition: None,
|
||||
hit_condition: None,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
},
|
||||
);
|
||||
|
||||
self.open_context_menu = Some((context_menu, position, subscription));
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn copy_variable_name(
|
||||
@@ -628,10 +795,10 @@ impl VariableList {
|
||||
return;
|
||||
};
|
||||
|
||||
let variable_name = match &entry.dap_kind {
|
||||
EntryKind::Variable(dap) => dap.name.clone(),
|
||||
EntryKind::Watcher(watcher) => watcher.expression.to_string(),
|
||||
EntryKind::Scope(_) => return,
|
||||
let variable_name = match &entry.entry {
|
||||
DapEntry::Variable(dap) => dap.name.clone(),
|
||||
DapEntry::Watcher(watcher) => watcher.expression.to_string(),
|
||||
DapEntry::Scope(_) => return,
|
||||
};
|
||||
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(variable_name));
|
||||
@@ -651,10 +818,10 @@ impl VariableList {
|
||||
return;
|
||||
};
|
||||
|
||||
let variable_value = match &entry.dap_kind {
|
||||
EntryKind::Variable(dap) => dap.value.clone(),
|
||||
EntryKind::Watcher(watcher) => watcher.value.to_string(),
|
||||
EntryKind::Scope(_) => return,
|
||||
let variable_value = match &entry.entry {
|
||||
DapEntry::Variable(dap) => dap.value.clone(),
|
||||
DapEntry::Watcher(watcher) => watcher.value.to_string(),
|
||||
DapEntry::Scope(_) => return,
|
||||
};
|
||||
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(variable_value));
|
||||
@@ -669,10 +836,10 @@ impl VariableList {
|
||||
return;
|
||||
};
|
||||
|
||||
let variable_value = match &entry.dap_kind {
|
||||
EntryKind::Watcher(watcher) => watcher.value.to_string(),
|
||||
EntryKind::Variable(variable) => variable.value.clone(),
|
||||
EntryKind::Scope(_) => return,
|
||||
let variable_value = match &entry.entry {
|
||||
DapEntry::Watcher(watcher) => watcher.value.to_string(),
|
||||
DapEntry::Variable(variable) => variable.value.clone(),
|
||||
DapEntry::Scope(_) => return,
|
||||
};
|
||||
|
||||
let editor = Self::create_variable_editor(&variable_value, window, cx);
|
||||
@@ -753,7 +920,7 @@ impl VariableList {
|
||||
"{}{} {}{}",
|
||||
INDENT.repeat(state.depth - 1),
|
||||
if state.is_expanded { "v" } else { ">" },
|
||||
entry.dap_kind.name(),
|
||||
entry.entry.name(),
|
||||
if self.selection.as_ref() == Some(&entry.path) {
|
||||
" <=== selected"
|
||||
} else {
|
||||
@@ -770,8 +937,8 @@ impl VariableList {
|
||||
pub(crate) fn scopes(&self) -> Vec<dap::Scope> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter_map(|entry| match &entry.dap_kind {
|
||||
EntryKind::Scope(scope) => Some(scope),
|
||||
.filter_map(|entry| match &entry.entry {
|
||||
DapEntry::Scope(scope) => Some(scope),
|
||||
_ => None,
|
||||
})
|
||||
.cloned()
|
||||
@@ -785,10 +952,10 @@ impl VariableList {
|
||||
let mut idx = 0;
|
||||
|
||||
for entry in self.entries.iter() {
|
||||
match &entry.dap_kind {
|
||||
EntryKind::Watcher { .. } => continue,
|
||||
EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()),
|
||||
EntryKind::Scope(scope) => {
|
||||
match &entry.entry {
|
||||
DapEntry::Watcher { .. } => continue,
|
||||
DapEntry::Variable(dap) => scopes[idx].1.push(dap.clone()),
|
||||
DapEntry::Scope(scope) => {
|
||||
if scopes.len() > 0 {
|
||||
idx += 1;
|
||||
}
|
||||
@@ -806,8 +973,8 @@ impl VariableList {
|
||||
pub(crate) fn variables(&self) -> Vec<dap::Variable> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter_map(|entry| match &entry.dap_kind {
|
||||
EntryKind::Variable(variable) => Some(variable),
|
||||
.filter_map(|entry| match &entry.entry {
|
||||
DapEntry::Variable(variable) => Some(variable),
|
||||
_ => None,
|
||||
})
|
||||
.cloned()
|
||||
@@ -1358,6 +1525,8 @@ impl Render for VariableList {
|
||||
.on_action(cx.listener(Self::edit_variable))
|
||||
.on_action(cx.listener(Self::add_watcher))
|
||||
.on_action(cx.listener(Self::remove_watcher))
|
||||
.on_action(cx.listener(Self::toggle_data_breakpoint))
|
||||
.on_action(cx.listener(Self::jump_to_variable_memory))
|
||||
.child(
|
||||
uniform_list(
|
||||
"variable-list",
|
||||
|
||||
@@ -427,7 +427,7 @@ async fn test_handle_start_debugging_request(
|
||||
let sessions = workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
|
||||
debug_panel.read(cx).sessions()
|
||||
debug_panel.read(cx).sessions().collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(sessions.len(), 1);
|
||||
@@ -451,7 +451,7 @@ async fn test_handle_start_debugging_request(
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.session(cx);
|
||||
let current_sessions = debug_panel.read(cx).sessions();
|
||||
let current_sessions = debug_panel.read(cx).sessions().collect::<Vec<_>>();
|
||||
assert_eq!(active_session, current_sessions[1].read(cx).session(cx));
|
||||
assert_eq!(
|
||||
active_session.read(cx).parent_session(),
|
||||
@@ -1796,7 +1796,7 @@ async fn test_debug_adapters_shutdown_on_app_quit(
|
||||
let panel = workspace.panel::<DebugPanel>(cx).unwrap();
|
||||
panel.read_with(cx, |panel, _| {
|
||||
assert!(
|
||||
!panel.sessions().is_empty(),
|
||||
panel.sessions().next().is_some(),
|
||||
"Debug session should be active"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2241,3 +2241,34 @@ func main() {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_trim_multi_line_inline_value(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
let variables = [("y", "hello\n world")];
|
||||
|
||||
let before = r#"
|
||||
fn main() {
|
||||
let y = "hello\n world";
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let after = r#"
|
||||
fn main() {
|
||||
let y: hello… = "hello\n world";
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
test_inline_values_util(
|
||||
&variables,
|
||||
&[],
|
||||
&before,
|
||||
&after,
|
||||
None,
|
||||
rust_lang(),
|
||||
executor,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -111,7 +111,6 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
});
|
||||
|
||||
running_state.update_in(cx, |this, window, cx| {
|
||||
this.ensure_pane_item(DebuggerPaneItem::Modules, window, cx);
|
||||
this.activate_item(DebuggerPaneItem::Modules, window, cx);
|
||||
cx.refresh_windows();
|
||||
});
|
||||
|
||||
@@ -144,7 +144,6 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
|
||||
style: BlockStyle::Flex,
|
||||
render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)),
|
||||
priority: 1,
|
||||
render_in_minimap: false,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -656,7 +656,6 @@ impl ProjectDiagnosticsEditor {
|
||||
block.render_block(editor.clone(), bcx)
|
||||
}),
|
||||
priority: 1,
|
||||
render_in_minimap: false,
|
||||
}
|
||||
});
|
||||
let block_ids = this.editor.update(cx, |editor, cx| {
|
||||
|
||||
@@ -410,6 +410,8 @@ actions!(
|
||||
ToggleFold,
|
||||
/// Toggles recursive folding at the current position.
|
||||
ToggleFoldRecursive,
|
||||
/// Toggles all folds in a buffer or all excerpts in multibuffer.
|
||||
ToggleFoldAll,
|
||||
/// Formats the entire document.
|
||||
Format,
|
||||
/// Formats only the selected text.
|
||||
|
||||
@@ -271,7 +271,6 @@ impl DisplayMap {
|
||||
height: Some(height),
|
||||
style,
|
||||
priority,
|
||||
render_in_minimap: true,
|
||||
}
|
||||
}),
|
||||
);
|
||||
@@ -1663,7 +1662,6 @@ pub mod tests {
|
||||
height: Some(height),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority,
|
||||
render_in_minimap: true,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -2029,7 +2027,6 @@ pub mod tests {
|
||||
style: BlockStyle::Sticky,
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
@@ -2227,7 +2224,6 @@ pub mod tests {
|
||||
style: BlockStyle::Sticky,
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
BlockProperties {
|
||||
placement: BlockPlacement::Below(
|
||||
@@ -2237,7 +2233,6 @@ pub mod tests {
|
||||
style: BlockStyle::Sticky,
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
@@ -2344,7 +2339,6 @@ pub mod tests {
|
||||
style: BlockStyle::Sticky,
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
}],
|
||||
cx,
|
||||
)
|
||||
@@ -2420,7 +2414,6 @@ pub mod tests {
|
||||
style: BlockStyle::Fixed,
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -193,7 +193,6 @@ pub struct CustomBlock {
|
||||
style: BlockStyle,
|
||||
render: Arc<Mutex<RenderBlock>>,
|
||||
priority: usize,
|
||||
pub(crate) render_in_minimap: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -205,7 +204,6 @@ pub struct BlockProperties<P> {
|
||||
pub style: BlockStyle,
|
||||
pub render: RenderBlock,
|
||||
pub priority: usize,
|
||||
pub render_in_minimap: bool,
|
||||
}
|
||||
|
||||
impl<P: Debug> Debug for BlockProperties<P> {
|
||||
@@ -1044,7 +1042,6 @@ impl BlockMapWriter<'_> {
|
||||
render: Arc::new(Mutex::new(block.render)),
|
||||
style: block.style,
|
||||
priority: block.priority,
|
||||
render_in_minimap: block.render_in_minimap,
|
||||
});
|
||||
self.0.custom_blocks.insert(block_ix, new_block.clone());
|
||||
self.0.custom_blocks_by_id.insert(id, new_block);
|
||||
@@ -1079,7 +1076,6 @@ impl BlockMapWriter<'_> {
|
||||
style: block.style,
|
||||
render: block.render.clone(),
|
||||
priority: block.priority,
|
||||
render_in_minimap: block.render_in_minimap,
|
||||
};
|
||||
let new_block = Arc::new(new_block);
|
||||
*block = new_block.clone();
|
||||
@@ -1976,7 +1972,6 @@ mod tests {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
@@ -1984,7 +1979,6 @@ mod tests {
|
||||
height: Some(2),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
@@ -1992,7 +1986,6 @@ mod tests {
|
||||
height: Some(3),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -2217,7 +2210,6 @@ mod tests {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
@@ -2225,7 +2217,6 @@ mod tests {
|
||||
height: Some(2),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
@@ -2233,7 +2224,6 @@ mod tests {
|
||||
height: Some(3),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -2322,7 +2312,6 @@ mod tests {
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
height: Some(1),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
@@ -2330,7 +2319,6 @@ mod tests {
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
height: Some(1),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -2370,7 +2358,6 @@ mod tests {
|
||||
height: Some(4),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
}])[0];
|
||||
|
||||
let blocks_snapshot = block_map.read(wraps_snapshot, Default::default());
|
||||
@@ -2424,7 +2411,6 @@ mod tests {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
@@ -2432,7 +2418,6 @@ mod tests {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
@@ -2440,7 +2425,6 @@ mod tests {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
]);
|
||||
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
|
||||
@@ -2455,7 +2439,6 @@ mod tests {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
@@ -2463,7 +2446,6 @@ mod tests {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
@@ -2471,7 +2453,6 @@ mod tests {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
]);
|
||||
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
|
||||
@@ -2571,7 +2552,6 @@ mod tests {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
@@ -2579,7 +2559,6 @@ mod tests {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
@@ -2587,7 +2566,6 @@ mod tests {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
]);
|
||||
let excerpt_blocks_3 = writer.insert(vec![
|
||||
@@ -2597,7 +2575,6 @@ mod tests {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
@@ -2605,7 +2582,6 @@ mod tests {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -2653,7 +2629,6 @@ mod tests {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
}]);
|
||||
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
|
||||
let blocks = blocks_snapshot
|
||||
@@ -3011,7 +2986,6 @@ mod tests {
|
||||
height: Some(height),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -3032,7 +3006,6 @@ mod tests {
|
||||
style: props.style,
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
}));
|
||||
|
||||
for (block_properties, block_id) in block_properties.iter().zip(block_ids) {
|
||||
@@ -3557,7 +3530,6 @@ mod tests {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
}])[0];
|
||||
|
||||
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
|
||||
|
||||
@@ -1795,6 +1795,7 @@ impl Editor {
|
||||
);
|
||||
|
||||
let full_mode = mode.is_full();
|
||||
let is_minimap = mode.is_minimap();
|
||||
let diagnostics_max_severity = if full_mode {
|
||||
EditorSettings::get_global(cx)
|
||||
.diagnostics_max_severity
|
||||
@@ -1855,13 +1856,19 @@ impl Editor {
|
||||
|
||||
let selections = SelectionsCollection::new(display_map.clone(), buffer.clone());
|
||||
|
||||
let blink_manager = cx.new(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
|
||||
let blink_manager = cx.new(|cx| {
|
||||
let mut blink_manager = BlinkManager::new(CURSOR_BLINK_INTERVAL, cx);
|
||||
if is_minimap {
|
||||
blink_manager.disable(cx);
|
||||
}
|
||||
blink_manager
|
||||
});
|
||||
|
||||
let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. })
|
||||
.then(|| language_settings::SoftWrap::None);
|
||||
|
||||
let mut project_subscriptions = Vec::new();
|
||||
if mode.is_full() {
|
||||
if full_mode {
|
||||
if let Some(project) = project.as_ref() {
|
||||
project_subscriptions.push(cx.subscribe_in(
|
||||
project,
|
||||
@@ -1972,18 +1979,23 @@ impl Editor {
|
||||
let inlay_hint_settings =
|
||||
inlay_hint_settings(selections.newest_anchor().head(), &buffer_snapshot, cx);
|
||||
let focus_handle = cx.focus_handle();
|
||||
cx.on_focus(&focus_handle, window, Self::handle_focus)
|
||||
.detach();
|
||||
cx.on_focus_in(&focus_handle, window, Self::handle_focus_in)
|
||||
.detach();
|
||||
cx.on_focus_out(&focus_handle, window, Self::handle_focus_out)
|
||||
.detach();
|
||||
cx.on_blur(&focus_handle, window, Self::handle_blur)
|
||||
.detach();
|
||||
cx.observe_pending_input(window, Self::observe_pending_input)
|
||||
.detach();
|
||||
if !is_minimap {
|
||||
cx.on_focus(&focus_handle, window, Self::handle_focus)
|
||||
.detach();
|
||||
cx.on_focus_in(&focus_handle, window, Self::handle_focus_in)
|
||||
.detach();
|
||||
cx.on_focus_out(&focus_handle, window, Self::handle_focus_out)
|
||||
.detach();
|
||||
cx.on_blur(&focus_handle, window, Self::handle_blur)
|
||||
.detach();
|
||||
cx.observe_pending_input(window, Self::observe_pending_input)
|
||||
.detach();
|
||||
}
|
||||
|
||||
let show_indent_guides = if matches!(mode, EditorMode::SingleLine { .. }) {
|
||||
let show_indent_guides = if matches!(
|
||||
mode,
|
||||
EditorMode::SingleLine { .. } | EditorMode::Minimap { .. }
|
||||
) {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
@@ -2049,10 +2061,10 @@ impl Editor {
|
||||
minimap_visibility: MinimapVisibility::for_mode(&mode, cx),
|
||||
offset_content: !matches!(mode, EditorMode::SingleLine { .. }),
|
||||
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
|
||||
show_gutter: mode.is_full(),
|
||||
show_line_numbers: None,
|
||||
show_gutter: full_mode,
|
||||
show_line_numbers: (!full_mode).then_some(false),
|
||||
use_relative_line_numbers: None,
|
||||
disable_expand_excerpt_buttons: false,
|
||||
disable_expand_excerpt_buttons: !full_mode,
|
||||
show_git_diff_gutter: None,
|
||||
show_code_actions: None,
|
||||
show_runnables: None,
|
||||
@@ -2086,7 +2098,7 @@ impl Editor {
|
||||
document_highlights_task: None,
|
||||
linked_editing_range_task: None,
|
||||
pending_rename: None,
|
||||
searchable: true,
|
||||
searchable: !is_minimap,
|
||||
cursor_shape: EditorSettings::get_global(cx)
|
||||
.cursor_shape
|
||||
.unwrap_or_default(),
|
||||
@@ -2094,9 +2106,9 @@ impl Editor {
|
||||
autoindent_mode: Some(AutoindentMode::EachLine),
|
||||
collapse_matches: false,
|
||||
workspace: None,
|
||||
input_enabled: true,
|
||||
use_modal_editing: mode.is_full(),
|
||||
read_only: mode.is_minimap(),
|
||||
input_enabled: !is_minimap,
|
||||
use_modal_editing: full_mode,
|
||||
read_only: is_minimap,
|
||||
use_autoclose: true,
|
||||
use_auto_surround: true,
|
||||
auto_replace_emoji_shortcode: false,
|
||||
@@ -2112,11 +2124,10 @@ impl Editor {
|
||||
edit_prediction_preview: EditPredictionPreview::Inactive {
|
||||
released_too_fast: false,
|
||||
},
|
||||
inline_diagnostics_enabled: mode.is_full(),
|
||||
diagnostics_enabled: mode.is_full(),
|
||||
inline_diagnostics_enabled: full_mode,
|
||||
diagnostics_enabled: full_mode,
|
||||
inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints),
|
||||
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
|
||||
|
||||
gutter_hovered: false,
|
||||
pixel_position_of_newest_cursor: None,
|
||||
last_bounds: None,
|
||||
@@ -2139,9 +2150,10 @@ impl Editor {
|
||||
show_git_blame_inline: false,
|
||||
show_selection_menu: None,
|
||||
show_git_blame_inline_delay_task: None,
|
||||
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
|
||||
git_blame_inline_enabled: full_mode
|
||||
&& ProjectSettings::get_global(cx).git.inline_blame_enabled(),
|
||||
render_diff_hunk_controls: Arc::new(render_diff_hunk_controls),
|
||||
serialize_dirty_buffers: !mode.is_minimap()
|
||||
serialize_dirty_buffers: !is_minimap
|
||||
&& ProjectSettings::get_global(cx)
|
||||
.session
|
||||
.restore_unsaved_buffers,
|
||||
@@ -2152,27 +2164,31 @@ impl Editor {
|
||||
breakpoint_store,
|
||||
gutter_breakpoint_indicator: (None, None),
|
||||
hovered_diff_hunk_row: None,
|
||||
_subscriptions: vec![
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
cx.subscribe_in(&buffer, window, Self::on_buffer_event),
|
||||
cx.observe_in(&display_map, window, Self::on_display_map_changed),
|
||||
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
|
||||
observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()),
|
||||
cx.observe_window_activation(window, |editor, window, cx| {
|
||||
let active = window.is_window_active();
|
||||
editor.blink_manager.update(cx, |blink_manager, cx| {
|
||||
if active {
|
||||
blink_manager.enable(cx);
|
||||
} else {
|
||||
blink_manager.disable(cx);
|
||||
}
|
||||
});
|
||||
if active {
|
||||
editor.show_mouse_cursor(cx);
|
||||
}
|
||||
}),
|
||||
],
|
||||
_subscriptions: (!is_minimap)
|
||||
.then(|| {
|
||||
vec![
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
cx.subscribe_in(&buffer, window, Self::on_buffer_event),
|
||||
cx.observe_in(&display_map, window, Self::on_display_map_changed),
|
||||
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
|
||||
observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()),
|
||||
cx.observe_window_activation(window, |editor, window, cx| {
|
||||
let active = window.is_window_active();
|
||||
editor.blink_manager.update(cx, |blink_manager, cx| {
|
||||
if active {
|
||||
blink_manager.enable(cx);
|
||||
} else {
|
||||
blink_manager.disable(cx);
|
||||
}
|
||||
});
|
||||
if active {
|
||||
editor.show_mouse_cursor(cx);
|
||||
}
|
||||
}),
|
||||
]
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
tasks_update_task: None,
|
||||
pull_diagnostics_task: Task::ready(()),
|
||||
colors: None,
|
||||
@@ -2203,6 +2219,11 @@ impl Editor {
|
||||
selection_drag_state: SelectionDragState::None,
|
||||
folding_newlines: Task::ready(()),
|
||||
};
|
||||
|
||||
if is_minimap {
|
||||
return editor;
|
||||
}
|
||||
|
||||
if let Some(breakpoints) = editor.breakpoint_store.as_ref() {
|
||||
editor
|
||||
._subscriptions
|
||||
@@ -2322,7 +2343,10 @@ impl Editor {
|
||||
editor.update_lsp_data(false, None, window, cx);
|
||||
}
|
||||
|
||||
editor.report_editor_event("Editor Opened", None, cx);
|
||||
if editor.mode.is_full() {
|
||||
editor.report_editor_event("Editor Opened", None, cx);
|
||||
}
|
||||
|
||||
editor
|
||||
}
|
||||
|
||||
@@ -10442,7 +10466,6 @@ impl Editor {
|
||||
cloned_prompt.clone().into_any_element()
|
||||
}),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
}];
|
||||
|
||||
let focus_handle = bp_prompt.focus_handle(cx);
|
||||
@@ -16138,7 +16161,6 @@ impl Editor {
|
||||
}
|
||||
}),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
}],
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
@@ -17072,6 +17094,46 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_fold_all(
|
||||
&mut self,
|
||||
_: &actions::ToggleFoldAll,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.buffer.read(cx).is_singleton() {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let has_folds = display_map
|
||||
.folds_in_range(0..display_map.buffer_snapshot.len())
|
||||
.next()
|
||||
.is_some();
|
||||
|
||||
if has_folds {
|
||||
self.unfold_all(&actions::UnfoldAll, window, cx);
|
||||
} else {
|
||||
self.fold_all(&actions::FoldAll, window, cx);
|
||||
}
|
||||
} else {
|
||||
let buffer_ids = self.buffer.read(cx).excerpt_buffer_ids();
|
||||
let should_unfold = buffer_ids
|
||||
.iter()
|
||||
.any(|buffer_id| self.is_buffer_folded(*buffer_id, cx));
|
||||
|
||||
self.toggle_fold_multiple_buffers = cx.spawn_in(window, async move |editor, cx| {
|
||||
editor
|
||||
.update_in(cx, |editor, _, cx| {
|
||||
for buffer_id in buffer_ids {
|
||||
if should_unfold {
|
||||
editor.unfold_buffer(buffer_id, cx);
|
||||
} else {
|
||||
editor.fold_buffer(buffer_id, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn fold_at_level(
|
||||
&mut self,
|
||||
fold_at: &FoldAtLevel,
|
||||
@@ -18001,7 +18063,7 @@ impl Editor {
|
||||
parent: cx.weak_entity(),
|
||||
},
|
||||
self.buffer.clone(),
|
||||
self.project.clone(),
|
||||
None,
|
||||
Some(self.display_map.clone()),
|
||||
window,
|
||||
cx,
|
||||
@@ -19655,8 +19717,9 @@ impl Editor {
|
||||
Anchor::in_buffer(excerpt_id, buffer_id, hint.position),
|
||||
hint.text(),
|
||||
);
|
||||
|
||||
new_inlays.push(inlay);
|
||||
if !inlay.text.chars().contains(&'\n') {
|
||||
new_inlays.push(inlay);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19884,14 +19947,12 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let new_severity = if self.diagnostics_enabled() {
|
||||
EditorSettings::get_global(cx)
|
||||
if self.diagnostics_enabled() {
|
||||
let new_severity = EditorSettings::get_global(cx)
|
||||
.diagnostics_max_severity
|
||||
.unwrap_or(DiagnosticSeverity::Hint)
|
||||
} else {
|
||||
DiagnosticSeverity::Off
|
||||
};
|
||||
self.set_max_diagnostics_severity(new_severity, cx);
|
||||
.unwrap_or(DiagnosticSeverity::Hint);
|
||||
self.set_max_diagnostics_severity(new_severity, cx);
|
||||
}
|
||||
self.tasks_update_task = Some(self.refresh_runnables(window, cx));
|
||||
self.update_edit_prediction_settings(cx);
|
||||
self.refresh_inline_completion(true, false, window, cx);
|
||||
|
||||
@@ -5081,7 +5081,6 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
|
||||
height: Some(1),
|
||||
render: Arc::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
}],
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
@@ -5124,7 +5123,6 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
|
||||
style: BlockStyle::Sticky,
|
||||
render: Arc::new(|_| gpui::div().into_any_element()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
}],
|
||||
None,
|
||||
cx,
|
||||
@@ -21465,7 +21463,7 @@ println!("5");
|
||||
.unwrap();
|
||||
pane_1
|
||||
.update_in(cx, |pane, window, cx| {
|
||||
pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
|
||||
pane.close_inactive_items(&CloseInactiveItems::default(), None, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -21501,7 +21499,7 @@ println!("5");
|
||||
.unwrap();
|
||||
pane_2
|
||||
.update_in(cx, |pane, window, cx| {
|
||||
pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
|
||||
pane.close_inactive_items(&CloseInactiveItems::default(), None, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
LineUp, MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
|
||||
PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase,
|
||||
SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint,
|
||||
ToggleFold,
|
||||
ToggleFold, ToggleFoldAll,
|
||||
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
|
||||
display_map::{
|
||||
Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
|
||||
@@ -416,6 +416,7 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::fold_recursive);
|
||||
register_action(editor, window, Editor::toggle_fold);
|
||||
register_action(editor, window, Editor::toggle_fold_recursive);
|
||||
register_action(editor, window, Editor::toggle_fold_all);
|
||||
register_action(editor, window, Editor::unfold_lines);
|
||||
register_action(editor, window, Editor::unfold_recursive);
|
||||
register_action(editor, window, Editor::unfold_all);
|
||||
@@ -2093,16 +2094,19 @@ impl EditorElement {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> HashMap<DisplayRow, AnyElement> {
|
||||
if self.editor.read(cx).mode().is_minimap() {
|
||||
return HashMap::default();
|
||||
}
|
||||
|
||||
let max_severity = match ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.inline
|
||||
.max_severity
|
||||
.unwrap_or_else(|| self.editor.read(cx).diagnostics_max_severity)
|
||||
.into_lsp()
|
||||
let max_severity = match self
|
||||
.editor
|
||||
.read(cx)
|
||||
.inline_diagnostics_enabled()
|
||||
.then(|| {
|
||||
ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.inline
|
||||
.max_severity
|
||||
.unwrap_or_else(|| self.editor.read(cx).diagnostics_max_severity)
|
||||
.into_lsp()
|
||||
})
|
||||
.flatten()
|
||||
{
|
||||
Some(max_severity) => max_severity,
|
||||
None => return HashMap::default(),
|
||||
@@ -2618,9 +2622,6 @@ impl EditorElement {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Vec<IndentGuideLayout>> {
|
||||
if self.editor.read(cx).mode().is_minimap() {
|
||||
return None;
|
||||
}
|
||||
let indent_guides = self.editor.update(cx, |editor, cx| {
|
||||
editor.indent_guides(visible_buffer_range, snapshot, cx)
|
||||
})?;
|
||||
@@ -3084,9 +3085,9 @@ impl EditorElement {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Arc<HashMap<MultiBufferRow, LineNumberLayout>> {
|
||||
let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| {
|
||||
EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode.is_full()
|
||||
});
|
||||
let include_line_numbers = snapshot
|
||||
.show_line_numbers
|
||||
.unwrap_or_else(|| EditorSettings::get_global(cx).gutter.line_numbers);
|
||||
if !include_line_numbers {
|
||||
return Arc::default();
|
||||
}
|
||||
@@ -3399,22 +3400,18 @@ impl EditorElement {
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.children(
|
||||
(!snapshot.mode.is_minimap() || custom.render_in_minimap).then(|| {
|
||||
custom.render(&mut BlockContext {
|
||||
window,
|
||||
app: cx,
|
||||
anchor_x,
|
||||
margins: editor_margins,
|
||||
line_height,
|
||||
em_width,
|
||||
block_id,
|
||||
selected,
|
||||
max_width: text_hitbox.size.width.max(*scroll_width),
|
||||
editor_style: &self.style,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(custom.render(&mut BlockContext {
|
||||
window,
|
||||
app: cx,
|
||||
anchor_x,
|
||||
margins: editor_margins,
|
||||
line_height,
|
||||
em_width,
|
||||
block_id,
|
||||
selected,
|
||||
max_width: text_hitbox.size.width.max(*scroll_width),
|
||||
editor_style: &self.style,
|
||||
}))
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -3620,24 +3617,37 @@ impl EditorElement {
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
Tooltip::with_meta_in(
|
||||
"Toggle Excerpt Fold",
|
||||
&ToggleFold,
|
||||
Some(&ToggleFold),
|
||||
"Alt+click to toggle all",
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |_, _, cx| {
|
||||
if is_folded {
|
||||
.on_click(move |event, window, cx| {
|
||||
if event.modifiers().alt {
|
||||
// Alt+click toggles all buffers
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.unfold_buffer(buffer_id, cx);
|
||||
editor.toggle_fold_all(
|
||||
&ToggleFoldAll,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.fold_buffer(buffer_id, cx);
|
||||
});
|
||||
// Regular click toggles single buffer
|
||||
if is_folded {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.unfold_buffer(buffer_id, cx);
|
||||
});
|
||||
} else {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.fold_buffer(buffer_id, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
@@ -6762,7 +6772,7 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
fn paint_mouse_listeners(&mut self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
|
||||
if self.editor.read(cx).mode.is_minimap() {
|
||||
if layout.mode.is_minimap() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7889,9 +7899,14 @@ impl Element for EditorElement {
|
||||
line_height: Some(self.style.text.line_height),
|
||||
..Default::default()
|
||||
};
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
window.set_view_id(self.editor.entity_id());
|
||||
window.set_focus_handle(&focus_handle, cx);
|
||||
|
||||
let is_minimap = self.editor.read(cx).mode.is_minimap();
|
||||
|
||||
if !is_minimap {
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
window.set_view_id(self.editor.entity_id());
|
||||
window.set_focus_handle(&focus_handle, cx);
|
||||
}
|
||||
|
||||
let rem_size = self.rem_size(cx);
|
||||
window.with_rem_size(rem_size, |window| {
|
||||
@@ -8035,23 +8050,25 @@ impl Element for EditorElement {
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Autoscrolling for both axes
|
||||
let mut autoscroll_request = None;
|
||||
let mut autoscroll_containing_element = false;
|
||||
let mut autoscroll_horizontally = false;
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
autoscroll_request = editor.autoscroll_request();
|
||||
autoscroll_containing_element =
|
||||
let (
|
||||
autoscroll_request,
|
||||
autoscroll_containing_element,
|
||||
needs_horizontal_autoscroll,
|
||||
) = self.editor.update(cx, |editor, cx| {
|
||||
let autoscroll_request = editor.autoscroll_request();
|
||||
let autoscroll_containing_element =
|
||||
autoscroll_request.is_some() || editor.has_pending_selection();
|
||||
// TODO: Is this horizontal or vertical?!
|
||||
autoscroll_horizontally = editor.autoscroll_vertically(
|
||||
bounds,
|
||||
line_height,
|
||||
max_scroll_top,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
snapshot = editor.snapshot(window, cx);
|
||||
|
||||
let (needs_horizontal_autoscroll, was_scrolled) = editor
|
||||
.autoscroll_vertically(bounds, line_height, max_scroll_top, window, cx);
|
||||
if was_scrolled.0 {
|
||||
snapshot = editor.snapshot(window, cx);
|
||||
}
|
||||
(
|
||||
autoscroll_request,
|
||||
autoscroll_containing_element,
|
||||
needs_horizontal_autoscroll,
|
||||
)
|
||||
});
|
||||
|
||||
let mut scroll_position = snapshot.scroll_position();
|
||||
@@ -8327,18 +8344,22 @@ impl Element for EditorElement {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let new_renrerer_widths = line_layouts
|
||||
.iter()
|
||||
.flat_map(|layout| &layout.fragments)
|
||||
.filter_map(|fragment| {
|
||||
if let LineFragment::Element { id, size, .. } = fragment {
|
||||
Some((*id, size.width))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if self.editor.update(cx, |editor, cx| {
|
||||
editor.update_renderer_widths(new_renrerer_widths, cx)
|
||||
let new_renderer_widths = (!is_minimap).then(|| {
|
||||
line_layouts
|
||||
.iter()
|
||||
.flat_map(|layout| &layout.fragments)
|
||||
.filter_map(|fragment| {
|
||||
if let LineFragment::Element { id, size, .. } = fragment {
|
||||
Some((*id, size.width))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
if new_renderer_widths.is_some_and(|new_renderer_widths| {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.update_renderer_widths(new_renderer_widths, cx)
|
||||
})
|
||||
}) {
|
||||
// If the fold widths have changed, we need to prepaint
|
||||
// the element again to account for any changes in
|
||||
@@ -8401,27 +8422,31 @@ impl Element for EditorElement {
|
||||
let sticky_header_excerpt_id =
|
||||
sticky_header_excerpt.as_ref().map(|top| top.excerpt.id);
|
||||
|
||||
let blocks = window.with_element_namespace("blocks", |window| {
|
||||
self.render_blocks(
|
||||
start_row..end_row,
|
||||
&snapshot,
|
||||
&hitbox,
|
||||
&text_hitbox,
|
||||
editor_width,
|
||||
&mut scroll_width,
|
||||
&editor_margins,
|
||||
em_width,
|
||||
gutter_dimensions.full_width(),
|
||||
line_height,
|
||||
&mut line_layouts,
|
||||
&local_selections,
|
||||
&selected_buffer_ids,
|
||||
is_row_soft_wrapped,
|
||||
sticky_header_excerpt_id,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let blocks = (!is_minimap)
|
||||
.then(|| {
|
||||
window.with_element_namespace("blocks", |window| {
|
||||
self.render_blocks(
|
||||
start_row..end_row,
|
||||
&snapshot,
|
||||
&hitbox,
|
||||
&text_hitbox,
|
||||
editor_width,
|
||||
&mut scroll_width,
|
||||
&editor_margins,
|
||||
em_width,
|
||||
gutter_dimensions.full_width(),
|
||||
line_height,
|
||||
&mut line_layouts,
|
||||
&local_selections,
|
||||
&selected_buffer_ids,
|
||||
is_row_soft_wrapped,
|
||||
sticky_header_excerpt_id,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| Ok((Vec::default(), HashMap::default())));
|
||||
let (mut blocks, row_block_types) = match blocks {
|
||||
Ok(blocks) => blocks,
|
||||
Err(resized_blocks) => {
|
||||
@@ -8460,10 +8485,12 @@ impl Element for EditorElement {
|
||||
);
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
|
||||
if editor.scroll_manager.clamp_scroll_left(scroll_max.x) {
|
||||
scroll_position.x = scroll_position.x.min(scroll_max.x);
|
||||
}
|
||||
|
||||
let autoscrolled = if autoscroll_horizontally {
|
||||
editor.autoscroll_horizontally(
|
||||
if needs_horizontal_autoscroll.0
|
||||
&& let Some(new_scroll_position) = editor.autoscroll_horizontally(
|
||||
start_row,
|
||||
editor_content_width,
|
||||
scroll_width,
|
||||
@@ -8472,13 +8499,8 @@ impl Element for EditorElement {
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if clamped || autoscrolled {
|
||||
snapshot = editor.snapshot(window, cx);
|
||||
scroll_position = snapshot.scroll_position();
|
||||
{
|
||||
scroll_position = new_scroll_position;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8593,7 +8615,9 @@ impl Element for EditorElement {
|
||||
}
|
||||
} else {
|
||||
log::error!(
|
||||
"bug: line_ix {} is out of bounds - row_infos.len(): {}, line_layouts.len(): {}, crease_trailers.len(): {}",
|
||||
"bug: line_ix {} is out of bounds - row_infos.len(): {}, \
|
||||
line_layouts.len(): {}, \
|
||||
crease_trailers.len(): {}",
|
||||
line_ix,
|
||||
row_infos.len(),
|
||||
line_layouts.len(),
|
||||
@@ -8614,29 +8638,6 @@ impl Element for EditorElement {
|
||||
cx,
|
||||
);
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
|
||||
|
||||
let autoscrolled = if autoscroll_horizontally {
|
||||
editor.autoscroll_horizontally(
|
||||
start_row,
|
||||
editor_content_width,
|
||||
scroll_width,
|
||||
em_advance,
|
||||
&line_layouts,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if clamped || autoscrolled {
|
||||
snapshot = editor.snapshot(window, cx);
|
||||
scroll_position = snapshot.scroll_position();
|
||||
}
|
||||
});
|
||||
|
||||
let line_elements = self.prepaint_lines(
|
||||
start_row,
|
||||
&mut line_layouts,
|
||||
@@ -8862,7 +8863,7 @@ impl Element for EditorElement {
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
None
|
||||
None,
|
||||
);
|
||||
let space_invisible = window.text_system().shape_line(
|
||||
"•".into(),
|
||||
@@ -8875,7 +8876,7 @@ impl Element for EditorElement {
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
}],
|
||||
None
|
||||
None,
|
||||
);
|
||||
|
||||
let mode = snapshot.mode.clone();
|
||||
@@ -8977,19 +8978,21 @@ impl Element for EditorElement {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
let key_context = self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.key_context(window, cx));
|
||||
if !layout.mode.is_minimap() {
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
let key_context = self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.key_context(window, cx));
|
||||
|
||||
window.set_key_context(key_context);
|
||||
window.handle_input(
|
||||
&focus_handle,
|
||||
ElementInputHandler::new(bounds, self.editor.clone()),
|
||||
cx,
|
||||
);
|
||||
self.register_actions(window, cx);
|
||||
self.register_key_listeners(window, cx, layout);
|
||||
window.set_key_context(key_context);
|
||||
window.handle_input(
|
||||
&focus_handle,
|
||||
ElementInputHandler::new(bounds, self.editor.clone()),
|
||||
cx,
|
||||
);
|
||||
self.register_actions(window, cx);
|
||||
self.register_key_listeners(window, cx, layout);
|
||||
}
|
||||
|
||||
let text_style = TextStyleRefinement {
|
||||
font_size: Some(self.style.text.font_size),
|
||||
@@ -10298,7 +10301,6 @@ mod tests {
|
||||
height: Some(3),
|
||||
render: Arc::new(|cx| div().h(3. * cx.window.line_height()).into_any()),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
}],
|
||||
None,
|
||||
cx,
|
||||
|
||||
@@ -813,7 +813,13 @@ impl Item for Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.report_editor_event("Editor Saved", None, cx);
|
||||
// Add meta data tracking # of auto saves
|
||||
if options.autosave {
|
||||
self.report_editor_event("Editor Autosaved", None, cx);
|
||||
} else {
|
||||
self.report_editor_event("Editor Saved", None, cx);
|
||||
}
|
||||
|
||||
let buffers = self.buffer().clone().read(cx).all_buffers();
|
||||
let buffers = buffers
|
||||
.into_iter()
|
||||
@@ -1220,7 +1226,20 @@ impl SerializableItem for Editor {
|
||||
abs_path: None,
|
||||
contents: None,
|
||||
..
|
||||
} => Task::ready(Err(anyhow!("No path or contents found for buffer"))),
|
||||
} => window.spawn(cx, async move |cx| {
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.create_buffer(cx))?
|
||||
.await?;
|
||||
|
||||
cx.update(|window, cx| {
|
||||
cx.new(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project), window, cx);
|
||||
|
||||
editor.read_metadata_from_db(item_id, workspace_id, window, cx);
|
||||
editor
|
||||
})
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2092,5 +2111,38 @@ mod tests {
|
||||
assert!(editor.has_conflict(cx)); // The editor should have a conflict
|
||||
});
|
||||
}
|
||||
|
||||
// Test case 5: Deserialize with no path, no content, no language, and no old mtime (new, empty, unsaved buffer)
|
||||
{
|
||||
let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
|
||||
|
||||
let item_id = 10000 as ItemId;
|
||||
let serialized_editor = SerializedEditor {
|
||||
abs_path: None,
|
||||
contents: None,
|
||||
language: None,
|
||||
mtime: None,
|
||||
};
|
||||
|
||||
DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let deserialized =
|
||||
deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
|
||||
|
||||
deserialized.update(cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "");
|
||||
assert!(!editor.is_dirty(cx));
|
||||
assert!(!editor.has_conflict(cx));
|
||||
|
||||
let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
|
||||
assert!(buffer.file().is_none());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ use workspace::{ItemId, WorkspaceId};
|
||||
pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
|
||||
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
pub struct WasScrolled(pub(crate) bool);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ScrollbarAutoHide(pub bool);
|
||||
|
||||
@@ -215,87 +217,56 @@ impl ScrollManager {
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let (new_anchor, top_row) = if scroll_position.y <= 0. && scroll_position.x <= 0. {
|
||||
(
|
||||
ScrollAnchor {
|
||||
anchor: Anchor::min(),
|
||||
offset: scroll_position.max(&gpui::Point::default()),
|
||||
},
|
||||
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 {
|
||||
ScrollBeyondLastLine::OnePage => scroll_top,
|
||||
ScrollBeyondLastLine::Off => {
|
||||
if let Some(height_in_lines) = self.visible_line_count {
|
||||
let max_row = map.max_point().row().0 as f32;
|
||||
scroll_top.min(max_row - height_in_lines + 1.).max(0.)
|
||||
} else {
|
||||
scroll_top
|
||||
}
|
||||
) -> WasScrolled {
|
||||
let scroll_top = scroll_position.y.max(0.);
|
||||
let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line {
|
||||
ScrollBeyondLastLine::OnePage => scroll_top,
|
||||
ScrollBeyondLastLine::Off => {
|
||||
if let Some(height_in_lines) = self.visible_line_count {
|
||||
let max_row = map.max_point().row().0 as f32;
|
||||
scroll_top.min(max_row - height_in_lines + 1.).max(0.)
|
||||
} else {
|
||||
scroll_top
|
||||
}
|
||||
ScrollBeyondLastLine::VerticalScrollMargin => {
|
||||
if let Some(height_in_lines) = self.visible_line_count {
|
||||
let max_row = map.max_point().row().0 as f32;
|
||||
scroll_top
|
||||
.min(max_row - height_in_lines + 1. + self.vertical_scroll_margin)
|
||||
.max(0.)
|
||||
} else {
|
||||
scroll_top
|
||||
}
|
||||
}
|
||||
ScrollBeyondLastLine::VerticalScrollMargin => {
|
||||
if let Some(height_in_lines) = self.visible_line_count {
|
||||
let max_row = map.max_point().row().0 as f32;
|
||||
scroll_top
|
||||
.min(max_row - height_in_lines + 1. + self.vertical_scroll_margin)
|
||||
.max(0.)
|
||||
} else {
|
||||
scroll_top
|
||||
}
|
||||
};
|
||||
|
||||
let scroll_top_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);
|
||||
|
||||
(
|
||||
ScrollAnchor {
|
||||
anchor: top_anchor,
|
||||
offset: point(
|
||||
scroll_position.x.max(0.),
|
||||
scroll_top - top_anchor.to_display_point(map).row().as_f32(),
|
||||
),
|
||||
},
|
||||
scroll_top_buffer_point.row,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let scroll_top_row = DisplayRow(scroll_top as u32);
|
||||
let scroll_top_buffer_point = map
|
||||
.clip_point(
|
||||
DisplayPoint::new(scroll_top_row, scroll_position.x as u32),
|
||||
Bias::Left,
|
||||
)
|
||||
.to_point(map);
|
||||
let top_anchor = map
|
||||
.buffer_snapshot
|
||||
.anchor_at(scroll_top_buffer_point, Bias::Right);
|
||||
|
||||
self.set_anchor(
|
||||
new_anchor,
|
||||
top_row,
|
||||
ScrollAnchor {
|
||||
anchor: top_anchor,
|
||||
offset: point(
|
||||
scroll_position.x.max(0.),
|
||||
scroll_top - top_anchor.to_display_point(map).row().as_f32(),
|
||||
),
|
||||
},
|
||||
scroll_top_buffer_point.row,
|
||||
local,
|
||||
autoscroll,
|
||||
workspace_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
fn set_anchor(
|
||||
@@ -307,7 +278,7 @@ impl ScrollManager {
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
) -> WasScrolled {
|
||||
let adjusted_anchor = if self.forbid_vertical_scroll {
|
||||
ScrollAnchor {
|
||||
offset: gpui::Point::new(anchor.offset.x, self.anchor.offset.y),
|
||||
@@ -317,10 +288,14 @@ impl ScrollManager {
|
||||
anchor
|
||||
};
|
||||
|
||||
self.autoscroll_request.take();
|
||||
if self.anchor == adjusted_anchor {
|
||||
return WasScrolled(false);
|
||||
}
|
||||
|
||||
self.anchor = adjusted_anchor;
|
||||
cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
|
||||
self.show_scrollbars(window, cx);
|
||||
self.autoscroll_request.take();
|
||||
if let Some(workspace_id) = workspace_id {
|
||||
let item_id = cx.entity().entity_id().as_u64() as ItemId;
|
||||
|
||||
@@ -342,6 +317,8 @@ impl ScrollManager {
|
||||
.detach()
|
||||
}
|
||||
cx.notify();
|
||||
|
||||
WasScrolled(true)
|
||||
}
|
||||
|
||||
pub fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
|
||||
@@ -552,13 +529,13 @@ impl Editor {
|
||||
scroll_position: gpui::Point<f32>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
) -> WasScrolled {
|
||||
let mut position = scroll_position;
|
||||
if self.scroll_manager.forbid_vertical_scroll {
|
||||
let current_position = self.scroll_position(cx);
|
||||
position.y = current_position.y;
|
||||
}
|
||||
self.set_scroll_position_internal(position, true, false, window, cx);
|
||||
self.set_scroll_position_internal(position, true, false, window, cx)
|
||||
}
|
||||
|
||||
/// Scrolls so that `row` is at the top of the editor view.
|
||||
@@ -590,7 +567,7 @@ impl Editor {
|
||||
autoscroll: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
) -> WasScrolled {
|
||||
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
self.set_scroll_position_taking_display_map(
|
||||
scroll_position,
|
||||
@@ -599,7 +576,7 @@ impl Editor {
|
||||
map,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
fn set_scroll_position_taking_display_map(
|
||||
@@ -610,7 +587,7 @@ impl Editor {
|
||||
display_map: DisplaySnapshot,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
) -> WasScrolled {
|
||||
hide_hover(self, cx);
|
||||
let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
|
||||
|
||||
@@ -624,7 +601,7 @@ impl Editor {
|
||||
scroll_position
|
||||
};
|
||||
|
||||
self.scroll_manager.set_scroll_position(
|
||||
let editor_was_scrolled = self.scroll_manager.set_scroll_position(
|
||||
adjusted_position,
|
||||
&display_map,
|
||||
local,
|
||||
@@ -636,6 +613,7 @@ impl Editor {
|
||||
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
||||
self.refresh_colors(false, None, window, cx);
|
||||
editor_was_scrolled
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<f32> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, SelectionEffects,
|
||||
display_map::ToDisplayPoint,
|
||||
display_map::ToDisplayPoint, scroll::WasScrolled,
|
||||
};
|
||||
use gpui::{Bounds, Context, Pixels, Window, px};
|
||||
use language::Point;
|
||||
@@ -99,19 +99,21 @@ impl AutoscrollStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool);
|
||||
|
||||
impl Editor {
|
||||
pub fn autoscroll_request(&self) -> Option<Autoscroll> {
|
||||
self.scroll_manager.autoscroll_request()
|
||||
}
|
||||
|
||||
pub fn autoscroll_vertically(
|
||||
pub(crate) fn autoscroll_vertically(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
line_height: Pixels,
|
||||
max_scroll_top: f32,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> bool {
|
||||
) -> (NeedsHorizontalAutoscroll, WasScrolled) {
|
||||
let viewport_height = bounds.size.height;
|
||||
let visible_lines = viewport_height / line_height;
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
@@ -129,12 +131,14 @@ impl Editor {
|
||||
scroll_position.y = max_scroll_top;
|
||||
}
|
||||
|
||||
if original_y != scroll_position.y {
|
||||
self.set_scroll_position(scroll_position, window, cx);
|
||||
}
|
||||
let editor_was_scrolled = if original_y != scroll_position.y {
|
||||
self.set_scroll_position(scroll_position, window, cx)
|
||||
} else {
|
||||
WasScrolled(false)
|
||||
};
|
||||
|
||||
let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else {
|
||||
return false;
|
||||
return (NeedsHorizontalAutoscroll(false), editor_was_scrolled);
|
||||
};
|
||||
|
||||
let mut target_top;
|
||||
@@ -212,7 +216,7 @@ impl Editor {
|
||||
target_bottom = target_top + 1.;
|
||||
}
|
||||
|
||||
match strategy {
|
||||
let was_autoscrolled = match strategy {
|
||||
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
|
||||
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
|
||||
let target_top = (target_top - margin).max(0.0);
|
||||
@@ -225,39 +229,42 @@ impl Editor {
|
||||
|
||||
if needs_scroll_up && !needs_scroll_down {
|
||||
scroll_position.y = target_top;
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
}
|
||||
if !needs_scroll_up && needs_scroll_down {
|
||||
} else if !needs_scroll_up && needs_scroll_down {
|
||||
scroll_position.y = target_bottom - visible_lines;
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
}
|
||||
|
||||
if needs_scroll_up ^ needs_scroll_down {
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||
} else {
|
||||
WasScrolled(false)
|
||||
}
|
||||
}
|
||||
AutoscrollStrategy::Center => {
|
||||
scroll_position.y = (target_top - margin).max(0.0);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||
}
|
||||
AutoscrollStrategy::Focused => {
|
||||
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
|
||||
scroll_position.y = (target_top - margin).max(0.0);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||
}
|
||||
AutoscrollStrategy::Top => {
|
||||
scroll_position.y = (target_top).max(0.0);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||
}
|
||||
AutoscrollStrategy::Bottom => {
|
||||
scroll_position.y = (target_bottom - visible_lines).max(0.0);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||
}
|
||||
AutoscrollStrategy::TopRelative(lines) => {
|
||||
scroll_position.y = target_top - lines as f32;
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||
}
|
||||
AutoscrollStrategy::BottomRelative(lines) => {
|
||||
scroll_position.y = target_bottom + lines as f32;
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.scroll_manager.last_autoscroll = Some((
|
||||
self.scroll_manager.anchor.offset,
|
||||
@@ -266,7 +273,8 @@ impl Editor {
|
||||
strategy,
|
||||
));
|
||||
|
||||
true
|
||||
let was_scrolled = WasScrolled(editor_was_scrolled.0 || was_autoscrolled.0);
|
||||
(NeedsHorizontalAutoscroll(true), was_scrolled)
|
||||
}
|
||||
|
||||
pub(crate) fn autoscroll_horizontally(
|
||||
@@ -278,7 +286,7 @@ impl Editor {
|
||||
layouts: &[LineWithInvisibles],
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
) -> Option<gpui::Point<f32>> {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
|
||||
@@ -319,22 +327,26 @@ impl Editor {
|
||||
target_right = target_right.min(scroll_width);
|
||||
|
||||
if target_right - target_left > viewport_width {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
|
||||
let scroll_left = self.scroll_manager.anchor.offset.x * em_advance;
|
||||
let scroll_right = scroll_left + viewport_width;
|
||||
|
||||
if target_left < scroll_left {
|
||||
let was_scrolled = if target_left < scroll_left {
|
||||
scroll_position.x = target_left / em_advance;
|
||||
self.set_scroll_position_internal(scroll_position, true, true, window, cx);
|
||||
true
|
||||
self.set_scroll_position_internal(scroll_position, true, true, window, cx)
|
||||
} else if target_right > scroll_right {
|
||||
scroll_position.x = (target_right - viewport_width) / em_advance;
|
||||
self.set_scroll_position_internal(scroll_position, true, true, window, cx);
|
||||
true
|
||||
self.set_scroll_position_internal(scroll_position, true, true, window, cx)
|
||||
} else {
|
||||
false
|
||||
WasScrolled(false)
|
||||
};
|
||||
|
||||
if was_scrolled.0 {
|
||||
Some(scroll_position)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ mod tool_metrics;
|
||||
|
||||
use assertions::{AssertionsReport, display_error_row};
|
||||
use instance::{ExampleInstance, JudgeOutput, RunOutput, run_git};
|
||||
use language_extension::LspAccess;
|
||||
pub(crate) use tool_metrics::*;
|
||||
|
||||
use ::fs::RealFs;
|
||||
@@ -415,7 +416,11 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
|
||||
|
||||
language::init(cx);
|
||||
debug_adapter_extension::init(extension_host_proxy.clone(), cx);
|
||||
language_extension::init(extension_host_proxy.clone(), languages.clone());
|
||||
language_extension::init(
|
||||
LspAccess::Noop,
|
||||
extension_host_proxy.clone(),
|
||||
languages.clone(),
|
||||
);
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), cx);
|
||||
languages::init(languages.clone(), node_runtime.clone(), cx);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
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>
|
||||
<DisplayName>Zed Nightly</DisplayName>
|
||||
<PublisherDisplayName>Zed Industries</PublisherDisplayName>
|
||||
<!-- TODO: Use actual icon here. -->
|
||||
<Logo>resources\logo_150x150.png</Logo>
|
||||
@@ -45,8 +45,8 @@
|
||||
<!-- TODO: Use actual icon here. -->
|
||||
<uap:VisualElements
|
||||
AppListEntry="none"
|
||||
DisplayName="Zed Editor Nightly"
|
||||
Description="Zed Editor Nightly explorer command injector"
|
||||
DisplayName="Zed Nightly"
|
||||
Description="Zed Nightly explorer command injector"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="resources\logo_150x150.png"
|
||||
Square44x44Logo="resources\logo_70x70.png">
|
||||
@@ -67,7 +67,7 @@
|
||||
</desktop4:Extension>
|
||||
<com:Extension Category="windows.comServer">
|
||||
<com:ComServer>
|
||||
<com:SurrogateServer DisplayName="Zed Editor Nightly">
|
||||
<com:SurrogateServer DisplayName="Zed Nightly">
|
||||
<com:Class Id="266f2cfe-1653-42af-b55c-fe3590c83871" Path="zed_explorer_command_injector.dll" ThreadingModel="STA"/>
|
||||
</com:SurrogateServer>
|
||||
</com:ComServer>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
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>
|
||||
<DisplayName>Zed Preview</DisplayName>
|
||||
<PublisherDisplayName>Zed Industries</PublisherDisplayName>
|
||||
<!-- TODO: Use actual icon here. -->
|
||||
<Logo>resources\logo_150x150.png</Logo>
|
||||
@@ -45,8 +45,8 @@
|
||||
<!-- TODO: Use actual icon here. -->
|
||||
<uap:VisualElements
|
||||
AppListEntry="none"
|
||||
DisplayName="Zed Editor Preview"
|
||||
Description="Zed Editor Preview explorer command injector"
|
||||
DisplayName="Zed Preview"
|
||||
Description="Zed Preview explorer command injector"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="resources\logo_150x150.png"
|
||||
Square44x44Logo="resources\logo_70x70.png">
|
||||
@@ -67,7 +67,7 @@
|
||||
</desktop4:Extension>
|
||||
<com:Extension Category="windows.comServer">
|
||||
<com:ComServer>
|
||||
<com:SurrogateServer DisplayName="Zed Editor Preview">
|
||||
<com:SurrogateServer DisplayName="Zed Preview">
|
||||
<com:Class Id="af8e85ea-fb20-4db2-93cf-56513c1ec697" Path="zed_explorer_command_injector.dll" ThreadingModel="STA"/>
|
||||
</com:SurrogateServer>
|
||||
</com:ComServer>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
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>
|
||||
<DisplayName>Zed</DisplayName>
|
||||
|
||||
<PublisherDisplayName>Zed Industries</PublisherDisplayName>
|
||||
<!-- TODO: Use actual icon here. -->
|
||||
@@ -46,8 +46,8 @@
|
||||
<!-- TODO: Use actual icon here. -->
|
||||
<uap:VisualElements
|
||||
AppListEntry="none"
|
||||
DisplayName="Zed Editor"
|
||||
Description="Zed Editor explorer command injector"
|
||||
DisplayName="Zed"
|
||||
Description="Zed explorer command injector"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="resources\logo_150x150.png"
|
||||
Square44x44Logo="resources\logo_70x70.png">
|
||||
@@ -68,7 +68,7 @@
|
||||
</desktop4:Extension>
|
||||
<com:Extension Category="windows.comServer">
|
||||
<com:ComServer>
|
||||
<com:SurrogateServer DisplayName="Zed Editor">
|
||||
<com:SurrogateServer DisplayName="Zed">
|
||||
<com:Class Id="6a1f6b13-3b82-48a1-9e06-7bb0a6d0bffd" Path="zed_explorer_command_injector.dll" ThreadingModel="STA"/>
|
||||
</com:SurrogateServer>
|
||||
</com:ComServer>
|
||||
|
||||
@@ -286,7 +286,8 @@ pub trait ExtensionLanguageServerProxy: Send + Sync + 'static {
|
||||
&self,
|
||||
language: &LanguageName,
|
||||
language_server_id: &LanguageServerName,
|
||||
);
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>>;
|
||||
|
||||
fn update_language_server_status(
|
||||
&self,
|
||||
@@ -313,12 +314,13 @@ impl ExtensionLanguageServerProxy for ExtensionHostProxy {
|
||||
&self,
|
||||
language: &LanguageName,
|
||||
language_server_id: &LanguageServerName,
|
||||
) {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(proxy) = self.language_server_proxy.read().clone() else {
|
||||
return;
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
|
||||
proxy.remove_language_server(language, language_server_id)
|
||||
proxy.remove_language_server(language, language_server_id, cx)
|
||||
}
|
||||
|
||||
fn update_language_server_status(
|
||||
@@ -350,6 +352,8 @@ impl ExtensionSnippetProxy for ExtensionHostProxy {
|
||||
|
||||
pub trait ExtensionSlashCommandProxy: Send + Sync + 'static {
|
||||
fn register_slash_command(&self, extension: Arc<dyn Extension>, command: SlashCommand);
|
||||
|
||||
fn unregister_slash_command(&self, command_name: Arc<str>);
|
||||
}
|
||||
|
||||
impl ExtensionSlashCommandProxy for ExtensionHostProxy {
|
||||
@@ -360,6 +364,14 @@ impl ExtensionSlashCommandProxy for ExtensionHostProxy {
|
||||
|
||||
proxy.register_slash_command(extension, command)
|
||||
}
|
||||
|
||||
fn unregister_slash_command(&self, command_name: Arc<str>) {
|
||||
let Some(proxy) = self.slash_command_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.unregister_slash_command(command_name)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionContextServerProxy: Send + Sync + 'static {
|
||||
@@ -398,6 +410,8 @@ impl ExtensionContextServerProxy for ExtensionHostProxy {
|
||||
|
||||
pub trait ExtensionIndexedDocsProviderProxy: Send + Sync + 'static {
|
||||
fn register_indexed_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>);
|
||||
|
||||
fn unregister_indexed_docs_provider(&self, provider_id: Arc<str>);
|
||||
}
|
||||
|
||||
impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy {
|
||||
@@ -408,6 +422,14 @@ impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy {
|
||||
|
||||
proxy.register_indexed_docs_provider(extension, provider_id)
|
||||
}
|
||||
|
||||
fn unregister_indexed_docs_provider(&self, provider_id: Arc<str>) {
|
||||
let Some(proxy) = self.indexed_docs_provider_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.unregister_indexed_docs_provider(provider_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionDebugAdapterProviderProxy: Send + Sync + 'static {
|
||||
|
||||
@@ -289,6 +289,24 @@ async fn copy_extension_resources(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(snippets_path) = manifest.snippets.as_ref() {
|
||||
let parent = snippets_path.parent();
|
||||
if let Some(parent) = parent.filter(|p| p.components().next().is_some()) {
|
||||
fs::create_dir_all(output_dir.join(parent))?;
|
||||
}
|
||||
copy_recursive(
|
||||
fs.as_ref(),
|
||||
&extension_path.join(&snippets_path),
|
||||
&output_dir.join(&snippets_path),
|
||||
CopyOptions {
|
||||
overwrite: true,
|
||||
ignore_if_exists: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to copy snippets from '{}'", snippets_path.display()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ use extension::{
|
||||
ExtensionSnippetProxy, ExtensionThemeProxy,
|
||||
};
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::future::join_all;
|
||||
use futures::{
|
||||
AsyncReadExt as _, Future, FutureExt as _, StreamExt as _,
|
||||
channel::{
|
||||
@@ -860,8 +861,8 @@ impl ExtensionStore {
|
||||
btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove),
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let _finish = cx.on_drop(&this, {
|
||||
cx.spawn(async move |extension_store, cx| {
|
||||
let _finish = cx.on_drop(&extension_store, {
|
||||
let extension_id = extension_id.clone();
|
||||
move |this, cx| {
|
||||
this.outstanding_operations.remove(extension_id.as_ref());
|
||||
@@ -876,22 +877,39 @@ impl ExtensionStore {
|
||||
ignore_if_not_exists: true,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.with_context(|| format!("Removing extension dir {extension_dir:?}"))?;
|
||||
|
||||
// todo(windows)
|
||||
// Stop the server here.
|
||||
this.update(cx, |this, cx| this.reload(None, cx))?.await;
|
||||
extension_store
|
||||
.update(cx, |extension_store, cx| extension_store.reload(None, cx))?
|
||||
.await;
|
||||
|
||||
fs.remove_dir(
|
||||
&work_dir,
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
ignore_if_not_exists: true,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
// There's a race between wasm extension fully stopping and the directory removal.
|
||||
// On Windows, it's impossible to remove a directory that has a process running in it.
|
||||
for i in 0..3 {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(i * 100))
|
||||
.await;
|
||||
let removal_result = fs
|
||||
.remove_dir(
|
||||
&work_dir,
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
ignore_if_not_exists: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
match removal_result {
|
||||
Ok(()) => break,
|
||||
Err(e) => {
|
||||
if i == 2 {
|
||||
log::error!("Failed to remove extension work dir {work_dir:?} : {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.update(cx, |_, cx| {
|
||||
extension_store.update(cx, |_, cx| {
|
||||
cx.emit(Event::ExtensionUninstalled(extension_id.clone()));
|
||||
if let Some(events) = ExtensionEvents::try_global(cx) {
|
||||
if let Some(manifest) = extension_manifest {
|
||||
@@ -1143,27 +1161,38 @@ impl ExtensionStore {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut grammars_to_remove = Vec::new();
|
||||
let mut server_removal_tasks = Vec::with_capacity(extensions_to_unload.len());
|
||||
for extension_id in &extensions_to_unload {
|
||||
let Some(extension) = old_index.extensions.get(extension_id) else {
|
||||
continue;
|
||||
};
|
||||
grammars_to_remove.extend(extension.manifest.grammars.keys().cloned());
|
||||
for (language_server_name, config) in extension.manifest.language_servers.iter() {
|
||||
for (language_server_name, config) in &extension.manifest.language_servers {
|
||||
for language in config.languages() {
|
||||
self.proxy
|
||||
.remove_language_server(&language, language_server_name);
|
||||
server_removal_tasks.push(self.proxy.remove_language_server(
|
||||
&language,
|
||||
language_server_name,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
for (server_id, _) in extension.manifest.context_servers.iter() {
|
||||
for (server_id, _) in &extension.manifest.context_servers {
|
||||
self.proxy.unregister_context_server(server_id.clone(), cx);
|
||||
}
|
||||
for (adapter, _) in extension.manifest.debug_adapters.iter() {
|
||||
for (adapter, _) in &extension.manifest.debug_adapters {
|
||||
self.proxy.unregister_debug_adapter(adapter.clone());
|
||||
}
|
||||
for (locator, _) in extension.manifest.debug_locators.iter() {
|
||||
for (locator, _) in &extension.manifest.debug_locators {
|
||||
self.proxy.unregister_debug_locator(locator.clone());
|
||||
}
|
||||
for (command_name, _) in &extension.manifest.slash_commands {
|
||||
self.proxy.unregister_slash_command(command_name.clone());
|
||||
}
|
||||
for (provider_id, _) in &extension.manifest.indexed_docs_providers {
|
||||
self.proxy
|
||||
.unregister_indexed_docs_provider(provider_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
self.wasm_extensions
|
||||
@@ -1268,14 +1297,15 @@ impl ExtensionStore {
|
||||
cx.background_spawn({
|
||||
let fs = fs.clone();
|
||||
async move {
|
||||
for theme_path in themes_to_add.into_iter() {
|
||||
let _ = join_all(server_removal_tasks).await;
|
||||
for theme_path in themes_to_add {
|
||||
proxy
|
||||
.load_user_theme(theme_path, fs.clone())
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
for (icon_theme_path, icons_root_path) in icon_themes_to_add.into_iter() {
|
||||
for (icon_theme_path, icons_root_path) in icon_themes_to_add {
|
||||
proxy
|
||||
.load_icon_theme(icon_theme_path, icons_root_path, fs.clone())
|
||||
.await
|
||||
|
||||
@@ -11,6 +11,7 @@ use futures::{AsyncReadExt, StreamExt, io::BufReader};
|
||||
use gpui::{AppContext as _, SemanticVersion, TestAppContext};
|
||||
use http_client::{FakeHttpClient, Response};
|
||||
use language::{BinaryStatus, LanguageMatcher, LanguageRegistry};
|
||||
use language_extension::LspAccess;
|
||||
use lsp::LanguageServerName;
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
@@ -271,7 +272,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
|
||||
theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
|
||||
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
language_extension::init(proxy.clone(), language_registry.clone());
|
||||
language_extension::init(LspAccess::Noop, proxy.clone(), language_registry.clone());
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
|
||||
let store = cx.new(|cx| {
|
||||
@@ -554,7 +555,11 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
|
||||
theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
|
||||
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||
language_extension::init(proxy.clone(), language_registry.clone());
|
||||
language_extension::init(
|
||||
LspAccess::ViaLspStore(project.update(cx, |project, _| project.lsp_store())),
|
||||
proxy.clone(),
|
||||
language_registry.clone(),
|
||||
);
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
|
||||
let mut status_updates = language_registry.language_server_binary_statuses();
|
||||
@@ -815,7 +820,6 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
extension_store
|
||||
.update(cx, |store, cx| store.reload(Some("gleam".into()), cx))
|
||||
.await;
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
project.update(cx, |project, cx| {
|
||||
project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
|
||||
|
||||
@@ -11,6 +11,7 @@ use extension::{
|
||||
ExtensionLanguageServerProxy, ExtensionManifest,
|
||||
};
|
||||
use fs::{Fs, RemoveOptions, RenameOptions};
|
||||
use futures::future::join_all;
|
||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity};
|
||||
use http_client::HttpClient;
|
||||
use language::{LanguageConfig, LanguageName, LanguageQueries, LoadedLanguage};
|
||||
@@ -230,18 +231,27 @@ impl HeadlessExtensionStore {
|
||||
.unwrap_or_default();
|
||||
self.proxy.remove_languages(&languages_to_remove, &[]);
|
||||
|
||||
for (language_server_name, language) in self
|
||||
let servers_to_remove = self
|
||||
.loaded_language_servers
|
||||
.remove(extension_id)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
self.proxy
|
||||
.remove_language_server(&language, &language_server_name);
|
||||
}
|
||||
|
||||
.unwrap_or_default();
|
||||
let proxy = self.proxy.clone();
|
||||
let path = self.extension_dir.join(&extension_id.to_string());
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(async move |_, _| {
|
||||
cx.spawn(async move |_, cx| {
|
||||
let mut removal_tasks = Vec::with_capacity(servers_to_remove.len());
|
||||
cx.update(|cx| {
|
||||
for (language_server_name, language) in servers_to_remove {
|
||||
removal_tasks.push(proxy.remove_language_server(
|
||||
&language,
|
||||
&language_server_name,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
let _ = join_all(removal_tasks).await;
|
||||
|
||||
fs.remove_dir(
|
||||
&path,
|
||||
RemoveOptions {
|
||||
@@ -250,6 +260,7 @@ impl HeadlessExtensionStore {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Removing directory {path:?}"))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ pub struct WasmHost {
|
||||
main_thread_message_tx: mpsc::UnboundedSender<MainThreadCall>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WasmExtension {
|
||||
tx: UnboundedSender<ExtensionCall>,
|
||||
pub manifest: Arc<ExtensionManifest>,
|
||||
@@ -63,6 +63,12 @@ pub struct WasmExtension {
|
||||
pub zed_api_version: SemanticVersion,
|
||||
}
|
||||
|
||||
impl Drop for WasmExtension {
|
||||
fn drop(&mut self) {
|
||||
self.tx.close_channel();
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl extension::Extension for WasmExtension {
|
||||
fn manifest(&self) -> Arc<ExtensionManifest> {
|
||||
@@ -742,7 +748,6 @@ impl WasmExtension {
|
||||
{
|
||||
let (return_tx, return_rx) = oneshot::channel();
|
||||
self.tx
|
||||
.clone()
|
||||
.unbounded_send(Box::new(move |extension, store| {
|
||||
async {
|
||||
let result = f(extension, store).await;
|
||||
|
||||
@@ -54,6 +54,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
|
||||
("nu", &["nu"]),
|
||||
("ocaml", &["ml", "mli"]),
|
||||
("php", &["php"]),
|
||||
("powershell", &["ps1", "psm1"]),
|
||||
("prisma", &["prisma"]),
|
||||
("proto", &["proto"]),
|
||||
("purescript", &["purs"]),
|
||||
|
||||
@@ -98,17 +98,6 @@ 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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::FakeFs;
|
||||
use crate::{FakeFs, Fs};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::future::{self, BoxFuture};
|
||||
use futures::future::{self, BoxFuture, join_all};
|
||||
use git::{
|
||||
blame::Blame,
|
||||
repository::{
|
||||
@@ -356,18 +356,46 @@ impl GitRepository for FakeGitRepository {
|
||||
|
||||
fn stage_paths(
|
||||
&self,
|
||||
_paths: Vec<RepoPath>,
|
||||
paths: Vec<RepoPath>,
|
||||
_env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<'_, Result<()>> {
|
||||
unimplemented!()
|
||||
Box::pin(async move {
|
||||
let contents = paths
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
let abs_path = self.dot_git_path.parent().unwrap().join(&path);
|
||||
Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) })
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let contents = join_all(contents).await;
|
||||
self.with_state_async(true, move |state| {
|
||||
for (path, content) in contents {
|
||||
if let Some(content) = content {
|
||||
state.index_contents.insert(path, content);
|
||||
} else {
|
||||
state.index_contents.remove(&path);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
fn unstage_paths(
|
||||
&self,
|
||||
_paths: Vec<RepoPath>,
|
||||
paths: Vec<RepoPath>,
|
||||
_env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<'_, Result<()>> {
|
||||
unimplemented!()
|
||||
self.with_state_async(true, move |state| {
|
||||
for path in paths {
|
||||
match state.head_contents.get(&path) {
|
||||
Some(content) => state.index_contents.insert(path, content.clone()),
|
||||
None => state.index_contents.remove(&path),
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn commit(
|
||||
|
||||
@@ -31,8 +31,10 @@ actions!(
|
||||
git,
|
||||
[
|
||||
// per-hunk
|
||||
/// Toggles the staged state of the hunk at cursor.
|
||||
/// Toggles the staged state of the hunk or status entry at cursor.
|
||||
ToggleStaged,
|
||||
/// Stage status entries between an anchor entry and the cursor.
|
||||
StageRange,
|
||||
/// Stages the current hunk and moves to the next one.
|
||||
StageAndNext,
|
||||
/// Unstages the current hunk and moves to the next one.
|
||||
|
||||
@@ -11,10 +11,7 @@ use gpui::{
|
||||
use language::{Anchor, Buffer, BufferId};
|
||||
use project::{ConflictRegion, ConflictSet, ConflictSetUpdate, ProjectItem as _};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use ui::{
|
||||
ActiveTheme, AnyElement, Element as _, StatefulInteractiveElement, Styled,
|
||||
StyledTypography as _, Window, div, h_flex, rems,
|
||||
};
|
||||
use ui::{ActiveTheme, Element as _, Styled, Window, prelude::*};
|
||||
use util::{ResultExt as _, debug_panic, maybe};
|
||||
|
||||
pub(crate) struct ConflictAddon {
|
||||
@@ -300,7 +297,6 @@ fn conflicts_updated(
|
||||
move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx)
|
||||
}),
|
||||
priority: 0,
|
||||
render_in_minimap: true,
|
||||
})
|
||||
}
|
||||
let new_block_ids = editor.insert_blocks(blocks, None, cx);
|
||||
@@ -391,20 +387,15 @@ fn render_conflict_buttons(
|
||||
cx: &mut BlockContext,
|
||||
) -> AnyElement {
|
||||
h_flex()
|
||||
.h(cx.line_height)
|
||||
.items_end()
|
||||
.ml(cx.margins.gutter.width)
|
||||
.id(cx.block_id)
|
||||
.gap_0p5()
|
||||
.h(cx.line_height)
|
||||
.ml(cx.margins.gutter.width)
|
||||
.items_end()
|
||||
.gap_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
div()
|
||||
.id("ours")
|
||||
.px_1()
|
||||
.child("Take Ours")
|
||||
.rounded_t(rems(0.2))
|
||||
.text_ui_sm(cx)
|
||||
.hover(|this| this.bg(cx.theme().colors().element_background))
|
||||
.cursor_pointer()
|
||||
Button::new("head", "Use HEAD")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let conflict = conflict.clone();
|
||||
@@ -423,14 +414,8 @@ fn render_conflict_buttons(
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("theirs")
|
||||
.px_1()
|
||||
.child("Take Theirs")
|
||||
.rounded_t(rems(0.2))
|
||||
.text_ui_sm(cx)
|
||||
.hover(|this| this.bg(cx.theme().colors().element_background))
|
||||
.cursor_pointer()
|
||||
Button::new("origin", "Use Origin")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let conflict = conflict.clone();
|
||||
@@ -449,14 +434,8 @@ fn render_conflict_buttons(
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("both")
|
||||
.px_1()
|
||||
.child("Take Both")
|
||||
.rounded_t(rems(0.2))
|
||||
.text_ui_sm(cx)
|
||||
.hover(|this| this.bg(cx.theme().colors().element_background))
|
||||
.cursor_pointer()
|
||||
Button::new("both", "Use Both")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let conflict = conflict.clone();
|
||||
|
||||
@@ -30,10 +30,9 @@ use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
|
||||
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
|
||||
ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
|
||||
MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task,
|
||||
Transformation, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, percentage,
|
||||
uniform_list,
|
||||
ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, Point,
|
||||
PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle,
|
||||
WeakEntity, actions, anchored, deferred, percentage, uniform_list,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, File};
|
||||
@@ -48,7 +47,7 @@ use panel::{
|
||||
PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
|
||||
panel_icon_button,
|
||||
};
|
||||
use project::git_store::RepositoryEvent;
|
||||
use project::git_store::{RepositoryEvent, RepositoryId};
|
||||
use project::{
|
||||
Fs, Project, ProjectPath,
|
||||
git_store::{GitStoreEvent, Repository},
|
||||
@@ -212,14 +211,14 @@ impl GitHeaderEntry {
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
enum GitListEntry {
|
||||
GitStatusEntry(GitStatusEntry),
|
||||
Status(GitStatusEntry),
|
||||
Header(GitHeaderEntry),
|
||||
}
|
||||
|
||||
impl GitListEntry {
|
||||
fn status_entry(&self) -> Option<&GitStatusEntry> {
|
||||
match self {
|
||||
GitListEntry::GitStatusEntry(entry) => Some(entry),
|
||||
GitListEntry::Status(entry) => Some(entry),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -323,7 +322,6 @@ pub struct GitPanel {
|
||||
pub(crate) commit_editor: Entity<Editor>,
|
||||
conflicted_count: usize,
|
||||
conflicted_staged_count: usize,
|
||||
current_modifiers: Modifiers,
|
||||
add_coauthors: bool,
|
||||
generate_commit_message_task: Option<Task<Option<()>>>,
|
||||
entries: Vec<GitListEntry>,
|
||||
@@ -355,9 +353,16 @@ pub struct GitPanel {
|
||||
show_placeholders: bool,
|
||||
local_committer: Option<GitCommitter>,
|
||||
local_committer_task: Option<Task<()>>,
|
||||
bulk_staging: Option<BulkStaging>,
|
||||
_settings_subscription: Subscription,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct BulkStaging {
|
||||
repo_id: RepositoryId,
|
||||
anchor: RepoPath,
|
||||
}
|
||||
|
||||
const MAX_PANEL_EDITOR_LINES: usize = 6;
|
||||
|
||||
pub(crate) fn commit_message_editor(
|
||||
@@ -497,7 +502,6 @@ impl GitPanel {
|
||||
commit_editor,
|
||||
conflicted_count: 0,
|
||||
conflicted_staged_count: 0,
|
||||
current_modifiers: window.modifiers(),
|
||||
add_coauthors: true,
|
||||
generate_commit_message_task: None,
|
||||
entries: Vec::new(),
|
||||
@@ -529,6 +533,7 @@ impl GitPanel {
|
||||
entry_count: 0,
|
||||
horizontal_scrollbar,
|
||||
vertical_scrollbar,
|
||||
bulk_staging: None,
|
||||
_settings_subscription,
|
||||
};
|
||||
|
||||
@@ -735,16 +740,6 @@ impl GitPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_modifiers_changed(
|
||||
&mut self,
|
||||
event: &ModifiersChangedEvent,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.current_modifiers = event.modifiers;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn scroll_to_selected_entry(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(selected_entry) = self.selected_entry {
|
||||
self.scroll_handle
|
||||
@@ -1265,10 +1260,18 @@ impl GitPanel {
|
||||
return;
|
||||
};
|
||||
let (stage, repo_paths) = match entry {
|
||||
GitListEntry::GitStatusEntry(status_entry) => {
|
||||
GitListEntry::Status(status_entry) => {
|
||||
if status_entry.status.staging().is_fully_staged() {
|
||||
if let Some(op) = self.bulk_staging.clone()
|
||||
&& op.anchor == status_entry.repo_path
|
||||
{
|
||||
self.bulk_staging = None;
|
||||
}
|
||||
|
||||
(false, vec![status_entry.clone()])
|
||||
} else {
|
||||
self.set_bulk_staging_anchor(status_entry.repo_path.clone(), cx);
|
||||
|
||||
(true, vec![status_entry.clone()])
|
||||
}
|
||||
}
|
||||
@@ -1383,6 +1386,13 @@ impl GitPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn stage_range(&mut self, _: &git::StageRange, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(index) = self.selected_entry else {
|
||||
return;
|
||||
};
|
||||
self.stage_bulk(index, cx);
|
||||
}
|
||||
|
||||
fn stage_selected(&mut self, _: &git::StageFile, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(selected_entry) = self.get_selected_entry() else {
|
||||
return;
|
||||
@@ -2449,6 +2459,11 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
|
||||
let bulk_staging = self.bulk_staging.take();
|
||||
let last_staged_path_prev_index = bulk_staging
|
||||
.as_ref()
|
||||
.and_then(|op| self.entry_by_path(&op.anchor, cx));
|
||||
|
||||
self.entries.clear();
|
||||
self.single_staged_entry.take();
|
||||
self.single_tracked_entry.take();
|
||||
@@ -2465,7 +2480,7 @@ impl GitPanel {
|
||||
let mut changed_entries = Vec::new();
|
||||
let mut new_entries = Vec::new();
|
||||
let mut conflict_entries = Vec::new();
|
||||
let mut last_staged = None;
|
||||
let mut single_staged_entry = None;
|
||||
let mut staged_count = 0;
|
||||
let mut max_width_item: Option<(RepoPath, usize)> = None;
|
||||
|
||||
@@ -2503,7 +2518,7 @@ impl GitPanel {
|
||||
|
||||
if staging.has_staged() {
|
||||
staged_count += 1;
|
||||
last_staged = Some(entry.clone());
|
||||
single_staged_entry = Some(entry.clone());
|
||||
}
|
||||
|
||||
let width_estimate = Self::item_width_estimate(
|
||||
@@ -2534,27 +2549,27 @@ impl GitPanel {
|
||||
|
||||
let mut pending_staged_count = 0;
|
||||
let mut last_pending_staged = None;
|
||||
let mut pending_status_for_last_staged = None;
|
||||
let mut pending_status_for_single_staged = None;
|
||||
for pending in self.pending.iter() {
|
||||
if pending.target_status == TargetStatus::Staged {
|
||||
pending_staged_count += pending.entries.len();
|
||||
last_pending_staged = pending.entries.iter().next().cloned();
|
||||
}
|
||||
if let Some(last_staged) = &last_staged {
|
||||
if let Some(single_staged) = &single_staged_entry {
|
||||
if pending
|
||||
.entries
|
||||
.iter()
|
||||
.any(|entry| entry.repo_path == last_staged.repo_path)
|
||||
.any(|entry| entry.repo_path == single_staged.repo_path)
|
||||
{
|
||||
pending_status_for_last_staged = Some(pending.target_status);
|
||||
pending_status_for_single_staged = Some(pending.target_status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if conflict_entries.len() == 0 && staged_count == 1 && pending_staged_count == 0 {
|
||||
match pending_status_for_last_staged {
|
||||
match pending_status_for_single_staged {
|
||||
Some(TargetStatus::Staged) | None => {
|
||||
self.single_staged_entry = last_staged;
|
||||
self.single_staged_entry = single_staged_entry;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -2570,11 +2585,8 @@ impl GitPanel {
|
||||
self.entries.push(GitListEntry::Header(GitHeaderEntry {
|
||||
header: Section::Conflict,
|
||||
}));
|
||||
self.entries.extend(
|
||||
conflict_entries
|
||||
.into_iter()
|
||||
.map(GitListEntry::GitStatusEntry),
|
||||
);
|
||||
self.entries
|
||||
.extend(conflict_entries.into_iter().map(GitListEntry::Status));
|
||||
}
|
||||
|
||||
if changed_entries.len() > 0 {
|
||||
@@ -2583,31 +2595,39 @@ impl GitPanel {
|
||||
header: Section::Tracked,
|
||||
}));
|
||||
}
|
||||
self.entries.extend(
|
||||
changed_entries
|
||||
.into_iter()
|
||||
.map(GitListEntry::GitStatusEntry),
|
||||
);
|
||||
self.entries
|
||||
.extend(changed_entries.into_iter().map(GitListEntry::Status));
|
||||
}
|
||||
if new_entries.len() > 0 {
|
||||
self.entries.push(GitListEntry::Header(GitHeaderEntry {
|
||||
header: Section::New,
|
||||
}));
|
||||
self.entries
|
||||
.extend(new_entries.into_iter().map(GitListEntry::GitStatusEntry));
|
||||
.extend(new_entries.into_iter().map(GitListEntry::Status));
|
||||
}
|
||||
|
||||
if let Some((repo_path, _)) = max_width_item {
|
||||
self.max_width_item_index = self.entries.iter().position(|entry| match entry {
|
||||
GitListEntry::GitStatusEntry(git_status_entry) => {
|
||||
git_status_entry.repo_path == repo_path
|
||||
}
|
||||
GitListEntry::Status(git_status_entry) => git_status_entry.repo_path == repo_path,
|
||||
GitListEntry::Header(_) => false,
|
||||
});
|
||||
}
|
||||
|
||||
self.update_counts(repo);
|
||||
|
||||
let bulk_staging_anchor_new_index = bulk_staging
|
||||
.as_ref()
|
||||
.filter(|op| op.repo_id == repo.id)
|
||||
.and_then(|op| self.entry_by_path(&op.anchor, cx));
|
||||
if bulk_staging_anchor_new_index == last_staged_path_prev_index
|
||||
&& let Some(index) = bulk_staging_anchor_new_index
|
||||
&& let Some(entry) = self.entries.get(index)
|
||||
&& let Some(entry) = entry.status_entry()
|
||||
&& self.entry_staging(entry) == StageStatus::Staged
|
||||
{
|
||||
self.bulk_staging = bulk_staging;
|
||||
}
|
||||
|
||||
self.select_first_entry_if_none(cx);
|
||||
|
||||
let suggested_commit_message = self.suggest_commit_message(cx);
|
||||
@@ -3743,7 +3763,7 @@ impl GitPanel {
|
||||
|
||||
for ix in range {
|
||||
match &this.entries.get(ix) {
|
||||
Some(GitListEntry::GitStatusEntry(entry)) => {
|
||||
Some(GitListEntry::Status(entry)) => {
|
||||
items.push(this.render_entry(
|
||||
ix,
|
||||
entry,
|
||||
@@ -4000,8 +4020,6 @@ impl GitPanel {
|
||||
let marked = self.marked_entries.contains(&ix);
|
||||
let status_style = GitPanelSettings::get_global(cx).status_style;
|
||||
let status = entry.status;
|
||||
let modifiers = self.current_modifiers;
|
||||
let shift_held = modifiers.shift;
|
||||
|
||||
let has_conflict = status.is_conflicted();
|
||||
let is_modified = status.is_modified();
|
||||
@@ -4120,12 +4138,6 @@ impl GitPanel {
|
||||
cx.stop_propagation();
|
||||
},
|
||||
)
|
||||
// .on_secondary_mouse_down(cx.listener(
|
||||
// move |this, event: &MouseDownEvent, window, cx| {
|
||||
// this.deploy_entry_context_menu(event.position, ix, window, cx);
|
||||
// cx.stop_propagation();
|
||||
// },
|
||||
// ))
|
||||
.child(
|
||||
div()
|
||||
.id(checkbox_wrapper_id)
|
||||
@@ -4137,46 +4149,35 @@ impl GitPanel {
|
||||
.disabled(!has_write_access)
|
||||
.fill()
|
||||
.elevation(ElevationIndex::Surface)
|
||||
.on_click({
|
||||
.on_click_ext({
|
||||
let entry = entry.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
if !has_write_access {
|
||||
return;
|
||||
}
|
||||
this.toggle_staged_for_entry(
|
||||
&GitListEntry::GitStatusEntry(entry.clone()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.stop_propagation();
|
||||
})
|
||||
let this = cx.weak_entity();
|
||||
move |_, click, window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
if !has_write_access {
|
||||
return;
|
||||
}
|
||||
if click.modifiers().shift {
|
||||
this.stage_bulk(ix, cx);
|
||||
} else {
|
||||
this.toggle_staged_for_entry(
|
||||
&GitListEntry::Status(entry.clone()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.tooltip(move |window, cx| {
|
||||
let is_staged = entry_staging.is_fully_staged();
|
||||
|
||||
let action = if is_staged { "Unstage" } else { "Stage" };
|
||||
let tooltip_name = if shift_held {
|
||||
format!("{} section", action)
|
||||
} else {
|
||||
action.to_string()
|
||||
};
|
||||
let tooltip_name = action.to_string();
|
||||
|
||||
let meta = if shift_held {
|
||||
format!(
|
||||
"Release shift to {} single entry",
|
||||
action.to_lowercase()
|
||||
)
|
||||
} else {
|
||||
format!("Shift click to {} section", action.to_lowercase())
|
||||
};
|
||||
|
||||
Tooltip::with_meta(
|
||||
tooltip_name,
|
||||
Some(&ToggleStaged),
|
||||
meta,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
Tooltip::for_action(tooltip_name, &ToggleStaged, window, cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -4242,6 +4243,41 @@ impl GitPanel {
|
||||
panel
|
||||
})
|
||||
}
|
||||
|
||||
fn stage_bulk(&mut self, mut index: usize, cx: &mut Context<'_, Self>) {
|
||||
let Some(op) = self.bulk_staging.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(mut anchor_index) = self.entry_by_path(&op.anchor, cx) else {
|
||||
return;
|
||||
};
|
||||
if let Some(entry) = self.entries.get(index)
|
||||
&& let Some(entry) = entry.status_entry()
|
||||
{
|
||||
self.set_bulk_staging_anchor(entry.repo_path.clone(), cx);
|
||||
}
|
||||
if index < anchor_index {
|
||||
std::mem::swap(&mut index, &mut anchor_index);
|
||||
}
|
||||
let entries = self
|
||||
.entries
|
||||
.get(anchor_index..=index)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter_map(|entry| entry.status_entry().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
self.change_file_stage(true, entries, cx);
|
||||
}
|
||||
|
||||
fn set_bulk_staging_anchor(&mut self, path: RepoPath, cx: &mut Context<'_, GitPanel>) {
|
||||
let Some(repo) = self.active_repository.as_ref() else {
|
||||
return;
|
||||
};
|
||||
self.bulk_staging = Some(BulkStaging {
|
||||
repo_id: repo.read(cx).id,
|
||||
anchor: path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
|
||||
@@ -4279,9 +4315,9 @@ impl Render for GitPanel {
|
||||
.id("git_panel")
|
||||
.key_context(self.dispatch_context(window, cx))
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
|
||||
.when(has_write_access && !project.is_read_only(cx), |this| {
|
||||
this.on_action(cx.listener(Self::toggle_staged_for_selected))
|
||||
.on_action(cx.listener(Self::stage_range))
|
||||
.on_action(cx.listener(GitPanel::commit))
|
||||
.on_action(cx.listener(GitPanel::amend))
|
||||
.on_action(cx.listener(GitPanel::cancel))
|
||||
@@ -4953,7 +4989,7 @@ impl Component for PanelRepoFooter {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use git::status::StatusCode;
|
||||
use git::status::{StatusCode, UnmergedStatus, UnmergedStatusCode};
|
||||
use gpui::{TestAppContext, VisualTestContext};
|
||||
use project::{FakeFs, WorktreeSettings};
|
||||
use serde_json::json;
|
||||
@@ -5052,13 +5088,13 @@ mod tests {
|
||||
GitListEntry::Header(GitHeaderEntry {
|
||||
header: Section::Tracked
|
||||
}),
|
||||
GitListEntry::GitStatusEntry(GitStatusEntry {
|
||||
GitListEntry::Status(GitStatusEntry {
|
||||
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
|
||||
repo_path: "crates/gpui/gpui.rs".into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
staging: StageStatus::Unstaged,
|
||||
}),
|
||||
GitListEntry::GitStatusEntry(GitStatusEntry {
|
||||
GitListEntry::Status(GitStatusEntry {
|
||||
abs_path: path!("/root/zed/crates/util/util.rs").into(),
|
||||
repo_path: "crates/util/util.rs".into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
@@ -5067,54 +5103,6 @@ mod tests {
|
||||
],
|
||||
);
|
||||
|
||||
// TODO(cole) restore this once repository deduplication is implemented properly.
|
||||
//cx.update_window_entity(&panel, |panel, window, cx| {
|
||||
// panel.select_last(&Default::default(), window, cx);
|
||||
// assert_eq!(panel.selected_entry, Some(2));
|
||||
// panel.open_diff(&Default::default(), window, cx);
|
||||
//});
|
||||
//cx.run_until_parked();
|
||||
|
||||
//let worktree_roots = workspace.update(cx, |workspace, cx| {
|
||||
// workspace
|
||||
// .worktrees(cx)
|
||||
// .map(|worktree| worktree.read(cx).abs_path())
|
||||
// .collect::<Vec<_>>()
|
||||
//});
|
||||
//pretty_assertions::assert_eq!(
|
||||
// worktree_roots,
|
||||
// vec![
|
||||
// Path::new(path!("/root/zed/crates/gpui")).into(),
|
||||
// Path::new(path!("/root/zed/crates/util/util.rs")).into(),
|
||||
// ]
|
||||
//);
|
||||
|
||||
//project.update(cx, |project, cx| {
|
||||
// let git_store = project.git_store().read(cx);
|
||||
// // The repo that comes from the single-file worktree can't be selected through the UI.
|
||||
// let filtered_entries = filtered_repository_entries(git_store, cx)
|
||||
// .iter()
|
||||
// .map(|repo| repo.read(cx).worktree_abs_path.clone())
|
||||
// .collect::<Vec<_>>();
|
||||
// assert_eq!(
|
||||
// filtered_entries,
|
||||
// [Path::new(path!("/root/zed/crates/gpui")).into()]
|
||||
// );
|
||||
// // But we can select it artificially here.
|
||||
// let repo_from_single_file_worktree = git_store
|
||||
// .repositories()
|
||||
// .values()
|
||||
// .find(|repo| {
|
||||
// repo.read(cx).worktree_abs_path.as_ref()
|
||||
// == Path::new(path!("/root/zed/crates/util/util.rs"))
|
||||
// })
|
||||
// .unwrap()
|
||||
// .clone();
|
||||
|
||||
// // Paths still make sense when we somehow activate a repo that comes from a single-file worktree.
|
||||
// repo_from_single_file_worktree.update(cx, |repo, cx| repo.set_as_active_repository(cx));
|
||||
//});
|
||||
|
||||
let handle = cx.update_window_entity(&panel, |panel, _, _| {
|
||||
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
|
||||
});
|
||||
@@ -5127,13 +5115,13 @@ mod tests {
|
||||
GitListEntry::Header(GitHeaderEntry {
|
||||
header: Section::Tracked
|
||||
}),
|
||||
GitListEntry::GitStatusEntry(GitStatusEntry {
|
||||
GitListEntry::Status(GitStatusEntry {
|
||||
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
|
||||
repo_path: "crates/gpui/gpui.rs".into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
staging: StageStatus::Unstaged,
|
||||
}),
|
||||
GitListEntry::GitStatusEntry(GitStatusEntry {
|
||||
GitListEntry::Status(GitStatusEntry {
|
||||
abs_path: path!("/root/zed/crates/util/util.rs").into(),
|
||||
repo_path: "crates/util/util.rs".into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
@@ -5142,4 +5130,196 @@ mod tests {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_bulk_staging(cx: &mut TestAppContext) {
|
||||
use GitListEntry::*;
|
||||
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"project": {
|
||||
".git": {},
|
||||
"src": {
|
||||
"main.rs": "fn main() {}",
|
||||
"lib.rs": "pub fn hello() {}",
|
||||
"utils.rs": "pub fn util() {}"
|
||||
},
|
||||
"tests": {
|
||||
"test.rs": "fn test() {}"
|
||||
},
|
||||
"new_file.txt": "new content",
|
||||
"another_new.rs": "// new file",
|
||||
"conflict.txt": "conflicted content"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_status_for_repo(
|
||||
Path::new(path!("/root/project/.git")),
|
||||
&[
|
||||
(Path::new("src/main.rs"), StatusCode::Modified.worktree()),
|
||||
(Path::new("src/lib.rs"), StatusCode::Modified.worktree()),
|
||||
(Path::new("tests/test.rs"), StatusCode::Modified.worktree()),
|
||||
(Path::new("new_file.txt"), FileStatus::Untracked),
|
||||
(Path::new("another_new.rs"), FileStatus::Untracked),
|
||||
(Path::new("src/utils.rs"), FileStatus::Untracked),
|
||||
(
|
||||
Path::new("conflict.txt"),
|
||||
UnmergedStatus {
|
||||
first_head: UnmergedStatusCode::Updated,
|
||||
second_head: UnmergedStatusCode::Updated,
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await;
|
||||
let workspace =
|
||||
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
cx.read(|cx| {
|
||||
project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.nth(0)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.as_local()
|
||||
.unwrap()
|
||||
.scan_complete()
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let panel = workspace.update(cx, GitPanel::new).unwrap();
|
||||
|
||||
let handle = cx.update_window_entity(&panel, |panel, _, _| {
|
||||
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
|
||||
});
|
||||
cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
|
||||
handle.await;
|
||||
|
||||
let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
|
||||
#[rustfmt::skip]
|
||||
pretty_assertions::assert_matches!(
|
||||
entries.as_slice(),
|
||||
&[
|
||||
Header(GitHeaderEntry { header: Section::Conflict }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||
Header(GitHeaderEntry { header: Section::Tracked }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||
Header(GitHeaderEntry { header: Section::New }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||
],
|
||||
);
|
||||
|
||||
let second_status_entry = entries[3].clone();
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel.toggle_staged_for_entry(&second_status_entry, window, cx);
|
||||
});
|
||||
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel.selected_entry = Some(7);
|
||||
panel.stage_range(&git::StageRange, window, cx);
|
||||
});
|
||||
|
||||
cx.read(|cx| {
|
||||
project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.nth(0)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.as_local()
|
||||
.unwrap()
|
||||
.scan_complete()
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let handle = cx.update_window_entity(&panel, |panel, _, _| {
|
||||
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
|
||||
});
|
||||
cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
|
||||
handle.await;
|
||||
|
||||
let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
|
||||
#[rustfmt::skip]
|
||||
pretty_assertions::assert_matches!(
|
||||
entries.as_slice(),
|
||||
&[
|
||||
Header(GitHeaderEntry { header: Section::Conflict }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||
Header(GitHeaderEntry { header: Section::Tracked }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||
Header(GitHeaderEntry { header: Section::New }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||
],
|
||||
);
|
||||
|
||||
let third_status_entry = entries[4].clone();
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel.toggle_staged_for_entry(&third_status_entry, window, cx);
|
||||
});
|
||||
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel.selected_entry = Some(9);
|
||||
panel.stage_range(&git::StageRange, window, cx);
|
||||
});
|
||||
|
||||
cx.read(|cx| {
|
||||
project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.nth(0)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.as_local()
|
||||
.unwrap()
|
||||
.scan_complete()
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let handle = cx.update_window_entity(&panel, |panel, _, _| {
|
||||
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
|
||||
});
|
||||
cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
|
||||
handle.await;
|
||||
|
||||
let entries = panel.read_with(cx, |panel, _| panel.entries.clone());
|
||||
#[rustfmt::skip]
|
||||
pretty_assertions::assert_matches!(
|
||||
entries.as_slice(),
|
||||
&[
|
||||
Header(GitHeaderEntry { header: Section::Conflict }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||
Header(GitHeaderEntry { header: Section::Tracked }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||
Header(GitHeaderEntry { header: Section::New }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||
Status(GitStatusEntry { staging: StageStatus::Staged, .. }),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ x11 = [
|
||||
"x11-clipboard",
|
||||
"filedescriptor",
|
||||
"open",
|
||||
"scap?/x11",
|
||||
]
|
||||
screen-capture = [
|
||||
"scap",
|
||||
@@ -150,6 +151,9 @@ metal.workspace = true
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies]
|
||||
pathfinder_geometry = "0.5"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies]
|
||||
scap = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
||||
# Always used
|
||||
flume = "0.11"
|
||||
@@ -168,7 +172,6 @@ cosmic-text = { version = "0.14.0", optional = true }
|
||||
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5474cfad4b719a72ec8ed2cb7327b2b01fd10568", features = [
|
||||
"source-fontconfig-dlopen",
|
||||
], optional = true }
|
||||
scap = { workspace = true, optional = true }
|
||||
|
||||
calloop = { version = "0.13.0" }
|
||||
filedescriptor = { version = "0.8.2", optional = true }
|
||||
|
||||
@@ -903,7 +903,7 @@ pub trait InteractiveElement: Sized {
|
||||
/// Apply the given style when the given data type is dragged over this element
|
||||
fn drag_over<S: 'static>(
|
||||
mut self,
|
||||
f: impl 'static + Fn(StyleRefinement, &S, &Window, &App) -> StyleRefinement,
|
||||
f: impl 'static + Fn(StyleRefinement, &S, &mut Window, &mut App) -> StyleRefinement,
|
||||
) -> Self {
|
||||
self.interactivity().drag_over_styles.push((
|
||||
TypeId::of::<S>(),
|
||||
|
||||
@@ -26,8 +26,13 @@ mod windows;
|
||||
|
||||
#[cfg(all(
|
||||
feature = "screen-capture",
|
||||
any(target_os = "linux", target_os = "freebsd"),
|
||||
any(feature = "wayland", feature = "x11"),
|
||||
any(
|
||||
target_os = "windows",
|
||||
all(
|
||||
any(target_os = "linux", target_os = "freebsd"),
|
||||
any(feature = "wayland", feature = "x11"),
|
||||
)
|
||||
)
|
||||
))]
|
||||
pub(crate) mod scap_screen_capture;
|
||||
|
||||
|
||||