Compare commits
1 Commits
update-deb
...
vim-wait-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1061d99cf2 |
8
.github/actions/run_tests_windows/action.yml
vendored
8
.github/actions/run_tests_windows/action.yml
vendored
@@ -10,8 +10,8 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install test runner
|
||||
shell: powershell
|
||||
- name: Install Rust
|
||||
shell: pwsh
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: cargo install cargo-nextest --locked
|
||||
|
||||
@@ -21,6 +21,6 @@ runs:
|
||||
node-version: "18"
|
||||
|
||||
- name: Run tests
|
||||
shell: powershell
|
||||
shell: pwsh
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: cargo nextest run --workspace --no-fail-fast
|
||||
run: cargo nextest run --workspace --no-fail-fast --config='profile.dev.debug="limited"'
|
||||
|
||||
110
.github/workflows/ci.yml
vendored
110
.github/workflows/ci.yml
vendored
@@ -373,6 +373,64 @@ jobs:
|
||||
if: always()
|
||||
run: rm -rf ./../.cargo
|
||||
|
||||
windows_clippy:
|
||||
timeout-minutes: 60
|
||||
name: (Windows) Run Clippy
|
||||
needs: [job_spec]
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
runs-on: windows-2025-16
|
||||
steps:
|
||||
# more info here:- https://github.com/rust-lang/cargo/issues/13020
|
||||
- name: Enable longer pathnames for git
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Create Dev Drive using ReFS
|
||||
run: ./script/setup-dev-driver.ps1
|
||||
|
||||
# actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone...
|
||||
- name: Copy Git Repo to Dev Drive
|
||||
run: |
|
||||
Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.ZED_WORKSPACE }}" -Recurse
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
workspaces: ${{ env.ZED_WORKSPACE }}
|
||||
cache-provider: "github"
|
||||
|
||||
- name: Configure CI
|
||||
run: |
|
||||
mkdir -p ${{ env.CARGO_HOME }} -ErrorAction Ignore
|
||||
cp ./.cargo/ci-config.toml ${{ env.CARGO_HOME }}/config.toml
|
||||
|
||||
- name: cargo clippy
|
||||
working-directory: ${{ env.ZED_WORKSPACE }}
|
||||
run: ./script/clippy.ps1
|
||||
|
||||
- name: Check dev drive space
|
||||
working-directory: ${{ env.ZED_WORKSPACE }}
|
||||
# `setup-dev-driver.ps1` creates a 100GB drive, with CI taking up ~45GB of the drive.
|
||||
run: ./script/exit-ci-if-dev-drive-is-full.ps1 95
|
||||
|
||||
# Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug.
|
||||
- name: Clean CI config file
|
||||
if: always()
|
||||
run: |
|
||||
if (Test-Path "${{ env.CARGO_HOME }}/config.toml") {
|
||||
Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force
|
||||
}
|
||||
|
||||
# Windows CI takes twice as long as our other platforms and fast github hosted runners are expensive.
|
||||
# But we still want to do CI, so let's only run tests on main and come back to this when we're
|
||||
# ready to self host our Windows CI (e.g. during the push for full Windows support)
|
||||
windows_tests:
|
||||
timeout-minutes: 60
|
||||
name: (Windows) Run Tests
|
||||
@@ -380,45 +438,51 @@ jobs:
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
runs-on: [self-hosted, Windows, X64]
|
||||
# Use bigger runners for PRs (speed); smaller for async (cost)
|
||||
runs-on: ${{ github.event_name == 'pull_request' && 'windows-2025-32' || 'windows-2025-16' }}
|
||||
steps:
|
||||
- name: Environment Setup
|
||||
run: |
|
||||
$RunnerDir = Split-Path -Parent $env:RUNNER_WORKSPACE
|
||||
Write-Output `
|
||||
"RUSTUP_HOME=$RunnerDir\.rustup" `
|
||||
"CARGO_HOME=$RunnerDir\.cargo" `
|
||||
"PATH=$RunnerDir\.cargo\bin;$env:PATH" `
|
||||
>> $env:GITHUB_ENV
|
||||
# more info here:- https://github.com/rust-lang/cargo/issues/13020
|
||||
- name: Enable longer pathnames for git
|
||||
run: git config --system core.longpaths true
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Setup Cargo and Rustup
|
||||
- name: Create Dev Drive using ReFS
|
||||
run: ./script/setup-dev-driver.ps1
|
||||
|
||||
# actions/checkout does not let us clone into anywhere outside ${{ github.workspace }}, so we have to copy the clone...
|
||||
- name: Copy Git Repo to Dev Drive
|
||||
run: |
|
||||
Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.ZED_WORKSPACE }}" -Recurse
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
workspaces: ${{ env.ZED_WORKSPACE }}
|
||||
cache-provider: "github"
|
||||
|
||||
- name: Configure CI
|
||||
run: |
|
||||
mkdir -p ${{ env.CARGO_HOME }} -ErrorAction Ignore
|
||||
cp ./.cargo/ci-config.toml ${{ env.CARGO_HOME }}/config.toml
|
||||
.\script\install-rustup.ps1
|
||||
|
||||
- name: cargo clippy
|
||||
run: |
|
||||
.\script\clippy.ps1
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests_windows
|
||||
with:
|
||||
working-directory: ${{ env.ZED_WORKSPACE }}
|
||||
|
||||
- name: Build Zed
|
||||
working-directory: ${{ env.ZED_WORKSPACE }}
|
||||
run: cargo build
|
||||
|
||||
- name: Limit target directory size
|
||||
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
||||
|
||||
# - name: Check dev drive space
|
||||
# working-directory: ${{ env.ZED_WORKSPACE }}
|
||||
# # `setup-dev-driver.ps1` creates a 100GB drive, with CI taking up ~45GB of the drive.
|
||||
# run: ./script/exit-ci-if-dev-drive-is-full.ps1 95
|
||||
- name: Check dev drive space
|
||||
working-directory: ${{ env.ZED_WORKSPACE }}
|
||||
# `setup-dev-driver.ps1` creates a 100GB drive, with CI taking up ~45GB of the drive.
|
||||
run: ./script/exit-ci-if-dev-drive-is-full.ps1 95
|
||||
|
||||
# Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug.
|
||||
- name: Clean CI config file
|
||||
@@ -441,6 +505,7 @@ jobs:
|
||||
- linux_tests
|
||||
- build_remote_server
|
||||
- macos_tests
|
||||
- windows_clippy
|
||||
- windows_tests
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
@@ -460,6 +525,7 @@ jobs:
|
||||
[[ "${{ needs.macos_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "macOS tests failed"; }
|
||||
[[ "${{ needs.linux_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Linux tests failed"; }
|
||||
[[ "${{ needs.windows_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows tests failed"; }
|
||||
[[ "${{ needs.windows_clippy.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows clippy failed"; }
|
||||
[[ "${{ needs.build_remote_server.result }}" != 'success' ]] && { RET_CODE=1; echo "Remote server build failed"; }
|
||||
# This check is intentionally disabled. See: https://github.com/zed-industries/zed/pull/28431
|
||||
# [[ "${{ needs.migration_checks.result }}" != 'success' ]] && { RET_CODE=1; echo "Migration Checks failed"; }
|
||||
|
||||
27
Cargo.lock
generated
27
Cargo.lock
generated
@@ -2041,7 +2041,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-graphics"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
|
||||
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
@@ -2074,7 +2074,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-macros"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
|
||||
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2084,7 +2084,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-util"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
|
||||
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
|
||||
dependencies = [
|
||||
"blade-graphics",
|
||||
"bytemuck",
|
||||
@@ -2822,7 +2822,6 @@ dependencies = [
|
||||
"collections",
|
||||
"credentials_provider",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"gpui_tokio",
|
||||
@@ -2835,7 +2834,6 @@ dependencies = [
|
||||
"paths",
|
||||
"postage",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
"rustls-pki-types",
|
||||
@@ -4242,7 +4240,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"serde_json",
|
||||
"task",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -4268,7 +4265,6 @@ dependencies = [
|
||||
name = "debugger_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"alacritty_terminal",
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
@@ -4278,6 +4274,7 @@ dependencies = [
|
||||
"db",
|
||||
"debugger_tools",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"file_icons",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
@@ -4294,7 +4291,6 @@ dependencies = [
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"settings",
|
||||
"shlex",
|
||||
"sysinfo",
|
||||
@@ -4302,8 +4298,6 @@ dependencies = [
|
||||
"tasks_ui",
|
||||
"terminal_view",
|
||||
"theme",
|
||||
"tree-sitter",
|
||||
"tree-sitter-json",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
@@ -4748,6 +4742,7 @@ dependencies = [
|
||||
"dap",
|
||||
"db",
|
||||
"emojis",
|
||||
"feature_flags",
|
||||
"file_icons",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
@@ -6218,7 +6213,6 @@ dependencies = [
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"watch",
|
||||
"windows 0.61.1",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
@@ -8947,7 +8941,6 @@ dependencies = [
|
||||
"http_client",
|
||||
"icons",
|
||||
"image",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"proto",
|
||||
"schemars",
|
||||
@@ -8983,7 +8976,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"gpui_tokio",
|
||||
"http_client",
|
||||
"language",
|
||||
"language_model",
|
||||
"lmstudio",
|
||||
"log",
|
||||
@@ -13224,7 +13216,6 @@ dependencies = [
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
"debug_adapter_extension",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"extension",
|
||||
"extension_host",
|
||||
@@ -13263,7 +13254,6 @@ dependencies = [
|
||||
"unindent",
|
||||
"util",
|
||||
"watch",
|
||||
"workspace",
|
||||
"worktree",
|
||||
"zlog",
|
||||
]
|
||||
@@ -14615,12 +14605,12 @@ dependencies = [
|
||||
"fs",
|
||||
"gpui",
|
||||
"log",
|
||||
"paths",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -15481,7 +15471,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -15952,7 +15941,6 @@ dependencies = [
|
||||
"theme",
|
||||
"thiserror 2.0.12",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"util",
|
||||
"windows 0.61.1",
|
||||
"workspace-hack",
|
||||
@@ -19956,7 +19944,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.193.0"
|
||||
version = "0.192.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
@@ -19996,6 +19984,7 @@ dependencies = [
|
||||
"extension",
|
||||
"extension_host",
|
||||
"extensions_ui",
|
||||
"feature_flags",
|
||||
"feedback",
|
||||
"file_finder",
|
||||
"fs",
|
||||
|
||||
@@ -417,9 +417,9 @@ aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
|
||||
base64 = "0.22"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
|
||||
@@ -1 +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-blocks-icon lucide-blocks"><rect width="7" height="7" x="14" y="3" rx="1"/><path d="M10 21V8a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H3"/></svg>
|
||||
<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-blocks"><rect width="7" height="7" x="14" y="3" rx="1"/><path d="M10 21V8a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H3"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 386 B After Width: | Height: | Size: 368 B |
@@ -56,9 +56,6 @@
|
||||
"[ shift-b": ["pane::ActivateItem", 0],
|
||||
"] space": "vim::InsertEmptyLineBelow",
|
||||
"[ space": "vim::InsertEmptyLineAbove",
|
||||
"[ e": "editor::MoveLineUp",
|
||||
"] e": "editor::MoveLineDown",
|
||||
|
||||
// Word motions
|
||||
"w": "vim::NextWordStart",
|
||||
"e": "vim::NextWordEnd",
|
||||
|
||||
@@ -27,11 +27,11 @@ If you are unsure how to fulfill the user's request, gather more information wit
|
||||
If appropriate, use tool calls to explore the current project, which contains the following root directories:
|
||||
|
||||
{{#each worktrees}}
|
||||
- `{{abs_path}}`
|
||||
- `{{root_name}}`
|
||||
{{/each}}
|
||||
|
||||
- Bias towards not asking the user for help if you can find the answer yourself.
|
||||
- When providing paths to tools, the path should always start with the name of a project root directory listed above.
|
||||
- When providing paths to tools, the path should always begin with a path that starts with a project root directory listed above.
|
||||
- Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path!
|
||||
{{# if (has_tool 'grep') }}
|
||||
- When looking for symbols in the project, prefer the `grep` tool.
|
||||
|
||||
@@ -400,13 +400,6 @@
|
||||
// 3. Never show the minimap:
|
||||
// "never" (default)
|
||||
"show": "never",
|
||||
// Where to show the minimap in the editor.
|
||||
// This setting can take two values:
|
||||
// 1. Show the minimap on the focused editor only:
|
||||
// "active_editor" (default)
|
||||
// 2. Show the minimap on all open editors:
|
||||
// "all_editors"
|
||||
"display_in": "active_editor",
|
||||
// When to show the minimap thumb.
|
||||
// This setting can take two values:
|
||||
// 1. Show the minimap thumb if the mouse is over the minimap:
|
||||
@@ -1045,19 +1038,6 @@
|
||||
// Automatically update Zed. This setting may be ignored on Linux if
|
||||
// installed through a package manager.
|
||||
"auto_update": true,
|
||||
// How to render LSP `textDocument/documentColor` colors in the editor.
|
||||
//
|
||||
// Possible values:
|
||||
//
|
||||
// 1. Do not query and render document colors.
|
||||
// "lsp_document_colors": "none",
|
||||
// 2. Render document colors as inlay hints near the color text (default).
|
||||
// "lsp_document_colors": "inlay",
|
||||
// 3. Draw a border around the color text.
|
||||
// "lsp_document_colors": "border",
|
||||
// 4. Draw a background behind the color text..
|
||||
// "lsp_document_colors": "background",
|
||||
"lsp_document_colors": "inlay",
|
||||
// Diagnostics configuration.
|
||||
"diagnostics": {
|
||||
// Whether to show the project diagnostics button in the status bar.
|
||||
|
||||
@@ -7,10 +7,7 @@ use gpui::{
|
||||
InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
|
||||
Styled, Transformation, Window, actions, percentage,
|
||||
};
|
||||
use language::{
|
||||
BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName,
|
||||
LanguageServerStatusUpdate, ServerHealth,
|
||||
};
|
||||
use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
|
||||
use project::{
|
||||
EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
|
||||
ProjectEnvironmentEvent,
|
||||
@@ -19,7 +16,6 @@ use project::{
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
collections::HashSet,
|
||||
fmt::Write,
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
@@ -34,9 +30,9 @@ const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0);
|
||||
actions!(activity_indicator, [ShowErrorMessage]);
|
||||
|
||||
pub enum Event {
|
||||
ShowStatus {
|
||||
server_name: LanguageServerName,
|
||||
status: SharedString,
|
||||
ShowError {
|
||||
server_name: SharedString,
|
||||
error: String,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -49,8 +45,8 @@ pub struct ActivityIndicator {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ServerStatus {
|
||||
name: LanguageServerName,
|
||||
status: LanguageServerStatusUpdate,
|
||||
name: SharedString,
|
||||
status: BinaryStatus,
|
||||
}
|
||||
|
||||
struct PendingWork<'a> {
|
||||
@@ -149,19 +145,19 @@ impl ActivityIndicator {
|
||||
});
|
||||
|
||||
cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
|
||||
Event::ShowStatus {
|
||||
server_name,
|
||||
status,
|
||||
} => {
|
||||
Event::ShowError { server_name, error } => {
|
||||
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
|
||||
let project = project.clone();
|
||||
let status = status.clone();
|
||||
let error = error.clone();
|
||||
let server_name = server_name.clone();
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
let buffer = create_buffer.await?;
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[(0..0, format!("Language server {server_name}:\n\n{status}"))],
|
||||
[(
|
||||
0..0,
|
||||
format!("Language server error: {}\n\n{}", server_name, error),
|
||||
)],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
@@ -170,10 +166,7 @@ impl ActivityIndicator {
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_buffer(buffer, Some(project.clone()), window, cx);
|
||||
editor.set_read_only(true);
|
||||
editor
|
||||
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
|
||||
})),
|
||||
None,
|
||||
true,
|
||||
@@ -192,34 +185,19 @@ impl ActivityIndicator {
|
||||
}
|
||||
|
||||
fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let mut status_message_shown = false;
|
||||
self.statuses.retain(|status| match &status.status {
|
||||
LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { error })
|
||||
if !status_message_shown =>
|
||||
{
|
||||
cx.emit(Event::ShowStatus {
|
||||
self.statuses.retain(|status| {
|
||||
if let BinaryStatus::Failed { error } = &status.status {
|
||||
cx.emit(Event::ShowError {
|
||||
server_name: status.name.clone(),
|
||||
status: SharedString::from(error),
|
||||
error: error.clone(),
|
||||
});
|
||||
status_message_shown = true;
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
LanguageServerStatusUpdate::Health(
|
||||
ServerHealth::Error | ServerHealth::Warning,
|
||||
status_string,
|
||||
) if !status_message_shown => match status_string {
|
||||
Some(error) => {
|
||||
cx.emit(Event::ShowStatus {
|
||||
server_name: status.name.clone(),
|
||||
status: error.clone(),
|
||||
});
|
||||
status_message_shown = true;
|
||||
false
|
||||
}
|
||||
None => false,
|
||||
},
|
||||
_ => true,
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn dismiss_error_message(
|
||||
@@ -289,52 +267,48 @@ impl ActivityIndicator {
|
||||
});
|
||||
}
|
||||
// Show any language server has pending activity.
|
||||
let mut pending_work = self.pending_language_server_work(cx);
|
||||
if let Some(PendingWork {
|
||||
progress_token,
|
||||
progress,
|
||||
..
|
||||
}) = pending_work.next()
|
||||
{
|
||||
let mut pending_work = self.pending_language_server_work(cx);
|
||||
if let Some(PendingWork {
|
||||
progress_token,
|
||||
progress,
|
||||
..
|
||||
}) = pending_work.next()
|
||||
{
|
||||
let mut message = progress
|
||||
.title
|
||||
.as_deref()
|
||||
.unwrap_or(progress_token)
|
||||
.to_string();
|
||||
let mut message = progress
|
||||
.title
|
||||
.as_deref()
|
||||
.unwrap_or(progress_token)
|
||||
.to_string();
|
||||
|
||||
if let Some(percentage) = progress.percentage {
|
||||
write!(&mut message, " ({}%)", percentage).unwrap();
|
||||
}
|
||||
|
||||
if let Some(progress_message) = progress.message.as_ref() {
|
||||
message.push_str(": ");
|
||||
message.push_str(progress_message);
|
||||
}
|
||||
|
||||
let additional_work_count = pending_work.count();
|
||||
if additional_work_count > 0 {
|
||||
write!(&mut message, " + {} more", additional_work_count).unwrap();
|
||||
}
|
||||
|
||||
return Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(delta)))
|
||||
},
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
message,
|
||||
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
|
||||
tooltip_message: None,
|
||||
});
|
||||
if let Some(percentage) = progress.percentage {
|
||||
write!(&mut message, " ({}%)", percentage).unwrap();
|
||||
}
|
||||
|
||||
if let Some(progress_message) = progress.message.as_ref() {
|
||||
message.push_str(": ");
|
||||
message.push_str(progress_message);
|
||||
}
|
||||
|
||||
let additional_work_count = pending_work.count();
|
||||
if additional_work_count > 0 {
|
||||
write!(&mut message, " + {} more", additional_work_count).unwrap();
|
||||
}
|
||||
|
||||
return Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
message,
|
||||
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(session) = self
|
||||
@@ -395,38 +369,14 @@ impl ActivityIndicator {
|
||||
let mut downloading = SmallVec::<[_; 3]>::new();
|
||||
let mut checking_for_update = SmallVec::<[_; 3]>::new();
|
||||
let mut failed = SmallVec::<[_; 3]>::new();
|
||||
let mut health_messages = SmallVec::<[_; 3]>::new();
|
||||
let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default();
|
||||
for status in &self.statuses {
|
||||
match &status.status {
|
||||
LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => {
|
||||
checking_for_update.push(status.name.clone());
|
||||
}
|
||||
LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) => {
|
||||
downloading.push(status.name.clone());
|
||||
}
|
||||
LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { .. }) => {
|
||||
failed.push(status.name.clone());
|
||||
}
|
||||
LanguageServerStatusUpdate::Binary(BinaryStatus::None) => {}
|
||||
LanguageServerStatusUpdate::Health(health, server_status) => match server_status {
|
||||
Some(server_status) => {
|
||||
health_messages.push((status.name.clone(), *health, server_status.clone()));
|
||||
}
|
||||
None => {
|
||||
servers_to_clear_statuses.insert(status.name.clone());
|
||||
}
|
||||
},
|
||||
match status.status {
|
||||
BinaryStatus::CheckingForUpdate => checking_for_update.push(status.name.clone()),
|
||||
BinaryStatus::Downloading => downloading.push(status.name.clone()),
|
||||
BinaryStatus::Failed { .. } => failed.push(status.name.clone()),
|
||||
BinaryStatus::None => {}
|
||||
}
|
||||
}
|
||||
self.statuses
|
||||
.retain(|status| !servers_to_clear_statuses.contains(&status.name));
|
||||
|
||||
health_messages.sort_by_key(|(_, health, _)| match health {
|
||||
ServerHealth::Error => 2,
|
||||
ServerHealth::Warning => 1,
|
||||
ServerHealth::Ok => 0,
|
||||
});
|
||||
|
||||
if !downloading.is_empty() {
|
||||
return Some(Content {
|
||||
@@ -507,7 +457,7 @@ impl ActivityIndicator {
|
||||
}),
|
||||
),
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.show_error_message(&ShowErrorMessage, window, cx)
|
||||
this.show_error_message(&Default::default(), window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
@@ -521,7 +471,7 @@ impl ActivityIndicator {
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Formatting failed: {failure}. Click to see logs."),
|
||||
message: format!("Formatting failed: {}. Click to see logs.", failure),
|
||||
on_click: Some(Arc::new(|indicator, window, cx| {
|
||||
indicator.project.update(cx, |project, cx| {
|
||||
project.reset_last_formatting_failure(cx);
|
||||
@@ -532,56 +482,6 @@ impl ActivityIndicator {
|
||||
});
|
||||
}
|
||||
|
||||
// Show any health messages for the language servers
|
||||
if let Some((server_name, health, message)) = health_messages.pop() {
|
||||
let health_str = match health {
|
||||
ServerHealth::Ok => format!("({server_name}) "),
|
||||
ServerHealth::Warning => format!("({server_name}) Warning: "),
|
||||
ServerHealth::Error => format!("({server_name}) Error: "),
|
||||
};
|
||||
let single_line_message = message
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let line = line.trim();
|
||||
if line.is_empty() { None } else { Some(line) }
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let mut altered_message = single_line_message != message;
|
||||
let truncated_message = truncate_and_trailoff(
|
||||
&single_line_message,
|
||||
MAX_MESSAGE_LEN.saturating_sub(health_str.len()),
|
||||
);
|
||||
altered_message |= truncated_message != single_line_message;
|
||||
let final_message = format!("{health_str}{truncated_message}");
|
||||
|
||||
let tooltip_message = if altered_message {
|
||||
Some(format!("{health_str}{message}"))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
return Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: final_message,
|
||||
tooltip_message,
|
||||
on_click: Some(Arc::new(move |activity_indicator, window, cx| {
|
||||
if altered_message {
|
||||
activity_indicator.show_error_message(&ShowErrorMessage, window, cx)
|
||||
} else {
|
||||
activity_indicator
|
||||
.statuses
|
||||
.retain(|status| status.name != server_name);
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
// Show any application auto-update info.
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
return match &updater.read(cx).status() {
|
||||
|
||||
@@ -750,7 +750,7 @@ struct EditingMessageState {
|
||||
editor: Entity<Editor>,
|
||||
context_strip: Entity<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
last_estimated_token_count: Option<u64>,
|
||||
last_estimated_token_count: Option<usize>,
|
||||
_subscriptions: [Subscription; 2],
|
||||
_update_token_count_task: Option<Task<()>>,
|
||||
}
|
||||
@@ -857,7 +857,7 @@ impl ActiveThread {
|
||||
}
|
||||
|
||||
/// Returns the editing message id and the estimated token count in the content
|
||||
pub fn editing_message_id(&self) -> Option<(MessageId, u64)> {
|
||||
pub fn editing_message_id(&self) -> Option<(MessageId, usize)> {
|
||||
self.editing_message
|
||||
.as_ref()
|
||||
.map(|(id, state)| (*id, state.last_estimated_token_count.unwrap_or(0)))
|
||||
@@ -1681,10 +1681,7 @@ impl ActiveThread {
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
editor::EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: 4,
|
||||
},
|
||||
editor::EditorMode::AutoHeight { max_lines: 4 },
|
||||
buffer,
|
||||
None,
|
||||
window,
|
||||
|
||||
@@ -586,7 +586,7 @@ impl AgentConfiguration {
|
||||
if let Some(server) =
|
||||
this.get_server(&context_server_id)
|
||||
{
|
||||
this.start_server(server, cx);
|
||||
this.start_server(server, cx).log_err();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use context_server::ContextServerCommand;
|
||||
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
|
||||
use project::project_settings::{ContextServerSettings, ProjectSettings};
|
||||
use project::project_settings::{ContextServerConfiguration, ProjectSettings};
|
||||
use serde_json::json;
|
||||
use settings::update_settings_file;
|
||||
use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
|
||||
use ui_input::SingleLineInput;
|
||||
@@ -80,12 +81,13 @@ impl AddContextServerModal {
|
||||
update_settings_file::<ProjectSettings>(fs.clone(), cx, |settings, _| {
|
||||
settings.context_servers.insert(
|
||||
name.into(),
|
||||
ContextServerSettings::Custom {
|
||||
command: ContextServerCommand {
|
||||
ContextServerConfiguration {
|
||||
command: Some(ContextServerCommand {
|
||||
path,
|
||||
args,
|
||||
env: None,
|
||||
},
|
||||
}),
|
||||
settings: Some(json!({})),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{
|
||||
context_server_store::{ContextServerStatus, ContextServerStore},
|
||||
project_settings::{ContextServerSettings, ProjectSettings},
|
||||
project_settings::{ContextServerConfiguration, ProjectSettings},
|
||||
};
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use theme::ThemeSettings;
|
||||
@@ -89,7 +89,7 @@ impl ConfigureContextServerModal {
|
||||
}),
|
||||
settings_validator,
|
||||
settings_editor: cx.new(|cx| {
|
||||
let mut editor = Editor::auto_height(1, 16, window, cx);
|
||||
let mut editor = Editor::auto_height(16, window, cx);
|
||||
editor.set_text(config.default_settings.trim(), window, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_soft_wrap_mode(
|
||||
@@ -175,9 +175,8 @@ impl ConfigureContextServerModal {
|
||||
let settings_changed = ProjectSettings::get_global(cx)
|
||||
.context_servers
|
||||
.get(&id.0)
|
||||
.map_or(true, |settings| match settings {
|
||||
ContextServerSettings::Custom { .. } => false,
|
||||
ContextServerSettings::Extension { settings } => settings != &settings_value,
|
||||
.map_or(true, |config| {
|
||||
config.settings.as_ref() != Some(&settings_value)
|
||||
});
|
||||
|
||||
let is_running = self.context_server_store.read(cx).status_for_server(&id)
|
||||
@@ -222,12 +221,17 @@ impl ConfigureContextServerModal {
|
||||
update_settings_file::<ProjectSettings>(workspace.read(cx).app_state().fs.clone(), cx, {
|
||||
let id = id.clone();
|
||||
|settings, _| {
|
||||
settings.context_servers.insert(
|
||||
id.0,
|
||||
ContextServerSettings::Extension {
|
||||
settings: settings_value,
|
||||
},
|
||||
);
|
||||
if let Some(server_config) = settings.context_servers.get_mut(&id.0) {
|
||||
server_config.settings = Some(settings_value);
|
||||
} else {
|
||||
settings.context_servers.insert(
|
||||
id.0,
|
||||
ContextServerConfiguration {
|
||||
settings: Some(settings_value),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -520,15 +520,10 @@ impl AgentPanel {
|
||||
});
|
||||
|
||||
let message_editor_subscription =
|
||||
cx.subscribe(&message_editor, |this, _, event, cx| match event {
|
||||
cx.subscribe(&message_editor, |_, _, event, cx| match event {
|
||||
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
||||
cx.notify();
|
||||
}
|
||||
MessageEditorEvent::ScrollThreadToBottom => {
|
||||
this.thread.update(cx, |thread, cx| {
|
||||
thread.scroll_to_bottom(cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let thread_id = thread.read(cx).id().clone();
|
||||
@@ -808,15 +803,10 @@ impl AgentPanel {
|
||||
self.message_editor.focus_handle(cx).focus(window);
|
||||
|
||||
let message_editor_subscription =
|
||||
cx.subscribe(&self.message_editor, |this, _, event, cx| match event {
|
||||
cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
|
||||
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
||||
cx.notify();
|
||||
}
|
||||
MessageEditorEvent::ScrollThreadToBottom => {
|
||||
this.thread.update(cx, |thread, cx| {
|
||||
thread.scroll_to_bottom(cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
self._active_thread_subscriptions = vec![
|
||||
@@ -1028,15 +1018,10 @@ impl AgentPanel {
|
||||
self.message_editor.focus_handle(cx).focus(window);
|
||||
|
||||
let message_editor_subscription =
|
||||
cx.subscribe(&self.message_editor, |this, _, event, cx| match event {
|
||||
cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
|
||||
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
||||
cx.notify();
|
||||
}
|
||||
MessageEditorEvent::ScrollThreadToBottom => {
|
||||
this.thread.update(cx, |thread, cx| {
|
||||
thread.scroll_to_bottom(cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
self._active_thread_subscriptions = vec![
|
||||
|
||||
@@ -214,7 +214,6 @@ fn search(
|
||||
&entry_candidates,
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
100,
|
||||
&Arc::new(AtomicBool::default()),
|
||||
executor,
|
||||
|
||||
@@ -307,7 +307,6 @@ pub(crate) fn search_symbols(
|
||||
&visible_match_candidates,
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
MAX_MATCHES,
|
||||
&cancellation_flag,
|
||||
cx.background_executor().clone(),
|
||||
@@ -316,7 +315,6 @@ pub(crate) fn search_symbols(
|
||||
&external_match_candidates,
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
|
||||
&cancellation_flag,
|
||||
cx.background_executor().clone(),
|
||||
|
||||
@@ -342,7 +342,6 @@ pub(crate) fn search_threads(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
100,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
|
||||
@@ -765,6 +765,9 @@ impl InlineAssistant {
|
||||
PromptEditorEvent::CancelRequested => {
|
||||
self.finish_assist(assist_id, true, window, cx);
|
||||
}
|
||||
PromptEditorEvent::DismissRequested => {
|
||||
self.dismiss_assist(assist_id, window, cx);
|
||||
}
|
||||
PromptEditorEvent::Resized { .. } => {
|
||||
// This only matters for the terminal inline assistant
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
|
||||
let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
|
||||
self.editor = cx.new(|cx| {
|
||||
let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
|
||||
let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx);
|
||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
||||
editor.set_placeholder_text("Add a prompt…", cx);
|
||||
editor.set_text(prompt, window, cx);
|
||||
@@ -403,7 +403,9 @@ impl<T: 'static> PromptEditor<T> {
|
||||
CodegenStatus::Idle => {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}
|
||||
CodegenStatus::Pending => {}
|
||||
CodegenStatus::Pending => {
|
||||
cx.emit(PromptEditorEvent::DismissRequested);
|
||||
}
|
||||
CodegenStatus::Done => {
|
||||
if self.edited_since_done {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
@@ -829,6 +831,7 @@ pub enum PromptEditorEvent {
|
||||
StopRequested,
|
||||
ConfirmRequested { execute: bool },
|
||||
CancelRequested,
|
||||
DismissRequested,
|
||||
Resized { height_in_lines: u8 },
|
||||
}
|
||||
|
||||
@@ -869,7 +872,6 @@ impl PromptEditor<BufferCodegen> {
|
||||
let prompt_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: Self::MAX_LINES as usize,
|
||||
},
|
||||
prompt_buffer,
|
||||
@@ -1048,7 +1050,6 @@ impl PromptEditor<TerminalCodegen> {
|
||||
let prompt_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: Self::MAX_LINES as usize,
|
||||
},
|
||||
prompt_buffer,
|
||||
|
||||
@@ -39,9 +39,7 @@ use proto::Plan;
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
Callout, Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*,
|
||||
};
|
||||
use ui::{Callout, Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use util::{ResultExt as _, maybe};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
@@ -76,12 +74,11 @@ pub struct MessageEditor {
|
||||
profile_selector: Entity<ProfileSelector>,
|
||||
edits_expanded: bool,
|
||||
editor_is_expanded: bool,
|
||||
last_estimated_token_count: Option<u64>,
|
||||
last_estimated_token_count: Option<usize>,
|
||||
update_token_count_task: Option<Task<()>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
const MIN_EDITOR_LINES: usize = 4;
|
||||
const MAX_EDITOR_LINES: usize = 8;
|
||||
|
||||
pub(crate) fn create_editor(
|
||||
@@ -105,7 +102,6 @@ pub(crate) fn create_editor(
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let mut editor = Editor::new(
|
||||
editor::EditorMode::AutoHeight {
|
||||
min_lines: MIN_EDITOR_LINES,
|
||||
max_lines: MAX_EDITOR_LINES,
|
||||
},
|
||||
buffer,
|
||||
@@ -257,7 +253,6 @@ impl MessageEditor {
|
||||
})
|
||||
} else {
|
||||
editor.set_mode(EditorMode::AutoHeight {
|
||||
min_lines: MIN_EDITOR_LINES,
|
||||
max_lines: MAX_EDITOR_LINES,
|
||||
})
|
||||
}
|
||||
@@ -301,7 +296,6 @@ impl MessageEditor {
|
||||
self.set_editor_is_expanded(false, cx);
|
||||
self.send_to_model(window, cx);
|
||||
|
||||
cx.emit(MessageEditorEvent::ScrollThreadToBottom);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -508,46 +502,6 @@ impl MessageEditor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_reject_file_changes(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.thread.read(cx).has_pending_edit_tool_uses() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
let buffer_snapshot = buffer.read(cx);
|
||||
let start = buffer_snapshot.anchor_before(Point::new(0, 0));
|
||||
let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point());
|
||||
thread
|
||||
.reject_edits_in_ranges(buffer, vec![start..end], cx)
|
||||
.detach();
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_accept_file_changes(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.thread.read(cx).has_pending_edit_tool_uses() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
let buffer_snapshot = buffer.read(cx);
|
||||
let start = buffer_snapshot.anchor_before(Point::new(0, 0));
|
||||
let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point());
|
||||
thread.keep_edits_in_range(buffer, start..end, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let thread = self.thread.read(cx);
|
||||
let model = thread.configured_model();
|
||||
@@ -717,39 +671,44 @@ impl MessageEditor {
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.gap_1()
|
||||
.gap_4()
|
||||
.when(is_editor_expanded, |this| {
|
||||
this.h(vh(0.8, window)).justify_between()
|
||||
})
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = TextSize::Small
|
||||
.rems(cx)
|
||||
.to_pixels(settings.agent_font_size(cx));
|
||||
let line_height = settings.buffer_line_height.value() * font_size;
|
||||
.child(
|
||||
v_flex()
|
||||
.min_h_16()
|
||||
.when(is_editor_expanded, |this| this.h_full())
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = TextSize::Small
|
||||
.rems(cx)
|
||||
.to_pixels(settings.agent_font_size(cx));
|
||||
let line_height = settings.buffer_line_height.value() * font_size;
|
||||
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
};
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: editor_bg_color,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.into_any()
|
||||
})
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: editor_bg_color,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.into_any()
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
@@ -907,7 +866,7 @@ impl MessageEditor {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_edits_bar(
|
||||
fn render_changed_buffers(
|
||||
&self,
|
||||
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
|
||||
window: &mut Window,
|
||||
@@ -1031,7 +990,7 @@ impl MessageEditor {
|
||||
this.handle_review_click(window, cx)
|
||||
})),
|
||||
)
|
||||
.child(Divider::vertical().color(DividerColor::Border))
|
||||
.child(ui::Divider::vertical().color(ui::DividerColor::Border))
|
||||
.child(
|
||||
Button::new("reject-all-changes", "Reject All")
|
||||
.label_size(LabelSize::Small)
|
||||
@@ -1081,7 +1040,7 @@ impl MessageEditor {
|
||||
let file = buffer.read(cx).file()?;
|
||||
let path = file.path();
|
||||
|
||||
let file_path = path.parent().and_then(|parent| {
|
||||
let parent_label = path.parent().and_then(|parent| {
|
||||
let parent_str = parent.to_string_lossy();
|
||||
|
||||
if parent_str.is_empty() {
|
||||
@@ -1100,7 +1059,7 @@ impl MessageEditor {
|
||||
}
|
||||
});
|
||||
|
||||
let file_name = path.file_name().map(|name| {
|
||||
let name_label = path.file_name().map(|name| {
|
||||
Label::new(name.to_string_lossy().to_string())
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx)
|
||||
@@ -1115,22 +1074,36 @@ impl MessageEditor {
|
||||
.size(IconSize::Small)
|
||||
});
|
||||
|
||||
let hover_color = cx
|
||||
.theme()
|
||||
.colors()
|
||||
.element_background
|
||||
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
|
||||
|
||||
let overlay_gradient = linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(editor_bg_color, 1.),
|
||||
linear_color_stop(editor_bg_color.opacity(0.2), 0.),
|
||||
);
|
||||
|
||||
let overlay_gradient_hover = linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(hover_color, 1.),
|
||||
linear_color_stop(hover_color.opacity(0.2), 0.),
|
||||
);
|
||||
|
||||
let element = h_flex()
|
||||
.group("edited-code")
|
||||
.id(("file-container", index))
|
||||
.cursor_pointer()
|
||||
.relative()
|
||||
.py_1()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.bg(editor_bg_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.hover(|style| style.bg(hover_color))
|
||||
.when(index < changed_buffers.len() - 1, |parent| {
|
||||
parent.border_color(border_color).border_b_1()
|
||||
})
|
||||
@@ -1145,75 +1118,47 @@ impl MessageEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.children(file_name)
|
||||
.children(file_path),
|
||||
.children(name_label)
|
||||
.children(parent_label),
|
||||
), // TODO: Implement line diff
|
||||
// .child(Label::new("+").color(Color::Created))
|
||||
// .child(Label::new("-").color(Color::Deleted)),
|
||||
)
|
||||
.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.handle_file_click(
|
||||
buffer.clone(),
|
||||
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();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.handle_reject_file_changes(
|
||||
buffer.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("accept-file", "Accept")
|
||||
.label_size(LabelSize::Small)
|
||||
.disabled(pending_edits)
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.handle_accept_file_changes(
|
||||
buffer.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}),
|
||||
),
|
||||
div().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.handle_file_click(
|
||||
buffer.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("gradient-overlay")
|
||||
.absolute()
|
||||
.h_full()
|
||||
.h_5_6()
|
||||
.w_12()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.right(px(152.))
|
||||
.bg(overlay_gradient),
|
||||
);
|
||||
.right(px(52.))
|
||||
.bg(overlay_gradient)
|
||||
.group_hover("edited-code", |style| {
|
||||
style.bg(overlay_gradient_hover)
|
||||
}),
|
||||
)
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.handle_file_click(buffer.clone(), window, cx);
|
||||
})
|
||||
});
|
||||
|
||||
Some(element)
|
||||
},
|
||||
@@ -1335,7 +1280,7 @@ impl MessageEditor {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn last_estimated_token_count(&self) -> Option<u64> {
|
||||
pub fn last_estimated_token_count(&self) -> Option<usize> {
|
||||
self.last_estimated_token_count
|
||||
}
|
||||
|
||||
@@ -1511,7 +1456,6 @@ impl EventEmitter<MessageEditorEvent> for MessageEditor {}
|
||||
pub enum MessageEditorEvent {
|
||||
EstimatedTokenCount,
|
||||
Changed,
|
||||
ScrollThreadToBottom,
|
||||
}
|
||||
|
||||
impl Focusable for MessageEditor {
|
||||
@@ -1539,7 +1483,7 @@ impl Render for MessageEditor {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.when(changed_buffers.len() > 0, |parent| {
|
||||
parent.child(self.render_edits_bar(&changed_buffers, window, cx))
|
||||
parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
|
||||
})
|
||||
.child(self.render_editor(window, cx))
|
||||
.children({
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
[The following is an auto-generated notification; do not reply]
|
||||
|
||||
These files have changed since the last read:
|
||||
These files changed since last read:
|
||||
|
||||
@@ -167,6 +167,9 @@ impl TerminalInlineAssistant {
|
||||
PromptEditorEvent::CancelRequested => {
|
||||
self.finish_assist(assist_id, true, false, window, cx);
|
||||
}
|
||||
PromptEditorEvent::DismissRequested => {
|
||||
self.dismiss_assist(assist_id, window, cx);
|
||||
}
|
||||
PromptEditorEvent::Resized { height_in_lines } => {
|
||||
self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, window, cx);
|
||||
}
|
||||
|
||||
@@ -272,8 +272,8 @@ impl DetailedSummaryState {
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct TotalTokenUsage {
|
||||
pub total: u64,
|
||||
pub max: u64,
|
||||
pub total: usize,
|
||||
pub max: usize,
|
||||
}
|
||||
|
||||
impl TotalTokenUsage {
|
||||
@@ -299,7 +299,7 @@ impl TotalTokenUsage {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&self, tokens: u64) -> TotalTokenUsage {
|
||||
pub fn add(&self, tokens: usize) -> TotalTokenUsage {
|
||||
TotalTokenUsage {
|
||||
total: self.total + tokens,
|
||||
max: self.max,
|
||||
@@ -396,7 +396,7 @@ pub struct ExceededWindowError {
|
||||
/// Model used when last message exceeded context window
|
||||
model_id: LanguageModelId,
|
||||
/// Token count including last message
|
||||
token_count: u64,
|
||||
token_count: usize,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
@@ -1389,7 +1389,7 @@ impl Thread {
|
||||
request.messages[message_ix_to_cache].cache = true;
|
||||
}
|
||||
|
||||
self.attach_tracked_files_state(&mut request.messages, cx);
|
||||
self.attached_tracked_files_state(&mut request.messages, cx);
|
||||
|
||||
request.tools = available_tools;
|
||||
request.mode = if model.supports_max_mode() {
|
||||
@@ -1453,57 +1453,43 @@ impl Thread {
|
||||
request
|
||||
}
|
||||
|
||||
fn attach_tracked_files_state(
|
||||
fn attached_tracked_files_state(
|
||||
&self,
|
||||
messages: &mut Vec<LanguageModelRequestMessage>,
|
||||
cx: &App,
|
||||
) {
|
||||
let mut stale_files = String::new();
|
||||
const STALE_FILES_HEADER: &str = include_str!("./prompts/stale_files_prompt_header.txt");
|
||||
|
||||
let mut stale_message = String::new();
|
||||
|
||||
let action_log = self.action_log.read(cx);
|
||||
|
||||
for stale_file in action_log.stale_buffers(cx) {
|
||||
if let Some(file) = stale_file.read(cx).file() {
|
||||
writeln!(&mut stale_files, "- {}", file.path().display()).ok();
|
||||
let Some(file) = stale_file.read(cx).file() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if stale_message.is_empty() {
|
||||
write!(&mut stale_message, "{}\n", STALE_FILES_HEADER.trim()).ok();
|
||||
}
|
||||
|
||||
writeln!(&mut stale_message, "- {}", file.path().display()).ok();
|
||||
}
|
||||
|
||||
if stale_files.is_empty() {
|
||||
return;
|
||||
let mut content = Vec::with_capacity(2);
|
||||
|
||||
if !stale_message.is_empty() {
|
||||
content.push(stale_message.into());
|
||||
}
|
||||
|
||||
// NOTE: Changes to this prompt require a symmetric update in the LLM Worker
|
||||
const STALE_FILES_HEADER: &str = include_str!("./prompts/stale_files_prompt_header.txt");
|
||||
let content = MessageContent::Text(
|
||||
format!("{STALE_FILES_HEADER}{stale_files}").replace("\r\n", "\n"),
|
||||
);
|
||||
if !content.is_empty() {
|
||||
let context_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content,
|
||||
cache: false,
|
||||
};
|
||||
|
||||
// Insert our message before the last Assistant message.
|
||||
// Inserting it to the tail distracts the agent too much
|
||||
let insert_position = messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rfind(|(_, message)| message.role == Role::Assistant)
|
||||
.map_or(messages.len(), |(i, _)| i);
|
||||
|
||||
let request_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![content],
|
||||
cache: false,
|
||||
};
|
||||
|
||||
messages.insert(insert_position, request_message);
|
||||
|
||||
// It makes no sense to cache messages after this one because
|
||||
// the cache is invalidated when this message is gone.
|
||||
// Move the cache marker before this message.
|
||||
let has_cached_messages_after = messages
|
||||
.iter()
|
||||
.skip(insert_position + 1)
|
||||
.any(|message| message.cache);
|
||||
|
||||
if has_cached_messages_after {
|
||||
messages[insert_position - 1].cache = true;
|
||||
messages.push(context_message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2769,7 +2755,7 @@ impl Thread {
|
||||
.unwrap_or_default();
|
||||
|
||||
TotalTokenUsage {
|
||||
total: token_usage.total_tokens(),
|
||||
total: token_usage.total_tokens() as usize,
|
||||
max,
|
||||
}
|
||||
}
|
||||
@@ -2791,7 +2777,7 @@ impl Thread {
|
||||
let total = self
|
||||
.token_usage_at_last_message()
|
||||
.unwrap_or_default()
|
||||
.total_tokens();
|
||||
.total_tokens() as usize;
|
||||
|
||||
Some(TotalTokenUsage { total, max })
|
||||
}
|
||||
@@ -3309,24 +3295,12 @@ fn main() {{
|
||||
assert_eq!(last_message.role, Role::User);
|
||||
|
||||
// Check the exact content of the message
|
||||
let expected_content = "[The following is an auto-generated notification; do not reply]
|
||||
|
||||
These files have changed since the last read:
|
||||
- code.rs
|
||||
";
|
||||
let expected_content = "These files changed since last read:\n- code.rs\n";
|
||||
assert_eq!(
|
||||
last_message.string_contents(),
|
||||
expected_content,
|
||||
"Last message should be exactly the stale buffer notification"
|
||||
);
|
||||
|
||||
// The message before the notification should be cached
|
||||
let index = new_request.messages.len() - 2;
|
||||
let previous_message = new_request.messages.get(index).unwrap();
|
||||
assert!(
|
||||
previous_message.cache,
|
||||
"Message before the stale buffer notification should be cached"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -224,7 +224,6 @@ impl ThreadHistory {
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
MAX_MATCHES,
|
||||
&Default::default(),
|
||||
executor,
|
||||
|
||||
@@ -305,19 +305,17 @@ impl ThreadStore {
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
|
||||
let tree = worktree.read(cx);
|
||||
let root_name = tree.root_name().into();
|
||||
let abs_path = tree.abs_path();
|
||||
|
||||
let mut context = WorktreeContext {
|
||||
root_name,
|
||||
abs_path,
|
||||
rules_file: None,
|
||||
};
|
||||
let root_name = worktree.read(cx).root_name().into();
|
||||
|
||||
let rules_task = Self::load_worktree_rules_file(worktree, project, cx);
|
||||
let Some(rules_task) = rules_task else {
|
||||
return Task::ready((context, None));
|
||||
return Task::ready((
|
||||
WorktreeContext {
|
||||
root_name,
|
||||
rules_file: None,
|
||||
},
|
||||
None,
|
||||
));
|
||||
};
|
||||
|
||||
cx.spawn(async move |_| {
|
||||
@@ -330,8 +328,11 @@ impl ThreadStore {
|
||||
}),
|
||||
),
|
||||
};
|
||||
context.rules_file = rules_file;
|
||||
(context, rules_file_error)
|
||||
let worktree_info = WorktreeContext {
|
||||
root_name,
|
||||
rules_file,
|
||||
};
|
||||
(worktree_info, rules_file_error)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -340,12 +341,12 @@ impl ThreadStore {
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Result<RulesFileContext>>> {
|
||||
let worktree = worktree.read(cx);
|
||||
let worktree_id = worktree.id();
|
||||
let worktree_ref = worktree.read(cx);
|
||||
let worktree_id = worktree_ref.id();
|
||||
let selected_rules_file = RULES_FILE_NAMES
|
||||
.into_iter()
|
||||
.filter_map(|name| {
|
||||
worktree
|
||||
worktree_ref
|
||||
.entry_for_path(name)
|
||||
.filter(|entry| entry.is_file())
|
||||
.map(|entry| entry.path.clone())
|
||||
|
||||
@@ -427,7 +427,7 @@ impl ToolUseState {
|
||||
|
||||
// Protect from overly large output
|
||||
let tool_output_limit = configured_model
|
||||
.map(|model| model.model.max_token_count() as usize * BYTES_PER_TOKEN_ESTIMATE)
|
||||
.map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE)
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
let content = match tool_result {
|
||||
|
||||
@@ -386,9 +386,7 @@ impl AgentSettingsContent {
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AgentProviderContentV1::LmStudio {
|
||||
default_model: Some(lmstudio::Model::new(
|
||||
&model, None, None, false, false,
|
||||
)),
|
||||
default_model: Some(lmstudio::Model::new(&model, None, None, false)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com";
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
pub struct AnthropicModelCacheConfiguration {
|
||||
pub min_total_token: u64,
|
||||
pub min_total_token: usize,
|
||||
pub should_speculate: bool,
|
||||
pub max_cache_anchors: usize,
|
||||
}
|
||||
@@ -68,14 +68,14 @@ pub enum Model {
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
name: String,
|
||||
max_tokens: u64,
|
||||
max_tokens: usize,
|
||||
/// The name displayed in the UI, such as in the assistant panel model dropdown menu.
|
||||
display_name: Option<String>,
|
||||
/// Override this model with a different Anthropic model for tool calls.
|
||||
tool_override: Option<String>,
|
||||
/// Indicates whether this custom model supports caching.
|
||||
cache_configuration: Option<AnthropicModelCacheConfiguration>,
|
||||
max_output_tokens: Option<u64>,
|
||||
max_output_tokens: Option<u32>,
|
||||
default_temperature: Option<f32>,
|
||||
#[serde(default)]
|
||||
extra_beta_headers: Vec<String>,
|
||||
@@ -211,7 +211,7 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_token_count(&self) -> u64 {
|
||||
pub fn max_token_count(&self) -> usize {
|
||||
match self {
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
@@ -228,7 +228,7 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_output_tokens(&self) -> u64 {
|
||||
pub fn max_output_tokens(&self) -> u32 {
|
||||
match self {
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
@@ -693,7 +693,7 @@ pub enum StringOrContents {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Request {
|
||||
pub model: String,
|
||||
pub max_tokens: u64,
|
||||
pub max_tokens: u32,
|
||||
pub messages: Vec<Message>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub tools: Vec<Tool>,
|
||||
@@ -730,13 +730,13 @@ pub struct Metadata {
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Usage {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub input_tokens: Option<u64>,
|
||||
pub input_tokens: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub output_tokens: Option<u64>,
|
||||
pub output_tokens: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_tokens: Option<u64>,
|
||||
pub cache_creation_input_tokens: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_tokens: Option<u64>,
|
||||
pub cache_read_input_tokens: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -846,7 +846,7 @@ impl ApiError {
|
||||
matches!(self.error_type.as_str(), "rate_limit_error")
|
||||
}
|
||||
|
||||
pub fn match_window_exceeded(&self) -> Option<u64> {
|
||||
pub fn match_window_exceeded(&self) -> Option<usize> {
|
||||
let Some(ApiErrorCode::InvalidRequestError) = self.code() else {
|
||||
return None;
|
||||
};
|
||||
@@ -855,12 +855,12 @@ impl ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_prompt_too_long(message: &str) -> Option<u64> {
|
||||
pub fn parse_prompt_too_long(message: &str) -> Option<usize> {
|
||||
message
|
||||
.strip_prefix("prompt is too long: ")?
|
||||
.split_once(" tokens")?
|
||||
.0
|
||||
.parse()
|
||||
.parse::<usize>()
|
||||
.ok()
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,11 @@ use gpui::{AsyncApp, BackgroundExecutor, Task};
|
||||
#[cfg(unix)]
|
||||
use smol::fs;
|
||||
#[cfg(unix)]
|
||||
use smol::net::unix::UnixListener;
|
||||
use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
|
||||
#[cfg(unix)]
|
||||
use util::{ResultExt as _, fs::make_file_executable, get_shell_safe_zed_path};
|
||||
use util::ResultExt as _;
|
||||
#[cfg(unix)]
|
||||
use util::get_shell_safe_zed_path;
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum AskPassResult {
|
||||
@@ -120,7 +122,7 @@ impl AskPassSession {
|
||||
shebang = "#!/bin/sh",
|
||||
);
|
||||
fs::write(&askpass_script_path, askpass_script).await?;
|
||||
make_file_executable(&askpass_script_path).await?;
|
||||
fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755)).await?;
|
||||
|
||||
Ok(Self {
|
||||
script_path: askpass_script_path,
|
||||
|
||||
@@ -678,7 +678,7 @@ pub struct AssistantContext {
|
||||
summary_task: Task<Option<()>>,
|
||||
completion_count: usize,
|
||||
pending_completions: Vec<PendingCompletion>,
|
||||
token_count: Option<u64>,
|
||||
token_count: Option<usize>,
|
||||
pending_token_count: Task<Option<()>>,
|
||||
pending_save: Task<Result<()>>,
|
||||
pending_cache_warming_task: Task<Option<()>>,
|
||||
@@ -1250,7 +1250,7 @@ impl AssistantContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn token_count(&self) -> Option<u64> {
|
||||
pub fn token_count(&self) -> Option<usize> {
|
||||
self.token_count
|
||||
}
|
||||
|
||||
|
||||
@@ -3121,12 +3121,12 @@ fn invoked_slash_command_fold_placeholder(
|
||||
|
||||
enum TokenState {
|
||||
NoTokensLeft {
|
||||
max_token_count: u64,
|
||||
token_count: u64,
|
||||
max_token_count: usize,
|
||||
token_count: usize,
|
||||
},
|
||||
HasMoreTokens {
|
||||
max_token_count: u64,
|
||||
token_count: u64,
|
||||
max_token_count: usize,
|
||||
token_count: usize,
|
||||
over_warn_threshold: bool,
|
||||
},
|
||||
}
|
||||
@@ -3139,7 +3139,9 @@ fn token_state(context: &Entity<AssistantContext>, cx: &App) -> Option<TokenStat
|
||||
.model;
|
||||
let token_count = context.read(cx).token_count()?;
|
||||
let max_token_count = model.max_token_count();
|
||||
let token_state = if max_token_count.saturating_sub(token_count) == 0 {
|
||||
|
||||
let remaining_tokens = max_token_count as isize - token_count as isize;
|
||||
let token_state = if remaining_tokens <= 0 {
|
||||
TokenState::NoTokensLeft {
|
||||
max_token_count,
|
||||
token_count,
|
||||
@@ -3180,7 +3182,7 @@ fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn humanize_token_count(count: u64) -> String {
|
||||
pub fn humanize_token_count(count: usize) -> String {
|
||||
match count {
|
||||
0..=999 => count.to_string(),
|
||||
1000..=9999 => {
|
||||
|
||||
@@ -745,7 +745,6 @@ impl ContextStore {
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
100,
|
||||
&Default::default(),
|
||||
executor,
|
||||
|
||||
@@ -310,7 +310,6 @@ impl ModelMatcher {
|
||||
&self.candidates,
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
100,
|
||||
&Default::default(),
|
||||
self.bg_executor.clone(),
|
||||
@@ -665,7 +664,7 @@ mod tests {
|
||||
format!("{}/{}", self.provider_id.0, self.name.0)
|
||||
}
|
||||
|
||||
fn max_token_count(&self) -> u64 {
|
||||
fn max_token_count(&self) -> usize {
|
||||
1000
|
||||
}
|
||||
|
||||
@@ -673,7 +672,7 @@ mod tests {
|
||||
&self,
|
||||
_: LanguageModelRequest,
|
||||
_: &App,
|
||||
) -> BoxFuture<'static, http_client::Result<u64>> {
|
||||
) -> BoxFuture<'static, http_client::Result<usize>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ impl SlashCommandCompletionProvider {
|
||||
&candidates,
|
||||
&command_name,
|
||||
true,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
|
||||
@@ -147,7 +147,6 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
&Options::match_candidates_for_args(),
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
10,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
|
||||
@@ -261,7 +261,6 @@ fn tab_items_for_queries(
|
||||
&match_candidates,
|
||||
query,
|
||||
true,
|
||||
true,
|
||||
usize::MAX,
|
||||
&cancel,
|
||||
background_executor.clone(),
|
||||
|
||||
@@ -456,18 +456,18 @@ impl ActionLog {
|
||||
})?
|
||||
}
|
||||
|
||||
/// Track a buffer as read by agent, so we can notify the model about user edits.
|
||||
/// Track a buffer as read, so we can notify the model about user edits.
|
||||
pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.track_buffer_internal(buffer, false, cx);
|
||||
}
|
||||
|
||||
/// Mark a buffer as created by agent, so we can refresh it in the context
|
||||
/// Mark a buffer as edited, so we can refresh it in the context
|
||||
pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.edited_since_project_diagnostics_check = true;
|
||||
self.track_buffer_internal(buffer.clone(), true, cx);
|
||||
}
|
||||
|
||||
/// Mark a buffer as edited by agent, so we can refresh it in the context
|
||||
/// Mark a buffer as edited, so we can refresh it in the context
|
||||
pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.edited_since_project_diagnostics_check = true;
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::{Template, Templates};
|
||||
use anyhow::Result;
|
||||
use assistant_tool::ActionLog;
|
||||
use create_file_parser::{CreateFileParser, CreateFileParserEvent};
|
||||
pub use edit_parser::EditFormat;
|
||||
use edit_parser::{EditParser, EditParserEvent, EditParserMetrics};
|
||||
use futures::{
|
||||
Stream, StreamExt,
|
||||
@@ -42,23 +41,13 @@ impl Template for CreateFilePromptTemplate {
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EditFileXmlPromptTemplate {
|
||||
struct EditFilePromptTemplate {
|
||||
path: Option<PathBuf>,
|
||||
edit_description: String,
|
||||
}
|
||||
|
||||
impl Template for EditFileXmlPromptTemplate {
|
||||
const TEMPLATE_NAME: &'static str = "edit_file_prompt_xml.hbs";
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EditFileDiffFencedPromptTemplate {
|
||||
path: Option<PathBuf>,
|
||||
edit_description: String,
|
||||
}
|
||||
|
||||
impl Template for EditFileDiffFencedPromptTemplate {
|
||||
const TEMPLATE_NAME: &'static str = "edit_file_prompt_diff_fenced.hbs";
|
||||
impl Template for EditFilePromptTemplate {
|
||||
const TEMPLATE_NAME: &'static str = "edit_file_prompt.hbs";
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -81,7 +70,6 @@ pub struct EditAgent {
|
||||
action_log: Entity<ActionLog>,
|
||||
project: Entity<Project>,
|
||||
templates: Arc<Templates>,
|
||||
edit_format: EditFormat,
|
||||
}
|
||||
|
||||
impl EditAgent {
|
||||
@@ -90,14 +78,12 @@ impl EditAgent {
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
templates: Arc<Templates>,
|
||||
edit_format: EditFormat,
|
||||
) -> Self {
|
||||
EditAgent {
|
||||
model,
|
||||
project,
|
||||
action_log,
|
||||
templates,
|
||||
edit_format,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,23 +209,14 @@ impl EditAgent {
|
||||
let this = self.clone();
|
||||
let (events_tx, events_rx) = mpsc::unbounded();
|
||||
let conversation = conversation.clone();
|
||||
let edit_format = self.edit_format;
|
||||
let output = cx.spawn(async move |cx| {
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
|
||||
let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?;
|
||||
let prompt = match edit_format {
|
||||
EditFormat::XmlTags => EditFileXmlPromptTemplate {
|
||||
path,
|
||||
edit_description,
|
||||
}
|
||||
.render(&this.templates)?,
|
||||
EditFormat::DiffFenced => EditFileDiffFencedPromptTemplate {
|
||||
path,
|
||||
edit_description,
|
||||
}
|
||||
.render(&this.templates)?,
|
||||
};
|
||||
|
||||
let prompt = EditFilePromptTemplate {
|
||||
path,
|
||||
edit_description,
|
||||
}
|
||||
.render(&this.templates)?;
|
||||
let edit_chunks = this
|
||||
.request(conversation, CompletionIntent::EditFile, prompt, cx)
|
||||
.await?;
|
||||
@@ -259,7 +236,7 @@ impl EditAgent {
|
||||
self.action_log
|
||||
.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx))?;
|
||||
|
||||
let (output, edit_events) = Self::parse_edit_chunks(edit_chunks, self.edit_format, cx);
|
||||
let (output, edit_events) = Self::parse_edit_chunks(edit_chunks, cx);
|
||||
let mut edit_events = edit_events.peekable();
|
||||
while let Some(edit_event) = Pin::new(&mut edit_events).peek().await {
|
||||
// Skip events until we're at the start of a new edit.
|
||||
@@ -309,13 +286,7 @@ impl EditAgent {
|
||||
_ => {
|
||||
let ranges = resolved_old_text
|
||||
.into_iter()
|
||||
.map(|text| {
|
||||
let start_line =
|
||||
(snapshot.offset_to_point(text.range.start).row + 1) as usize;
|
||||
let end_line =
|
||||
(snapshot.offset_to_point(text.range.end).row + 1) as usize;
|
||||
start_line..end_line
|
||||
})
|
||||
.map(|text| text.range)
|
||||
.collect();
|
||||
output_events
|
||||
.unbounded_send(EditAgentOutputEvent::AmbiguousEditRange(ranges))
|
||||
@@ -373,7 +344,6 @@ impl EditAgent {
|
||||
|
||||
fn parse_edit_chunks(
|
||||
chunks: impl 'static + Send + Stream<Item = Result<String, LanguageModelCompletionError>>,
|
||||
edit_format: EditFormat,
|
||||
cx: &mut AsyncApp,
|
||||
) -> (
|
||||
Task<Result<EditAgentOutput>>,
|
||||
@@ -383,7 +353,7 @@ impl EditAgent {
|
||||
let output = cx.background_spawn(async move {
|
||||
pin_mut!(chunks);
|
||||
|
||||
let mut parser = EditParser::new(edit_format);
|
||||
let mut parser = EditParser::new();
|
||||
let mut raw_edits = String::new();
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
match chunk {
|
||||
@@ -459,25 +429,25 @@ impl EditAgent {
|
||||
let task = cx.background_spawn(async move {
|
||||
let mut matcher = StreamingFuzzyMatcher::new(snapshot);
|
||||
while let Some(edit_event) = edit_events.next().await {
|
||||
let EditParserEvent::OldTextChunk {
|
||||
chunk,
|
||||
done,
|
||||
line_hint,
|
||||
} = edit_event?
|
||||
else {
|
||||
let EditParserEvent::OldTextChunk { chunk, done } = edit_event? else {
|
||||
break;
|
||||
};
|
||||
|
||||
old_range_tx.send(matcher.push(&chunk, line_hint))?;
|
||||
old_range_tx.send(matcher.push(&chunk))?;
|
||||
if done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let matches = matcher.finish();
|
||||
let best_match = matcher.select_best_match();
|
||||
|
||||
old_range_tx.send(best_match.clone())?;
|
||||
let old_range = if matches.len() == 1 {
|
||||
matches.first()
|
||||
} else {
|
||||
// No matches or multiple ambiguous matches
|
||||
None
|
||||
};
|
||||
old_range_tx.send(old_range.cloned())?;
|
||||
|
||||
let indent = LineIndent::from_iter(
|
||||
matcher
|
||||
@@ -486,18 +456,10 @@ impl EditAgent {
|
||||
.unwrap_or(&String::new())
|
||||
.chars(),
|
||||
);
|
||||
|
||||
let resolved_old_texts = if let Some(best_match) = best_match {
|
||||
vec![ResolvedOldText {
|
||||
range: best_match,
|
||||
indent,
|
||||
}]
|
||||
} else {
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|range| ResolvedOldText { range, indent })
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let resolved_old_texts = matches
|
||||
.into_iter()
|
||||
.map(|range| ResolvedOldText { range, indent })
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok((edit_events, resolved_old_texts))
|
||||
});
|
||||
@@ -1379,13 +1341,7 @@ mod tests {
|
||||
let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
EditAgent::new(
|
||||
model,
|
||||
project,
|
||||
action_log,
|
||||
Templates::new(),
|
||||
EditFormat::XmlTags,
|
||||
)
|
||||
EditAgent::new(model, project, action_log, Templates::new())
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -1418,12 +1374,10 @@ mod tests {
|
||||
&agent,
|
||||
indoc! {"
|
||||
<old_text>
|
||||
return 42;
|
||||
}
|
||||
return 42;
|
||||
</old_text>
|
||||
<new_text>
|
||||
return 100;
|
||||
}
|
||||
return 100;
|
||||
</new_text>
|
||||
"},
|
||||
&mut rng,
|
||||
@@ -1453,7 +1407,7 @@ mod tests {
|
||||
|
||||
// And AmbiguousEditRange even should be emitted
|
||||
let events = drain_events(&mut events);
|
||||
let ambiguous_ranges = vec![2..3, 6..7, 10..11];
|
||||
let ambiguous_ranges = vec![17..31, 52..66, 87..101];
|
||||
assert!(
|
||||
events.contains(&EditAgentOutputEvent::AmbiguousEditRange(ambiguous_ranges)),
|
||||
"Should emit AmbiguousEditRange for non-unique text"
|
||||
|
||||
@@ -1,31 +1,18 @@
|
||||
use anyhow::bail;
|
||||
use derive_more::{Add, AddAssign};
|
||||
use language_model::LanguageModel;
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use std::{mem, ops::Range, str::FromStr, sync::Arc};
|
||||
use std::{mem, ops::Range};
|
||||
|
||||
const OLD_TEXT_END_TAG: &str = "</old_text>";
|
||||
const NEW_TEXT_END_TAG: &str = "</new_text>";
|
||||
const EDITS_END_TAG: &str = "</edits>";
|
||||
const SEARCH_MARKER: &str = "<<<<<<< SEARCH";
|
||||
const SEPARATOR_MARKER: &str = "=======";
|
||||
const REPLACE_MARKER: &str = ">>>>>>> REPLACE";
|
||||
const END_TAGS: [&str; 3] = [OLD_TEXT_END_TAG, NEW_TEXT_END_TAG, EDITS_END_TAG];
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EditParserEvent {
|
||||
OldTextChunk {
|
||||
chunk: String,
|
||||
done: bool,
|
||||
line_hint: Option<u32>,
|
||||
},
|
||||
NewTextChunk {
|
||||
chunk: String,
|
||||
done: bool,
|
||||
},
|
||||
OldTextChunk { chunk: String, done: bool },
|
||||
NewTextChunk { chunk: String, done: bool },
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@@ -36,164 +23,45 @@ pub struct EditParserMetrics {
|
||||
pub mismatched_tags: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EditFormat {
|
||||
/// XML-like tags:
|
||||
/// <old_text>...</old_text>
|
||||
/// <new_text>...</new_text>
|
||||
XmlTags,
|
||||
/// Diff-fenced format, in which:
|
||||
/// - Text before the SEARCH marker is ignored
|
||||
/// - Fences are optional
|
||||
/// - Line hint is optional.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```diff
|
||||
/// <<<<<<< SEARCH line=42
|
||||
/// ...
|
||||
/// =======
|
||||
/// ...
|
||||
/// >>>>>>> REPLACE
|
||||
/// ```
|
||||
DiffFenced,
|
||||
}
|
||||
|
||||
impl FromStr for EditFormat {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> anyhow::Result<Self> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"xml_tags" | "xml" => Ok(EditFormat::XmlTags),
|
||||
"diff_fenced" | "diff-fenced" | "diff" => Ok(EditFormat::DiffFenced),
|
||||
_ => bail!("Unknown EditFormat: {}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EditFormat {
|
||||
/// Return an optimal edit format for the language model
|
||||
pub fn from_model(model: Arc<dyn LanguageModel>) -> anyhow::Result<Self> {
|
||||
if model.provider_id().0 == "google" || model.id().0.to_lowercase().contains("gemini") {
|
||||
Ok(EditFormat::DiffFenced)
|
||||
} else {
|
||||
Ok(EditFormat::XmlTags)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an optimal edit format for the language model,
|
||||
/// with the ability to override it by setting the
|
||||
/// `ZED_EDIT_FORMAT` environment variable
|
||||
#[allow(dead_code)]
|
||||
pub fn from_env(model: Arc<dyn LanguageModel>) -> anyhow::Result<Self> {
|
||||
let default = EditFormat::from_model(model)?;
|
||||
std::env::var("ZED_EDIT_FORMAT").map_or(Ok(default), |s| EditFormat::from_str(&s))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EditFormatParser: Send + std::fmt::Debug {
|
||||
fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]>;
|
||||
fn take_metrics(&mut self) -> EditParserMetrics;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct XmlEditParser {
|
||||
state: XmlParserState,
|
||||
pub struct EditParser {
|
||||
state: EditParserState,
|
||||
buffer: String,
|
||||
metrics: EditParserMetrics,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum XmlParserState {
|
||||
enum EditParserState {
|
||||
Pending,
|
||||
WithinOldText { start: bool, line_hint: Option<u32> },
|
||||
WithinOldText { start: bool },
|
||||
AfterOldText,
|
||||
WithinNewText { start: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DiffFencedEditParser {
|
||||
state: DiffParserState,
|
||||
buffer: String,
|
||||
metrics: EditParserMetrics,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum DiffParserState {
|
||||
Pending,
|
||||
WithinSearch { start: bool, line_hint: Option<u32> },
|
||||
WithinReplace { start: bool },
|
||||
}
|
||||
|
||||
/// Main parser that delegates to format-specific parsers
|
||||
pub struct EditParser {
|
||||
parser: Box<dyn EditFormatParser>,
|
||||
}
|
||||
|
||||
impl XmlEditParser {
|
||||
impl EditParser {
|
||||
pub fn new() -> Self {
|
||||
XmlEditParser {
|
||||
state: XmlParserState::Pending,
|
||||
EditParser {
|
||||
state: EditParserState::Pending,
|
||||
buffer: String::new(),
|
||||
metrics: EditParserMetrics::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_end_tag(&self) -> Option<Range<usize>> {
|
||||
let (tag, start_ix) = END_TAGS
|
||||
.iter()
|
||||
.flat_map(|tag| Some((tag, self.buffer.find(tag)?)))
|
||||
.min_by_key(|(_, ix)| *ix)?;
|
||||
Some(start_ix..start_ix + tag.len())
|
||||
}
|
||||
|
||||
fn ends_with_tag_prefix(&self) -> bool {
|
||||
let mut end_prefixes = END_TAGS
|
||||
.iter()
|
||||
.flat_map(|tag| (1..tag.len()).map(move |i| &tag[..i]))
|
||||
.chain(["\n"]);
|
||||
end_prefixes.any(|prefix| self.buffer.ends_with(&prefix))
|
||||
}
|
||||
|
||||
fn parse_line_hint(&self, tag: &str) -> Option<u32> {
|
||||
use std::sync::LazyLock;
|
||||
static LINE_HINT_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r#"line=(?:"?)(\d+)"#).unwrap());
|
||||
|
||||
LINE_HINT_REGEX
|
||||
.captures(tag)
|
||||
.and_then(|caps| caps.get(1))
|
||||
.and_then(|m| m.as_str().parse::<u32>().ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl EditFormatParser for XmlEditParser {
|
||||
fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> {
|
||||
pub fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> {
|
||||
self.buffer.push_str(chunk);
|
||||
|
||||
let mut edit_events = SmallVec::new();
|
||||
loop {
|
||||
match &mut self.state {
|
||||
XmlParserState::Pending => {
|
||||
if let Some(start) = self.buffer.find("<old_text") {
|
||||
if let Some(tag_end) = self.buffer[start..].find('>') {
|
||||
let tag_end = start + tag_end + 1;
|
||||
let tag = &self.buffer[start..tag_end];
|
||||
let line_hint = self.parse_line_hint(tag);
|
||||
self.buffer.drain(..tag_end);
|
||||
self.state = XmlParserState::WithinOldText {
|
||||
start: true,
|
||||
line_hint,
|
||||
};
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
EditParserState::Pending => {
|
||||
if let Some(start) = self.buffer.find("<old_text>") {
|
||||
self.buffer.drain(..start + "<old_text>".len());
|
||||
self.state = EditParserState::WithinOldText { start: true };
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
XmlParserState::WithinOldText { start, line_hint } => {
|
||||
EditParserState::WithinOldText { start } => {
|
||||
if !self.buffer.is_empty() {
|
||||
if *start && self.buffer.starts_with('\n') {
|
||||
self.buffer.remove(0);
|
||||
@@ -201,7 +69,6 @@ impl EditFormatParser for XmlEditParser {
|
||||
*start = false;
|
||||
}
|
||||
|
||||
let line_hint = *line_hint;
|
||||
if let Some(tag_range) = self.find_end_tag() {
|
||||
let mut chunk = self.buffer[..tag_range.start].to_string();
|
||||
if chunk.ends_with('\n') {
|
||||
@@ -214,32 +81,27 @@ impl EditFormatParser for XmlEditParser {
|
||||
}
|
||||
|
||||
self.buffer.drain(..tag_range.end);
|
||||
self.state = XmlParserState::AfterOldText;
|
||||
edit_events.push(EditParserEvent::OldTextChunk {
|
||||
chunk,
|
||||
done: true,
|
||||
line_hint,
|
||||
});
|
||||
self.state = EditParserState::AfterOldText;
|
||||
edit_events.push(EditParserEvent::OldTextChunk { chunk, done: true });
|
||||
} else {
|
||||
if !self.ends_with_tag_prefix() {
|
||||
edit_events.push(EditParserEvent::OldTextChunk {
|
||||
chunk: mem::take(&mut self.buffer),
|
||||
done: false,
|
||||
line_hint,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
XmlParserState::AfterOldText => {
|
||||
EditParserState::AfterOldText => {
|
||||
if let Some(start) = self.buffer.find("<new_text>") {
|
||||
self.buffer.drain(..start + "<new_text>".len());
|
||||
self.state = XmlParserState::WithinNewText { start: true };
|
||||
self.state = EditParserState::WithinNewText { start: true };
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
XmlParserState::WithinNewText { start } => {
|
||||
EditParserState::WithinNewText { start } => {
|
||||
if !self.buffer.is_empty() {
|
||||
if *start && self.buffer.starts_with('\n') {
|
||||
self.buffer.remove(0);
|
||||
@@ -259,7 +121,7 @@ impl EditFormatParser for XmlEditParser {
|
||||
}
|
||||
|
||||
self.buffer.drain(..tag_range.end);
|
||||
self.state = XmlParserState::Pending;
|
||||
self.state = EditParserState::Pending;
|
||||
edit_events.push(EditParserEvent::NewTextChunk { chunk, done: true });
|
||||
} else {
|
||||
if !self.ends_with_tag_prefix() {
|
||||
@@ -276,163 +138,24 @@ impl EditFormatParser for XmlEditParser {
|
||||
edit_events
|
||||
}
|
||||
|
||||
fn take_metrics(&mut self) -> EditParserMetrics {
|
||||
std::mem::take(&mut self.metrics)
|
||||
}
|
||||
}
|
||||
|
||||
impl DiffFencedEditParser {
|
||||
pub fn new() -> Self {
|
||||
DiffFencedEditParser {
|
||||
state: DiffParserState::Pending,
|
||||
buffer: String::new(),
|
||||
metrics: EditParserMetrics::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ends_with_diff_marker_prefix(&self) -> bool {
|
||||
let diff_markers = [SEPARATOR_MARKER, REPLACE_MARKER];
|
||||
let mut diff_prefixes = diff_markers
|
||||
fn find_end_tag(&self) -> Option<Range<usize>> {
|
||||
let (tag, start_ix) = END_TAGS
|
||||
.iter()
|
||||
.flat_map(|marker| (1..marker.len()).map(move |i| &marker[..i]))
|
||||
.flat_map(|tag| Some((tag, self.buffer.find(tag)?)))
|
||||
.min_by_key(|(_, ix)| *ix)?;
|
||||
Some(start_ix..start_ix + tag.len())
|
||||
}
|
||||
|
||||
fn ends_with_tag_prefix(&self) -> bool {
|
||||
let mut end_prefixes = END_TAGS
|
||||
.iter()
|
||||
.flat_map(|tag| (1..tag.len()).map(move |i| &tag[..i]))
|
||||
.chain(["\n"]);
|
||||
diff_prefixes.any(|prefix| self.buffer.ends_with(&prefix))
|
||||
end_prefixes.any(|prefix| self.buffer.ends_with(&prefix))
|
||||
}
|
||||
|
||||
fn parse_line_hint(&self, search_line: &str) -> Option<u32> {
|
||||
use regex::Regex;
|
||||
use std::sync::LazyLock;
|
||||
static LINE_HINT_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r#"line=(?:"?)(\d+)"#).unwrap());
|
||||
|
||||
LINE_HINT_REGEX
|
||||
.captures(search_line)
|
||||
.and_then(|caps| caps.get(1))
|
||||
.and_then(|m| m.as_str().parse::<u32>().ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl EditFormatParser for DiffFencedEditParser {
|
||||
fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> {
|
||||
self.buffer.push_str(chunk);
|
||||
|
||||
let mut edit_events = SmallVec::new();
|
||||
loop {
|
||||
match &mut self.state {
|
||||
DiffParserState::Pending => {
|
||||
if let Some(diff) = self.buffer.find(SEARCH_MARKER) {
|
||||
let search_end = diff + SEARCH_MARKER.len();
|
||||
if let Some(newline_pos) = self.buffer[search_end..].find('\n') {
|
||||
let search_line = &self.buffer[diff..search_end + newline_pos];
|
||||
let line_hint = self.parse_line_hint(search_line);
|
||||
self.buffer.drain(..search_end + newline_pos + 1);
|
||||
self.state = DiffParserState::WithinSearch {
|
||||
start: true,
|
||||
line_hint,
|
||||
};
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
DiffParserState::WithinSearch { start, line_hint } => {
|
||||
if !self.buffer.is_empty() {
|
||||
if *start && self.buffer.starts_with('\n') {
|
||||
self.buffer.remove(0);
|
||||
}
|
||||
*start = false;
|
||||
}
|
||||
|
||||
let line_hint = *line_hint;
|
||||
if let Some(separator_pos) = self.buffer.find(SEPARATOR_MARKER) {
|
||||
let mut chunk = self.buffer[..separator_pos].to_string();
|
||||
if chunk.ends_with('\n') {
|
||||
chunk.pop();
|
||||
}
|
||||
|
||||
let separator_end = separator_pos + SEPARATOR_MARKER.len();
|
||||
if let Some(newline_pos) = self.buffer[separator_end..].find('\n') {
|
||||
self.buffer.drain(..separator_end + newline_pos + 1);
|
||||
self.state = DiffParserState::WithinReplace { start: true };
|
||||
edit_events.push(EditParserEvent::OldTextChunk {
|
||||
chunk,
|
||||
done: true,
|
||||
line_hint,
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if !self.ends_with_diff_marker_prefix() {
|
||||
edit_events.push(EditParserEvent::OldTextChunk {
|
||||
chunk: mem::take(&mut self.buffer),
|
||||
done: false,
|
||||
line_hint,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
DiffParserState::WithinReplace { start } => {
|
||||
if !self.buffer.is_empty() {
|
||||
if *start && self.buffer.starts_with('\n') {
|
||||
self.buffer.remove(0);
|
||||
}
|
||||
*start = false;
|
||||
}
|
||||
|
||||
if let Some(replace_pos) = self.buffer.find(REPLACE_MARKER) {
|
||||
let mut chunk = self.buffer[..replace_pos].to_string();
|
||||
if chunk.ends_with('\n') {
|
||||
chunk.pop();
|
||||
}
|
||||
|
||||
self.buffer.drain(..replace_pos + REPLACE_MARKER.len());
|
||||
if let Some(newline_pos) = self.buffer.find('\n') {
|
||||
self.buffer.drain(..newline_pos + 1);
|
||||
} else {
|
||||
self.buffer.clear();
|
||||
}
|
||||
|
||||
self.state = DiffParserState::Pending;
|
||||
edit_events.push(EditParserEvent::NewTextChunk { chunk, done: true });
|
||||
} else {
|
||||
if !self.ends_with_diff_marker_prefix() {
|
||||
edit_events.push(EditParserEvent::NewTextChunk {
|
||||
chunk: mem::take(&mut self.buffer),
|
||||
done: false,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
edit_events
|
||||
}
|
||||
|
||||
fn take_metrics(&mut self) -> EditParserMetrics {
|
||||
std::mem::take(&mut self.metrics)
|
||||
}
|
||||
}
|
||||
|
||||
impl EditParser {
|
||||
pub fn new(format: EditFormat) -> Self {
|
||||
let parser: Box<dyn EditFormatParser> = match format {
|
||||
EditFormat::XmlTags => Box::new(XmlEditParser::new()),
|
||||
EditFormat::DiffFenced => Box::new(DiffFencedEditParser::new()),
|
||||
};
|
||||
EditParser { parser }
|
||||
}
|
||||
|
||||
pub fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> {
|
||||
self.parser.push(chunk)
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> EditParserMetrics {
|
||||
self.parser.take_metrics()
|
||||
pub fn finish(self) -> EditParserMetrics {
|
||||
self.metrics
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,8 +167,8 @@ mod tests {
|
||||
use std::cmp;
|
||||
|
||||
#[gpui::test(iterations = 1000)]
|
||||
fn test_xml_single_edit(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
fn test_single_edit(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new();
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
"<old_text>original</old_text><new_text>updated</new_text>",
|
||||
@@ -455,7 +178,6 @@ mod tests {
|
||||
vec![Edit {
|
||||
old_text: "original".to_string(),
|
||||
new_text: "updated".to_string(),
|
||||
line_hint: None,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -468,8 +190,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 1000)]
|
||||
fn test_xml_multiple_edits(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
fn test_multiple_edits(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new();
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
@@ -487,12 +209,10 @@ mod tests {
|
||||
Edit {
|
||||
old_text: "first old".to_string(),
|
||||
new_text: "first new".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
Edit {
|
||||
old_text: "second old".to_string(),
|
||||
new_text: "second new".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -506,8 +226,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 1000)]
|
||||
fn test_xml_edits_with_extra_text(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
fn test_edits_with_extra_text(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new();
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
@@ -524,17 +244,14 @@ mod tests {
|
||||
Edit {
|
||||
old_text: "content".to_string(),
|
||||
new_text: "updated content".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
Edit {
|
||||
old_text: "second item".to_string(),
|
||||
new_text: "modified second item".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
Edit {
|
||||
old_text: "third case".to_string(),
|
||||
new_text: "improved third case".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -548,8 +265,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 1000)]
|
||||
fn test_xml_nested_tags(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
fn test_nested_tags(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new();
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
"<old_text>code with <tag>nested</tag> elements</old_text><new_text>new <code>content</code></new_text>",
|
||||
@@ -559,7 +276,6 @@ mod tests {
|
||||
vec![Edit {
|
||||
old_text: "code with <tag>nested</tag> elements".to_string(),
|
||||
new_text: "new <code>content</code>".to_string(),
|
||||
line_hint: None,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -572,8 +288,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 1000)]
|
||||
fn test_xml_empty_old_and_new_text(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
fn test_empty_old_and_new_text(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new();
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
"<old_text></old_text><new_text></new_text>",
|
||||
@@ -583,7 +299,6 @@ mod tests {
|
||||
vec![Edit {
|
||||
old_text: "".to_string(),
|
||||
new_text: "".to_string(),
|
||||
line_hint: None,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -596,8 +311,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_xml_multiline_content(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
fn test_multiline_content(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new();
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
"<old_text>line1\nline2\nline3</old_text><new_text>line1\nmodified line2\nline3</new_text>",
|
||||
@@ -607,7 +322,6 @@ mod tests {
|
||||
vec![Edit {
|
||||
old_text: "line1\nline2\nline3".to_string(),
|
||||
new_text: "line1\nmodified line2\nline3".to_string(),
|
||||
line_hint: None,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -620,8 +334,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 1000)]
|
||||
fn test_xml_mismatched_tags(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
fn test_mismatched_tags(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new();
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
// Reduced from an actual Sonnet 3.7 output
|
||||
@@ -654,12 +368,10 @@ mod tests {
|
||||
Edit {
|
||||
old_text: "a\nb\nc".to_string(),
|
||||
new_text: "a\nB\nc".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
Edit {
|
||||
old_text: "d\ne\nf".to_string(),
|
||||
new_text: "D\ne\nF".to_string(),
|
||||
line_hint: None,
|
||||
}
|
||||
]
|
||||
);
|
||||
@@ -671,7 +383,7 @@ mod tests {
|
||||
}
|
||||
);
|
||||
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
let mut parser = EditParser::new();
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
// Reduced from an actual Opus 4 output
|
||||
@@ -690,7 +402,6 @@ mod tests {
|
||||
vec![Edit {
|
||||
old_text: "Lorem".to_string(),
|
||||
new_text: "LOREM".to_string(),
|
||||
line_hint: None,
|
||||
},]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -702,297 +413,10 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 1000)]
|
||||
fn test_diff_fenced_single_edit(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::DiffFenced);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
<<<<<<< SEARCH
|
||||
original text
|
||||
=======
|
||||
updated text
|
||||
>>>>>>> REPLACE
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
vec![Edit {
|
||||
old_text: "original text".to_string(),
|
||||
new_text: "updated text".to_string(),
|
||||
line_hint: None,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
parser.finish(),
|
||||
EditParserMetrics {
|
||||
tags: 0,
|
||||
mismatched_tags: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_diff_fenced_with_markdown_fences(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::DiffFenced);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
```diff
|
||||
<<<<<<< SEARCH
|
||||
from flask import Flask
|
||||
=======
|
||||
import math
|
||||
from flask import Flask
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
vec![Edit {
|
||||
old_text: "from flask import Flask".to_string(),
|
||||
new_text: "import math\nfrom flask import Flask".to_string(),
|
||||
line_hint: None,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
parser.finish(),
|
||||
EditParserMetrics {
|
||||
tags: 0,
|
||||
mismatched_tags: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_diff_fenced_multiple_edits(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::DiffFenced);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
<<<<<<< SEARCH
|
||||
first old
|
||||
=======
|
||||
first new
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
second old
|
||||
=======
|
||||
second new
|
||||
>>>>>>> REPLACE
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
vec![
|
||||
Edit {
|
||||
old_text: "first old".to_string(),
|
||||
new_text: "first new".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
Edit {
|
||||
old_text: "second old".to_string(),
|
||||
new_text: "second new".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
parser.finish(),
|
||||
EditParserMetrics {
|
||||
tags: 0,
|
||||
mismatched_tags: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_mixed_formats(mut rng: StdRng) {
|
||||
// Test XML format parser only parses XML tags
|
||||
let mut xml_parser = EditParser::new(EditFormat::XmlTags);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
<old_text>xml style old</old_text><new_text>xml style new</new_text>
|
||||
|
||||
<<<<<<< SEARCH
|
||||
diff style old
|
||||
=======
|
||||
diff style new
|
||||
>>>>>>> REPLACE
|
||||
"},
|
||||
&mut xml_parser,
|
||||
&mut rng
|
||||
),
|
||||
vec![Edit {
|
||||
old_text: "xml style old".to_string(),
|
||||
new_text: "xml style new".to_string(),
|
||||
line_hint: None,
|
||||
},]
|
||||
);
|
||||
assert_eq!(
|
||||
xml_parser.finish(),
|
||||
EditParserMetrics {
|
||||
tags: 2,
|
||||
mismatched_tags: 0
|
||||
}
|
||||
);
|
||||
|
||||
// Test diff-fenced format parser only parses diff markers
|
||||
let mut diff_parser = EditParser::new(EditFormat::DiffFenced);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
<old_text>xml style old</old_text><new_text>xml style new</new_text>
|
||||
|
||||
<<<<<<< SEARCH
|
||||
diff style old
|
||||
=======
|
||||
diff style new
|
||||
>>>>>>> REPLACE
|
||||
"},
|
||||
&mut diff_parser,
|
||||
&mut rng
|
||||
),
|
||||
vec![Edit {
|
||||
old_text: "diff style old".to_string(),
|
||||
new_text: "diff style new".to_string(),
|
||||
line_hint: None,
|
||||
},]
|
||||
);
|
||||
assert_eq!(
|
||||
diff_parser.finish(),
|
||||
EditParserMetrics {
|
||||
tags: 0,
|
||||
mismatched_tags: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_diff_fenced_empty_sections(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::DiffFenced);
|
||||
assert_eq!(
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
<<<<<<< SEARCH
|
||||
=======
|
||||
>>>>>>> REPLACE
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
vec![Edit {
|
||||
old_text: "".to_string(),
|
||||
new_text: "".to_string(),
|
||||
line_hint: None,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
parser.finish(),
|
||||
EditParserMetrics {
|
||||
tags: 0,
|
||||
mismatched_tags: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_diff_fenced_with_line_hint(mut rng: StdRng) {
|
||||
let mut parser = EditParser::new(EditFormat::DiffFenced);
|
||||
let edits = parse_random_chunks(
|
||||
indoc! {"
|
||||
<<<<<<< SEARCH line=42
|
||||
original text
|
||||
=======
|
||||
updated text
|
||||
>>>>>>> REPLACE
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng,
|
||||
);
|
||||
assert_eq!(
|
||||
edits,
|
||||
vec![Edit {
|
||||
old_text: "original text".to_string(),
|
||||
line_hint: Some(42),
|
||||
new_text: "updated text".to_string(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_xml_line_hints(mut rng: StdRng) {
|
||||
// Line hint is a single quoted line number
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
|
||||
let edits = parse_random_chunks(
|
||||
r#"
|
||||
<old_text line="23">original code</old_text>
|
||||
<new_text>updated code</new_text>"#,
|
||||
&mut parser,
|
||||
&mut rng,
|
||||
);
|
||||
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].old_text, "original code");
|
||||
assert_eq!(edits[0].line_hint, Some(23));
|
||||
assert_eq!(edits[0].new_text, "updated code");
|
||||
|
||||
// Line hint is a single unquoted line number
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
|
||||
let edits = parse_random_chunks(
|
||||
r#"
|
||||
<old_text line=45>original code</old_text>
|
||||
<new_text>updated code</new_text>"#,
|
||||
&mut parser,
|
||||
&mut rng,
|
||||
);
|
||||
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].old_text, "original code");
|
||||
assert_eq!(edits[0].line_hint, Some(45));
|
||||
assert_eq!(edits[0].new_text, "updated code");
|
||||
|
||||
// Line hint is a range
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
|
||||
let edits = parse_random_chunks(
|
||||
r#"
|
||||
<old_text line="23:50">original code</old_text>
|
||||
<new_text>updated code</new_text>"#,
|
||||
&mut parser,
|
||||
&mut rng,
|
||||
);
|
||||
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].old_text, "original code");
|
||||
assert_eq!(edits[0].line_hint, Some(23));
|
||||
assert_eq!(edits[0].new_text, "updated code");
|
||||
|
||||
// No line hint
|
||||
let mut parser = EditParser::new(EditFormat::XmlTags);
|
||||
let edits = parse_random_chunks(
|
||||
r#"
|
||||
<old_text>old</old_text>
|
||||
<new_text>new</new_text>"#,
|
||||
&mut parser,
|
||||
&mut rng,
|
||||
);
|
||||
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].old_text, "old");
|
||||
assert_eq!(edits[0].line_hint, None);
|
||||
assert_eq!(edits[0].new_text, "new");
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq)]
|
||||
struct Edit {
|
||||
old_text: String,
|
||||
new_text: String,
|
||||
line_hint: Option<u32>,
|
||||
}
|
||||
|
||||
fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec<Edit> {
|
||||
@@ -1009,15 +433,10 @@ mod tests {
|
||||
for chunk_ix in chunk_indices {
|
||||
for event in parser.push(&input[last_ix..chunk_ix]) {
|
||||
match event {
|
||||
EditParserEvent::OldTextChunk {
|
||||
chunk,
|
||||
done,
|
||||
line_hint,
|
||||
} => {
|
||||
EditParserEvent::OldTextChunk { chunk, done } => {
|
||||
old_text.as_mut().unwrap().push_str(&chunk);
|
||||
if done {
|
||||
pending_edit.old_text = old_text.take().unwrap();
|
||||
pending_edit.line_hint = line_hint;
|
||||
new_text = Some(String::new());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ use std::{
|
||||
cmp::Reverse,
|
||||
fmt::{self, Display},
|
||||
io::Write as _,
|
||||
path::Path,
|
||||
str::FromStr,
|
||||
sync::mpsc,
|
||||
};
|
||||
@@ -39,11 +38,10 @@ fn eval_extract_handle_command_output() {
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ----------------------------|----------
|
||||
// claude-3.7-sonnet | 0.99 (2025-06-14)
|
||||
// claude-sonnet-4 | 0.97 (2025-06-14)
|
||||
// gemini-2.5-pro-06-05 | 0.98 (2025-06-16)
|
||||
// gemini-2.5-flash | 0.11 (2025-05-22)
|
||||
// gpt-4.1 | 1.00 (2025-05-22)
|
||||
// claude-3.7-sonnet | 0.98
|
||||
// gemini-2.5-pro-06-05 | 0.77
|
||||
// gemini-2.5-flash | 0.11
|
||||
// gpt-4.1 | 1.00
|
||||
|
||||
let input_file_path = "root/blame.rs";
|
||||
let input_file_content = include_str!("evals/fixtures/extract_handle_command_output/before.rs");
|
||||
@@ -59,7 +57,7 @@ fn eval_extract_handle_command_output() {
|
||||
let edit_description = "Extract `handle_command_output` method from `run_git_blame`.";
|
||||
eval(
|
||||
100,
|
||||
0.95,
|
||||
0.7, // Taking the lower bar for Gemini
|
||||
0.05,
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
@@ -112,13 +110,6 @@ fn eval_extract_handle_command_output() {
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_delete_run_git_blame() {
|
||||
// Model | Pass rate
|
||||
// ----------------------------|----------
|
||||
// claude-3.7-sonnet | 1.0 (2025-06-14)
|
||||
// claude-sonnet-4 | 0.96 (2025-06-14)
|
||||
// gemini-2.5-pro-06-05 | 1.0 (2025-06-16)
|
||||
// gemini-2.5-flash |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/blame.rs";
|
||||
let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs");
|
||||
let output_file_content = include_str!("evals/fixtures/delete_run_git_blame/after.rs");
|
||||
@@ -174,12 +165,13 @@ fn eval_delete_run_git_blame() {
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_translate_doc_comments() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet | 1.0 (2025-06-14)
|
||||
// claude-sonnet-4 | 1.0 (2025-06-14)
|
||||
// gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22)
|
||||
// claude-3.7-sonnet |
|
||||
// gemini-2.5-pro-preview-03-25 | 1.0
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/canvas.rs";
|
||||
@@ -236,12 +228,13 @@ fn eval_translate_doc_comments() {
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet | 0.96 (2025-06-14)
|
||||
// claude-sonnet-4 | 0.11 (2025-06-14)
|
||||
// gemini-2.5-pro-preview-latest | 0.99 (2025-06-16)
|
||||
// claude-3.7-sonnet | 0.98
|
||||
// gemini-2.5-pro-preview-03-25 | 0.99
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/lib.rs";
|
||||
@@ -361,12 +354,13 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_disable_cursor_blinking() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// 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)
|
||||
// claude-3.7-sonnet |
|
||||
// gemini-2.5-pro-preview-03-25 | 1.0
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/editor.rs";
|
||||
@@ -444,20 +438,14 @@ fn eval_disable_cursor_blinking() {
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_from_pixels_constructor() {
|
||||
// Results for 2025-06-13
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// The outcome of this evaluation depends heavily on the LINE_HINT_TOLERANCE
|
||||
// value. Higher values improve the pass rate but may sometimes cause
|
||||
// edits to be misapplied. In the context of this eval, this means
|
||||
// the agent might add from_pixels tests in incorrect locations
|
||||
// (e.g., at the beginning of the file), yet the evaluation may still
|
||||
// rate it highly.
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// Model | Date | Pass rate
|
||||
// =========================================================
|
||||
// claude-4.0-sonnet | 2025-06-14 | 0.99
|
||||
// claude-3.7-sonnet | 2025-06-14 | 0.88
|
||||
// gemini-2.5-pro-preview-06-05 | 2025-06-16 | 0.98
|
||||
// claude-3.7-sonnet |
|
||||
// gemini-2.5-pro-preview-03-25 | 0.94
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/canvas.rs";
|
||||
let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs");
|
||||
@@ -467,7 +455,7 @@ fn eval_from_pixels_constructor() {
|
||||
0.95,
|
||||
// For whatever reason, this eval produces more mismatched tags.
|
||||
// Increasing for now, let's see if we can bring this down.
|
||||
0.25,
|
||||
0.2,
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
message(
|
||||
@@ -653,14 +641,15 @@ fn eval_from_pixels_constructor() {
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_zode() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet | 1.0 (2025-06-14)
|
||||
// claude-sonnet-4 | 1.0 (2025-06-14)
|
||||
// gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22)
|
||||
// gemini-2.5-flash-preview-04-17 | 1.0 (2025-05-22)
|
||||
// gpt-4.1 | 1.0 (2025-05-22)
|
||||
// claude-3.7-sonnet | 1.0
|
||||
// gemini-2.5-pro-preview-03-25 | 1.0
|
||||
// gemini-2.5-flash-preview-04-17 | 1.0
|
||||
// gpt-4.1 | 1.0
|
||||
let input_file_path = "root/zode.py";
|
||||
let input_content = None;
|
||||
let edit_description = "Create the main Zode CLI script";
|
||||
@@ -759,12 +748,13 @@ fn eval_zode() {
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_add_overwrite_test() {
|
||||
// Results for 2025-05-22
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet | 0.65 (2025-06-14)
|
||||
// claude-sonnet-4 | 0.07 (2025-06-14)
|
||||
// gemini-2.5-pro-preview-03-25 | 0.35 (2025-05-22)
|
||||
// claude-3.7-sonnet | 0.16
|
||||
// gemini-2.5-pro-preview-03-25 | 0.35
|
||||
// gemini-2.5-flash-preview-04-17 |
|
||||
// gpt-4.1 |
|
||||
let input_file_path = "root/action_log.rs";
|
||||
@@ -994,14 +984,15 @@ fn eval_create_empty_file() {
|
||||
// thoughts into it. This issue is not specific to empty files, but
|
||||
// it's easier to reproduce with them.
|
||||
//
|
||||
// Results for 2025-05-21:
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// claude-3.7-sonnet | 1.00 (2025-06-14)
|
||||
// claude-sonnet-4 | 1.00 (2025-06-14)
|
||||
// gemini-2.5-pro-preview-03-25 | 1.00 (2025-05-21)
|
||||
// gemini-2.5-flash-preview-04-17 | 1.00 (2025-05-21)
|
||||
// gpt-4.1 | 1.00 (2025-05-21)
|
||||
// claude-3.7-sonnet | 1.00
|
||||
// gemini-2.5-pro-preview-03-25 | 1.00
|
||||
// gemini-2.5-flash-preview-04-17 | 1.00
|
||||
// gpt-4.1 | 1.00
|
||||
//
|
||||
//
|
||||
// TODO: gpt-4.1-mini errored 38 times:
|
||||
@@ -1497,16 +1488,8 @@ impl EditAgentTest {
|
||||
.await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
let edit_format = EditFormat::from_env(agent_model.clone()).unwrap();
|
||||
|
||||
Self {
|
||||
agent: EditAgent::new(
|
||||
agent_model,
|
||||
project.clone(),
|
||||
action_log,
|
||||
Templates::new(),
|
||||
edit_format,
|
||||
),
|
||||
agent: EditAgent::new(agent_model, project.clone(), action_log, Templates::new()),
|
||||
project,
|
||||
judge_model,
|
||||
}
|
||||
@@ -1566,7 +1549,6 @@ impl EditAgentTest {
|
||||
.collect::<Vec<_>>();
|
||||
let worktrees = vec![WorktreeContext {
|
||||
root_name: "root".to_string(),
|
||||
abs_path: Path::new("/path/to/root").into(),
|
||||
rules_file: None,
|
||||
}];
|
||||
let prompt_builder = PromptBuilder::new(None)?;
|
||||
@@ -1661,7 +1643,7 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) ->
|
||||
Ok(err) => match err {
|
||||
LanguageModelCompletionError::RateLimit(duration) => {
|
||||
// Wait for the duration supplied, with some jitter to avoid all requests being made at the same time.
|
||||
let jitter = duration.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
|
||||
let jitter = duration.mul_f64(rand::thread_rng().gen_range(0.0..0.5));
|
||||
eprintln!(
|
||||
"Attempt #{attempt}: Rate limit exceeded. Retry after {duration:?} + jitter of {jitter:?}"
|
||||
);
|
||||
|
||||
@@ -10,9 +10,8 @@ const DELETION_COST: u32 = 10;
|
||||
pub struct StreamingFuzzyMatcher {
|
||||
snapshot: TextBufferSnapshot,
|
||||
query_lines: Vec<String>,
|
||||
line_hint: Option<u32>,
|
||||
incomplete_line: String,
|
||||
matches: Vec<Range<usize>>,
|
||||
best_matches: Vec<Range<usize>>,
|
||||
matrix: SearchMatrix,
|
||||
}
|
||||
|
||||
@@ -22,9 +21,8 @@ impl StreamingFuzzyMatcher {
|
||||
Self {
|
||||
snapshot,
|
||||
query_lines: Vec::new(),
|
||||
line_hint: None,
|
||||
incomplete_line: String::new(),
|
||||
matches: Vec::new(),
|
||||
best_matches: Vec::new(),
|
||||
matrix: SearchMatrix::new(buffer_line_count + 1),
|
||||
}
|
||||
}
|
||||
@@ -43,14 +41,9 @@ impl StreamingFuzzyMatcher {
|
||||
///
|
||||
/// Returns `Some(range)` if a match has been found with the accumulated
|
||||
/// query so far, or `None` if no suitable match exists yet.
|
||||
pub fn push(&mut self, chunk: &str, line_hint: Option<u32>) -> Option<Range<usize>> {
|
||||
if line_hint.is_some() {
|
||||
self.line_hint = line_hint;
|
||||
}
|
||||
|
||||
pub fn push(&mut self, chunk: &str) -> Option<Range<usize>> {
|
||||
// Add the chunk to our incomplete line buffer
|
||||
self.incomplete_line.push_str(chunk);
|
||||
self.line_hint = line_hint;
|
||||
|
||||
if let Some((last_pos, _)) = self.incomplete_line.match_indices('\n').next_back() {
|
||||
let complete_part = &self.incomplete_line[..=last_pos];
|
||||
@@ -62,11 +55,20 @@ impl StreamingFuzzyMatcher {
|
||||
|
||||
self.incomplete_line.replace_range(..last_pos + 1, "");
|
||||
|
||||
self.matches = self.resolve_location_fuzzy();
|
||||
}
|
||||
self.best_matches = self.resolve_location_fuzzy();
|
||||
|
||||
let best_match = self.select_best_match();
|
||||
best_match.or_else(|| self.matches.first().cloned())
|
||||
if let Some(first_match) = self.best_matches.first() {
|
||||
Some(first_match.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
if let Some(first_match) = self.best_matches.first() {
|
||||
Some(first_match.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish processing and return the final best match(es).
|
||||
@@ -78,9 +80,9 @@ impl StreamingFuzzyMatcher {
|
||||
if !self.incomplete_line.is_empty() {
|
||||
self.query_lines.push(self.incomplete_line.clone());
|
||||
self.incomplete_line.clear();
|
||||
self.matches = self.resolve_location_fuzzy();
|
||||
self.best_matches = self.resolve_location_fuzzy();
|
||||
}
|
||||
self.matches.clone()
|
||||
self.best_matches.clone()
|
||||
}
|
||||
|
||||
fn resolve_location_fuzzy(&mut self) -> Vec<Range<usize>> {
|
||||
@@ -196,43 +198,6 @@ impl StreamingFuzzyMatcher {
|
||||
|
||||
valid_matches.into_iter().map(|(_, range)| range).collect()
|
||||
}
|
||||
|
||||
/// Return the best match with starting position close enough to line_hint.
|
||||
pub fn select_best_match(&self) -> Option<Range<usize>> {
|
||||
// Allow line hint to be off by that many lines.
|
||||
// Higher values increase probability of applying edits to a wrong place,
|
||||
// Lower values increase edits failures and overall conversation length.
|
||||
const LINE_HINT_TOLERANCE: u32 = 200;
|
||||
|
||||
if self.matches.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.matches.len() == 1 {
|
||||
return self.matches.first().cloned();
|
||||
}
|
||||
|
||||
let Some(line_hint) = self.line_hint else {
|
||||
// Multiple ambiguous matches
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut best_match = None;
|
||||
let mut best_distance = u32::MAX;
|
||||
|
||||
for range in &self.matches {
|
||||
let start_point = self.snapshot.offset_to_point(range.start);
|
||||
let start_line = start_point.row;
|
||||
let distance = start_line.abs_diff(line_hint);
|
||||
|
||||
if distance <= LINE_HINT_TOLERANCE && distance < best_distance {
|
||||
best_distance = distance;
|
||||
best_match = Some(range.clone());
|
||||
}
|
||||
}
|
||||
|
||||
best_match
|
||||
}
|
||||
}
|
||||
|
||||
fn fuzzy_eq(left: &str, right: &str) -> bool {
|
||||
@@ -675,52 +640,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_line_hint_selection() {
|
||||
let text = indoc! {r#"
|
||||
fn first_function() {
|
||||
return 42;
|
||||
}
|
||||
|
||||
fn second_function() {
|
||||
return 42;
|
||||
}
|
||||
|
||||
fn third_function() {
|
||||
return 42;
|
||||
}
|
||||
"#};
|
||||
|
||||
let buffer = TextBuffer::new(0, BufferId::new(1).unwrap(), text.to_string());
|
||||
let snapshot = buffer.snapshot();
|
||||
let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
|
||||
|
||||
// Given a query that matches all three functions
|
||||
let query = "return 42;\n";
|
||||
|
||||
// Test with line hint pointing to second function (around line 5)
|
||||
let best_match = matcher.push(query, Some(5)).expect("Failed to match query");
|
||||
|
||||
let matched_text = snapshot
|
||||
.text_for_range(best_match.clone())
|
||||
.collect::<String>();
|
||||
assert!(matched_text.contains("return 42;"));
|
||||
assert_eq!(
|
||||
best_match,
|
||||
63..77,
|
||||
"Expected to match `second_function` based on the line hint"
|
||||
);
|
||||
|
||||
let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
|
||||
matcher.push(query, None);
|
||||
matcher.finish();
|
||||
let best_match = matcher.select_best_match();
|
||||
assert!(
|
||||
best_match.is_none(),
|
||||
"Best match should be None when query cannot be uniquely resolved"
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) {
|
||||
let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false);
|
||||
@@ -734,7 +653,7 @@ mod tests {
|
||||
|
||||
// Push chunks incrementally
|
||||
for chunk in &chunks {
|
||||
matcher.push(chunk, None);
|
||||
matcher.push(chunk);
|
||||
}
|
||||
|
||||
let actual_ranges = matcher.finish();
|
||||
@@ -787,7 +706,7 @@ mod tests {
|
||||
|
||||
fn push(finder: &mut StreamingFuzzyMatcher, chunk: &str) -> Option<String> {
|
||||
finder
|
||||
.push(chunk, None)
|
||||
.push(chunk)
|
||||
.map(|range| finder.snapshot.text_for_range(range).collect::<String>())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
Templates,
|
||||
edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat},
|
||||
edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent},
|
||||
schema::json_schema_for,
|
||||
ui::{COLLAPSED_LINES, ToolOutputPreview},
|
||||
};
|
||||
@@ -69,13 +69,13 @@ pub struct EditFileToolInput {
|
||||
/// start each path with one of the project's root directories.
|
||||
///
|
||||
/// The following examples assume we have two root directories in the project:
|
||||
/// - /a/b/backend
|
||||
/// - /c/d/frontend
|
||||
/// - backend
|
||||
/// - frontend
|
||||
///
|
||||
/// <example>
|
||||
/// `backend/src/main.rs`
|
||||
///
|
||||
/// Notice how the file path starts with `backend`. Without that, the path
|
||||
/// Notice how the file path starts with root-1. Without that, the path
|
||||
/// would be ambiguous and the call would fail!
|
||||
/// </example>
|
||||
///
|
||||
@@ -201,14 +201,8 @@ impl Tool for EditFileTool {
|
||||
let card_clone = card.clone();
|
||||
let action_log_clone = action_log.clone();
|
||||
let task = cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
let edit_format = EditFormat::from_model(model.clone())?;
|
||||
let edit_agent = EditAgent::new(
|
||||
model,
|
||||
project.clone(),
|
||||
action_log_clone,
|
||||
Templates::new(),
|
||||
edit_format,
|
||||
);
|
||||
let edit_agent =
|
||||
EditAgent::new(model, project.clone(), action_log_clone, Templates::new());
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
@@ -339,18 +333,14 @@ impl Tool for EditFileTool {
|
||||
);
|
||||
anyhow::ensure!(
|
||||
ambiguous_ranges.is_empty(),
|
||||
{
|
||||
let line_numbers = ambiguous_ranges
|
||||
.iter()
|
||||
.map(|range| range.start.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
formatdoc! {"
|
||||
<old_text> matches more than one position in the file (lines: {line_numbers}). Read the
|
||||
relevant sections of {input_path} again and extend <old_text> so
|
||||
that I can perform the requested edits.
|
||||
"}
|
||||
}
|
||||
// TODO: Include ambiguous_ranges, converted to line numbers.
|
||||
// This would work best if we add `line_hint` parameter
|
||||
// to edit_file_tool
|
||||
formatdoc! {"
|
||||
<old_text> matches more than one position in the file. Read the
|
||||
relevant sections of {input_path} again and extend <old_text> so
|
||||
that I can perform the requested edits.
|
||||
"}
|
||||
);
|
||||
Ok(ToolResultOutput {
|
||||
content: ToolResultContent::Text("No edits were made.".into()),
|
||||
|
||||
@@ -31,8 +31,8 @@ pub struct ReadFileToolInput {
|
||||
/// <example>
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - /a/b/directory1
|
||||
/// - /c/d/directory2
|
||||
/// - directory1
|
||||
/// - directory2
|
||||
///
|
||||
/// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`.
|
||||
/// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`.
|
||||
|
||||
@@ -3,21 +3,21 @@ You MUST respond with a series of edits to a file, using the following format:
|
||||
```
|
||||
<edits>
|
||||
|
||||
<old_text line=10>
|
||||
<old_text>
|
||||
OLD TEXT 1 HERE
|
||||
</old_text>
|
||||
<new_text>
|
||||
NEW TEXT 1 HERE
|
||||
</new_text>
|
||||
|
||||
<old_text line=456>
|
||||
<old_text>
|
||||
OLD TEXT 2 HERE
|
||||
</old_text>
|
||||
<new_text>
|
||||
NEW TEXT 2 HERE
|
||||
</new_text>
|
||||
|
||||
<old_text line=42>
|
||||
<old_text>
|
||||
OLD TEXT 3 HERE
|
||||
</old_text>
|
||||
<new_text>
|
||||
@@ -33,7 +33,6 @@ NEW TEXT 3 HERE
|
||||
- `<old_text>` must exactly match existing file content, including indentation
|
||||
- `<old_text>` must come from the actual file, not an outline
|
||||
- `<old_text>` cannot be empty
|
||||
- `line` should be a starting line number for the text to be replaced
|
||||
- Be minimal with replacements:
|
||||
- For unique lines, include only those lines
|
||||
- For non-unique lines, include enough context to identify them
|
||||
@@ -49,7 +48,7 @@ Claude and gpt-4.1 don't really need it. --}}
|
||||
<example>
|
||||
<edits>
|
||||
|
||||
<old_text line=3>
|
||||
<old_text>
|
||||
struct User {
|
||||
name: String,
|
||||
email: String,
|
||||
@@ -63,7 +62,7 @@ struct User {
|
||||
}
|
||||
</new_text>
|
||||
|
||||
<old_text line=25>
|
||||
<old_text>
|
||||
let user = User {
|
||||
name: String::from("John"),
|
||||
email: String::from("john@example.com"),
|
||||
@@ -1,77 +0,0 @@
|
||||
You MUST respond with a series of edits to a file, using the following diff format:
|
||||
|
||||
```
|
||||
<<<<<<< SEARCH line=1
|
||||
from flask import Flask
|
||||
=======
|
||||
import math
|
||||
from flask import Flask
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH line=325
|
||||
return 0
|
||||
=======
|
||||
print("Done")
|
||||
|
||||
return 0
|
||||
>>>>>>> REPLACE
|
||||
|
||||
```
|
||||
|
||||
# File Editing Instructions
|
||||
|
||||
- Use the SEARCH/REPLACE diff format shown above
|
||||
- The SEARCH section must exactly match existing file content, including indentation
|
||||
- The SEARCH section must come from the actual file, not an outline
|
||||
- The SEARCH section cannot be empty
|
||||
- `line` should be a starting line number for the text to be replaced
|
||||
- Be minimal with replacements:
|
||||
- For unique lines, include only those lines
|
||||
- For non-unique lines, include enough context to identify them
|
||||
- Do not escape quotes, newlines, or other characters
|
||||
- For multiple occurrences, repeat the same diff block for each instance
|
||||
- Edits are sequential - each assumes previous edits are already applied
|
||||
- Only edit the specified file
|
||||
|
||||
# Example
|
||||
|
||||
```
|
||||
<<<<<<< SEARCH line=3
|
||||
struct User {
|
||||
name: String,
|
||||
email: String,
|
||||
}
|
||||
=======
|
||||
struct User {
|
||||
name: String,
|
||||
email: String,
|
||||
active: bool,
|
||||
}
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH line=25
|
||||
let user = User {
|
||||
name: String::from("John"),
|
||||
email: String::from("john@example.com"),
|
||||
};
|
||||
=======
|
||||
let user = User {
|
||||
name: String::from("John"),
|
||||
email: String::from("john@example.com"),
|
||||
active: true,
|
||||
};
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
|
||||
|
||||
# Final instructions
|
||||
|
||||
Tool calls have been disabled. You MUST respond using the SEARCH/REPLACE diff format only.
|
||||
|
||||
<file_to_edit>
|
||||
{{path}}
|
||||
</file_to_edit>
|
||||
|
||||
<edit_description>
|
||||
{{edit_description}}
|
||||
</edit_description>
|
||||
@@ -82,10 +82,7 @@ fn view_release_notes_locally(
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
let buffer = project.create_local_buffer("", markdown, cx);
|
||||
project
|
||||
.mark_buffer_as_non_searchable(buffer.read(cx).remote_id(), cx);
|
||||
buffer
|
||||
project.create_local_buffer("", markdown, cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, body.release_notes)], None, cx)
|
||||
|
||||
@@ -152,7 +152,7 @@ pub enum Thinking {
|
||||
#[derive(Debug)]
|
||||
pub struct Request {
|
||||
pub model: String,
|
||||
pub max_tokens: u64,
|
||||
pub max_tokens: u32,
|
||||
pub messages: Vec<BedrockMessage>,
|
||||
pub tools: Option<BedrockToolConfig>,
|
||||
pub thinking: Option<Thinking>,
|
||||
|
||||
@@ -99,10 +99,10 @@ pub enum Model {
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
name: String,
|
||||
max_tokens: u64,
|
||||
max_tokens: usize,
|
||||
/// The name displayed in the UI, such as in the assistant panel model dropdown menu.
|
||||
display_name: Option<String>,
|
||||
max_output_tokens: Option<u64>,
|
||||
max_output_tokens: Option<u32>,
|
||||
default_temperature: Option<f32>,
|
||||
},
|
||||
}
|
||||
@@ -309,7 +309,7 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_token_count(&self) -> u64 {
|
||||
pub fn max_token_count(&self) -> usize {
|
||||
match self {
|
||||
Self::Claude3_5SonnetV2
|
||||
| Self::Claude3Opus
|
||||
@@ -328,7 +328,7 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_output_tokens(&self) -> u64 {
|
||||
pub fn max_output_tokens(&self) -> u32 {
|
||||
match self {
|
||||
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
|
||||
Self::Claude3_7Sonnet
|
||||
|
||||
@@ -1028,11 +1028,7 @@ impl BufferDiff {
|
||||
let (base_text_changed, mut changed_range) =
|
||||
match (state.base_text_exists, new_state.base_text_exists) {
|
||||
(false, false) => (true, None),
|
||||
(true, true)
|
||||
if state.base_text.remote_id() == new_state.base_text.remote_id()
|
||||
&& state.base_text.syntax_update_count()
|
||||
== new_state.base_text.syntax_update_count() =>
|
||||
{
|
||||
(true, true) if state.base_text.remote_id() == new_state.base_text.remote_id() => {
|
||||
(false, new_state.compare(&state, buffer))
|
||||
}
|
||||
_ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)),
|
||||
|
||||
@@ -13,7 +13,6 @@ pub enum CliRequest {
|
||||
Open {
|
||||
paths: Vec<String>,
|
||||
urls: Vec<String>,
|
||||
diff_paths: Vec<[String; 2]>,
|
||||
wait: bool,
|
||||
open_new_workspace: Option<bool>,
|
||||
env: Option<HashMap<String, String>>,
|
||||
|
||||
@@ -89,9 +89,6 @@ struct Args {
|
||||
/// Will attempt to give the correct command to run
|
||||
#[arg(long)]
|
||||
system_specs: bool,
|
||||
/// Pairs of file paths to diff. Can be specified multiple times.
|
||||
#[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])]
|
||||
diff: Vec<String>,
|
||||
/// Uninstall Zed from user system
|
||||
#[cfg(all(
|
||||
any(target_os = "linux", target_os = "macos"),
|
||||
@@ -235,17 +232,9 @@ fn main() -> Result<()> {
|
||||
let exit_status = Arc::new(Mutex::new(None));
|
||||
let mut paths = vec![];
|
||||
let mut urls = vec![];
|
||||
let mut diff_paths = vec![];
|
||||
let mut stdin_tmp_file: Option<fs::File> = None;
|
||||
let mut anonymous_fd_tmp_files = vec![];
|
||||
|
||||
for path in args.diff.chunks(2) {
|
||||
diff_paths.push([
|
||||
parse_path_with_position(&path[0])?,
|
||||
parse_path_with_position(&path[1])?,
|
||||
]);
|
||||
}
|
||||
|
||||
for path in args.paths_with_position.iter() {
|
||||
if path.starts_with("zed://")
|
||||
|| path.starts_with("http://")
|
||||
@@ -284,7 +273,6 @@ fn main() -> Result<()> {
|
||||
tx.send(CliRequest::Open {
|
||||
paths,
|
||||
urls,
|
||||
diff_paths,
|
||||
wait: args.wait,
|
||||
open_new_workspace,
|
||||
env,
|
||||
|
||||
@@ -39,7 +39,6 @@ paths.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
rand.workspace = true
|
||||
regex.workspace = true
|
||||
release_channel.workspace = true
|
||||
rpc = { workspace = true, features = ["gpui"] }
|
||||
schemars.workspace = true
|
||||
@@ -64,12 +63,11 @@ workspace-hack.workspace = true
|
||||
[dev-dependencies]
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
fs.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
@@ -8,11 +8,10 @@ use futures::{Future, FutureExt, StreamExt};
|
||||
use gpui::{App, AppContext as _, BackgroundExecutor, Task};
|
||||
use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
|
||||
use parking_lot::Mutex;
|
||||
use regex::Regex;
|
||||
use release_channel::ReleaseChannel;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::sync::LazyLock;
|
||||
@@ -46,13 +45,31 @@ struct TelemetryState {
|
||||
first_event_date_time: Option<Instant>,
|
||||
event_coalescer: EventCoalescer,
|
||||
max_queue_size: usize,
|
||||
worktrees_with_project_type_events_sent: HashSet<WorktreeId>,
|
||||
worktree_id_map: WorktreeIdMap,
|
||||
|
||||
os_name: String,
|
||||
app_version: String,
|
||||
os_version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct WorktreeIdMap(HashMap<String, ProjectCache>);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ProjectCache {
|
||||
name: String,
|
||||
worktree_ids_reported: HashSet<WorktreeId>,
|
||||
}
|
||||
|
||||
impl ProjectCache {
|
||||
fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
worktree_ids_reported: HashSet::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
const MAX_QUEUE_LEN: usize = 5;
|
||||
|
||||
@@ -74,10 +91,6 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| {
|
||||
})
|
||||
});
|
||||
|
||||
static DOTNET_PROJECT_FILES_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^(global\.json|Directory\.Build\.props|.*\.(csproj|fsproj|vbproj|sln))$").unwrap()
|
||||
});
|
||||
|
||||
pub fn os_name() -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -181,7 +194,20 @@ impl Telemetry {
|
||||
first_event_date_time: None,
|
||||
event_coalescer: EventCoalescer::new(clock.clone()),
|
||||
max_queue_size: MAX_QUEUE_LEN,
|
||||
worktrees_with_project_type_events_sent: HashSet::new(),
|
||||
worktree_id_map: WorktreeIdMap(HashMap::from_iter([
|
||||
(
|
||||
"pnpm-lock.yaml".to_string(),
|
||||
ProjectCache::new("pnpm".to_string()),
|
||||
),
|
||||
(
|
||||
"yarn.lock".to_string(),
|
||||
ProjectCache::new("yarn".to_string()),
|
||||
),
|
||||
(
|
||||
"package.json".to_string(),
|
||||
ProjectCache::new("node".to_string()),
|
||||
),
|
||||
])),
|
||||
|
||||
os_version: None,
|
||||
os_name: os_name(),
|
||||
@@ -345,14 +371,44 @@ impl Telemetry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_discovered_project_type_events(
|
||||
pub fn report_discovered_project_events(
|
||||
self: &Arc<Self>,
|
||||
worktree_id: WorktreeId,
|
||||
updated_entries_set: &UpdatedEntriesSet,
|
||||
) {
|
||||
let Some(project_type_names) = self.detect_project_types(worktree_id, updated_entries_set)
|
||||
else {
|
||||
return;
|
||||
let project_type_names: Vec<String> = {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.worktree_id_map
|
||||
.0
|
||||
.iter_mut()
|
||||
.filter_map(|(project_file_name, project_type_telemetry)| {
|
||||
if project_type_telemetry
|
||||
.worktree_ids_reported
|
||||
.contains(&worktree_id)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let project_file_found = updated_entries_set.iter().any(|(path, _, _)| {
|
||||
path.as_ref()
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(|name_str| name_str == project_file_name)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
if !project_file_found {
|
||||
return None;
|
||||
}
|
||||
|
||||
project_type_telemetry
|
||||
.worktree_ids_reported
|
||||
.insert(worktree_id);
|
||||
|
||||
Some(project_type_telemetry.name.clone())
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
for project_type_name in project_type_names {
|
||||
@@ -360,55 +416,6 @@ impl Telemetry {
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_project_types(
|
||||
self: &Arc<Self>,
|
||||
worktree_id: WorktreeId,
|
||||
updated_entries_set: &UpdatedEntriesSet,
|
||||
) -> Option<Vec<String>> {
|
||||
let mut state = self.state.lock();
|
||||
|
||||
if state
|
||||
.worktrees_with_project_type_events_sent
|
||||
.contains(&worktree_id)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut project_types: HashSet<&str> = HashSet::new();
|
||||
|
||||
for (path, _, _) in updated_entries_set.iter() {
|
||||
let Some(file_name) = path.file_name().and_then(|f| f.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let project_type = if file_name == "pnpm-lock.yaml" {
|
||||
Some("pnpm")
|
||||
} else if file_name == "yarn.lock" {
|
||||
Some("yarn")
|
||||
} else if file_name == "package.json" {
|
||||
Some("node")
|
||||
} else if DOTNET_PROJECT_FILES_REGEX.is_match(file_name) {
|
||||
Some("dotnet")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(project_type) = project_type {
|
||||
project_types.insert(project_type);
|
||||
};
|
||||
}
|
||||
|
||||
if !project_types.is_empty() {
|
||||
state
|
||||
.worktrees_with_project_type_events_sent
|
||||
.insert(worktree_id);
|
||||
}
|
||||
|
||||
let mut project_types: Vec<_> = project_types.into_iter().map(String::from).collect();
|
||||
project_types.sort();
|
||||
Some(project_types)
|
||||
}
|
||||
|
||||
fn report_event(self: &Arc<Self>, event: Event) {
|
||||
let mut state = self.state.lock();
|
||||
// RUST_LOG=telemetry=trace to debug telemetry events
|
||||
@@ -571,9 +578,7 @@ mod tests {
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::TestAppContext;
|
||||
use http_client::FakeHttpClient;
|
||||
use std::collections::HashMap;
|
||||
use telemetry_events::FlexibleEvent;
|
||||
use worktree::{PathChange, ProjectEntryId, WorktreeId};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
|
||||
@@ -691,115 +696,6 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_project_discovery_does_not_double_report(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http = FakeHttpClient::with_200_response();
|
||||
let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
|
||||
let worktree_id = 1;
|
||||
|
||||
// Scan of empty worktree finds nothing
|
||||
test_project_discovery_helper(telemetry.clone(), vec![], Some(vec![]), worktree_id);
|
||||
|
||||
// Files added, second scan of worktree 1 finds project type
|
||||
test_project_discovery_helper(
|
||||
telemetry.clone(),
|
||||
vec!["package.json"],
|
||||
Some(vec!["node"]),
|
||||
worktree_id,
|
||||
);
|
||||
|
||||
// Third scan of worktree does not double report, as we already reported
|
||||
test_project_discovery_helper(telemetry.clone(), vec!["package.json"], None, worktree_id);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_pnpm_project_discovery(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http = FakeHttpClient::with_200_response();
|
||||
let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
|
||||
|
||||
test_project_discovery_helper(
|
||||
telemetry.clone(),
|
||||
vec!["package.json", "pnpm-lock.yaml"],
|
||||
Some(vec!["node", "pnpm"]),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_yarn_project_discovery(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http = FakeHttpClient::with_200_response();
|
||||
let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
|
||||
|
||||
test_project_discovery_helper(
|
||||
telemetry.clone(),
|
||||
vec!["package.json", "yarn.lock"],
|
||||
Some(vec!["node", "yarn"]),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_dotnet_project_discovery(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http = FakeHttpClient::with_200_response();
|
||||
let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
|
||||
|
||||
// Using different worktrees, as production code blocks from reporting a
|
||||
// project type for the same worktree multiple times
|
||||
|
||||
test_project_discovery_helper(
|
||||
telemetry.clone().clone(),
|
||||
vec!["global.json"],
|
||||
Some(vec!["dotnet"]),
|
||||
1,
|
||||
);
|
||||
test_project_discovery_helper(
|
||||
telemetry.clone(),
|
||||
vec!["Directory.Build.props"],
|
||||
Some(vec!["dotnet"]),
|
||||
2,
|
||||
);
|
||||
test_project_discovery_helper(
|
||||
telemetry.clone(),
|
||||
vec!["file.csproj"],
|
||||
Some(vec!["dotnet"]),
|
||||
3,
|
||||
);
|
||||
test_project_discovery_helper(
|
||||
telemetry.clone(),
|
||||
vec!["file.fsproj"],
|
||||
Some(vec!["dotnet"]),
|
||||
4,
|
||||
);
|
||||
test_project_discovery_helper(
|
||||
telemetry.clone(),
|
||||
vec!["file.vbproj"],
|
||||
Some(vec!["dotnet"]),
|
||||
5,
|
||||
);
|
||||
test_project_discovery_helper(telemetry.clone(), vec!["file.sln"], Some(vec!["dotnet"]), 6);
|
||||
|
||||
// Each worktree should only send a single project type event, even when
|
||||
// encountering multiple files associated with that project type
|
||||
test_project_discovery_helper(
|
||||
telemetry,
|
||||
vec!["global.json", "Directory.Build.props"],
|
||||
Some(vec!["dotnet"]),
|
||||
7,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// Test settings
|
||||
// Update FakeHTTPClient to keep track of the number of requests and assert on it
|
||||
@@ -816,32 +712,4 @@ mod tests {
|
||||
&& telemetry.state.lock().flush_events_task.is_none()
|
||||
&& telemetry.state.lock().first_event_date_time.is_none()
|
||||
}
|
||||
|
||||
fn test_project_discovery_helper(
|
||||
telemetry: Arc<Telemetry>,
|
||||
file_paths: Vec<&str>,
|
||||
expected_project_types: Option<Vec<&str>>,
|
||||
worktree_id_num: usize,
|
||||
) {
|
||||
let worktree_id = WorktreeId::from_usize(worktree_id_num);
|
||||
let entries: Vec<_> = file_paths
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, path)| {
|
||||
(
|
||||
Arc::from(std::path::Path::new(path)),
|
||||
ProjectEntryId::from_proto(i as u64 + 1),
|
||||
PathChange::Added,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let updated_entries: UpdatedEntriesSet = Arc::from(entries.as_slice());
|
||||
|
||||
let detected_project_types = telemetry.detect_project_types(worktree_id, &updated_entries);
|
||||
|
||||
let expected_project_types =
|
||||
expected_project_types.map(|types| types.iter().map(|&t| t.to_string()).collect());
|
||||
|
||||
assert_eq!(detected_project_types, expected_project_types);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,7 +465,6 @@ CREATE TABLE extension_versions (
|
||||
provides_slash_commands BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
provides_indexed_docs_providers BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
provides_snippets BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
provides_debug_adapters BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
PRIMARY KEY (extension_id, version)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
alter table extension_versions
|
||||
add column provides_debug_adapters bool not null default false
|
||||
@@ -31,7 +31,7 @@ use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND};
|
||||
use crate::rpc::{ResultExt as _, Server};
|
||||
use crate::stripe_client::{
|
||||
StripeCancellationDetailsReason, StripeClient, StripeCustomerId, StripeSubscription,
|
||||
StripeSubscriptionId, UpdateCustomerParams,
|
||||
StripeSubscriptionId,
|
||||
};
|
||||
use crate::{AppState, Error, Result};
|
||||
use crate::{db::UserId, llm::db::LlmDatabase};
|
||||
@@ -353,17 +353,7 @@ async fn create_billing_subscription(
|
||||
}
|
||||
|
||||
let customer_id = if let Some(existing_customer) = &existing_billing_customer {
|
||||
let customer_id = StripeCustomerId(existing_customer.stripe_customer_id.clone().into());
|
||||
if let Some(email) = user.email_address.as_deref() {
|
||||
stripe_billing
|
||||
.client()
|
||||
.update_customer(&customer_id, UpdateCustomerParams { email: Some(email) })
|
||||
.await
|
||||
// Update of email address is best-effort - continue checkout even if it fails
|
||||
.context("error updating stripe customer email address")
|
||||
.log_err();
|
||||
}
|
||||
customer_id
|
||||
StripeCustomerId(existing_customer.stripe_customer_id.clone().into())
|
||||
} else {
|
||||
stripe_billing
|
||||
.find_or_create_customer_by_email(user.email_address.as_deref())
|
||||
|
||||
@@ -321,9 +321,6 @@ impl Database {
|
||||
provides_snippets: ActiveValue::Set(
|
||||
version.provides.contains(&ExtensionProvides::Snippets),
|
||||
),
|
||||
provides_debug_adapters: ActiveValue::Set(
|
||||
version.provides.contains(&ExtensionProvides::DebugAdapters),
|
||||
),
|
||||
download_count: ActiveValue::NotSet,
|
||||
}
|
||||
}))
|
||||
@@ -434,10 +431,6 @@ fn apply_provides_filter(
|
||||
condition = condition.add(extension_version::Column::ProvidesSnippets.eq(true));
|
||||
}
|
||||
|
||||
if provides_filter.contains(&ExtensionProvides::DebugAdapters) {
|
||||
condition = condition.add(extension_version::Column::ProvidesDebugAdapters.eq(true));
|
||||
}
|
||||
|
||||
condition
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ pub struct Model {
|
||||
pub provides_slash_commands: bool,
|
||||
pub provides_indexed_docs_providers: bool,
|
||||
pub provides_snippets: bool,
|
||||
pub provides_debug_adapters: bool,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
@@ -69,10 +68,6 @@ impl Model {
|
||||
provides.insert(ExtensionProvides::Snippets);
|
||||
}
|
||||
|
||||
if self.provides_debug_adapters {
|
||||
provides.insert(ExtensionProvides::DebugAdapters);
|
||||
}
|
||||
|
||||
provides
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +323,6 @@ impl Server {
|
||||
.add_request_handler(forward_read_only_project_request::<proto::SynchronizeBuffers>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetColorPresentation>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GetCodeLens>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitGetBranches>)
|
||||
|
||||
@@ -50,10 +50,6 @@ impl StripeBilling {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client(&self) -> &Arc<dyn StripeClient> {
|
||||
&self.client
|
||||
}
|
||||
|
||||
pub async fn initialize(&self) -> Result<()> {
|
||||
log::info!("StripeBilling: initializing");
|
||||
|
||||
|
||||
@@ -27,11 +27,6 @@ pub struct CreateCustomerParams<'a> {
|
||||
pub email: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UpdateCustomerParams<'a> {
|
||||
pub email: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display)]
|
||||
pub struct StripeSubscriptionId(pub Arc<str>);
|
||||
|
||||
@@ -198,12 +193,6 @@ pub trait StripeClient: Send + Sync {
|
||||
|
||||
async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result<StripeCustomer>;
|
||||
|
||||
async fn update_customer(
|
||||
&self,
|
||||
customer_id: &StripeCustomerId,
|
||||
params: UpdateCustomerParams<'_>,
|
||||
) -> Result<StripeCustomer>;
|
||||
|
||||
async fn list_subscriptions_for_customer(
|
||||
&self,
|
||||
customer_id: &StripeCustomerId,
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::stripe_client::{
|
||||
StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
|
||||
StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeMeter, StripeMeterId,
|
||||
StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId, StripeSubscriptionItem,
|
||||
StripeSubscriptionItemId, UpdateCustomerParams, UpdateSubscriptionParams,
|
||||
StripeSubscriptionItemId, UpdateSubscriptionParams,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -95,22 +95,6 @@ impl StripeClient for FakeStripeClient {
|
||||
Ok(customer)
|
||||
}
|
||||
|
||||
async fn update_customer(
|
||||
&self,
|
||||
customer_id: &StripeCustomerId,
|
||||
params: UpdateCustomerParams<'_>,
|
||||
) -> Result<StripeCustomer> {
|
||||
let mut customers = self.customers.lock();
|
||||
if let Some(customer) = customers.get_mut(customer_id) {
|
||||
if let Some(email) = params.email {
|
||||
customer.email = Some(email.to_string());
|
||||
}
|
||||
Ok(customer.clone())
|
||||
} else {
|
||||
Err(anyhow!("no customer found for {customer_id:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_subscriptions_for_customer(
|
||||
&self,
|
||||
customer_id: &StripeCustomerId,
|
||||
|
||||
@@ -11,7 +11,7 @@ use stripe::{
|
||||
CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior,
|
||||
CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod,
|
||||
CreateCustomer, Customer, CustomerId, ListCustomers, Price, PriceId, Recurring, Subscription,
|
||||
SubscriptionId, SubscriptionItem, SubscriptionItemId, UpdateCustomer, UpdateSubscriptionItems,
|
||||
SubscriptionId, SubscriptionItem, SubscriptionItemId, UpdateSubscriptionItems,
|
||||
UpdateSubscriptionTrialSettings, UpdateSubscriptionTrialSettingsEndBehavior,
|
||||
UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod,
|
||||
};
|
||||
@@ -25,8 +25,7 @@ use crate::stripe_client::{
|
||||
StripePriceId, StripePriceRecurring, StripeSubscription, StripeSubscriptionId,
|
||||
StripeSubscriptionItem, StripeSubscriptionItemId, StripeSubscriptionTrialSettings,
|
||||
StripeSubscriptionTrialSettingsEndBehavior,
|
||||
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateCustomerParams,
|
||||
UpdateSubscriptionParams,
|
||||
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionParams,
|
||||
};
|
||||
|
||||
pub struct RealStripeClient {
|
||||
@@ -79,24 +78,6 @@ impl StripeClient for RealStripeClient {
|
||||
Ok(StripeCustomer::from(customer))
|
||||
}
|
||||
|
||||
async fn update_customer(
|
||||
&self,
|
||||
customer_id: &StripeCustomerId,
|
||||
params: UpdateCustomerParams<'_>,
|
||||
) -> Result<StripeCustomer> {
|
||||
let customer = Customer::update(
|
||||
&self.client,
|
||||
&customer_id.try_into()?,
|
||||
UpdateCustomer {
|
||||
email: params.email,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(StripeCustomer::from(customer))
|
||||
}
|
||||
|
||||
async fn list_subscriptions_for_customer(
|
||||
&self,
|
||||
customer_id: &StripeCustomerId,
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
};
|
||||
use call::ActiveCall;
|
||||
use editor::{
|
||||
DocumentColorsRenderMode, Editor, EditorSettings, RowInfo,
|
||||
Editor, RowInfo,
|
||||
actions::{
|
||||
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
|
||||
ExpandMacroRecursively, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
|
||||
@@ -16,7 +16,7 @@ use editor::{
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
|
||||
use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
FakeLspAdapter,
|
||||
@@ -1951,283 +1951,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let expected_color = Rgba {
|
||||
r: 0.33,
|
||||
g: 0.33,
|
||||
b: 0.33,
|
||||
a: 0.33,
|
||||
};
|
||||
let mut server = TestServer::start(cx_a.executor()).await;
|
||||
let executor = cx_a.executor();
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
cx_a.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.lsp_document_colors = Some(DocumentColorsRenderMode::None);
|
||||
});
|
||||
});
|
||||
});
|
||||
cx_b.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
client_a.language_registry().add(rust_lang());
|
||||
client_b.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
// Client A opens a project.
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
path!("/a"),
|
||||
json!({
|
||||
"main.rs": "fn main() { a }",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client B joins the project
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
executor.start_waiting();
|
||||
|
||||
// The host opens a rust file.
|
||||
let _buffer_a = project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.open_local_buffer(path!("/a/main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let editor_a = workspace_a
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
|
||||
let requests_made = Arc::new(AtomicUsize::new(0));
|
||||
let closure_requests_made = Arc::clone(&requests_made);
|
||||
let mut color_request_handle = fake_language_server
|
||||
.set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
|
||||
let requests_made = Arc::clone(&closure_requests_made);
|
||||
async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
);
|
||||
requests_made.fetch_add(1, atomic::Ordering::Release);
|
||||
Ok(vec![lsp::ColorInformation {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 0,
|
||||
character: 1,
|
||||
},
|
||||
},
|
||||
color: lsp::Color {
|
||||
red: 0.33,
|
||||
green: 0.33,
|
||||
blue: 0.33,
|
||||
alpha: 0.33,
|
||||
},
|
||||
}])
|
||||
}
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
0,
|
||||
requests_made.load(atomic::Ordering::Acquire),
|
||||
"Host did not enable document colors, hence should query for none"
|
||||
);
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
assert_eq!(
|
||||
Vec::<Rgba>::new(),
|
||||
extract_color_inlays(editor, cx),
|
||||
"No query colors should result in no hints"
|
||||
);
|
||||
});
|
||||
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
let editor_b = workspace_b
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
color_request_handle.next().await.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
1,
|
||||
requests_made.load(atomic::Ordering::Acquire),
|
||||
"The client opened the file and got its first colors back"
|
||||
);
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_eq!(
|
||||
vec![expected_color],
|
||||
extract_color_inlays(editor, cx),
|
||||
"With document colors as inlays, color inlays should be pushed"
|
||||
);
|
||||
});
|
||||
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
|
||||
editor.handle_input(":", window, cx);
|
||||
});
|
||||
color_request_handle.next().await.unwrap();
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
2,
|
||||
requests_made.load(atomic::Ordering::Acquire),
|
||||
"After the host edits his file, the client should request the colors again"
|
||||
);
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
assert_eq!(
|
||||
Vec::<Rgba>::new(),
|
||||
extract_color_inlays(editor, cx),
|
||||
"Host has no colors still"
|
||||
);
|
||||
});
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_eq!(vec![expected_color], extract_color_inlays(editor, cx),);
|
||||
});
|
||||
|
||||
cx_b.update(|_, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.lsp_document_colors = Some(DocumentColorsRenderMode::Background);
|
||||
});
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
2,
|
||||
requests_made.load(atomic::Ordering::Acquire),
|
||||
"After the client have changed the colors settings, no extra queries should happen"
|
||||
);
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
assert_eq!(
|
||||
Vec::<Rgba>::new(),
|
||||
extract_color_inlays(editor, cx),
|
||||
"Host is unaffected by the client's settings changes"
|
||||
);
|
||||
});
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_eq!(
|
||||
Vec::<Rgba>::new(),
|
||||
extract_color_inlays(editor, cx),
|
||||
"Client should have no colors hints, as in the settings"
|
||||
);
|
||||
});
|
||||
|
||||
cx_b.update(|_, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
|
||||
});
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
2,
|
||||
requests_made.load(atomic::Ordering::Acquire),
|
||||
"After falling back to colors as inlays, no extra LSP queries are made"
|
||||
);
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
assert_eq!(
|
||||
Vec::<Rgba>::new(),
|
||||
extract_color_inlays(editor, cx),
|
||||
"Host is unaffected by the client's settings changes, again"
|
||||
);
|
||||
});
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_eq!(
|
||||
vec![expected_color],
|
||||
extract_color_inlays(editor, cx),
|
||||
"Client should have its color hints back"
|
||||
);
|
||||
});
|
||||
|
||||
cx_a.update(|_, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.lsp_document_colors = Some(DocumentColorsRenderMode::Border);
|
||||
});
|
||||
});
|
||||
});
|
||||
color_request_handle.next().await.unwrap();
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
3,
|
||||
requests_made.load(atomic::Ordering::Acquire),
|
||||
"After the host enables document colors, another LSP query should be made"
|
||||
);
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
assert_eq!(
|
||||
Vec::<Rgba>::new(),
|
||||
extract_color_inlays(editor, cx),
|
||||
"Host did not configure document colors as hints hence gets nothing"
|
||||
);
|
||||
});
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_eq!(
|
||||
vec![expected_color],
|
||||
extract_color_inlays(editor, cx),
|
||||
"Client should be unaffected by the host's settings changes"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let mut server = TestServer::start(cx_a.executor()).await;
|
||||
@@ -3111,16 +2834,6 @@ fn extract_hint_labels(editor: &Editor) -> Vec<String> {
|
||||
labels
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
|
||||
editor
|
||||
.all_inlays(cx)
|
||||
.into_iter()
|
||||
.filter_map(|inlay| inlay.get_color())
|
||||
.map(Rgba::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
|
||||
git::blame::BlameEntry {
|
||||
sha: sha.parse().unwrap(),
|
||||
|
||||
@@ -90,7 +90,7 @@ impl ChatPanel {
|
||||
languages.clone(),
|
||||
user_store.clone(),
|
||||
None,
|
||||
cx.new(|cx| Editor::auto_height(1, 4, window, cx)),
|
||||
cx.new(|cx| Editor::auto_height(4, window, cx)),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -293,7 +293,6 @@ impl MessageEditor {
|
||||
candidates,
|
||||
query,
|
||||
true,
|
||||
true,
|
||||
LIMIT,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
|
||||
@@ -499,7 +499,6 @@ impl CollabPanel {
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
executor.clone(),
|
||||
@@ -543,7 +542,6 @@ impl CollabPanel {
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
executor.clone(),
|
||||
@@ -595,7 +593,6 @@ impl CollabPanel {
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
executor.clone(),
|
||||
@@ -626,7 +623,6 @@ impl CollabPanel {
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
executor.clone(),
|
||||
@@ -703,7 +699,6 @@ impl CollabPanel {
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
executor.clone(),
|
||||
@@ -739,7 +734,6 @@ impl CollabPanel {
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
executor.clone(),
|
||||
@@ -764,7 +758,6 @@ impl CollabPanel {
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
executor.clone(),
|
||||
@@ -798,7 +791,6 @@ impl CollabPanel {
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
executor.clone(),
|
||||
|
||||
@@ -295,7 +295,6 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
|
||||
@@ -327,7 +327,6 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
10000,
|
||||
&Default::default(),
|
||||
executor,
|
||||
|
||||
@@ -24,7 +24,6 @@ use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServer
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use request::StatusNotification;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
|
||||
use std::collections::hash_map::Entry;
|
||||
@@ -62,15 +61,7 @@ pub fn init(
|
||||
node_runtime: NodeRuntime,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let language_settings = all_language_settings(None, cx);
|
||||
let configuration = copilot_chat::CopilotChatConfiguration {
|
||||
enterprise_uri: language_settings
|
||||
.edit_predictions
|
||||
.copilot
|
||||
.enterprise_uri
|
||||
.clone(),
|
||||
};
|
||||
copilot_chat::init(fs.clone(), http.clone(), configuration, cx);
|
||||
copilot_chat::init(fs.clone(), http.clone(), cx);
|
||||
|
||||
let copilot = cx.new({
|
||||
let node_runtime = node_runtime.clone();
|
||||
@@ -356,11 +347,8 @@ impl Copilot {
|
||||
_subscription: cx.on_app_quit(Self::shutdown_language_server),
|
||||
};
|
||||
this.start_copilot(true, false, cx);
|
||||
cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
this.start_copilot(true, false, cx);
|
||||
this.send_configuration_update(cx);
|
||||
})
|
||||
.detach();
|
||||
cx.observe_global::<SettingsStore>(move |this, cx| this.start_copilot(true, false, cx))
|
||||
.detach();
|
||||
this
|
||||
}
|
||||
|
||||
@@ -447,43 +435,6 @@ impl Copilot {
|
||||
if env.is_empty() { None } else { Some(env) }
|
||||
}
|
||||
|
||||
fn send_configuration_update(&mut self, cx: &mut Context<Self>) {
|
||||
let copilot_settings = all_language_settings(None, cx)
|
||||
.edit_predictions
|
||||
.copilot
|
||||
.clone();
|
||||
|
||||
let settings = json!({
|
||||
"http": {
|
||||
"proxy": copilot_settings.proxy,
|
||||
"proxyStrictSSL": !copilot_settings.proxy_no_verify.unwrap_or(false)
|
||||
},
|
||||
"github-enterprise": {
|
||||
"uri": copilot_settings.enterprise_uri
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(copilot_chat) = copilot_chat::CopilotChat::global(cx) {
|
||||
copilot_chat.update(cx, |chat, cx| {
|
||||
chat.set_configuration(
|
||||
copilot_chat::CopilotChatConfiguration {
|
||||
enterprise_uri: copilot_settings.enterprise_uri.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if let Ok(server) = self.server.as_running() {
|
||||
server
|
||||
.lsp
|
||||
.notify::<lsp::notification::DidChangeConfiguration>(
|
||||
&lsp::DidChangeConfigurationParams { settings },
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity<Self>, lsp::FakeLanguageServer) {
|
||||
use fs::FakeFs;
|
||||
@@ -590,6 +541,12 @@ impl Copilot {
|
||||
.into_response()
|
||||
.context("copilot: check status")?;
|
||||
|
||||
server
|
||||
.request::<request::SetEditorInfo>(editor_info)
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: set editor info")?;
|
||||
|
||||
anyhow::Ok((server, status))
|
||||
};
|
||||
|
||||
@@ -607,8 +564,6 @@ impl Copilot {
|
||||
});
|
||||
cx.emit(Event::CopilotLanguageServerStarted);
|
||||
this.update_sign_in_status(status, cx);
|
||||
// Send configuration now that the LSP is fully started
|
||||
this.send_configuration_update(cx);
|
||||
}
|
||||
Err(error) => {
|
||||
this.server = CopilotServer::Error(error.to_string().into());
|
||||
|
||||
@@ -19,47 +19,10 @@ use settings::watch_config_dir;
|
||||
pub const COPILOT_OAUTH_ENV_VAR: &str = "GH_COPILOT_TOKEN";
|
||||
|
||||
#[derive(Default, Clone, Debug, PartialEq)]
|
||||
pub struct CopilotChatConfiguration {
|
||||
pub enterprise_uri: Option<String>,
|
||||
}
|
||||
|
||||
impl CopilotChatConfiguration {
|
||||
pub fn token_url(&self) -> String {
|
||||
if let Some(enterprise_uri) = &self.enterprise_uri {
|
||||
let domain = Self::parse_domain(enterprise_uri);
|
||||
format!("https://api.{}/copilot_internal/v2/token", domain)
|
||||
} else {
|
||||
"https://api.github.com/copilot_internal/v2/token".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn oauth_domain(&self) -> String {
|
||||
if let Some(enterprise_uri) = &self.enterprise_uri {
|
||||
Self::parse_domain(enterprise_uri)
|
||||
} else {
|
||||
"github.com".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn api_url_from_endpoint(&self, endpoint: &str) -> String {
|
||||
format!("{}/chat/completions", endpoint)
|
||||
}
|
||||
|
||||
pub fn models_url_from_endpoint(&self, endpoint: &str) -> String {
|
||||
format!("{}/models", endpoint)
|
||||
}
|
||||
|
||||
fn parse_domain(enterprise_uri: &str) -> String {
|
||||
let uri = enterprise_uri.trim_end_matches('/');
|
||||
|
||||
if let Some(domain) = uri.strip_prefix("https://") {
|
||||
domain.split('/').next().unwrap_or(domain).to_string()
|
||||
} else if let Some(domain) = uri.strip_prefix("http://") {
|
||||
domain.split('/').next().unwrap_or(domain).to_string()
|
||||
} else {
|
||||
uri.split('/').next().unwrap_or(uri).to_string()
|
||||
}
|
||||
}
|
||||
pub struct CopilotChatSettings {
|
||||
pub api_url: Arc<str>,
|
||||
pub auth_url: Arc<str>,
|
||||
pub models_url: Arc<str>,
|
||||
}
|
||||
|
||||
// Copilot's base model; defined by Microsoft in premium requests table
|
||||
@@ -126,7 +89,7 @@ struct ModelLimits {
|
||||
#[serde(default)]
|
||||
max_output_tokens: usize,
|
||||
#[serde(default)]
|
||||
max_prompt_tokens: u64,
|
||||
max_prompt_tokens: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
@@ -182,7 +145,7 @@ impl Model {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
pub fn max_token_count(&self) -> u64 {
|
||||
pub fn max_token_count(&self) -> usize {
|
||||
self.capabilities.limits.max_prompt_tokens
|
||||
}
|
||||
|
||||
@@ -311,20 +274,6 @@ pub struct FunctionContent {
|
||||
pub struct ResponseEvent {
|
||||
pub choices: Vec<ResponseChoice>,
|
||||
pub id: String,
|
||||
pub usage: Option<Usage>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Usage {
|
||||
pub completion_tokens: u64,
|
||||
pub prompt_tokens: u64,
|
||||
pub prompt_tokens_details: PromptTokensDetails,
|
||||
pub total_tokens: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct PromptTokensDetails {
|
||||
pub cached_tokens: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -360,19 +309,12 @@ pub struct FunctionChunk {
|
||||
struct ApiTokenResponse {
|
||||
token: String,
|
||||
expires_at: i64,
|
||||
endpoints: ApiTokenResponseEndpoints,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ApiTokenResponseEndpoints {
|
||||
api: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ApiToken {
|
||||
api_key: String,
|
||||
expires_at: DateTime<chrono::Utc>,
|
||||
api_endpoint: String,
|
||||
}
|
||||
|
||||
impl ApiToken {
|
||||
@@ -393,7 +335,6 @@ impl TryFrom<ApiTokenResponse> for ApiToken {
|
||||
Ok(Self {
|
||||
api_key: response.token,
|
||||
expires_at,
|
||||
api_endpoint: response.endpoints.api,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -405,18 +346,13 @@ impl Global for GlobalCopilotChat {}
|
||||
pub struct CopilotChat {
|
||||
oauth_token: Option<String>,
|
||||
api_token: Option<ApiToken>,
|
||||
configuration: CopilotChatConfiguration,
|
||||
settings: CopilotChatSettings,
|
||||
models: Option<Vec<Model>>,
|
||||
client: Arc<dyn HttpClient>,
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
client: Arc<dyn HttpClient>,
|
||||
configuration: CopilotChatConfiguration,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let copilot_chat = cx.new(|cx| CopilotChat::new(fs, client, configuration, cx));
|
||||
pub fn init(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &mut App) {
|
||||
let copilot_chat = cx.new(|cx| CopilotChat::new(fs, client, cx));
|
||||
cx.set_global(GlobalCopilotChat(copilot_chat));
|
||||
}
|
||||
|
||||
@@ -444,15 +380,10 @@ impl CopilotChat {
|
||||
.map(|model| model.0.clone())
|
||||
}
|
||||
|
||||
fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
client: Arc<dyn HttpClient>,
|
||||
configuration: CopilotChatConfiguration,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
fn new(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &mut Context<Self>) -> Self {
|
||||
let config_paths: HashSet<PathBuf> = copilot_chat_config_paths().into_iter().collect();
|
||||
let dir_path = copilot_chat_config_dir();
|
||||
|
||||
let settings = CopilotChatSettings::default();
|
||||
cx.spawn(async move |this, cx| {
|
||||
let mut parent_watch_rx = watch_config_dir(
|
||||
cx.background_executor(),
|
||||
@@ -461,9 +392,7 @@ impl CopilotChat {
|
||||
config_paths,
|
||||
);
|
||||
while let Some(contents) = parent_watch_rx.next().await {
|
||||
let oauth_domain =
|
||||
this.read_with(cx, |this, _| this.configuration.oauth_domain())?;
|
||||
let oauth_token = extract_oauth_token(contents, &oauth_domain);
|
||||
let oauth_token = extract_oauth_token(contents);
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.oauth_token = oauth_token.clone();
|
||||
@@ -482,10 +411,9 @@ impl CopilotChat {
|
||||
oauth_token: std::env::var(COPILOT_OAUTH_ENV_VAR).ok(),
|
||||
api_token: None,
|
||||
models: None,
|
||||
configuration,
|
||||
settings,
|
||||
client,
|
||||
};
|
||||
|
||||
if this.oauth_token.is_some() {
|
||||
cx.spawn(async move |this, mut cx| Self::update_models(&this, &mut cx).await)
|
||||
.detach_and_log_err(cx);
|
||||
@@ -495,26 +423,30 @@ impl CopilotChat {
|
||||
}
|
||||
|
||||
async fn update_models(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
|
||||
let (oauth_token, client, configuration) = this.read_with(cx, |this, _| {
|
||||
let (oauth_token, client, auth_url) = this.read_with(cx, |this, _| {
|
||||
(
|
||||
this.oauth_token.clone(),
|
||||
this.client.clone(),
|
||||
this.configuration.clone(),
|
||||
this.settings.auth_url.clone(),
|
||||
)
|
||||
})?;
|
||||
let api_token = request_api_token(
|
||||
&oauth_token.ok_or_else(|| {
|
||||
anyhow!("OAuth token is missing while updating Copilot Chat models")
|
||||
})?,
|
||||
auth_url,
|
||||
client.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let oauth_token = oauth_token
|
||||
.ok_or_else(|| anyhow!("OAuth token is missing while updating Copilot Chat models"))?;
|
||||
|
||||
let token_url = configuration.token_url();
|
||||
let api_token = request_api_token(&oauth_token, token_url.into(), client.clone()).await?;
|
||||
|
||||
let models_url = configuration.models_url_from_endpoint(&api_token.api_endpoint);
|
||||
let models =
|
||||
get_models(models_url.into(), api_token.api_key.clone(), client.clone()).await?;
|
||||
let models_url = this.update(cx, |this, cx| {
|
||||
this.api_token = Some(api_token.clone());
|
||||
cx.notify();
|
||||
this.settings.models_url.clone()
|
||||
})?;
|
||||
let models = get_models(models_url, api_token.api_key, client.clone()).await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.api_token = Some(api_token);
|
||||
this.models = Some(models);
|
||||
cx.notify();
|
||||
})?;
|
||||
@@ -539,23 +471,23 @@ impl CopilotChat {
|
||||
.flatten()
|
||||
.context("Copilot chat is not enabled")?;
|
||||
|
||||
let (oauth_token, api_token, client, configuration) = this.read_with(&cx, |this, _| {
|
||||
(
|
||||
this.oauth_token.clone(),
|
||||
this.api_token.clone(),
|
||||
this.client.clone(),
|
||||
this.configuration.clone(),
|
||||
)
|
||||
})?;
|
||||
let (oauth_token, api_token, client, api_url, auth_url) =
|
||||
this.read_with(&cx, |this, _| {
|
||||
(
|
||||
this.oauth_token.clone(),
|
||||
this.api_token.clone(),
|
||||
this.client.clone(),
|
||||
this.settings.api_url.clone(),
|
||||
this.settings.auth_url.clone(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let oauth_token = oauth_token.context("No OAuth token available")?;
|
||||
|
||||
let token = match api_token {
|
||||
Some(api_token) if api_token.remaining_seconds() > 5 * 60 => api_token.clone(),
|
||||
_ => {
|
||||
let token_url = configuration.token_url();
|
||||
let token =
|
||||
request_api_token(&oauth_token, token_url.into(), client.clone()).await?;
|
||||
let token = request_api_token(&oauth_token, auth_url, client.clone()).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.api_token = Some(token.clone());
|
||||
cx.notify();
|
||||
@@ -564,19 +496,13 @@ impl CopilotChat {
|
||||
}
|
||||
};
|
||||
|
||||
let api_url = configuration.api_url_from_endpoint(&token.api_endpoint);
|
||||
stream_completion(client.clone(), token.api_key, api_url.into(), request).await
|
||||
stream_completion(client.clone(), token.api_key, api_url, request).await
|
||||
}
|
||||
|
||||
pub fn set_configuration(
|
||||
&mut self,
|
||||
configuration: CopilotChatConfiguration,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let same_configuration = self.configuration == configuration;
|
||||
self.configuration = configuration;
|
||||
if !same_configuration {
|
||||
self.api_token = None;
|
||||
pub fn set_settings(&mut self, settings: CopilotChatSettings, cx: &mut Context<Self>) {
|
||||
let same_settings = self.settings == settings;
|
||||
self.settings = settings;
|
||||
if !same_settings {
|
||||
cx.spawn(async move |this, cx| {
|
||||
Self::update_models(&this, cx).await?;
|
||||
Ok::<_, anyhow::Error>(())
|
||||
@@ -596,12 +522,16 @@ async fn get_models(
|
||||
let mut models: Vec<Model> = all_models
|
||||
.into_iter()
|
||||
.filter(|model| {
|
||||
// Ensure user has access to the model; Policy is present only for models that must be
|
||||
// enabled in the GitHub dashboard
|
||||
model.model_picker_enabled
|
||||
&& model
|
||||
.policy
|
||||
.as_ref()
|
||||
.is_none_or(|policy| policy.state == "enabled")
|
||||
})
|
||||
// The first model from the API response, in any given family, appear to be the non-tagged
|
||||
// models, which are likely the best choice (e.g. gpt-4o rather than gpt-4o-2024-11-20)
|
||||
.dedup_by(|a, b| a.capabilities.family == b.capabilities.family)
|
||||
.collect();
|
||||
|
||||
@@ -678,12 +608,12 @@ async fn request_api_token(
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_oauth_token(contents: String, domain: &str) -> Option<String> {
|
||||
fn extract_oauth_token(contents: String) -> Option<String> {
|
||||
serde_json::from_str::<serde_json::Value>(&contents)
|
||||
.map(|v| {
|
||||
v.as_object().and_then(|obj| {
|
||||
obj.iter().find_map(|(key, value)| {
|
||||
if key.starts_with(domain) {
|
||||
if key.starts_with("github.com") {
|
||||
value["oauth_token"].as_str().map(|v| v.to_string())
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use ::fs::Fs;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
pub use dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest};
|
||||
use fs::Fs;
|
||||
use futures::io::BufReader;
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
pub use http_client::{HttpClient, github::latest_github_release};
|
||||
@@ -337,7 +337,7 @@ pub async fn download_adapter_from_github(
|
||||
pub trait DebugAdapter: 'static + Send + Sync {
|
||||
fn name(&self) -> DebugAdapterName;
|
||||
|
||||
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario>;
|
||||
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario>;
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
@@ -355,7 +355,7 @@ pub trait DebugAdapter: 'static + Send + Sync {
|
||||
/// Extracts the kind (attach/launch) of debug configuration from the given JSON config.
|
||||
/// This method should only return error when the kind cannot be determined for a given configuration;
|
||||
/// in particular, it *should not* validate whether the request as a whole is valid, because that's best left to the debug adapter itself to decide.
|
||||
async fn request_kind(
|
||||
fn request_kind(
|
||||
&self,
|
||||
config: &serde_json::Value,
|
||||
) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
@@ -368,7 +368,7 @@ pub trait DebugAdapter: 'static + Send + Sync {
|
||||
}
|
||||
}
|
||||
|
||||
fn dap_schema(&self) -> serde_json::Value;
|
||||
async fn dap_schema(&self) -> serde_json::Value;
|
||||
|
||||
fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
|
||||
None
|
||||
@@ -394,11 +394,11 @@ impl DebugAdapter for FakeAdapter {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
fn dap_schema(&self) -> serde_json::Value {
|
||||
async fn dap_schema(&self) -> serde_json::Value {
|
||||
serde_json::Value::Null
|
||||
}
|
||||
|
||||
async fn request_kind(
|
||||
fn request_kind(
|
||||
&self,
|
||||
config: &serde_json::Value,
|
||||
) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
@@ -417,7 +417,7 @@ impl DebugAdapter for FakeAdapter {
|
||||
None
|
||||
}
|
||||
|
||||
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
let config = serde_json::to_value(zed_scenario.request).unwrap();
|
||||
|
||||
Ok(DebugScenario {
|
||||
@@ -443,7 +443,7 @@ impl DebugAdapter for FakeAdapter {
|
||||
envs: HashMap::default(),
|
||||
cwd: None,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
request: self.request_kind(&task_definition.config).await?,
|
||||
request: self.request_kind(&task_definition.config)?,
|
||||
configuration: task_definition.config.clone(),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -8,7 +8,8 @@ use dap_types::{
|
||||
requests::Request,
|
||||
};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::AsyncApp;
|
||||
use gpui::{AppContext, AsyncApp};
|
||||
use smol::channel::{Receiver, Sender};
|
||||
use std::{
|
||||
hash::Hash,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
@@ -43,56 +44,99 @@ impl DebugAdapterClient {
|
||||
id: SessionId,
|
||||
binary: DebugAdapterBinary,
|
||||
message_handler: DapMessageHandler,
|
||||
cx: &mut AsyncApp,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let transport_delegate = TransportDelegate::start(&binary, cx).await?;
|
||||
let ((server_rx, server_tx), transport_delegate) =
|
||||
TransportDelegate::start(&binary, cx.clone()).await?;
|
||||
let this = Self {
|
||||
id,
|
||||
binary,
|
||||
transport_delegate,
|
||||
sequence_count: AtomicU64::new(1),
|
||||
};
|
||||
this.connect(message_handler, cx).await?;
|
||||
log::info!("Successfully connected to debug adapter");
|
||||
|
||||
let client_id = this.id;
|
||||
|
||||
// start handling events/reverse requests
|
||||
cx.background_spawn(Self::handle_receive_messages(
|
||||
client_id,
|
||||
server_rx,
|
||||
server_tx.clone(),
|
||||
message_handler,
|
||||
))
|
||||
.detach();
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
pub fn should_reconnect_for_ssh(&self) -> bool {
|
||||
self.transport_delegate.tcp_arguments().is_some()
|
||||
&& self.binary.command.as_deref() == Some("ssh")
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
&self,
|
||||
message_handler: DapMessageHandler,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
self.transport_delegate.connect(message_handler, cx).await
|
||||
}
|
||||
|
||||
pub async fn create_child_connection(
|
||||
pub async fn reconnect(
|
||||
&self,
|
||||
session_id: SessionId,
|
||||
binary: DebugAdapterBinary,
|
||||
message_handler: DapMessageHandler,
|
||||
cx: &mut AsyncApp,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let binary = if let Some(connection) = self.transport_delegate.tcp_arguments() {
|
||||
DebugAdapterBinary {
|
||||
command: None,
|
||||
arguments: Default::default(),
|
||||
envs: Default::default(),
|
||||
cwd: Default::default(),
|
||||
connection: Some(connection),
|
||||
let binary = match self.transport_delegate.transport() {
|
||||
crate::transport::Transport::Tcp(tcp_transport) => DebugAdapterBinary {
|
||||
command: binary.command,
|
||||
arguments: binary.arguments,
|
||||
envs: binary.envs,
|
||||
cwd: binary.cwd,
|
||||
connection: Some(crate::adapters::TcpArguments {
|
||||
host: tcp_transport.host,
|
||||
port: tcp_transport.port,
|
||||
timeout: Some(tcp_transport.timeout),
|
||||
}),
|
||||
request_args: binary.request_args,
|
||||
}
|
||||
} else {
|
||||
self.binary.clone()
|
||||
},
|
||||
_ => self.binary.clone(),
|
||||
};
|
||||
|
||||
Self::start(session_id, binary, message_handler, cx).await
|
||||
}
|
||||
|
||||
async fn handle_receive_messages(
|
||||
client_id: SessionId,
|
||||
server_rx: Receiver<Message>,
|
||||
client_tx: Sender<Message>,
|
||||
mut message_handler: DapMessageHandler,
|
||||
) -> Result<()> {
|
||||
let result = loop {
|
||||
let message = match server_rx.recv().await {
|
||||
Ok(message) => message,
|
||||
Err(e) => break Err(e.into()),
|
||||
};
|
||||
match message {
|
||||
Message::Event(ev) => {
|
||||
log::debug!("Client {} received event `{}`", client_id.0, &ev);
|
||||
|
||||
message_handler(Message::Event(ev))
|
||||
}
|
||||
Message::Request(req) => {
|
||||
log::debug!(
|
||||
"Client {} received reverse request `{}`",
|
||||
client_id.0,
|
||||
&req.command
|
||||
);
|
||||
|
||||
message_handler(Message::Request(req))
|
||||
}
|
||||
Message::Response(response) => {
|
||||
log::debug!("Received response after request timeout: {:#?}", response);
|
||||
}
|
||||
}
|
||||
|
||||
smol::future::yield_now().await;
|
||||
};
|
||||
|
||||
drop(client_tx);
|
||||
|
||||
log::debug!("Handle receive messages dropped");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Send a request to an adapter and get a response back
|
||||
/// Note: This function will block until a response is sent back from the adapter
|
||||
pub async fn request<R: Request>(&self, arguments: R::Arguments) -> Result<R::Response> {
|
||||
@@ -108,7 +152,8 @@ impl DebugAdapterClient {
|
||||
arguments: Some(serialized_arguments),
|
||||
};
|
||||
self.transport_delegate
|
||||
.add_pending_request(sequence_id, callback_tx);
|
||||
.add_pending_request(sequence_id, callback_tx)
|
||||
.await;
|
||||
|
||||
log::debug!(
|
||||
"Client {} send `{}` request with sequence_id: {}",
|
||||
@@ -173,7 +218,7 @@ impl DebugAdapterClient {
|
||||
|
||||
pub fn add_log_handler<F>(&self, f: F, kind: LogKind)
|
||||
where
|
||||
F: 'static + Send + FnMut(IoKind, Option<&str>, &str),
|
||||
F: 'static + Send + FnMut(IoKind, &str),
|
||||
{
|
||||
self.transport_delegate.add_log_handler(f, kind);
|
||||
}
|
||||
@@ -185,11 +230,8 @@ impl DebugAdapterClient {
|
||||
+ Send
|
||||
+ FnMut(u64, R::Arguments) -> Result<R::Response, dap_types::ErrorResponse>,
|
||||
{
|
||||
self.transport_delegate
|
||||
.transport
|
||||
.lock()
|
||||
.as_fake()
|
||||
.on_request::<R, F>(handler);
|
||||
let transport = self.transport_delegate.transport().as_fake();
|
||||
transport.on_request::<R, F>(handler);
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -208,11 +250,8 @@ impl DebugAdapterClient {
|
||||
where
|
||||
F: 'static + Send + Fn(Response),
|
||||
{
|
||||
self.transport_delegate
|
||||
.transport
|
||||
.lock()
|
||||
.as_fake()
|
||||
.on_response::<R, F>(handler);
|
||||
let transport = self.transport_delegate.transport().as_fake();
|
||||
transport.on_response::<R, F>(handler).await;
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -269,7 +308,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
Box::new(|_| panic!("Did not expect to hit this code path")),
|
||||
&mut cx.to_async(),
|
||||
cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -351,7 +390,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}),
|
||||
&mut cx.to_async(),
|
||||
cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -409,7 +448,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}),
|
||||
&mut cx.to_async(),
|
||||
cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -51,26 +51,18 @@ pub fn send_telemetry(scenario: &DebugScenario, location: TelemetrySpawnLocation
|
||||
let Some(adapter) = cx.global::<DapRegistry>().adapter(&scenario.adapter) else {
|
||||
return;
|
||||
};
|
||||
let kind = adapter
|
||||
.request_kind(&scenario.config)
|
||||
.ok()
|
||||
.map(serde_json::to_value)
|
||||
.and_then(Result::ok);
|
||||
let dock = DebuggerSettings::get_global(cx).dock;
|
||||
let config = scenario.config.clone();
|
||||
let with_build_task = scenario.build.is_some();
|
||||
let adapter_name = scenario.adapter.clone();
|
||||
cx.spawn(async move |_| {
|
||||
let kind = adapter
|
||||
.request_kind(&config)
|
||||
.await
|
||||
.ok()
|
||||
.map(serde_json::to_value)
|
||||
.and_then(Result::ok);
|
||||
|
||||
telemetry::event!(
|
||||
"Debugger Session Started",
|
||||
spawn_location = location,
|
||||
with_build_task = with_build_task,
|
||||
kind = kind,
|
||||
adapter = adapter_name,
|
||||
dock_position = dock,
|
||||
);
|
||||
})
|
||||
.detach();
|
||||
telemetry::event!(
|
||||
"Debugger Session Started",
|
||||
spawn_location = location,
|
||||
with_build_task = scenario.build.is_some(),
|
||||
kind = kind,
|
||||
adapter = scenario.adapter.as_ref(),
|
||||
dock_position = dock,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::{
|
||||
};
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
/// Given a user build configuration, locator creates a fill-in debug target ([DebugScenario]) on behalf of the user.
|
||||
/// Given a user build configuration, locator creates a fill-in debug target ([DebugRequest]) on behalf of the user.
|
||||
#[async_trait]
|
||||
pub trait DapLocator: Send + Sync {
|
||||
fn name(&self) -> SharedString;
|
||||
@@ -50,32 +50,30 @@ impl DapRegistry {
|
||||
let name = adapter.name();
|
||||
let _previous_value = self.0.write().adapters.insert(name, adapter);
|
||||
}
|
||||
pub fn add_locator(&self, locator: Arc<dyn DapLocator>) {
|
||||
self.0.write().locators.insert(locator.name(), locator);
|
||||
}
|
||||
|
||||
pub fn remove_adapter(&self, name: &str) {
|
||||
self.0.write().adapters.remove(name);
|
||||
}
|
||||
|
||||
pub fn remove_locator(&self, locator: &str) {
|
||||
self.0.write().locators.remove(locator);
|
||||
}
|
||||
|
||||
pub fn adapter_language(&self, adapter_name: &str) -> Option<LanguageName> {
|
||||
self.adapter(adapter_name)
|
||||
.and_then(|adapter| adapter.adapter_language_name())
|
||||
}
|
||||
|
||||
pub fn add_locator(&self, locator: Arc<dyn DapLocator>) {
|
||||
let _previous_value = self.0.write().locators.insert(locator.name(), locator);
|
||||
debug_assert!(
|
||||
_previous_value.is_none(),
|
||||
"Attempted to insert a new debug locator when one is already registered"
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn adapters_schema(&self) -> task::AdapterSchemas {
|
||||
let mut schemas = AdapterSchemas(vec![]);
|
||||
|
||||
// Clone to avoid holding lock over await points
|
||||
let adapters = self.0.read().adapters.clone();
|
||||
|
||||
for (name, adapter) in adapters.into_iter() {
|
||||
schemas.0.push(AdapterSchema {
|
||||
adapter: name.into(),
|
||||
schema: adapter.dap_schema(),
|
||||
schema: adapter.dap_schema().await,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@ pub(crate) struct CodeLldbDebugAdapter {
|
||||
impl CodeLldbDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "CodeLLDB";
|
||||
|
||||
async fn request_args(
|
||||
fn request_args(
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
task_definition: &DebugTaskDefinition,
|
||||
@@ -37,7 +37,7 @@ impl CodeLldbDebugAdapter {
|
||||
obj.entry("cwd")
|
||||
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
|
||||
|
||||
let request = self.request_kind(&configuration).await?;
|
||||
let request = self.request_kind(&configuration)?;
|
||||
|
||||
Ok(dap::StartDebuggingRequestArguments {
|
||||
request,
|
||||
@@ -89,7 +89,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
let mut configuration = json!({
|
||||
"request": match zed_scenario.request {
|
||||
DebugRequest::Launch(_) => "launch",
|
||||
@@ -133,7 +133,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
fn dap_schema(&self) -> serde_json::Value {
|
||||
async fn dap_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"properties": {
|
||||
"request": {
|
||||
@@ -368,7 +368,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
"--settings".into(),
|
||||
json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
|
||||
],
|
||||
request_args: self.request_args(delegate, &config).await?,
|
||||
request_args: self.request_args(delegate, &config)?,
|
||||
envs: HashMap::default(),
|
||||
connection: None,
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@ impl DebugAdapter for GdbDebugAdapter {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
let mut obj = serde_json::Map::default();
|
||||
|
||||
match &zed_scenario.request {
|
||||
@@ -63,7 +63,7 @@ impl DebugAdapter for GdbDebugAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
fn dap_schema(&self) -> serde_json::Value {
|
||||
async fn dap_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -191,7 +191,7 @@ impl DebugAdapter for GdbDebugAdapter {
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
connection: None,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
request: self.request_kind(&config.config).await?,
|
||||
request: self.request_kind(&config.config)?,
|
||||
configuration,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use anyhow::{Context as _, bail};
|
||||
use collections::HashMap;
|
||||
use dap::{
|
||||
StartDebuggingRequestArguments,
|
||||
adapters::{
|
||||
@@ -10,7 +9,7 @@ use dap::{
|
||||
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use language::LanguageName;
|
||||
use std::{env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
|
||||
use std::{collections::HashMap, env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util;
|
||||
|
||||
@@ -96,7 +95,7 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
Some(SharedString::new_static("Go").into())
|
||||
}
|
||||
|
||||
fn dap_schema(&self) -> serde_json::Value {
|
||||
async fn dap_schema(&self) -> serde_json::Value {
|
||||
// Create common properties shared between launch and attach
|
||||
let common_properties = json!({
|
||||
"debugAdapter": {
|
||||
@@ -343,7 +342,7 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["mode"],
|
||||
"required": ["processId", "mode"],
|
||||
"properties": attach_properties
|
||||
}
|
||||
]
|
||||
@@ -352,7 +351,7 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
let mut args = match &zed_scenario.request {
|
||||
dap::DebugRequest::Attach(attach_config) => {
|
||||
json!({
|
||||
@@ -495,7 +494,7 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
connection,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration,
|
||||
request: self.request_kind(&task_definition.config).await?,
|
||||
request: self.request_kind(&task_definition.config)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -96,17 +96,6 @@ impl JsDebugAdapter {
|
||||
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
|
||||
|
||||
configuration.entry("type").and_modify(normalize_task_type);
|
||||
configuration
|
||||
.entry("console")
|
||||
.or_insert("externalTerminal".into());
|
||||
|
||||
configuration.entry("sourceMaps").or_insert(true.into());
|
||||
configuration
|
||||
.entry("pauseForSourceMap")
|
||||
.or_insert(true.into());
|
||||
configuration
|
||||
.entry("sourceMapRenames")
|
||||
.or_insert(true.into());
|
||||
}
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
@@ -135,7 +124,7 @@ impl JsDebugAdapter {
|
||||
}),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration,
|
||||
request: self.request_kind(&task_definition.config).await?,
|
||||
request: self.request_kind(&task_definition.config)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -147,7 +136,7 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
let mut args = json!({
|
||||
"type": "pwa-node",
|
||||
"request": match zed_scenario.request {
|
||||
@@ -193,7 +182,7 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
fn dap_schema(&self) -> serde_json::Value {
|
||||
async fn dap_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -277,16 +266,6 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
"description": "Use JavaScript source maps if they exist",
|
||||
"default": true
|
||||
},
|
||||
"pauseForSourceMap": {
|
||||
"type": "boolean",
|
||||
"description": "Wait for source maps to load before setting breakpoints.",
|
||||
"default": true
|
||||
},
|
||||
"sourceMapRenames": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to use the \"names\" mapping in sourcemaps.",
|
||||
"default": true
|
||||
},
|
||||
"sourceMapPathOverrides": {
|
||||
"type": "object",
|
||||
"description": "Rewrites the locations of source files from what the sourcemap says to their locations on disk",
|
||||
|
||||
@@ -102,8 +102,7 @@ impl PhpDebugAdapter {
|
||||
envs: HashMap::default(),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration,
|
||||
request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)
|
||||
.await?,
|
||||
request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -111,7 +110,7 @@ impl PhpDebugAdapter {
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for PhpDebugAdapter {
|
||||
fn dap_schema(&self) -> serde_json::Value {
|
||||
async fn dap_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"properties": {
|
||||
"request": {
|
||||
@@ -291,14 +290,11 @@ impl DebugAdapter for PhpDebugAdapter {
|
||||
Some(SharedString::new_static("PHP").into())
|
||||
}
|
||||
|
||||
async fn request_kind(
|
||||
&self,
|
||||
_: &serde_json::Value,
|
||||
) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
Ok(StartDebuggingRequestArgumentsRequest::Launch)
|
||||
}
|
||||
|
||||
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
let obj = match &zed_scenario.request {
|
||||
dap::DebugRequest::Attach(_) => {
|
||||
bail!("Php adapter doesn't support attaching")
|
||||
|
||||
@@ -81,12 +81,12 @@ impl PythonDebugAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
async fn request_args(
|
||||
fn request_args(
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
task_definition: &DebugTaskDefinition,
|
||||
) -> Result<StartDebuggingRequestArguments> {
|
||||
let request = self.request_kind(&task_definition.config).await?;
|
||||
let request = self.request_kind(&task_definition.config)?;
|
||||
|
||||
let mut configuration = task_definition.config.clone();
|
||||
if let Ok(console) = configuration.dot_get_mut("console") {
|
||||
@@ -202,7 +202,7 @@ impl PythonDebugAdapter {
|
||||
}),
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
envs: HashMap::default(),
|
||||
request_args: self.request_args(delegate, config).await?,
|
||||
request_args: self.request_args(delegate, config)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -217,7 +217,7 @@ impl DebugAdapter for PythonDebugAdapter {
|
||||
Some(SharedString::new_static("Python").into())
|
||||
}
|
||||
|
||||
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
let mut args = json!({
|
||||
"request": match zed_scenario.request {
|
||||
DebugRequest::Launch(_) => "launch",
|
||||
@@ -257,7 +257,7 @@ impl DebugAdapter for PythonDebugAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
fn dap_schema(&self) -> serde_json::Value {
|
||||
async fn dap_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"properties": {
|
||||
"request": {
|
||||
|
||||
@@ -45,14 +45,11 @@ impl DebugAdapter for RubyDebugAdapter {
|
||||
Some(SharedString::new_static("Ruby").into())
|
||||
}
|
||||
|
||||
async fn request_kind(
|
||||
&self,
|
||||
_: &serde_json::Value,
|
||||
) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
Ok(StartDebuggingRequestArgumentsRequest::Launch)
|
||||
}
|
||||
|
||||
fn dap_schema(&self) -> serde_json::Value {
|
||||
async fn dap_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -86,7 +83,7 @@ impl DebugAdapter for RubyDebugAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
match zed_scenario.request {
|
||||
DebugRequest::Launch(launch) => {
|
||||
let config = RubyDebugConfig {
|
||||
@@ -199,7 +196,7 @@ impl DebugAdapter for RubyDebugAdapter {
|
||||
),
|
||||
envs: ruby_config.env.into_iter().collect(),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
request: self.request_kind(&definition.config).await?,
|
||||
request: self.request_kind(&definition.config)?,
|
||||
configuration,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -74,7 +74,7 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, scope: &str) -> Threa
|
||||
}
|
||||
|
||||
async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnection> {
|
||||
log::info!("Opening database {}", db_path.display());
|
||||
log::info!("Opening main db");
|
||||
ThreadSafeConnection::builder::<M>(db_path.to_string_lossy().as_ref(), true)
|
||||
.with_db_initialization_query(DB_INITIALIZE_QUERY)
|
||||
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
|
||||
@@ -84,7 +84,7 @@ async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnectio
|
||||
}
|
||||
|
||||
async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection {
|
||||
log::warn!("Opening fallback in-memory database");
|
||||
log::info!("Opening fallback db");
|
||||
ThreadSafeConnection::builder::<M>(FALLBACK_DB_NAME, false)
|
||||
.with_db_initialization_query(DB_INITIALIZE_QUERY)
|
||||
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
|
||||
|
||||
@@ -12,7 +12,6 @@ dap.workspace = true
|
||||
extension.workspace = true
|
||||
gpui.workspace = true
|
||||
serde_json.workspace = true
|
||||
util.workspace = true
|
||||
task.workspace = true
|
||||
workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" }
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
mod extension_dap_adapter;
|
||||
mod extension_locator_adapter;
|
||||
|
||||
use std::{path::Path, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use dap::DapRegistry;
|
||||
use extension::{ExtensionDebugAdapterProviderProxy, ExtensionHostProxy};
|
||||
use extension_dap_adapter::ExtensionDapAdapter;
|
||||
use gpui::App;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::extension_locator_adapter::ExtensionLocatorAdapter;
|
||||
|
||||
pub fn init(extension_host_proxy: Arc<ExtensionHostProxy>, cx: &mut App) {
|
||||
let language_server_registry_proxy = DebugAdapterRegistryProxy::new(cx);
|
||||
@@ -34,33 +30,11 @@ impl ExtensionDebugAdapterProviderProxy for DebugAdapterRegistryProxy {
|
||||
&self,
|
||||
extension: Arc<dyn extension::Extension>,
|
||||
debug_adapter_name: Arc<str>,
|
||||
schema_path: &Path,
|
||||
) {
|
||||
if let Some(adapter) =
|
||||
ExtensionDapAdapter::new(extension, debug_adapter_name, schema_path).log_err()
|
||||
{
|
||||
self.debug_adapter_registry.add_adapter(Arc::new(adapter));
|
||||
}
|
||||
}
|
||||
|
||||
fn register_debug_locator(
|
||||
&self,
|
||||
extension: Arc<dyn extension::Extension>,
|
||||
locator_name: Arc<str>,
|
||||
) {
|
||||
self.debug_adapter_registry
|
||||
.add_locator(Arc::new(ExtensionLocatorAdapter::new(
|
||||
.add_adapter(Arc::new(ExtensionDapAdapter::new(
|
||||
extension,
|
||||
locator_name,
|
||||
debug_adapter_name,
|
||||
)));
|
||||
}
|
||||
|
||||
fn unregister_debug_adapter(&self, debug_adapter_name: Arc<str>) {
|
||||
self.debug_adapter_registry
|
||||
.remove_adapter(&debug_adapter_name);
|
||||
}
|
||||
|
||||
fn unregister_debug_locator(&self, locator_name: Arc<str>) {
|
||||
self.debug_adapter_registry.remove_locator(&locator_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use dap::{
|
||||
StartDebuggingRequestArgumentsRequest,
|
||||
adapters::{
|
||||
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
|
||||
},
|
||||
use dap::adapters::{
|
||||
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
|
||||
};
|
||||
use extension::{Extension, WorktreeDelegate};
|
||||
use gpui::AsyncApp;
|
||||
@@ -19,28 +12,17 @@ use task::{DebugScenario, ZedDebugConfig};
|
||||
pub(crate) struct ExtensionDapAdapter {
|
||||
extension: Arc<dyn Extension>,
|
||||
debug_adapter_name: Arc<str>,
|
||||
schema: serde_json::Value,
|
||||
}
|
||||
|
||||
impl ExtensionDapAdapter {
|
||||
pub(crate) fn new(
|
||||
extension: Arc<dyn extension::Extension>,
|
||||
debug_adapter_name: Arc<str>,
|
||||
schema_path: &Path,
|
||||
) -> Result<Self> {
|
||||
let schema = std::fs::read_to_string(&schema_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to read debug adapter schema for {debug_adapter_name} (from path: `{schema_path:?}`)"
|
||||
)
|
||||
})?;
|
||||
let schema = serde_json::Value::from_str(&schema).with_context(|| {
|
||||
format!("Debug adapter schema for {debug_adapter_name} is not a valid JSON")
|
||||
})?;
|
||||
Ok(Self {
|
||||
) -> Self {
|
||||
Self {
|
||||
extension,
|
||||
debug_adapter_name,
|
||||
schema,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +61,8 @@ impl DebugAdapter for ExtensionDapAdapter {
|
||||
self.debug_adapter_name.as_ref().into()
|
||||
}
|
||||
|
||||
fn dap_schema(&self) -> serde_json::Value {
|
||||
self.schema.clone()
|
||||
async fn dap_schema(&self) -> serde_json::Value {
|
||||
self.extension.get_dap_schema().await.unwrap_or_default()
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
@@ -100,16 +82,7 @@ impl DebugAdapter for ExtensionDapAdapter {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
self.extension.dap_config_to_scenario(zed_scenario).await
|
||||
}
|
||||
|
||||
async fn request_kind(
|
||||
&self,
|
||||
config: &serde_json::Value,
|
||||
) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
self.extension
|
||||
.dap_request_kind(self.debug_adapter_name.clone(), config.clone())
|
||||
.await
|
||||
fn config_from_zed_format(&self, _zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
Err(anyhow::anyhow!("DAP extensions are not implemented yet"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
|
||||
use extension::Extension;
|
||||
use gpui::SharedString;
|
||||
use std::sync::Arc;
|
||||
use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
|
||||
|
||||
pub(crate) struct ExtensionLocatorAdapter {
|
||||
extension: Arc<dyn Extension>,
|
||||
locator_name: SharedString,
|
||||
}
|
||||
|
||||
impl ExtensionLocatorAdapter {
|
||||
pub(crate) fn new(extension: Arc<dyn extension::Extension>, locator_name: Arc<str>) -> Self {
|
||||
Self {
|
||||
extension,
|
||||
locator_name: SharedString::from(locator_name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DapLocator for ExtensionLocatorAdapter {
|
||||
fn name(&self) -> SharedString {
|
||||
self.locator_name.clone()
|
||||
}
|
||||
/// Determines whether this locator can generate debug target for given task.
|
||||
async fn create_scenario(
|
||||
&self,
|
||||
build_config: &TaskTemplate,
|
||||
resolved_label: &str,
|
||||
adapter: &DebugAdapterName,
|
||||
) -> Option<DebugScenario> {
|
||||
self.extension
|
||||
.dap_locator_create_scenario(
|
||||
self.locator_name.as_ref().to_owned(),
|
||||
build_config.clone(),
|
||||
resolved_label.to_owned(),
|
||||
adapter.0.as_ref().to_owned(),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
|
||||
Err(anyhow::anyhow!("Not implemented"))
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
use dap::{
|
||||
adapters::DebugAdapterName,
|
||||
client::SessionId,
|
||||
debugger_settings::DebuggerSettings,
|
||||
transport::{IoKind, LogKind},
|
||||
@@ -32,13 +31,6 @@ use workspace::{
|
||||
ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
|
||||
};
|
||||
|
||||
// TODO:
|
||||
// - [x] stop sorting by session ID
|
||||
// - [x] pick the most recent session by default (logs if available, RPC messages otherwise)
|
||||
// - [ ] dump the launch/attach request somewhere (logs?)
|
||||
|
||||
const MAX_SESSIONS: usize = 10;
|
||||
|
||||
struct DapLogView {
|
||||
editor: Entity<Editor>,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -51,9 +43,9 @@ struct DapLogView {
|
||||
|
||||
pub struct LogStore {
|
||||
projects: HashMap<WeakEntity<Project>, ProjectState>,
|
||||
debug_sessions: VecDeque<DebugAdapterState>,
|
||||
rpc_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
|
||||
adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
|
||||
debug_clients: HashMap<SessionId, DebugAdapterState>,
|
||||
rpc_tx: UnboundedSender<(SessionId, IoKind, String)>,
|
||||
adapter_log_tx: UnboundedSender<(SessionId, IoKind, String)>,
|
||||
}
|
||||
|
||||
struct ProjectState {
|
||||
@@ -61,19 +53,13 @@ struct ProjectState {
|
||||
}
|
||||
|
||||
struct DebugAdapterState {
|
||||
id: SessionId,
|
||||
log_messages: VecDeque<SharedString>,
|
||||
log_messages: VecDeque<String>,
|
||||
rpc_messages: RpcMessages,
|
||||
adapter_name: DebugAdapterName,
|
||||
has_adapter_logs: bool,
|
||||
is_terminated: bool,
|
||||
}
|
||||
|
||||
struct RpcMessages {
|
||||
messages: VecDeque<SharedString>,
|
||||
messages: VecDeque<String>,
|
||||
last_message_kind: Option<MessageKind>,
|
||||
initialization_sequence: Vec<SharedString>,
|
||||
last_init_message_kind: Option<MessageKind>,
|
||||
}
|
||||
|
||||
impl RpcMessages {
|
||||
@@ -82,9 +68,7 @@ impl RpcMessages {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
last_message_kind: None,
|
||||
last_init_message_kind: None,
|
||||
messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT),
|
||||
initialization_sequence: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,27 +92,22 @@ impl MessageKind {
|
||||
}
|
||||
|
||||
impl DebugAdapterState {
|
||||
fn new(id: SessionId, adapter_name: DebugAdapterName, has_adapter_logs: bool) -> Self {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id,
|
||||
log_messages: VecDeque::new(),
|
||||
rpc_messages: RpcMessages::new(),
|
||||
adapter_name,
|
||||
has_adapter_logs,
|
||||
is_terminated: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LogStore {
|
||||
pub fn new(cx: &Context<Self>) -> Self {
|
||||
let (rpc_tx, mut rpc_rx) =
|
||||
unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
|
||||
let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, String)>();
|
||||
cx.spawn(async move |this, cx| {
|
||||
while let Some((session_id, io_kind, command, message)) = rpc_rx.next().await {
|
||||
while let Some((client_id, io_kind, message)) = rpc_rx.next().await {
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.add_debug_adapter_message(session_id, io_kind, command, message, cx);
|
||||
this.on_rpc_log(client_id, io_kind, &message, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -138,13 +117,12 @@ impl LogStore {
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
let (adapter_log_tx, mut adapter_log_rx) =
|
||||
unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
|
||||
let (adapter_log_tx, mut adapter_log_rx) = unbounded::<(SessionId, IoKind, String)>();
|
||||
cx.spawn(async move |this, cx| {
|
||||
while let Some((session_id, io_kind, _, message)) = adapter_log_rx.next().await {
|
||||
while let Some((client_id, io_kind, message)) = adapter_log_rx.next().await {
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.add_debug_adapter_log(session_id, io_kind, message, cx);
|
||||
this.on_adapter_log(client_id, io_kind, &message, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -157,10 +135,30 @@ impl LogStore {
|
||||
rpc_tx,
|
||||
adapter_log_tx,
|
||||
projects: HashMap::new(),
|
||||
debug_sessions: Default::default(),
|
||||
debug_clients: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_rpc_log(
|
||||
&mut self,
|
||||
client_id: SessionId,
|
||||
io_kind: IoKind,
|
||||
message: &str,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.add_debug_client_message(client_id, io_kind, message.to_string(), cx);
|
||||
}
|
||||
|
||||
fn on_adapter_log(
|
||||
&mut self,
|
||||
client_id: SessionId,
|
||||
io_kind: IoKind,
|
||||
message: &str,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.add_debug_client_log(client_id, io_kind, message.to_string(), cx);
|
||||
}
|
||||
|
||||
pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
|
||||
let weak_project = project.downgrade();
|
||||
self.projects.insert(
|
||||
@@ -176,15 +174,13 @@ impl LogStore {
|
||||
dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
|
||||
let session = dap_store.read(cx).session_by_id(session_id);
|
||||
if let Some(session) = session {
|
||||
this.add_debug_session(*session_id, session, cx);
|
||||
this.add_debug_client(*session_id, session, cx);
|
||||
}
|
||||
}
|
||||
dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
|
||||
this.get_debug_adapter_state(*session_id)
|
||||
.iter_mut()
|
||||
.for_each(|state| state.is_terminated = true);
|
||||
this.clean_sessions(cx);
|
||||
this.remove_debug_client(*session_id, cx);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
),
|
||||
@@ -194,88 +190,63 @@ impl LogStore {
|
||||
}
|
||||
|
||||
fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> {
|
||||
self.debug_sessions
|
||||
.iter_mut()
|
||||
.find(|adapter_state| adapter_state.id == id)
|
||||
self.debug_clients.get_mut(&id)
|
||||
}
|
||||
|
||||
fn add_debug_adapter_message(
|
||||
fn add_debug_client_message(
|
||||
&mut self,
|
||||
id: SessionId,
|
||||
io_kind: IoKind,
|
||||
command: Option<SharedString>,
|
||||
message: SharedString,
|
||||
message: String,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let is_init_seq = command.as_ref().is_some_and(|command| {
|
||||
matches!(
|
||||
command.as_ref(),
|
||||
"attach" | "launch" | "initialize" | "configurationDone"
|
||||
)
|
||||
});
|
||||
|
||||
let kind = match io_kind {
|
||||
IoKind::StdOut | IoKind::StdErr => MessageKind::Receive,
|
||||
IoKind::StdIn => MessageKind::Send,
|
||||
};
|
||||
|
||||
let rpc_messages = &mut debug_client_state.rpc_messages;
|
||||
|
||||
// Push a separator if the kind has changed
|
||||
if rpc_messages.last_message_kind != Some(kind) {
|
||||
Self::get_debug_adapter_entry(
|
||||
Self::add_debug_client_entry(
|
||||
&mut rpc_messages.messages,
|
||||
id,
|
||||
kind.label().into(),
|
||||
kind.label().to_string(),
|
||||
LogKind::Rpc,
|
||||
cx,
|
||||
);
|
||||
rpc_messages.last_message_kind = Some(kind);
|
||||
}
|
||||
|
||||
let entry = Self::get_debug_adapter_entry(
|
||||
&mut rpc_messages.messages,
|
||||
id,
|
||||
message,
|
||||
LogKind::Rpc,
|
||||
cx,
|
||||
);
|
||||
|
||||
if is_init_seq {
|
||||
if rpc_messages.last_init_message_kind != Some(kind) {
|
||||
rpc_messages
|
||||
.initialization_sequence
|
||||
.push(SharedString::from(kind.label()));
|
||||
rpc_messages.last_init_message_kind = Some(kind);
|
||||
}
|
||||
rpc_messages.initialization_sequence.push(entry);
|
||||
}
|
||||
Self::add_debug_client_entry(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn add_debug_adapter_log(
|
||||
fn add_debug_client_log(
|
||||
&mut self,
|
||||
id: SessionId,
|
||||
io_kind: IoKind,
|
||||
message: SharedString,
|
||||
message: String,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(debug_adapter_state) = self.get_debug_adapter_state(id) else {
|
||||
let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let message = match io_kind {
|
||||
IoKind::StdErr => format!("stderr: {message}").into(),
|
||||
IoKind::StdErr => {
|
||||
let mut message = message.clone();
|
||||
message.insert_str(0, "stderr: ");
|
||||
message
|
||||
}
|
||||
_ => message,
|
||||
};
|
||||
|
||||
Self::get_debug_adapter_entry(
|
||||
&mut debug_adapter_state.log_messages,
|
||||
Self::add_debug_client_entry(
|
||||
&mut debug_client_state.log_messages,
|
||||
id,
|
||||
message,
|
||||
LogKind::Adapter,
|
||||
@@ -284,13 +255,13 @@ impl LogStore {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn get_debug_adapter_entry(
|
||||
log_lines: &mut VecDeque<SharedString>,
|
||||
fn add_debug_client_entry(
|
||||
log_lines: &mut VecDeque<String>,
|
||||
id: SessionId,
|
||||
message: SharedString,
|
||||
message: String,
|
||||
kind: LogKind,
|
||||
cx: &mut Context<Self>,
|
||||
) -> SharedString {
|
||||
) {
|
||||
while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT {
|
||||
log_lines.pop_front();
|
||||
}
|
||||
@@ -304,69 +275,33 @@ impl LogStore {
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.map(SharedString::from)
|
||||
.unwrap_or(message)
|
||||
} else {
|
||||
message
|
||||
};
|
||||
log_lines.push_back(entry.clone());
|
||||
|
||||
cx.emit(Event::NewLogEntry {
|
||||
id,
|
||||
entry: entry.clone(),
|
||||
kind,
|
||||
});
|
||||
|
||||
entry
|
||||
cx.emit(Event::NewLogEntry { id, entry, kind });
|
||||
}
|
||||
|
||||
fn add_debug_session(
|
||||
fn add_debug_client(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
session: Entity<Session>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self
|
||||
.debug_sessions
|
||||
.iter_mut()
|
||||
.any(|adapter_state| adapter_state.id == session_id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
|
||||
(
|
||||
session.adapter(),
|
||||
session
|
||||
.adapter_client()
|
||||
.map(|client| client.has_adapter_logs())
|
||||
.unwrap_or(false),
|
||||
)
|
||||
});
|
||||
|
||||
self.debug_sessions.push_back(DebugAdapterState::new(
|
||||
session_id,
|
||||
adapter_name,
|
||||
has_adapter_logs,
|
||||
));
|
||||
|
||||
self.clean_sessions(cx);
|
||||
client_id: SessionId,
|
||||
client: Entity<Session>,
|
||||
cx: &App,
|
||||
) -> Option<&mut DebugAdapterState> {
|
||||
let client_state = self
|
||||
.debug_clients
|
||||
.entry(client_id)
|
||||
.or_insert_with(DebugAdapterState::new);
|
||||
|
||||
let io_tx = self.rpc_tx.clone();
|
||||
|
||||
let Some(client) = session.read(cx).adapter_client() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let client = client.read(cx).adapter_client()?;
|
||||
client.add_log_handler(
|
||||
move |io_kind, command, message| {
|
||||
move |io_kind, message| {
|
||||
io_tx
|
||||
.unbounded_send((
|
||||
session_id,
|
||||
io_kind,
|
||||
command.map(|command| command.to_owned().into()),
|
||||
message.to_owned().into(),
|
||||
))
|
||||
.unbounded_send((client_id, io_kind, message.to_string()))
|
||||
.ok();
|
||||
},
|
||||
LogKind::Rpc,
|
||||
@@ -374,66 +309,34 @@ impl LogStore {
|
||||
|
||||
let log_io_tx = self.adapter_log_tx.clone();
|
||||
client.add_log_handler(
|
||||
move |io_kind, command, message| {
|
||||
move |io_kind, message| {
|
||||
log_io_tx
|
||||
.unbounded_send((
|
||||
session_id,
|
||||
io_kind,
|
||||
command.map(|command| command.to_owned().into()),
|
||||
message.to_owned().into(),
|
||||
))
|
||||
.unbounded_send((client_id, io_kind, message.to_string()))
|
||||
.ok();
|
||||
},
|
||||
LogKind::Adapter,
|
||||
);
|
||||
|
||||
Some(client_state)
|
||||
}
|
||||
|
||||
fn clean_sessions(&mut self, cx: &mut Context<Self>) {
|
||||
let mut to_remove = self.debug_sessions.len().saturating_sub(MAX_SESSIONS);
|
||||
self.debug_sessions.retain(|session| {
|
||||
if to_remove > 0 && session.is_terminated {
|
||||
to_remove -= 1;
|
||||
return false;
|
||||
}
|
||||
true
|
||||
});
|
||||
fn remove_debug_client(&mut self, client_id: SessionId, cx: &mut Context<Self>) {
|
||||
self.debug_clients.remove(&client_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn log_messages_for_session(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
) -> Option<&mut VecDeque<SharedString>> {
|
||||
self.debug_sessions
|
||||
.iter_mut()
|
||||
.find(|session| session.id == session_id)
|
||||
.map(|state| &mut state.log_messages)
|
||||
fn log_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {
|
||||
Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages)
|
||||
}
|
||||
|
||||
fn rpc_messages_for_session(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
) -> Option<&mut VecDeque<SharedString>> {
|
||||
self.debug_sessions.iter_mut().find_map(|state| {
|
||||
if state.id == session_id {
|
||||
Some(&mut state.rpc_messages.messages)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn initialization_sequence_for_session(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
) -> Option<&mut Vec<SharedString>> {
|
||||
self.debug_sessions.iter_mut().find_map(|state| {
|
||||
if state.id == session_id {
|
||||
Some(&mut state.rpc_messages.initialization_sequence)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
fn rpc_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {
|
||||
Some(
|
||||
&mut self
|
||||
.debug_clients
|
||||
.get_mut(&client_id)?
|
||||
.rpc_messages
|
||||
.messages,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,15 +356,18 @@ impl Render for DapLogToolbarItemView {
|
||||
return Empty.into_any_element();
|
||||
};
|
||||
|
||||
let (menu_rows, current_session_id) = log_view.update(cx, |log_view, cx| {
|
||||
let (menu_rows, current_client_id) = log_view.update(cx, |log_view, cx| {
|
||||
(
|
||||
log_view.menu_items(cx),
|
||||
log_view.current_view.map(|(session_id, _)| session_id),
|
||||
log_view.menu_items(cx).unwrap_or_default(),
|
||||
log_view.current_view.map(|(client_id, _)| client_id),
|
||||
)
|
||||
});
|
||||
|
||||
let current_client = current_session_id
|
||||
.and_then(|session_id| menu_rows.iter().find(|row| row.session_id == session_id));
|
||||
let current_client = current_client_id.and_then(|current_client_id| {
|
||||
menu_rows
|
||||
.iter()
|
||||
.find(|row| row.client_id == current_client_id)
|
||||
});
|
||||
|
||||
let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView")
|
||||
.anchor(gpui::Corner::TopLeft)
|
||||
@@ -471,8 +377,8 @@ impl Render for DapLogToolbarItemView {
|
||||
.map(|sub_item| {
|
||||
Cow::Owned(format!(
|
||||
"{} ({}) - {}",
|
||||
sub_item.adapter_name,
|
||||
sub_item.session_id.0,
|
||||
sub_item.client_name,
|
||||
sub_item.client_id.0,
|
||||
match sub_item.selected_entry {
|
||||
LogKind::Adapter => ADAPTER_LOGS,
|
||||
LogKind::Rpc => RPC_MESSAGES,
|
||||
@@ -491,10 +397,9 @@ impl Render for DapLogToolbarItemView {
|
||||
.w_full()
|
||||
.pl_2()
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{}. {}",
|
||||
row.session_id.0, row.adapter_name,
|
||||
))
|
||||
Label::new(
|
||||
format!("{}. {}", row.client_id.0, row.client_name,),
|
||||
)
|
||||
.color(workspace::ui::Color::Muted),
|
||||
)
|
||||
.into_any_element()
|
||||
@@ -510,40 +415,23 @@ impl Render for DapLogToolbarItemView {
|
||||
.into_any_element()
|
||||
},
|
||||
window.handler_for(&log_view, move |view, window, cx| {
|
||||
view.show_log_messages_for_adapter(row.session_id, window, cx);
|
||||
view.show_log_messages_for_adapter(row.client_id, window, cx);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
menu = menu
|
||||
.custom_entry(
|
||||
move |_window, _cx| {
|
||||
div()
|
||||
.w_full()
|
||||
.pl_4()
|
||||
.child(Label::new(RPC_MESSAGES))
|
||||
.into_any_element()
|
||||
},
|
||||
window.handler_for(&log_view, move |view, window, cx| {
|
||||
view.show_rpc_trace_for_server(row.session_id, window, cx);
|
||||
}),
|
||||
)
|
||||
.custom_entry(
|
||||
move |_window, _cx| {
|
||||
div()
|
||||
.w_full()
|
||||
.pl_4()
|
||||
.child(Label::new(INITIALIZATION_SEQUENCE))
|
||||
.into_any_element()
|
||||
},
|
||||
window.handler_for(&log_view, move |view, window, cx| {
|
||||
view.show_initialization_sequence_for_server(
|
||||
row.session_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
);
|
||||
menu = menu.custom_entry(
|
||||
move |_window, _cx| {
|
||||
div()
|
||||
.w_full()
|
||||
.pl_4()
|
||||
.child(Label::new(RPC_MESSAGES))
|
||||
.into_any_element()
|
||||
},
|
||||
window.handler_for(&log_view, move |view, window, cx| {
|
||||
view.show_rpc_trace_for_server(row.client_id, window, cx);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
menu
|
||||
@@ -630,13 +518,7 @@ impl DapLogView {
|
||||
}
|
||||
});
|
||||
|
||||
let state_info = log_store
|
||||
.read(cx)
|
||||
.debug_sessions
|
||||
.back()
|
||||
.map(|session| (session.id, session.has_adapter_logs));
|
||||
|
||||
let mut this = Self {
|
||||
Self {
|
||||
editor,
|
||||
focus_handle,
|
||||
project,
|
||||
@@ -644,17 +526,7 @@ impl DapLogView {
|
||||
editor_subscriptions,
|
||||
current_view: None,
|
||||
_subscriptions: vec![events_subscriptions],
|
||||
};
|
||||
|
||||
if let Some((session_id, have_adapter_logs)) = state_info {
|
||||
if have_adapter_logs {
|
||||
this.show_log_messages_for_adapter(session_id, window, cx);
|
||||
} else {
|
||||
this.show_rpc_trace_for_server(session_id, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn editor_for_logs(
|
||||
@@ -687,34 +559,42 @@ impl DapLogView {
|
||||
(editor, vec![editor_subscription, search_subscription])
|
||||
}
|
||||
|
||||
fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> {
|
||||
self.log_store
|
||||
fn menu_items(&self, cx: &App) -> Option<Vec<DapMenuItem>> {
|
||||
let mut menu_items = self
|
||||
.project
|
||||
.read(cx)
|
||||
.debug_sessions
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|state| DapMenuItem {
|
||||
session_id: state.id,
|
||||
adapter_name: state.adapter_name.clone(),
|
||||
has_adapter_logs: state.has_adapter_logs,
|
||||
selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
|
||||
.dap_store()
|
||||
.read(cx)
|
||||
.sessions()
|
||||
.filter_map(|session| {
|
||||
let session = session.read(cx);
|
||||
session.adapter();
|
||||
let client = session.adapter_client()?;
|
||||
Some(DapMenuItem {
|
||||
client_id: client.id(),
|
||||
client_name: session.adapter().to_string(),
|
||||
has_adapter_logs: client.has_adapter_logs(),
|
||||
selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.collect::<Vec<_>>();
|
||||
menu_items.sort_by_key(|item| item.client_id.0);
|
||||
Some(menu_items)
|
||||
}
|
||||
|
||||
fn show_rpc_trace_for_server(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
client_id: SessionId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let rpc_log = self.log_store.update(cx, |log_store, _| {
|
||||
log_store
|
||||
.rpc_messages_for_session(session_id)
|
||||
.map(|state| log_contents(state.iter().cloned()))
|
||||
.rpc_messages_for_client(client_id)
|
||||
.map(|state| log_contents(&state))
|
||||
});
|
||||
if let Some(rpc_log) = rpc_log {
|
||||
self.current_view = Some((session_id, LogKind::Rpc));
|
||||
self.current_view = Some((client_id, LogKind::Rpc));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
|
||||
let language = self.project.read(cx).languages().language_for_name("JSON");
|
||||
editor
|
||||
@@ -746,17 +626,17 @@ impl DapLogView {
|
||||
|
||||
fn show_log_messages_for_adapter(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
client_id: SessionId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let message_log = self.log_store.update(cx, |log_store, _| {
|
||||
log_store
|
||||
.log_messages_for_session(session_id)
|
||||
.map(|state| log_contents(state.iter().cloned()))
|
||||
.log_messages_for_client(client_id)
|
||||
.map(|state| log_contents(&state))
|
||||
});
|
||||
if let Some(message_log) = message_log {
|
||||
self.current_view = Some((session_id, LogKind::Adapter));
|
||||
self.current_view = Some((client_id, LogKind::Adapter));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
|
||||
editor
|
||||
.read(cx)
|
||||
@@ -772,53 +652,14 @@ impl DapLogView {
|
||||
|
||||
cx.focus_self(window);
|
||||
}
|
||||
|
||||
fn show_initialization_sequence_for_server(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let rpc_log = self.log_store.update(cx, |log_store, _| {
|
||||
log_store
|
||||
.initialization_sequence_for_session(session_id)
|
||||
.map(|state| log_contents(state.iter().cloned()))
|
||||
});
|
||||
if let Some(rpc_log) = rpc_log {
|
||||
self.current_view = Some((session_id, LogKind::Rpc));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
|
||||
let language = self.project.read(cx).languages().language_for_name("JSON");
|
||||
editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.expect("log buffer should be a singleton")
|
||||
.update(cx, |_, cx| {
|
||||
cx.spawn({
|
||||
let buffer = cx.entity();
|
||||
async move |_, cx| {
|
||||
let language = language.await.ok();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_language(language, cx);
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
|
||||
self.editor = editor;
|
||||
self.editor_subscriptions = editor_subscriptions;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
cx.focus_self(window);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_contents(lines: impl Iterator<Item = SharedString>) -> String {
|
||||
lines.fold(String::new(), |mut acc, el| {
|
||||
acc.push_str(&el);
|
||||
fn log_contents(lines: &VecDeque<String>) -> String {
|
||||
let (a, b) = lines.as_slices();
|
||||
let a = a.iter().map(move |v| v.as_ref());
|
||||
let b = b.iter().map(move |v| v.as_ref());
|
||||
a.chain(b).fold(String::new(), |mut acc, el| {
|
||||
acc.push_str(el);
|
||||
acc.push('\n');
|
||||
acc
|
||||
})
|
||||
@@ -826,15 +667,14 @@ 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 client_id: SessionId,
|
||||
pub client_name: String,
|
||||
pub has_adapter_logs: bool,
|
||||
pub selected_entry: LogKind,
|
||||
}
|
||||
|
||||
const ADAPTER_LOGS: &str = "Adapter Logs";
|
||||
const RPC_MESSAGES: &str = "RPC Messages";
|
||||
const INITIALIZATION_SEQUENCE: &str = "Initialization Sequence";
|
||||
|
||||
impl Render for DapLogView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
@@ -996,7 +836,7 @@ impl Focusable for DapLogView {
|
||||
pub enum Event {
|
||||
NewLogEntry {
|
||||
id: SessionId,
|
||||
entry: SharedString,
|
||||
entry: String,
|
||||
kind: LogKind,
|
||||
},
|
||||
}
|
||||
@@ -1009,16 +849,12 @@ impl EventEmitter<SearchEvent> for DapLogView {}
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl LogStore {
|
||||
pub fn contained_session_ids(&self) -> Vec<SessionId> {
|
||||
self.debug_sessions
|
||||
.iter()
|
||||
.map(|session| session.id)
|
||||
.collect()
|
||||
self.debug_clients.keys().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
|
||||
self.debug_sessions
|
||||
.iter()
|
||||
.find(|adapter_state| adapter_state.id == session_id)
|
||||
pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
|
||||
self.debug_clients
|
||||
.get(&session_id)
|
||||
.expect("This session should exist if a test is calling")
|
||||
.rpc_messages
|
||||
.messages
|
||||
@@ -1026,10 +862,9 @@ impl LogStore {
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
|
||||
self.debug_sessions
|
||||
.iter()
|
||||
.find(|adapter_state| adapter_state.id == session_id)
|
||||
pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
|
||||
self.debug_clients
|
||||
.get(&session_id)
|
||||
.expect("This session should exist if a test is calling")
|
||||
.log_messages
|
||||
.clone()
|
||||
|
||||
@@ -26,7 +26,6 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
alacritty_terminal.workspace = true
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
@@ -35,6 +34,7 @@ dap.workspace = true
|
||||
dap_adapters = { workspace = true, optional = true }
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
file_icons.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
@@ -51,7 +51,7 @@ project.workspace = true
|
||||
rpc.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
# serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
shlex.workspace = true
|
||||
sysinfo.workspace = true
|
||||
@@ -59,8 +59,6 @@ task.workspace = true
|
||||
tasks_ui.workspace = true
|
||||
terminal_view.workspace = true
|
||||
theme.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
tree-sitter-json.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -183,7 +183,6 @@ impl PickerDelegate for AttachModalDelegate {
|
||||
.collect::<Vec<_>>(),
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
100,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
@@ -229,36 +228,26 @@ impl PickerDelegate for AttachModalDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
let Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
|
||||
registry.adapter(&self.definition.adapter)
|
||||
let Some(scenario) = cx.read_global::<DapRegistry, _>(|registry, _| {
|
||||
registry
|
||||
.adapter(&self.definition.adapter)
|
||||
.and_then(|adapter| adapter.config_from_zed_format(self.definition.clone()).ok())
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let definition = self.definition.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
|
||||
return;
|
||||
};
|
||||
let panel = self
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some(panel) = panel {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.start_session(scenario, Default::default(), None, None, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
let panel = workspace
|
||||
.update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some(panel) = panel {
|
||||
panel
|
||||
.update_in(cx, |panel, window, cx| {
|
||||
panel.start_session(scenario, Default::default(), None, None, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
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,
|
||||
NewProcessModal, NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop,
|
||||
ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::Result;
|
||||
use dap::adapters::DebugAdapterName;
|
||||
use dap::debugger_settings::DebugPanelDockPosition;
|
||||
use dap::{
|
||||
@@ -22,16 +21,14 @@ use gpui::{
|
||||
WeakEntity, actions, anchored, deferred,
|
||||
};
|
||||
|
||||
use itertools::Itertools as _;
|
||||
use language::Buffer;
|
||||
use project::debugger::session::{Session, SessionStateEvent};
|
||||
use project::{Fs, ProjectPath, WorktreeId};
|
||||
use project::{Fs, WorktreeId};
|
||||
use project::{Project, debugger::session::ThreadStatus};
|
||||
use rpc::proto::{self};
|
||||
use settings::Settings;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::sync::Arc;
|
||||
use task::{DebugScenario, TaskContext};
|
||||
use tree_sitter::{Query, StreamingIterator as _};
|
||||
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use util::maybe;
|
||||
use workspace::SplitDirection;
|
||||
@@ -73,7 +70,6 @@ pub struct DebugPanel {
|
||||
fs: Arc<dyn Fs>,
|
||||
is_zoomed: bool,
|
||||
_subscriptions: [Subscription; 1],
|
||||
breakpoint_list: Entity<BreakpointList>,
|
||||
}
|
||||
|
||||
impl DebugPanel {
|
||||
@@ -101,7 +97,6 @@ impl DebugPanel {
|
||||
sessions: vec![],
|
||||
active_session: None,
|
||||
focus_handle,
|
||||
breakpoint_list: BreakpointList::new(None, workspace.weak_handle(), &project, cx),
|
||||
project,
|
||||
workspace: workspace.weak_handle(),
|
||||
context_menu: None,
|
||||
@@ -181,7 +176,6 @@ impl DebugPanel {
|
||||
dap_store.new_session(
|
||||
scenario.label.clone(),
|
||||
DebugAdapterName(scenario.adapter.clone()),
|
||||
task_context.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
@@ -344,13 +338,12 @@ impl DebugPanel {
|
||||
let adapter = curr_session.read(cx).adapter().clone();
|
||||
let binary = curr_session.read(cx).binary().cloned().unwrap();
|
||||
let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
|
||||
let task_context = curr_session.read(cx).task_context().clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
task.await;
|
||||
|
||||
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, None, cx);
|
||||
|
||||
let task = session.update(cx, |session, cx| {
|
||||
session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
|
||||
@@ -400,17 +393,11 @@ impl DebugPanel {
|
||||
log::error!("Attempted to start a child-session without a binary");
|
||||
return;
|
||||
};
|
||||
let task_context = parent_session.read(cx).task_context().clone();
|
||||
binary.request_args = request.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
|
||||
let session = dap_store.new_session(
|
||||
label,
|
||||
adapter,
|
||||
task_context,
|
||||
Some(parent_session.clone()),
|
||||
cx,
|
||||
);
|
||||
let session =
|
||||
dap_store.new_session(label, adapter, Some(parent_session.clone()), cx);
|
||||
|
||||
let task = session.update(cx, |session, cx| {
|
||||
session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
|
||||
@@ -863,21 +850,16 @@ impl DebugPanel {
|
||||
let threads =
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
let session = running_state.session();
|
||||
session.read(cx).is_running().then(|| {
|
||||
session.update(cx, |session, cx| {
|
||||
session.threads(cx)
|
||||
})
|
||||
})
|
||||
session
|
||||
.update(cx, |session, cx| session.threads(cx))
|
||||
});
|
||||
|
||||
threads.and_then(|threads| {
|
||||
self.render_thread_dropdown(
|
||||
&running_state,
|
||||
threads,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
self.render_thread_dropdown(
|
||||
&running_state,
|
||||
threads,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.when(!is_side, |this| this.gap_2().child(Divider::vertical()))
|
||||
},
|
||||
@@ -962,98 +944,69 @@ impl DebugPanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn save_scenario(
|
||||
&self,
|
||||
scenario: &DebugScenario,
|
||||
worktree_id: WorktreeId,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<ProjectPath>> {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
|
||||
return Task::ready(Err(anyhow!("Couldn't get worktree path")));
|
||||
};
|
||||
// TODO: restore once we have proper comment preserving file edits
|
||||
// pub(crate) fn save_scenario(
|
||||
// &self,
|
||||
// scenario: &DebugScenario,
|
||||
// worktree_id: WorktreeId,
|
||||
// window: &mut Window,
|
||||
// cx: &mut App,
|
||||
// ) -> Task<Result<ProjectPath>> {
|
||||
// self.workspace
|
||||
// .update(cx, |workspace, cx| {
|
||||
// let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
|
||||
// return Task::ready(Err(anyhow!("Couldn't get worktree path")));
|
||||
// };
|
||||
|
||||
let serialized_scenario = serde_json::to_value(scenario);
|
||||
// let serialized_scenario = serde_json::to_value(scenario);
|
||||
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
let serialized_scenario = serialized_scenario?;
|
||||
let fs =
|
||||
workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
|
||||
// cx.spawn_in(window, async move |workspace, cx| {
|
||||
// let serialized_scenario = serialized_scenario?;
|
||||
// let fs =
|
||||
// workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
|
||||
|
||||
path.push(paths::local_settings_folder_relative_path());
|
||||
if !fs.is_dir(path.as_path()).await {
|
||||
fs.create_dir(path.as_path()).await?;
|
||||
}
|
||||
path.pop();
|
||||
// path.push(paths::local_settings_folder_relative_path());
|
||||
// if !fs.is_dir(path.as_path()).await {
|
||||
// fs.create_dir(path.as_path()).await?;
|
||||
// }
|
||||
// path.pop();
|
||||
|
||||
path.push(paths::local_debug_file_relative_path());
|
||||
let path = path.as_path();
|
||||
// path.push(paths::local_debug_file_relative_path());
|
||||
// let path = path.as_path();
|
||||
|
||||
if !fs.is_file(path).await {
|
||||
fs.create_file(path, Default::default()).await?;
|
||||
fs.write(
|
||||
path,
|
||||
settings::initial_local_debug_tasks_content()
|
||||
.to_string()
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
// if !fs.is_file(path).await {
|
||||
// fs.create_file(path, Default::default()).await?;
|
||||
// fs.write(
|
||||
// path,
|
||||
// initial_local_debug_tasks_content().to_string().as_bytes(),
|
||||
// )
|
||||
// .await?;
|
||||
// }
|
||||
|
||||
let mut content = fs.load(path).await?;
|
||||
let new_scenario = serde_json_lenient::to_string_pretty(&serialized_scenario)?
|
||||
.lines()
|
||||
.map(|l| format!(" {l}"))
|
||||
.join("\n");
|
||||
// let content = fs.load(path).await?;
|
||||
// let mut values =
|
||||
// serde_json_lenient::from_str::<Vec<serde_json::Value>>(&content)?;
|
||||
// values.push(serialized_scenario);
|
||||
// fs.save(
|
||||
// path,
|
||||
// &serde_json_lenient::to_string_pretty(&values).map(Into::into)?,
|
||||
// Default::default(),
|
||||
// )
|
||||
// .await?;
|
||||
|
||||
static ARRAY_QUERY: LazyLock<Query> = LazyLock::new(|| {
|
||||
Query::new(
|
||||
&tree_sitter_json::LANGUAGE.into(),
|
||||
"(document (array (object) @object))", // TODO: use "." anchor to only match last object
|
||||
)
|
||||
.expect("Failed to create ARRAY_QUERY")
|
||||
});
|
||||
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
parser
|
||||
.set_language(&tree_sitter_json::LANGUAGE.into())
|
||||
.unwrap();
|
||||
let mut cursor = tree_sitter::QueryCursor::new();
|
||||
let syntax_tree = parser.parse(&content, None).unwrap();
|
||||
let mut matches =
|
||||
cursor.matches(&ARRAY_QUERY, syntax_tree.root_node(), content.as_bytes());
|
||||
|
||||
// we don't have `.last()` since it's a lending iterator, so loop over
|
||||
// the whole thing to find the last one
|
||||
let mut last_offset = None;
|
||||
while let Some(mat) = matches.next() {
|
||||
if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end) {
|
||||
last_offset = Some(pos)
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pos) = last_offset {
|
||||
content.insert_str(pos, &new_scenario);
|
||||
content.insert_str(pos, ",\n");
|
||||
}
|
||||
|
||||
fs.write(path, content.as_bytes()).await?;
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(&path, cx)
|
||||
.context(
|
||||
"Couldn't get project path for .zed/debug.json in active worktree",
|
||||
)
|
||||
})?
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|err| Task::ready(Err(err)))
|
||||
}
|
||||
// workspace.update(cx, |workspace, cx| {
|
||||
// workspace
|
||||
// .project()
|
||||
// .read(cx)
|
||||
// .project_path_for_absolute_path(&path, cx)
|
||||
// .context(
|
||||
// "Couldn't get project path for .zed/debug.json in active worktree",
|
||||
// )
|
||||
// })?
|
||||
// })
|
||||
// })
|
||||
// .unwrap_or_else(|err| Task::ready(Err(err)))
|
||||
// }
|
||||
|
||||
pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.thread_picker_menu_handle.toggle(window, cx);
|
||||
@@ -1142,9 +1095,7 @@ async fn register_session_inner(
|
||||
let debug_session = DebugSession::running(
|
||||
this.project.clone(),
|
||||
this.workspace.clone(),
|
||||
parent_session
|
||||
.as_ref()
|
||||
.map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()),
|
||||
parent_session.map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()),
|
||||
session,
|
||||
serialized_layout,
|
||||
this.position(window, cx).axis(),
|
||||
@@ -1159,14 +1110,8 @@ async fn register_session_inner(
|
||||
|_, _, cx| cx.notify(),
|
||||
)
|
||||
.detach();
|
||||
let insert_position = this
|
||||
.sessions
|
||||
.iter()
|
||||
.position(|session| Some(session) == parent_session.as_ref())
|
||||
.map(|position| position + 1)
|
||||
.unwrap_or(this.sessions.len());
|
||||
// Maintain topological sort order of sessions
|
||||
this.sessions.insert(insert_position, debug_session.clone());
|
||||
|
||||
this.sessions.push(debug_session.clone());
|
||||
|
||||
debug_session
|
||||
})?;
|
||||
@@ -1470,63 +1415,21 @@ impl Render for DebugPanel {
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
h_flex().size_full()
|
||||
.items_start()
|
||||
|
||||
.child(v_flex().items_start().min_w_1_3().h_full().p_1()
|
||||
.child(h_flex().px_1().child(Label::new("Breakpoints").size(LabelSize::Small)))
|
||||
.child(Divider::horizontal())
|
||||
.child(self.breakpoint_list.clone()))
|
||||
.child(Divider::vertical())
|
||||
.child(
|
||||
v_flex().w_2_3().h_full().items_center().justify_center()
|
||||
.gap_2()
|
||||
.pr_8()
|
||||
.child(
|
||||
Button::new("spawn-new-session-empty-state", "New Session")
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(crate::Start.boxed_clone(), cx);
|
||||
})
|
||||
)
|
||||
.child(
|
||||
Button::new("edit-debug-settings", "Edit debug.json")
|
||||
.icon(IconName::Code)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(zed_actions::OpenProjectDebugTasks.boxed_clone(), cx);
|
||||
})
|
||||
)
|
||||
.child(
|
||||
Button::new("open-debugger-docs", "Debugger Docs")
|
||||
.icon(IconName::Book)
|
||||
.color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, _, cx| {
|
||||
cx.open_url("https://zed.dev/docs/debugger")
|
||||
})
|
||||
)
|
||||
.child(
|
||||
Button::new("spawn-new-session-install-extensions", "Debugger Extensions")
|
||||
.icon(IconName::Blocks)
|
||||
.color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(zed_actions::Extensions { category_filter: Some(zed_actions::ExtensionCategoryFilter::DebugAdapters)}.boxed_clone(), cx);
|
||||
})
|
||||
)
|
||||
)
|
||||
h_flex().child(
|
||||
Label::new("No Debugging Sessions")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex().flex_shrink().child(
|
||||
Button::new("spawn-new-session-empty-state", "New Session")
|
||||
.size(ButtonSize::Large)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(crate::Start.boxed_clone(), cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::any::TypeId;
|
||||
use dap::debugger_settings::DebuggerSettings;
|
||||
use debugger_panel::{DebugPanel, ToggleFocus};
|
||||
use editor::Editor;
|
||||
use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
|
||||
use gpui::{App, DispatchPhase, EntityInputHandler, actions};
|
||||
use new_process_modal::{NewProcessModal, NewProcessMode};
|
||||
use project::debugger::{self, breakpoint_store::SourceBreakpoint, session::ThreadStatus};
|
||||
@@ -61,163 +62,173 @@ pub fn init(cx: &mut App) {
|
||||
DebuggerSettings::register(cx);
|
||||
workspace::FollowableViewRegistry::register::<DebugSession>(cx);
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
workspace
|
||||
.register_action(spawn_task_or_modal)
|
||||
.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
|
||||
})
|
||||
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
|
||||
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
|
||||
})
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &RerunLastSession, window, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
cx.observe_new(|_: &mut Workspace, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
};
|
||||
|
||||
debug_panel.update(cx, |debug_panel, cx| {
|
||||
debug_panel.rerun_last_session(workspace, window, cx);
|
||||
})
|
||||
},
|
||||
)
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |store, cx| {
|
||||
store.shutdown_sessions(cx).detach();
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
.register_action_renderer(|div, workspace, _, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return div;
|
||||
};
|
||||
let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
else {
|
||||
return div;
|
||||
};
|
||||
let running_state = active_item.read(cx);
|
||||
if running_state.session().read(cx).is_terminated() {
|
||||
return div;
|
||||
}
|
||||
|
||||
let caps = running_state.capabilities(cx);
|
||||
let supports_step_back = caps.supports_step_back.unwrap_or_default();
|
||||
let supports_detach = running_state.session().read(cx).is_attached();
|
||||
let status = running_state.thread_status(cx);
|
||||
|
||||
let active_item = active_item.downgrade();
|
||||
div.when(status == Some(ThreadStatus::Running), |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &Pause, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.pause_thread(cx))
|
||||
.ok();
|
||||
})
|
||||
cx.when_flag_enabled::<DebuggerFeatureFlag>(window, |workspace, _, _| {
|
||||
workspace
|
||||
.register_action(spawn_task_or_modal)
|
||||
.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
|
||||
})
|
||||
.when(status == Some(ThreadStatus::Stopped), |div| {
|
||||
div.on_action({
|
||||
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
|
||||
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
|
||||
})
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &RerunLastSession, window, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
debug_panel.update(cx, |debug_panel, cx| {
|
||||
debug_panel.rerun_last_session(workspace, window, cx);
|
||||
})
|
||||
},
|
||||
)
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |store, cx| {
|
||||
store.shutdown_sessions(cx).detach();
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
.register_action_renderer(|div, workspace, _, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return div;
|
||||
};
|
||||
let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
else {
|
||||
return div;
|
||||
};
|
||||
let running_state = active_item.read(cx);
|
||||
if running_state.session().read(cx).is_terminated() {
|
||||
return div;
|
||||
}
|
||||
|
||||
let caps = running_state.capabilities(cx);
|
||||
let supports_step_back = caps.supports_step_back.unwrap_or_default();
|
||||
let supports_detach = running_state.session().read(cx).is_attached();
|
||||
let status = running_state.thread_status(cx);
|
||||
|
||||
let active_item = active_item.downgrade();
|
||||
div.when(status == Some(ThreadStatus::Running), |div| {
|
||||
let active_item = active_item.clone();
|
||||
move |_: &StepInto, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_in(cx)).ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &StepOver, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_over(cx)).ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &StepOut, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_out(cx)).ok();
|
||||
}
|
||||
})
|
||||
.when(supports_step_back, |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &StepBack, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_back(cx)).ok();
|
||||
div.on_action(move |_: &Pause, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.pause_thread(cx))
|
||||
.ok();
|
||||
})
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &Continue, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.continue_thread(cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action(cx.listener(
|
||||
|workspace, _: &ShowStackTrace, window, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx) {
|
||||
let is_active = workspace
|
||||
.active_item(cx)
|
||||
.is_some_and(|item| item.item_id() == existing.item_id());
|
||||
workspace.activate_item(&existing, true, !is_active, window, cx);
|
||||
} else {
|
||||
let Some(active_session) = debug_panel.read(cx).active_session()
|
||||
else {
|
||||
.when(status == Some(ThreadStatus::Stopped), |div| {
|
||||
div.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &StepInto, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_in(cx)).ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &StepOver, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_over(cx)).ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &StepOut, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_out(cx)).ok();
|
||||
}
|
||||
})
|
||||
.when(supports_step_back, |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &StepBack, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_back(cx)).ok();
|
||||
})
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &Continue, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.continue_thread(cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action(cx.listener(
|
||||
|workspace, _: &ShowStackTrace, window, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = workspace.project();
|
||||
if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx)
|
||||
{
|
||||
let is_active = workspace
|
||||
.active_item(cx)
|
||||
.is_some_and(|item| item.item_id() == existing.item_id());
|
||||
workspace
|
||||
.activate_item(&existing, true, !is_active, window, cx);
|
||||
} else {
|
||||
let Some(active_session) =
|
||||
debug_panel.read(cx).active_session()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let stack_trace_view = active_session.update(cx, |session, cx| {
|
||||
session.stack_trace_view(project, window, cx).clone()
|
||||
});
|
||||
let project = workspace.project();
|
||||
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(stack_trace_view),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
))
|
||||
})
|
||||
.when(supports_detach, |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &Detach, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.detach_client(cx))
|
||||
.ok();
|
||||
let stack_trace_view =
|
||||
active_session.update(cx, |session, cx| {
|
||||
session.stack_trace_view(project, window, cx).clone()
|
||||
});
|
||||
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(stack_trace_view),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
))
|
||||
})
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &Restart, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.restart_session(cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &Stop, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.stop_thread(cx)).ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &ToggleIgnoreBreakpoints, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
});
|
||||
.when(supports_detach, |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &Detach, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.detach_client(cx))
|
||||
.ok();
|
||||
})
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &Restart, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.restart_session(cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &Stop, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.stop_thread(cx)).ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &ToggleIgnoreBreakpoints, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};
|
||||
use project::debugger::session::{ThreadId, ThreadStatus};
|
||||
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
|
||||
@@ -73,21 +72,10 @@ impl DebugPanel {
|
||||
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();
|
||||
@@ -96,16 +84,16 @@ impl DebugPanel {
|
||||
weak_session
|
||||
.read_with(cx, |session, cx| {
|
||||
let context_menu = context_menu.clone();
|
||||
|
||||
let id: SharedString =
|
||||
format!("debug-session-{}", session_id.0)
|
||||
.into();
|
||||
|
||||
let id: SharedString = format!(
|
||||
"debug-session-{}",
|
||||
session.session_id(cx).0
|
||||
)
|
||||
.into();
|
||||
h_flex()
|
||||
.w_full()
|
||||
.group(id.clone())
|
||||
.justify_between()
|
||||
.child(session.label_element(self_depth, cx))
|
||||
.child(session.label_element(cx))
|
||||
.child(
|
||||
IconButton::new(
|
||||
"close-debug-session",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use anyhow::bail;
|
||||
use collections::{FxHashMap, HashMap};
|
||||
use language::LanguageRegistry;
|
||||
use paths::local_debug_file_relative_path;
|
||||
@@ -6,7 +5,6 @@ use std::{
|
||||
borrow::Cow,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
usize,
|
||||
};
|
||||
use tasks_ui::{TaskOverrides, TasksModal};
|
||||
@@ -40,12 +38,11 @@ use workspace::{ModalView, Workspace, pane};
|
||||
|
||||
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
|
||||
|
||||
#[allow(unused)]
|
||||
enum SaveScenarioState {
|
||||
Saving,
|
||||
Saved((ProjectPath, SharedString)),
|
||||
Failed(SharedString),
|
||||
}
|
||||
// enum SaveScenarioState {
|
||||
// Saving,
|
||||
// Saved((ProjectPath, SharedString)),
|
||||
// Failed(SharedString),
|
||||
// }
|
||||
|
||||
pub(super) struct NewProcessModal {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
@@ -56,7 +53,7 @@ pub(super) struct NewProcessModal {
|
||||
configure_mode: Entity<ConfigureMode>,
|
||||
task_mode: TaskMode,
|
||||
debugger: Option<DebugAdapterName>,
|
||||
save_scenario_state: Option<SaveScenarioState>,
|
||||
// save_scenario_state: Option<SaveScenarioState>,
|
||||
_subscriptions: [Subscription; 3],
|
||||
}
|
||||
|
||||
@@ -267,7 +264,7 @@ impl NewProcessModal {
|
||||
mode,
|
||||
debug_panel: debug_panel.downgrade(),
|
||||
workspace: workspace_handle,
|
||||
save_scenario_state: None,
|
||||
// save_scenario_state: None,
|
||||
_subscriptions,
|
||||
}
|
||||
});
|
||||
@@ -310,16 +307,16 @@ impl NewProcessModal {
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_scenario(&self, debugger: &str, cx: &App) -> Task<Option<DebugScenario>> {
|
||||
fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
|
||||
let request = match self.mode {
|
||||
NewProcessMode::Launch => {
|
||||
DebugRequest::Launch(self.configure_mode.read(cx).debug_request(cx))
|
||||
}
|
||||
NewProcessMode::Attach => {
|
||||
DebugRequest::Attach(self.attach_mode.read(cx).debug_request())
|
||||
}
|
||||
_ => return Task::ready(None),
|
||||
};
|
||||
NewProcessMode::Launch => Some(DebugRequest::Launch(
|
||||
self.configure_mode.read(cx).debug_request(cx),
|
||||
)),
|
||||
NewProcessMode::Attach => Some(DebugRequest::Attach(
|
||||
self.attach_mode.read(cx).debug_request(),
|
||||
)),
|
||||
_ => None,
|
||||
}?;
|
||||
let label = suggested_label(&request, debugger);
|
||||
|
||||
let stop_on_entry = if let NewProcessMode::Launch = &self.mode {
|
||||
@@ -331,15 +328,13 @@ impl NewProcessModal {
|
||||
let session_scenario = ZedDebugConfig {
|
||||
adapter: debugger.to_owned().into(),
|
||||
label,
|
||||
request,
|
||||
request: request,
|
||||
stop_on_entry,
|
||||
};
|
||||
|
||||
let adapter = cx
|
||||
.global::<DapRegistry>()
|
||||
.adapter(&session_scenario.adapter);
|
||||
|
||||
cx.spawn(async move |_| adapter?.config_from_zed_format(session_scenario).await.ok())
|
||||
cx.global::<DapRegistry>()
|
||||
.adapter(&session_scenario.adapter)
|
||||
.and_then(|adapter| adapter.config_from_zed_format(session_scenario).ok())
|
||||
}
|
||||
|
||||
fn start_new_session(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -354,13 +349,19 @@ impl NewProcessModal {
|
||||
return;
|
||||
}
|
||||
|
||||
if let NewProcessMode::Launch = &self.mode {
|
||||
if self.configure_mode.read(cx).save_to_debug_json.selected() {
|
||||
self.save_debug_scenario(window, cx);
|
||||
}
|
||||
}
|
||||
// TODO: Restore once we have proper, comment preserving edits
|
||||
// if let NewProcessMode::Launch = &self.mode {
|
||||
// if self.launch_mode.read(cx).save_to_debug_json.selected() {
|
||||
// self.save_debug_scenario(window, cx);
|
||||
// }
|
||||
// }
|
||||
|
||||
let Some(debugger) = self.debugger.clone() else {
|
||||
let Some(debugger) = self.debugger.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(config) = self.debug_scenario(debugger, cx) else {
|
||||
log::error!("debug config not found in mode: {}", self.mode);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -368,20 +369,11 @@ impl NewProcessModal {
|
||||
let Some(task_contexts) = self.task_contexts(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
send_telemetry(&config, TelemetrySpawnLocation::Custom, cx);
|
||||
let task_context = task_contexts.active_context().cloned().unwrap_or_default();
|
||||
let worktree_id = task_contexts.worktree();
|
||||
let mode = self.mode;
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let Some(config) = this
|
||||
.update(cx, |this, cx| this.debug_scenario(&debugger, cx))?
|
||||
.await
|
||||
else {
|
||||
bail!("debug config not found in mode: {mode}");
|
||||
};
|
||||
|
||||
debug_panel.update_in(cx, |debug_panel, window, cx| {
|
||||
send_telemetry(&config, TelemetrySpawnLocation::Custom, cx);
|
||||
debug_panel.start_session(config, task_context, None, worktree_id, window, cx)
|
||||
})?;
|
||||
this.update(cx, |_, cx| {
|
||||
@@ -419,64 +411,47 @@ impl NewProcessModal {
|
||||
self.debug_picker.read(cx).delegate.task_contexts.clone()
|
||||
}
|
||||
|
||||
fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task_contents = self.task_contexts(cx);
|
||||
let Some(adapter) = self.debugger.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let scenario = self.debug_scenario(&adapter, cx);
|
||||
// fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// let Some((save_scenario, scenario_label)) = self
|
||||
// .debugger
|
||||
// .as_ref()
|
||||
// .and_then(|debugger| self.debug_scenario(&debugger, cx))
|
||||
// .zip(self.task_contexts(cx).and_then(|tcx| tcx.worktree()))
|
||||
// .and_then(|(scenario, worktree_id)| {
|
||||
// self.debug_panel
|
||||
// .update(cx, |panel, cx| {
|
||||
// panel.save_scenario(&scenario, worktree_id, window, cx)
|
||||
// })
|
||||
// .ok()
|
||||
// .zip(Some(scenario.label.clone()))
|
||||
// })
|
||||
// else {
|
||||
// return;
|
||||
// };
|
||||
|
||||
self.save_scenario_state = Some(SaveScenarioState::Saving);
|
||||
// self.save_scenario_state = Some(SaveScenarioState::Saving);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let Some((scenario, worktree_id)) = scenario
|
||||
.await
|
||||
.zip(task_contents.and_then(|tcx| tcx.worktree()))
|
||||
else {
|
||||
this.update(cx, |this, _| {
|
||||
this.save_scenario_state = Some(SaveScenarioState::Failed(
|
||||
"Couldn't get scenario or task contents".into(),
|
||||
))
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
};
|
||||
// cx.spawn(async move |this, cx| {
|
||||
// let res = save_scenario.await;
|
||||
|
||||
let Some(save_scenario) = this
|
||||
.update_in(cx, |this, window, cx| {
|
||||
this.debug_panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.save_scenario(&scenario, worktree_id, window, cx)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let res = save_scenario.await;
|
||||
// this.update(cx, |this, _| match res {
|
||||
// Ok(saved_file) => {
|
||||
// this.save_scenario_state =
|
||||
// Some(SaveScenarioState::Saved((saved_file, scenario_label)))
|
||||
// }
|
||||
// Err(error) => {
|
||||
// this.save_scenario_state =
|
||||
// Some(SaveScenarioState::Failed(error.to_string().into()))
|
||||
// }
|
||||
// })
|
||||
// .ok();
|
||||
|
||||
this.update(cx, |this, _| match res {
|
||||
Ok(saved_file) => {
|
||||
this.save_scenario_state = Some(SaveScenarioState::Saved((
|
||||
saved_file,
|
||||
scenario.label.clone(),
|
||||
)))
|
||||
}
|
||||
Err(error) => {
|
||||
this.save_scenario_state =
|
||||
Some(SaveScenarioState::Failed(error.to_string().into()))
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||
this.update(cx, |this, _| this.save_scenario_state.take())
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
// cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||
// this.update(cx, |this, _| this.save_scenario_state.take())
|
||||
// .ok();
|
||||
// })
|
||||
// .detach();
|
||||
// }
|
||||
|
||||
fn adapter_drop_down_menu(
|
||||
&mut self,
|
||||
@@ -611,7 +586,7 @@ impl NewProcessModal {
|
||||
|
||||
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum NewProcessMode {
|
||||
Task,
|
||||
Launch,
|
||||
@@ -921,7 +896,7 @@ pub(super) struct ConfigureMode {
|
||||
program: Entity<Editor>,
|
||||
cwd: Entity<Editor>,
|
||||
stop_on_entry: ToggleState,
|
||||
save_to_debug_json: ToggleState,
|
||||
// save_to_debug_json: ToggleState,
|
||||
}
|
||||
|
||||
impl ConfigureMode {
|
||||
@@ -940,7 +915,7 @@ impl ConfigureMode {
|
||||
program,
|
||||
cwd,
|
||||
stop_on_entry: ToggleState::Unselected,
|
||||
save_to_debug_json: ToggleState::Unselected,
|
||||
// save_to_debug_json: ToggleState::Unselected,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1046,25 +1021,27 @@ impl ConfigureMode {
|
||||
)
|
||||
.checkbox_position(ui::IconPosition::End),
|
||||
)
|
||||
.child(
|
||||
CheckboxWithLabel::new(
|
||||
"debugger-save-to-debug-json",
|
||||
Label::new("Save to debug.json")
|
||||
.size(ui::LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
self.save_to_debug_json,
|
||||
{
|
||||
let this = cx.weak_entity();
|
||||
move |state, _, cx| {
|
||||
this.update(cx, |this, _| {
|
||||
this.save_to_debug_json = *state;
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
)
|
||||
.checkbox_position(ui::IconPosition::End),
|
||||
)
|
||||
// TODO: restore once we have proper, comment preserving
|
||||
// file edits.
|
||||
// .child(
|
||||
// CheckboxWithLabel::new(
|
||||
// "debugger-save-to-debug-json",
|
||||
// Label::new("Save to debug.json")
|
||||
// .size(ui::LabelSize::Small)
|
||||
// .color(Color::Muted),
|
||||
// self.save_to_debug_json,
|
||||
// {
|
||||
// let this = cx.weak_entity();
|
||||
// move |state, _, cx| {
|
||||
// this.update(cx, |this, _| {
|
||||
// this.save_to_debug_json = *state;
|
||||
// })
|
||||
// .ok();
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
// .checkbox_position(ui::IconPosition::End),
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1279,7 +1256,6 @@ impl PickerDelegate for DebugDelegate {
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
1000,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
|
||||
@@ -125,7 +125,7 @@ impl DebugSession {
|
||||
&self.running_state
|
||||
}
|
||||
|
||||
pub(crate) fn label_element(&self, depth: usize, cx: &App) -> AnyElement {
|
||||
pub(crate) fn label_element(&self, cx: &App) -> AnyElement {
|
||||
let label = self.label(cx);
|
||||
|
||||
let is_terminated = self
|
||||
@@ -153,7 +153,6 @@ impl DebugSession {
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.ml(depth * px(16.0))
|
||||
.gap_2()
|
||||
.when_some(icon, |this, indicator| this.child(indicator))
|
||||
.justify_between()
|
||||
|
||||
@@ -638,8 +638,7 @@ impl RunningState {
|
||||
)
|
||||
});
|
||||
|
||||
let breakpoint_list =
|
||||
BreakpointList::new(Some(session.clone()), workspace.clone(), &project, cx);
|
||||
let breakpoint_list = BreakpointList::new(session.clone(), workspace.clone(), &project, cx);
|
||||
|
||||
let _subscriptions = vec![
|
||||
cx.observe(&module_list, |_, _, cx| cx.notify()),
|
||||
@@ -817,13 +816,10 @@ impl RunningState {
|
||||
Self::relativize_paths(None, &mut config, &task_context);
|
||||
Self::substitute_variables_in_config(&mut config, &task_context);
|
||||
|
||||
let request_type = match dap_registry
|
||||
let request_type = dap_registry
|
||||
.adapter(&adapter)
|
||||
.with_context(|| format!("{}: is not a valid adapter name", &adapter)) {
|
||||
Ok(adapter) => adapter.request_kind(&config).await,
|
||||
Err(e) => Err(e)
|
||||
};
|
||||
|
||||
.with_context(|| format!("{}: is not a valid adapter name", &adapter))
|
||||
.and_then(|adapter| adapter.request_kind(&config));
|
||||
|
||||
let config_is_valid = request_type.is_ok();
|
||||
|
||||
@@ -962,8 +958,8 @@ impl RunningState {
|
||||
|
||||
let scenario = dap_registry
|
||||
.adapter(&adapter)
|
||||
.with_context(|| anyhow!("{}: is not a valid adapter name", &adapter))?.config_from_zed_format(zed_config)
|
||||
.await?;
|
||||
.with_context(|| anyhow!("{}: is not a valid adapter name", &adapter))
|
||||
.map(|adapter| adapter.config_from_zed_format(zed_config))??;
|
||||
config = scenario.config;
|
||||
Self::substitute_variables_in_config(&mut config, &task_context);
|
||||
} else {
|
||||
@@ -1016,8 +1012,7 @@ impl RunningState {
|
||||
None
|
||||
};
|
||||
|
||||
let mut envs: HashMap<String, String> =
|
||||
self.session.read(cx).task_context().project_env.clone();
|
||||
let mut envs: HashMap<String, String> = Default::default();
|
||||
if let Some(Value::Object(env)) = &request.env {
|
||||
for (key, value) in env {
|
||||
let value_str = match (key.as_str(), value) {
|
||||
|
||||
@@ -36,7 +36,7 @@ pub(crate) struct BreakpointList {
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
scrollbar_state: ScrollbarState,
|
||||
breakpoints: Vec<BreakpointEntry>,
|
||||
session: Option<Entity<Session>>,
|
||||
session: Entity<Session>,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
show_scrollbar: bool,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -51,8 +51,8 @@ impl Focusable for BreakpointList {
|
||||
}
|
||||
|
||||
impl BreakpointList {
|
||||
pub(crate) fn new(
|
||||
session: Option<Entity<Session>>,
|
||||
pub(super) fn new(
|
||||
session: Entity<Session>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut App,
|
||||
@@ -64,18 +64,21 @@ impl BreakpointList {
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
cx.new(|_| Self {
|
||||
breakpoint_store,
|
||||
worktree_store,
|
||||
scrollbar_state,
|
||||
breakpoints: Default::default(),
|
||||
hide_scrollbar_task: None,
|
||||
show_scrollbar: false,
|
||||
workspace,
|
||||
session,
|
||||
focus_handle,
|
||||
scroll_handle,
|
||||
selected_ix: None,
|
||||
cx.new(|_| {
|
||||
Self {
|
||||
breakpoint_store,
|
||||
worktree_store,
|
||||
scrollbar_state,
|
||||
// list_state,
|
||||
breakpoints: Default::default(),
|
||||
hide_scrollbar_task: None,
|
||||
show_scrollbar: false,
|
||||
workspace,
|
||||
session,
|
||||
focus_handle,
|
||||
scroll_handle,
|
||||
selected_ix: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -226,12 +229,10 @@ impl BreakpointList {
|
||||
self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
|
||||
}
|
||||
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
|
||||
if let Some(session) = &self.session {
|
||||
let id = exception_breakpoint.id.clone();
|
||||
session.update(cx, |session, cx| {
|
||||
session.toggle_exception_breakpoint(&id, cx);
|
||||
});
|
||||
}
|
||||
let id = exception_breakpoint.id.clone();
|
||||
self.session.update(cx, |session, cx| {
|
||||
session.toggle_exception_breakpoint(&id, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
@@ -384,8 +385,8 @@ impl Render for BreakpointList {
|
||||
})
|
||||
})
|
||||
});
|
||||
let exception_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
|
||||
session
|
||||
let exception_breakpoints =
|
||||
self.session
|
||||
.read(cx)
|
||||
.exception_breakpoints()
|
||||
.map(|(data, is_enabled)| BreakpointEntry {
|
||||
@@ -395,8 +396,7 @@ impl Render for BreakpointList {
|
||||
is_enabled: *is_enabled,
|
||||
}),
|
||||
weak: weak.clone(),
|
||||
})
|
||||
});
|
||||
});
|
||||
self.breakpoints
|
||||
.extend(breakpoints.chain(exception_breakpoints));
|
||||
v_flex()
|
||||
@@ -506,48 +506,44 @@ impl LineBreakpoint {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.end_hover_slot(
|
||||
h_flex()
|
||||
.child(
|
||||
IconButton::new(
|
||||
SharedString::from(format!(
|
||||
"breakpoint-ui-on-click-go-to-line-remove-{:?}/{}:{}",
|
||||
self.dir, self.name, self.line
|
||||
)),
|
||||
IconName::Close,
|
||||
)
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
let path = path.clone();
|
||||
move |_, _, cx| {
|
||||
weak.update(cx, |breakpoint_list, cx| {
|
||||
breakpoint_list.edit_line_breakpoint(
|
||||
path.clone(),
|
||||
row,
|
||||
BreakpointEditAction::Toggle,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Unset Breakpoint",
|
||||
&UnsetBreakpoint,
|
||||
&focus_handle,
|
||||
window,
|
||||
IconButton::new(
|
||||
SharedString::from(format!(
|
||||
"breakpoint-ui-on-click-go-to-line-remove-{:?}/{}:{}",
|
||||
self.dir, self.name, self.line
|
||||
)),
|
||||
IconName::Close,
|
||||
)
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
let path = path.clone();
|
||||
move |_, _, cx| {
|
||||
weak.update(cx, |breakpoint_list, cx| {
|
||||
breakpoint_list.edit_line_breakpoint(
|
||||
path.clone(),
|
||||
row,
|
||||
BreakpointEditAction::Toggle,
|
||||
cx,
|
||||
)
|
||||
);
|
||||
})
|
||||
.icon_size(ui::IconSize::XSmall),
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Unset Breakpoint",
|
||||
&UnsetBreakpoint,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.right_4(),
|
||||
})
|
||||
.icon_size(ui::IconSize::Indicator),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.py_1()
|
||||
.gap_1()
|
||||
.min_h(px(26.))
|
||||
.min_h(px(22.))
|
||||
.justify_center()
|
||||
.id(SharedString::from(format!(
|
||||
"breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
|
||||
@@ -639,12 +635,10 @@ impl ExceptionBreakpoint {
|
||||
let list = list.clone();
|
||||
move |_, _, cx| {
|
||||
list.update(cx, |this, cx| {
|
||||
if let Some(session) = &this.session {
|
||||
session.update(cx, |this, cx| {
|
||||
this.toggle_exception_breakpoint(&id, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
this.session.update(cx, |this, cx| {
|
||||
this.toggle_exception_breakpoint(&id, cx);
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -656,7 +650,7 @@ impl ExceptionBreakpoint {
|
||||
v_flex()
|
||||
.py_1()
|
||||
.gap_1()
|
||||
.min_h(px(26.))
|
||||
.min_h(px(22.))
|
||||
.justify_center()
|
||||
.id(("exception-breakpoint-label", ix))
|
||||
.child(
|
||||
|
||||
@@ -2,15 +2,13 @@ use super::{
|
||||
stack_frame_list::{StackFrameList, StackFrameListEvent},
|
||||
variable_list::VariableList,
|
||||
};
|
||||
use alacritty_terminal::vte::ansi;
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use dap::OutputEvent;
|
||||
use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
Context, Entity, FocusHandle, Focusable, HighlightStyle, Hsla, Render, Subscription, Task,
|
||||
TextStyle, WeakEntity,
|
||||
Context, Entity, FocusHandle, Focusable, Render, Subscription, Task, TextStyle, WeakEntity,
|
||||
};
|
||||
use language::{Buffer, CodeLabel, ToOffset};
|
||||
use menu::Confirm;
|
||||
@@ -19,8 +17,8 @@ use project::{
|
||||
debugger::session::{CompletionsQuery, OutputToken, Session, SessionEvent},
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc, usize};
|
||||
use theme::{Theme, ThemeSettings};
|
||||
use std::{cell::RefCell, rc::Rc, usize};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{Divider, prelude::*};
|
||||
|
||||
pub struct Console {
|
||||
@@ -138,193 +136,18 @@ impl Console {
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.console.update(cx, |console, cx| {
|
||||
console.set_read_only(false);
|
||||
|
||||
let mut to_insert = String::default();
|
||||
for event in events {
|
||||
let to_insert = format!("{}\n", event.output.trim_end());
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut ansi_handler = ConsoleHandler::default();
|
||||
let mut ansi_processor = ansi::Processor::<ansi::StdSyncHandler>::default();
|
||||
|
||||
let len = console.buffer().read(cx).len(cx);
|
||||
ansi_processor.advance(&mut ansi_handler, to_insert.as_bytes());
|
||||
let output = std::mem::take(&mut ansi_handler.output);
|
||||
let mut spans = std::mem::take(&mut ansi_handler.spans);
|
||||
let mut background_spans = std::mem::take(&mut ansi_handler.background_spans);
|
||||
if ansi_handler.current_range_start < output.len() {
|
||||
spans.push((
|
||||
ansi_handler.current_range_start..output.len(),
|
||||
ansi_handler.current_color,
|
||||
));
|
||||
}
|
||||
if ansi_handler.current_background_range_start < output.len() {
|
||||
background_spans.push((
|
||||
ansi_handler.current_background_range_start..output.len(),
|
||||
ansi_handler.current_background_color,
|
||||
));
|
||||
}
|
||||
console.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
console.insert(&output, window, cx);
|
||||
let buffer = console.buffer().read(cx).snapshot(cx);
|
||||
|
||||
struct ConsoleAnsiHighlight;
|
||||
|
||||
for (range, color) in spans {
|
||||
let Some(color) = color else { continue };
|
||||
let start_offset = len + range.start;
|
||||
let range = start_offset..len + range.end;
|
||||
let range = buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
|
||||
let style = HighlightStyle {
|
||||
color: Some(terminal_view::terminal_element::convert_color(
|
||||
&color,
|
||||
cx.theme(),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
console.highlight_text_key::<ConsoleAnsiHighlight>(
|
||||
start_offset,
|
||||
vec![range],
|
||||
style,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
for (range, color) in background_spans {
|
||||
let Some(color) = color else { continue };
|
||||
let start_offset = len + range.start;
|
||||
let range = start_offset..len + range.end;
|
||||
let range = buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
|
||||
|
||||
let color_fetcher: fn(&Theme) -> Hsla = match color {
|
||||
// Named and theme defined colors
|
||||
ansi::Color::Named(n) => match n {
|
||||
ansi::NamedColor::Black => |theme| theme.colors().terminal_ansi_black,
|
||||
ansi::NamedColor::Red => |theme| theme.colors().terminal_ansi_red,
|
||||
ansi::NamedColor::Green => |theme| theme.colors().terminal_ansi_green,
|
||||
ansi::NamedColor::Yellow => |theme| theme.colors().terminal_ansi_yellow,
|
||||
ansi::NamedColor::Blue => |theme| theme.colors().terminal_ansi_blue,
|
||||
ansi::NamedColor::Magenta => {
|
||||
|theme| theme.colors().terminal_ansi_magenta
|
||||
}
|
||||
ansi::NamedColor::Cyan => |theme| theme.colors().terminal_ansi_cyan,
|
||||
ansi::NamedColor::White => |theme| theme.colors().terminal_ansi_white,
|
||||
ansi::NamedColor::BrightBlack => {
|
||||
|theme| theme.colors().terminal_ansi_bright_black
|
||||
}
|
||||
ansi::NamedColor::BrightRed => {
|
||||
|theme| theme.colors().terminal_ansi_bright_red
|
||||
}
|
||||
ansi::NamedColor::BrightGreen => {
|
||||
|theme| theme.colors().terminal_ansi_bright_green
|
||||
}
|
||||
ansi::NamedColor::BrightYellow => {
|
||||
|theme| theme.colors().terminal_ansi_bright_yellow
|
||||
}
|
||||
ansi::NamedColor::BrightBlue => {
|
||||
|theme| theme.colors().terminal_ansi_bright_blue
|
||||
}
|
||||
ansi::NamedColor::BrightMagenta => {
|
||||
|theme| theme.colors().terminal_ansi_bright_magenta
|
||||
}
|
||||
ansi::NamedColor::BrightCyan => {
|
||||
|theme| theme.colors().terminal_ansi_bright_cyan
|
||||
}
|
||||
ansi::NamedColor::BrightWhite => {
|
||||
|theme| theme.colors().terminal_ansi_bright_white
|
||||
}
|
||||
ansi::NamedColor::Foreground => {
|
||||
|theme| theme.colors().terminal_foreground
|
||||
}
|
||||
ansi::NamedColor::Background => {
|
||||
|theme| theme.colors().terminal_background
|
||||
}
|
||||
ansi::NamedColor::Cursor => |theme| theme.players().local().cursor,
|
||||
ansi::NamedColor::DimBlack => {
|
||||
|theme| theme.colors().terminal_ansi_dim_black
|
||||
}
|
||||
ansi::NamedColor::DimRed => {
|
||||
|theme| theme.colors().terminal_ansi_dim_red
|
||||
}
|
||||
ansi::NamedColor::DimGreen => {
|
||||
|theme| theme.colors().terminal_ansi_dim_green
|
||||
}
|
||||
ansi::NamedColor::DimYellow => {
|
||||
|theme| theme.colors().terminal_ansi_dim_yellow
|
||||
}
|
||||
ansi::NamedColor::DimBlue => {
|
||||
|theme| theme.colors().terminal_ansi_dim_blue
|
||||
}
|
||||
ansi::NamedColor::DimMagenta => {
|
||||
|theme| theme.colors().terminal_ansi_dim_magenta
|
||||
}
|
||||
ansi::NamedColor::DimCyan => {
|
||||
|theme| theme.colors().terminal_ansi_dim_cyan
|
||||
}
|
||||
ansi::NamedColor::DimWhite => {
|
||||
|theme| theme.colors().terminal_ansi_dim_white
|
||||
}
|
||||
ansi::NamedColor::BrightForeground => {
|
||||
|theme| theme.colors().terminal_bright_foreground
|
||||
}
|
||||
ansi::NamedColor::DimForeground => {
|
||||
|theme| theme.colors().terminal_dim_foreground
|
||||
}
|
||||
},
|
||||
// 'True' colors
|
||||
ansi::Color::Spec(_) => |theme| theme.colors().editor_background,
|
||||
// 8 bit, indexed colors
|
||||
ansi::Color::Indexed(i) => {
|
||||
match i {
|
||||
// 0-15 are the same as the named colors above
|
||||
0 => |theme| theme.colors().terminal_ansi_black,
|
||||
1 => |theme| theme.colors().terminal_ansi_red,
|
||||
2 => |theme| theme.colors().terminal_ansi_green,
|
||||
3 => |theme| theme.colors().terminal_ansi_yellow,
|
||||
4 => |theme| theme.colors().terminal_ansi_blue,
|
||||
5 => |theme| theme.colors().terminal_ansi_magenta,
|
||||
6 => |theme| theme.colors().terminal_ansi_cyan,
|
||||
7 => |theme| theme.colors().terminal_ansi_white,
|
||||
8 => |theme| theme.colors().terminal_ansi_bright_black,
|
||||
9 => |theme| theme.colors().terminal_ansi_bright_red,
|
||||
10 => |theme| theme.colors().terminal_ansi_bright_green,
|
||||
11 => |theme| theme.colors().terminal_ansi_bright_yellow,
|
||||
12 => |theme| theme.colors().terminal_ansi_bright_blue,
|
||||
13 => |theme| theme.colors().terminal_ansi_bright_magenta,
|
||||
14 => |theme| theme.colors().terminal_ansi_bright_cyan,
|
||||
15 => |theme| theme.colors().terminal_ansi_bright_white,
|
||||
// 16-231 are a 6x6x6 RGB color cube, mapped to 0-255 using steps defined by XTerm.
|
||||
// See: https://github.com/xterm-x11/xterm-snapshots/blob/master/256colres.pl
|
||||
// 16..=231 => {
|
||||
// let (r, g, b) = rgb_for_index(index as u8);
|
||||
// rgba_color(
|
||||
// if r == 0 { 0 } else { r * 40 + 55 },
|
||||
// if g == 0 { 0 } else { g * 40 + 55 },
|
||||
// if b == 0 { 0 } else { b * 40 + 55 },
|
||||
// )
|
||||
// }
|
||||
// 232-255 are a 24-step grayscale ramp from (8, 8, 8) to (238, 238, 238).
|
||||
// 232..=255 => {
|
||||
// let i = index as u8 - 232; // Align index to 0..24
|
||||
// let value = i * 10 + 8;
|
||||
// rgba_color(value, value, value)
|
||||
// }
|
||||
// For compatibility with the alacritty::Colors interface
|
||||
// See: https://github.com/alacritty/alacritty/blob/master/alacritty_terminal/src/term/color.rs
|
||||
_ => |_| gpui::black(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
console.highlight_background_key::<ConsoleAnsiHighlight>(
|
||||
start_offset,
|
||||
&[range],
|
||||
color_fetcher,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
_ = write!(to_insert, "{}\n", event.output.trim_end());
|
||||
}
|
||||
|
||||
console.set_read_only(false);
|
||||
console.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
console.insert(&to_insert, window, cx);
|
||||
console.set_read_only(true);
|
||||
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
@@ -527,7 +350,6 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
&string_matches,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
LIMIT,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
@@ -637,69 +459,3 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ConsoleHandler {
|
||||
output: String,
|
||||
spans: Vec<(Range<usize>, Option<ansi::Color>)>,
|
||||
background_spans: Vec<(Range<usize>, Option<ansi::Color>)>,
|
||||
current_range_start: usize,
|
||||
current_background_range_start: usize,
|
||||
current_color: Option<ansi::Color>,
|
||||
current_background_color: Option<ansi::Color>,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl ConsoleHandler {
|
||||
fn break_span(&mut self, color: Option<ansi::Color>) {
|
||||
self.spans.push((
|
||||
self.current_range_start..self.output.len(),
|
||||
self.current_color,
|
||||
));
|
||||
self.current_color = color;
|
||||
self.current_range_start = self.pos;
|
||||
}
|
||||
|
||||
fn break_background_span(&mut self, color: Option<ansi::Color>) {
|
||||
self.background_spans.push((
|
||||
self.current_background_range_start..self.output.len(),
|
||||
self.current_background_color,
|
||||
));
|
||||
self.current_background_color = color;
|
||||
self.current_background_range_start = self.pos;
|
||||
}
|
||||
}
|
||||
|
||||
impl ansi::Handler for ConsoleHandler {
|
||||
fn input(&mut self, c: char) {
|
||||
self.output.push(c);
|
||||
self.pos += c.len_utf8();
|
||||
}
|
||||
|
||||
fn linefeed(&mut self) {
|
||||
self.output.push('\n');
|
||||
self.pos += 1;
|
||||
}
|
||||
|
||||
fn put_tab(&mut self, count: u16) {
|
||||
self.output
|
||||
.extend(std::iter::repeat('\t').take(count as usize));
|
||||
self.pos += count as usize;
|
||||
}
|
||||
|
||||
fn terminal_attribute(&mut self, attr: ansi::Attr) {
|
||||
match attr {
|
||||
ansi::Attr::Foreground(color) => {
|
||||
self.break_span(Some(color));
|
||||
}
|
||||
ansi::Attr::Background(color) => {
|
||||
self.break_background_span(Some(color));
|
||||
}
|
||||
ansi::Attr::Reset => {
|
||||
self.break_span(None);
|
||||
self.break_background_span(None);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ pub struct ModuleList {
|
||||
focus_handle: FocusHandle,
|
||||
scrollbar_state: ScrollbarState,
|
||||
entries: Vec<Module>,
|
||||
_rebuild_task: Option<Task<()>>,
|
||||
_rebuild_task: Task<()>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
@@ -34,16 +34,14 @@ impl ModuleList {
|
||||
|
||||
let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
|
||||
SessionEvent::Stopped(_) | SessionEvent::Modules => {
|
||||
if this._rebuild_task.is_some() {
|
||||
this.schedule_rebuild(cx);
|
||||
}
|
||||
this.schedule_rebuild(cx);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
|
||||
Self {
|
||||
let mut this = Self {
|
||||
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
|
||||
scroll_handle,
|
||||
session,
|
||||
@@ -52,12 +50,14 @@ impl ModuleList {
|
||||
entries: Vec::new(),
|
||||
selected_ix: None,
|
||||
_subscription,
|
||||
_rebuild_task: None,
|
||||
}
|
||||
_rebuild_task: Task::ready(()),
|
||||
};
|
||||
this.schedule_rebuild(cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn schedule_rebuild(&mut self, cx: &mut Context<Self>) {
|
||||
self._rebuild_task = Some(cx.spawn(async move |this, cx| {
|
||||
self._rebuild_task = cx.spawn(async move |this, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
let modules = this
|
||||
.session
|
||||
@@ -66,7 +66,7 @@ impl ModuleList {
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -300,9 +300,6 @@ impl Focusable for ModuleList {
|
||||
|
||||
impl Render for ModuleList {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self._rebuild_task.is_none() {
|
||||
self.schedule_rebuild(cx);
|
||||
}
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user