Compare commits

..

2 Commits

Author SHA1 Message Date
Ben Brandt
ea9198ecd1 clean up errors 2025-05-12 16:41:45 +02:00
Richard Feldman
a8cb3446a3 Start adding new loaded context 2025-05-12 16:33:01 +02:00
221 changed files with 3671 additions and 8704 deletions

View File

@@ -1,35 +0,0 @@
name: Bug Report (Debugger)
description: Zed Debugger-Related Bugs
type: "Bug"
labels: ["debugger"]
title: "Debugger: <a short description of the Debugger bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

1
.gitignore vendored
View File

@@ -2,7 +2,6 @@
**/cargo-target
**/target
**/venv
**/.direnv
*.wasm
*.xcodeproj
.DS_Store

View File

@@ -2,14 +2,16 @@
{
"label": "Debug Zed (CodeLLDB)",
"adapter": "CodeLLDB",
"program": "target/debug/zed",
"request": "launch"
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
},
{
"label": "Debug Zed (GDB)",
"adapter": "GDB",
"program": "target/debug/zed",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT",
"initialize_args": {
"stopAtBeginningOfMainSubprogram": true
}

54
Cargo.lock generated
View File

@@ -682,7 +682,6 @@ dependencies = [
"portable-pty",
"pretty_assertions",
"project",
"prompt_store",
"rand 0.8.5",
"regex",
"reqwest_client",
@@ -4135,18 +4134,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "debug_adapter_extension"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"dap",
"extension",
"gpui",
"workspace-hack",
]
[[package]]
name = "debugger_tools"
version = "0.1.0"
@@ -4180,7 +4167,6 @@ dependencies = [
"editor",
"env_logger 0.11.8",
"feature_flags",
"file_icons",
"futures 0.3.31",
"fuzzy",
"gpui",
@@ -4367,15 +4353,6 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "diffy"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291"
dependencies = [
"nu-ansi-term 0.50.1",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -4963,7 +4940,6 @@ dependencies = [
"clap",
"client",
"collections",
"debug_adapter_extension",
"dirs 4.0.0",
"dotenv",
"env_logger 0.11.8",
@@ -5062,7 +5038,6 @@ dependencies = [
"async-tar",
"async-trait",
"collections",
"dap",
"fs",
"futures 0.3.31",
"gpui",
@@ -5075,7 +5050,6 @@ dependencies = [
"semantic_version",
"serde",
"serde_json",
"task",
"toml 0.8.20",
"util",
"wasm-encoder 0.221.3",
@@ -5119,7 +5093,6 @@ dependencies = [
"client",
"collections",
"ctor",
"dap",
"env_logger 0.11.8",
"extension",
"fs",
@@ -7710,7 +7683,6 @@ dependencies = [
"clock",
"collections",
"ctor",
"diffy",
"ec4rs",
"env_logger 0.11.8",
"fs",
@@ -9181,15 +9153,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "num"
version = "0.4.3"
@@ -10074,7 +10037,7 @@ name = "perplexity"
version = "0.1.0"
dependencies = [
"serde",
"zed_extension_api 0.6.0",
"zed_extension_api 0.5.0",
]
[[package]]
@@ -11882,7 +11845,6 @@ dependencies = [
"clock",
"dap",
"dap_adapters",
"debug_adapter_extension",
"env_logger 0.11.8",
"extension",
"extension_host",
@@ -15324,7 +15286,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term 0.46.0",
"nu-ansi-term",
"once_cell",
"regex",
"serde",
@@ -18055,6 +18017,7 @@ dependencies = [
"aho-corasick",
"anstream",
"arrayvec",
"async-compression",
"async-std",
"async-tungstenite",
"aws-config",
@@ -18562,7 +18525,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.187.7"
version = "0.187.0"
dependencies = [
"activity_indicator",
"agent",
@@ -18594,7 +18557,6 @@ dependencies = [
"dap",
"dap_adapters",
"db",
"debug_adapter_extension",
"debugger_tools",
"debugger_ui",
"diagnostics",
@@ -18733,7 +18695,7 @@ dependencies = [
[[package]]
name = "zed_extension_api"
version = "0.6.0"
version = "0.5.0"
dependencies = [
"serde",
"serde_json",
@@ -18756,9 +18718,9 @@ dependencies = [
[[package]]
name = "zed_llm_client"
version = "0.8.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9be71e2f9b271e1eb8eb3e0d986075e770d1a0a299fb036abc3f1fc13a2fa7eb"
checksum = "16d993fc42f9ec43ab76fa46c6eb579a66e116bb08cd2bc9a67f3afcaa05d39d"
dependencies = [
"anyhow",
"serde",
@@ -18793,7 +18755,7 @@ dependencies = [
name = "zed_test_extension"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.6.0",
"zed_extension_api 0.5.0",
]
[[package]]

View File

@@ -37,7 +37,6 @@ members = [
"crates/dap",
"crates/dap_adapters",
"crates/db",
"crates/debug_adapter_extension",
"crates/debugger_tools",
"crates/debugger_ui",
"crates/deepseek",
@@ -244,7 +243,6 @@ credentials_provider = { path = "crates/credentials_provider" }
dap = { path = "crates/dap" }
dap_adapters = { path = "crates/dap_adapters" }
db = { path = "crates/db" }
debug_adapter_extension = { path = "crates/debug_adapter_extension" }
debugger_tools = { path = "crates/debugger_tools" }
debugger_ui = { path = "crates/debugger_ui" }
deepseek = { path = "crates/deepseek" }
@@ -608,7 +606,7 @@ wasmtime-wasi = "29"
which = "6.0.0"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "0.8.2"
zed_llm_client = "0.8.1"
zstd = "0.11"
[workspace.dependencies.async-stripe]

View File

@@ -538,6 +538,7 @@
"ctrl-alt-b": "workspace::ToggleRightDock",
"ctrl-b": "workspace::ToggleLeftDock",
"ctrl-j": "workspace::ToggleBottomDock",
"ctrl-w": "workspace::CloseActiveDock",
"ctrl-alt-y": "workspace::CloseAllDocks",
"shift-find": "pane::DeploySearch",
"ctrl-shift-f": "pane::DeploySearch",
@@ -928,7 +929,6 @@
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
"ctrl-delete": ["terminal::SendText", "\u001bd"],
// Overrides for conflicting keybindings
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
@@ -979,12 +979,5 @@
"bindings": {
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
}
},
{
"context": "DebugConsole > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "menu::Confirm"
}
}
]

View File

@@ -608,6 +608,7 @@
"cmd-b": "workspace::ToggleLeftDock",
"cmd-r": "workspace::ToggleRightDock",
"cmd-j": "workspace::ToggleBottomDock",
"cmd-w": "workspace::CloseActiveDock",
"alt-cmd-y": "workspace::CloseAllDocks",
"cmd-shift-f": "pane::DeploySearch",
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
@@ -1011,7 +1012,7 @@
"alt-right": ["terminal::SendText", "\u001bf"],
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"ctrl-delete": ["terminal::SendText", "\u001bd"],
"alt-.": ["terminal::SendText", "\u001b."],
// There are conflicting bindings for these keys in the global context.
// these bindings override them, remove at your own risk:
"up": ["terminal::SendKeystroke", "up"],
@@ -1084,12 +1085,5 @@
"bindings": {
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
}
},
{
"context": "DebugConsole > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "menu::Confirm"
}
}
]

View File

@@ -51,7 +51,9 @@
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"
"ctrl-delete": "editor::DeleteToNextWordEnd",
"f3": "editor::FindNextMatch",
"shift-f3": "editor::FindPreviousMatch"
}
},
{

View File

@@ -53,7 +53,9 @@
"cmd-shift-j": "editor::JoinLines",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"
"ctrl-delete": "editor::DeleteToNextWordEnd",
"cmd-g": "editor::FindNextMatch",
"cmd-shift-g": "editor::FindPreviousMatch"
}
},
{

View File

@@ -113,8 +113,8 @@
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Time to wait in milliseconds before showing the informational hover box.
"hover_popover_delay": 300,
// Time to wait before showing the informational hover box
"hover_popover_delay": 350,
// Whether to confirm before quitting Zed.
"confirm_quit": false,
// Whether to restore last closed project when fresh Zed instance is opened.
@@ -230,11 +230,11 @@
// Possible values:
// - "off" — no diagnostics are allowed
// - "error"
// - "warning"
// - "warning" (default)
// - "info"
// - "hint"
// - null — allow all diagnostics (default)
"diagnostics_max_severity": null,
// - null — allow all diagnostics
"diagnostics_max_severity": "warning",
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any

View File

@@ -33,9 +33,7 @@ use language_model::{
LanguageModelRequestMessage, LanguageModelToolUseId, MessageContent, Role, StopReason,
};
use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
use markdown::{
HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown, PathWithRange,
};
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown};
use project::{ProjectEntryId, ProjectItem as _};
use rope::Point;
use settings::{Settings as _, SettingsStore, update_settings_file};
@@ -383,25 +381,18 @@ fn render_markdown_code_block(
)
} else {
let content = if let Some(parent) = path_range.path.parent() {
let file_name = file_name.to_string_lossy().to_string();
let path = parent.to_string_lossy().to_string();
let path_and_file = format!("{}/{}", path, file_name);
h_flex()
.id(("code-block-header-label", ix))
.ml_1()
.gap_1()
.child(Label::new(file_name).size(LabelSize::Small))
.child(Label::new(path).color(Color::Muted).size(LabelSize::Small))
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Jump to File",
None,
path_and_file.clone(),
window,
cx,
)
})
.child(
Label::new(file_name.to_string_lossy().to_string())
.size(LabelSize::Small),
)
.child(
Label::new(parent.to_string_lossy().to_string())
.color(Color::Muted)
.size(LabelSize::Small),
)
.into_any_element()
} else {
Label::new(path_range.path.to_string_lossy().to_string())
@@ -411,7 +402,7 @@ fn render_markdown_code_block(
};
h_flex()
.id(("code-block-header-button", ix))
.id(("code-block-header-label", ix))
.w_full()
.max_w_full()
.px_1()
@@ -419,6 +410,7 @@ fn render_markdown_code_block(
.cursor_pointer()
.rounded_sm()
.hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
.tooltip(Tooltip::text("Jump to File"))
.child(
h_flex()
.gap_0p5()
@@ -438,8 +430,49 @@ fn render_markdown_code_block(
let path_range = path_range.clone();
move |_, window, cx| {
workspace
.update(cx, |workspace, cx| {
open_path(&path_range, window, workspace, cx)
.update(cx, {
|workspace, cx| {
let Some(project_path) = workspace
.project()
.read(cx)
.find_project_path(&path_range.path, cx)
else {
return;
};
let Some(target) = path_range.range.as_ref().map(|range| {
Point::new(
// Line number is 1-based
range.start.line.saturating_sub(1),
range.start.col.unwrap_or(0),
)
}) else {
return;
};
let open_task = workspace.open_path(
project_path,
None,
true,
window,
cx,
);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(active_editor) =
item.downcast::<Editor>()
{
active_editor
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(
target, window, cx,
);
})
.ok();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
})
.ok();
}
@@ -468,87 +501,10 @@ fn render_markdown_code_block(
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.01));
let control_buttons = h_flex()
.visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
.absolute()
.top_0()
.right_0()
.h_full()
.bg(codeblock_header_bg)
.rounded_tr_md()
.px_1()
.gap_1()
.child(
IconButton::new(
("copy-markdown-code", ix),
if codeblock_was_copied {
IconName::Check
} else {
IconName::Copy
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Copy Code"))
.on_click({
let active_thread = active_thread.clone();
let parsed_markdown = parsed_markdown.clone();
let code_block_range = metadata.content_range.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.copied_code_block_ids.insert((message_id, ix));
let code = parsed_markdown.source()[code_block_range.clone()].to_string();
cx.write_to_clipboard(ClipboardItem::new_string(code));
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
cx.update(|cx| {
this.update(cx, |this, cx| {
this.copied_code_block_ids.remove(&(message_id, ix));
cx.notify();
})
})
.ok();
})
.detach();
});
}
}),
)
.when(can_expand, |header| {
header.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
} else {
IconName::ChevronDown
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
} else {
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.toggle_codeblock_expanded(message_id, ix);
cx.notify();
});
}
}),
)
});
let codeblock_header = h_flex()
.relative()
.p_1()
.py_1()
.pl_1p5()
.pr_1()
.gap_1()
.justify_between()
.border_b_1()
@@ -556,7 +512,79 @@ fn render_markdown_code_block(
.bg(codeblock_header_bg)
.rounded_t_md()
.children(label)
.child(control_buttons);
.child(
h_flex()
.visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
.gap_1()
.child(
IconButton::new(
("copy-markdown-code", ix),
if codeblock_was_copied {
IconName::Check
} else {
IconName::Copy
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Copy Code"))
.on_click({
let active_thread = active_thread.clone();
let parsed_markdown = parsed_markdown.clone();
let code_block_range = metadata.content_range.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.copied_code_block_ids.insert((message_id, ix));
let code =
parsed_markdown.source()[code_block_range.clone()].to_string();
cx.write_to_clipboard(ClipboardItem::new_string(code));
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
cx.update(|cx| {
this.update(cx, |this, cx| {
this.copied_code_block_ids.remove(&(message_id, ix));
cx.notify();
})
})
.ok();
})
.detach();
});
}
}),
)
.when(can_expand, |header| {
header.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
} else {
IconName::ChevronDown
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
} else {
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.toggle_codeblock_expanded(message_id, ix);
cx.notify();
});
}
}),
)
}),
);
v_flex()
.group(CODEBLOCK_CONTAINER_GROUP)
@@ -570,45 +598,6 @@ fn render_markdown_code_block(
.when(can_expand && !is_expanded, |this| this.max_h_80())
}
fn open_path(
path_range: &PathWithRange,
window: &mut Window,
workspace: &mut Workspace,
cx: &mut Context<'_, Workspace>,
) {
let Some(project_path) = workspace
.project()
.read(cx)
.find_project_path(&path_range.path, cx)
else {
return; // TODO instead of just bailing out, open that path in a buffer.
};
let Some(target) = path_range.range.as_ref().map(|range| {
Point::new(
// Line number is 1-based
range.start.line.saturating_sub(1),
range.start.col.unwrap_or(0),
)
}) else {
return;
};
let open_task = workspace.open_path(project_path, None, true, window, cx);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(active_editor) = item.downcast::<Editor>() {
active_editor
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(target, window, cx);
})
.ok();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn render_code_language(
language: Option<&Arc<Language>>,
name_fallback: SharedString,

View File

@@ -49,7 +49,7 @@ pub use crate::context::{ContextLoadResult, LoadedContext};
pub use crate::inline_assistant::InlineAssistant;
use crate::slash_command_settings::SlashCommandSettings;
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
pub use crate::thread_store::{SerializedThread, TextThreadStore, ThreadStore};
pub use crate::thread_store::{TextThreadStore, ThreadStore};
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
pub use context_store::ContextStore;
pub use ui::preview::{all_agent_previews, get_agent_preview};
@@ -85,7 +85,6 @@ actions!(
KeepAll,
Follow,
ResetTrialUpsell,
ResetTrialEndUpsell,
]
);

View File

@@ -422,7 +422,6 @@ impl AgentConfiguration {
.unwrap_or(ContextServerStatus::Stopped);
let is_running = matches!(server_status, ContextServerStatus::Running);
let item_id = SharedString::from(context_server_id.0.clone());
let error = if let ContextServerStatus::Error(error) = server_status.clone() {
Some(error)
@@ -444,38 +443,9 @@ impl AgentConfiguration {
let tool_count = tools.len();
let border_color = cx.theme().colors().border.opacity(0.6);
let success_color = Color::Success.color(cx);
let (status_indicator, tooltip_text) = match server_status {
ContextServerStatus::Starting => (
Indicator::dot()
.color(Color::Success)
.with_animation(
SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 1.)),
move |this, delta| this.color(success_color.alpha(delta).into()),
)
.into_any_element(),
"Server is starting.",
),
ContextServerStatus::Running => (
Indicator::dot().color(Color::Success).into_any_element(),
"Server is running.",
),
ContextServerStatus::Error(_) => (
Indicator::dot().color(Color::Error).into_any_element(),
"Server has an error.",
),
ContextServerStatus::Stopped => (
Indicator::dot().color(Color::Muted).into_any_element(),
"Server is stopped.",
),
};
v_flex()
.id(item_id.clone())
.id(SharedString::from(context_server_id.0.clone()))
.border_1()
.rounded_md()
.border_color(border_color)
@@ -510,12 +480,35 @@ impl AgentConfiguration {
}
})),
)
.child(
div()
.id(item_id.clone())
.tooltip(Tooltip::text(tooltip_text))
.child(status_indicator),
)
.child(match server_status {
ContextServerStatus::Starting => {
let color = Color::Success.color(cx);
Indicator::dot()
.color(Color::Success)
.with_animation(
SharedString::from(format!(
"{}-starting",
context_server_id.0.clone(),
)),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 1.)),
move |this, delta| {
this.color(color.alpha(delta).into())
},
)
.into_any_element()
}
ContextServerStatus::Running => {
Indicator::dot().color(Color::Success).into_any_element()
}
ContextServerStatus::Error(_) => {
Indicator::dot().color(Color::Error).into_any_element()
}
ContextServerStatus::Stopped => {
Indicator::dot().color(Color::Muted).into_any_element()
}
})
.child(Label::new(context_server_id.0.clone()).ml_0p5())
.when(is_running, |this| {
this.child(

View File

@@ -1348,7 +1348,6 @@ impl AgentDiff {
ThreadEvent::NewRequest
| ThreadEvent::Stopped(Ok(StopReason::EndTurn))
| ThreadEvent::Stopped(Ok(StopReason::MaxTokens))
| ThreadEvent::Stopped(Ok(StopReason::Refusal))
| ThreadEvent::Stopped(Err(_))
| ThreadEvent::ShowError(_)
| ThreadEvent::CompletionCanceled => {

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use db::kvp::KEY_VALUE_STORE;
use markdown::Markdown;
use serde::{Deserialize, Serialize};
@@ -66,8 +66,8 @@ use crate::ui::AgentOnboardingModal;
use crate::{
AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, TextThreadStore, ThreadEvent,
ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
OpenHistory, ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleContextPicker,
ToggleNavigationMenu, ToggleOptionsMenu,
};
const AGENT_PANEL_KEY: &str = "agent_panel";
@@ -157,10 +157,7 @@ pub fn init(cx: &mut App) {
window.refresh();
})
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
TrialUpsell::set_dismissed(false, cx);
})
.register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
TrialEndUpsell::set_dismissed(false, cx);
set_trial_upsell_dismissed(false, cx);
});
},
)
@@ -1914,23 +1911,12 @@ impl AgentPanel {
}
}
fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
if TrialEndUpsell::dismissed() {
return false;
}
let plan = self.user_store.read(cx).current_plan();
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
matches!(plan, Some(Plan::Free)) && has_previous_trial
}
fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
if !matches!(self.active_view, ActiveView::Thread { .. }) {
return false;
}
if self.hide_trial_upsell || TrialUpsell::dismissed() {
if self.hide_trial_upsell || dismissed_trial_upsell() {
return false;
}
@@ -1976,115 +1962,125 @@ impl AgentPanel {
move |toggle_state, _window, cx| {
let toggle_state_bool = toggle_state.selected();
TrialUpsell::set_dismissed(toggle_state_bool, cx);
set_trial_upsell_dismissed(toggle_state_bool, cx);
},
);
let contents = div()
.size_full()
.gap_2()
.flex()
.flex_col()
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
.child(
Label::new("Try Zed Pro for free for 14 days - no credit card required.")
.size(LabelSize::Small),
)
.child(
Label::new(
"Use your own API keys or enable usage-based billing once you hit the cap.",
)
.color(Color::Muted),
)
.child(
h_flex()
Some(
div().p_2().child(
v_flex()
.w_full()
.px_neg_1()
.justify_between()
.items_center()
.child(h_flex().items_center().gap_1().child(checkbox))
.elevation_2(cx)
.rounded(px(8.))
.bg(cx.theme().colors().background.alpha(0.5))
.p(px(3.))
.child(
h_flex()
div()
.gap_2()
.flex()
.flex_col()
.size_full()
.border_1()
.rounded(px(5.))
.border_color(cx.theme().colors().text.alpha(0.1))
.overflow_hidden()
.relative()
.bg(cx.theme().colors().panel_background)
.px_4()
.py_3()
.child(
Button::new("dismiss-button", "Not Now")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(cx, |this, cx| {
this.hide_trial_upsell = true;
cx.notify();
});
}
}),
div()
.absolute()
.top_0()
.right(px(-1.0))
.w(px(441.))
.h(px(167.))
.child(
Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1)))
)
)
.child(
Button::new("cta-button", "Start Trial")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
),
),
);
Some(self.render_upsell_container(cx, contents))
}
fn render_trial_end_upsell(
&self,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
if !self.should_render_trial_end_upsell(cx) {
return None;
}
Some(
self.render_upsell_container(
cx,
div()
.size_full()
.gap_2()
.flex()
.flex_col()
.child(
Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small),
)
.child(
Label::new("You've been automatically reset to the free plan.")
.size(LabelSize::Small),
)
.child(
h_flex()
.w_full()
.px_neg_1()
.justify_between()
.items_center()
.child(div())
div()
.absolute()
.top(px(-8.0))
.right_0()
.w(px(400.))
.h(px(92.))
.child(
Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32)))
)
)
// .child(
// div()
// .absolute()
// .top_0()
// .right(px(360.))
// .size(px(401.))
// .overflow_hidden()
// .bg(cx.theme().colors().panel_background)
// )
.child(
div()
.absolute()
.top_0()
.right_0()
.w(px(660.))
.h(px(401.))
.overflow_hidden()
.bg(linear_gradient(
75.,
linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0),
linear_color_stop(cx.theme().colors().panel_background, 0.45),
))
)
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
.child(Label::new("Try Zed Pro for free for 14 days - no credit card required.").size(LabelSize::Small))
.child(Label::new("Use your own API keys or enable usage-based billing once you hit the cap.").color(Color::Muted))
.child(
h_flex()
.gap_2()
.w_full()
.px_neg_1()
.justify_between()
.items_center()
.child(h_flex().items_center().gap_1().child(checkbox))
.child(
Button::new("dismiss-button", "Stay on Free")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(cx, |_this, cx| {
TrialEndUpsell::set_dismissed(true, cx);
cx.notify();
});
}
}),
)
.child(
Button::new("cta-button", "Upgrade to Zed Pro")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| {
cx.open_url(&zed_urls::account_url(cx))
}),
h_flex()
.gap_2()
.child(
Button::new("dismiss-button", "Not Now")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(
cx,
|this, cx| {
let hidden =
this.hide_trial_upsell;
println!("hidden: {}", hidden);
this.hide_trial_upsell = true;
let new_hidden =
this.hide_trial_upsell;
println!(
"new_hidden: {}",
new_hidden
);
cx.notify();
},
);
}
}),
)
.child(
Button::new("cta-button", "Start Trial")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| {
cx.open_url(&zed_urls::account_url(cx))
}),
),
),
),
),
@@ -2092,91 +2088,6 @@ impl AgentPanel {
)
}
fn render_upsell_container(&self, cx: &mut Context<Self>, content: Div) -> Div {
div().p_2().child(
v_flex()
.w_full()
.elevation_2(cx)
.rounded(px(8.))
.bg(cx.theme().colors().background.alpha(0.5))
.p(px(3.))
.child(
div()
.gap_2()
.flex()
.flex_col()
.size_full()
.border_1()
.rounded(px(5.))
.border_color(cx.theme().colors().text.alpha(0.1))
.overflow_hidden()
.relative()
.bg(cx.theme().colors().panel_background)
.px_4()
.py_3()
.child(
div()
.absolute()
.top_0()
.right(px(-1.0))
.w(px(441.))
.h(px(167.))
.child(
Vector::new(
VectorName::Grid,
rems_from_px(441.),
rems_from_px(167.),
)
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
),
)
.child(
div()
.absolute()
.top(px(-8.0))
.right_0()
.w(px(400.))
.h(px(92.))
.child(
Vector::new(
VectorName::AiGrid,
rems_from_px(400.),
rems_from_px(92.),
)
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
),
)
// .child(
// div()
// .absolute()
// .top_0()
// .right(px(360.))
// .size(px(401.))
// .overflow_hidden()
// .bg(cx.theme().colors().panel_background)
// )
.child(
div()
.absolute()
.top_0()
.right_0()
.w(px(660.))
.h(px(401.))
.overflow_hidden()
.bg(linear_gradient(
75.,
linear_color_stop(
cx.theme().colors().panel_background.alpha(0.01),
1.0,
),
linear_color_stop(cx.theme().colors().panel_background, 0.45),
)),
)
.child(content),
),
)
}
fn render_active_thread_or_empty_state(
&self,
window: &mut Window,
@@ -2528,6 +2439,9 @@ impl AgentPanel {
.occlude()
.child(match last_error {
ThreadError::PaymentRequired => self.render_payment_required_error(cx),
ThreadError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
}
ThreadError::ModelRequestLimitReached { plan } => {
self.render_model_request_limit_reached_error(plan, cx)
}
@@ -2587,6 +2501,56 @@ impl AgentPanel {
.into_any()
}
fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(ERROR_MESSAGE)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.gap_1()
.child(self.create_copy_button(ERROR_MESSAGE))
.child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
}),
),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
},
))),
)
.into_any()
}
fn render_model_request_limit_reached_error(
&self,
plan: Plan,
@@ -2894,7 +2858,6 @@ impl Render for AgentPanel {
.on_action(cx.listener(Self::toggle_zoom))
.child(self.render_toolbar(window, cx))
.children(self.render_trial_upsell(window, cx))
.children(self.render_trial_end_upsell(window, cx))
.map(|parent| match &self.active_view {
ActiveView::Thread { .. } => parent
.relative()
@@ -3082,14 +3045,25 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
}
}
struct TrialUpsell;
const DISMISSED_TRIAL_UPSELL_KEY: &str = "dismissed-trial-upsell";
impl Dismissable for TrialUpsell {
const KEY: &'static str = "dismissed-trial-upsell";
fn dismissed_trial_upsell() -> bool {
db::kvp::KEY_VALUE_STORE
.read_kvp(DISMISSED_TRIAL_UPSELL_KEY)
.log_err()
.map_or(false, |s| s.is_some())
}
struct TrialEndUpsell;
impl Dismissable for TrialEndUpsell {
const KEY: &'static str = "dismissed-trial-end-upsell";
fn set_trial_upsell_dismissed(is_dismissed: bool, cx: &mut App) {
db::write_and_log(cx, move || async move {
if is_dismissed {
db::kvp::KEY_VALUE_STORE
.write_kvp(DISMISSED_TRIAL_UPSELL_KEY.into(), "1".into())
.await
} else {
db::kvp::KEY_VALUE_STORE
.delete_kvp(DISMISSED_TRIAL_UPSELL_KEY.into())
.await
}
})
}

View File

@@ -844,7 +844,6 @@ pub fn load_context(
let load_results = future::join_all(load_tasks).await;
let mut contexts = Vec::new();
let mut text = String::new();
let mut referenced_buffers = HashSet::default();
for context in load_results {
let Some((context, buffers)) = context else {
@@ -863,10 +862,18 @@ pub fn load_context(
let mut text_thread_context = Vec::new();
let mut rules_context = Vec::new();
let mut images = Vec::new();
let mut loaded_files = Vec::new();
let mut loaded_dirs = Vec::new();
for context in &contexts {
match context {
AgentContext::File(context) => file_context.push(context),
AgentContext::Directory(context) => directory_context.push(context),
AgentContext::File(context) => {
file_context.push(context);
loaded_files.push(context.full_path.clone());
}
AgentContext::Directory(context) => {
directory_context.push(context);
loaded_dirs.push(context.full_path.clone());
}
AgentContext::Symbol(context) => symbol_context.push(context),
AgentContext::Selection(context) => selection_context.push(context),
AgentContext::FetchedUrl(context) => fetched_url_context.push(context),
@@ -891,18 +898,17 @@ pub fn load_context(
return ContextLoadResult {
loaded_context: LoadedContext {
contexts,
text,
text: String::new(),
images,
},
referenced_buffers,
};
}
text.push_str(
"\n<context>\n\
let mut text = "\n<context>\n\
The following items were attached by the user. \
They are up-to-date and don't need to be re-read.\n\n",
);
They are up-to-date and don't need to be re-read.\n\n"
.to_string();
if !file_context.is_empty() {
text.push_str("<files>");

View File

@@ -11,7 +11,6 @@ use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
use crate::{RemoveAllContext, ToggleContextPicker};
use client::ErrorExt;
use collections::VecDeque;
use db::kvp::Dismissable;
use editor::display_map::EditorMargins;
use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
@@ -34,6 +33,7 @@ use ui::utils::WithRemSize;
use ui::{
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
};
use util::ResultExt;
use workspace::Workspace;
pub struct PromptEditor<T> {
@@ -722,7 +722,7 @@ impl<T: 'static> PromptEditor<T> {
.child(CheckboxWithLabel::new(
"dont-show-again",
Label::new("Don't show again"),
if RateLimitNotice::dismissed() {
if dismissed_rate_limit_notice() {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
@@ -734,7 +734,7 @@ impl<T: 'static> PromptEditor<T> {
ui::ToggleState::Selected => true,
};
RateLimitNotice::set_dismissed(is_dismissed, cx);
set_rate_limit_notice_dismissed(is_dismissed, cx)
},
))
.child(
@@ -974,7 +974,7 @@ impl PromptEditor<BufferCodegen> {
CodegenStatus::Error(error) => {
if cx.has_flag::<ZedProFeatureFlag>()
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
&& !RateLimitNotice::dismissed()
&& !dismissed_rate_limit_notice()
{
self.show_rate_limit_notice = true;
cx.notify();
@@ -1180,10 +1180,27 @@ impl PromptEditor<TerminalCodegen> {
}
}
struct RateLimitNotice;
const DISMISSED_RATE_LIMIT_NOTICE_KEY: &str = "dismissed-rate-limit-notice";
impl Dismissable for RateLimitNotice {
const KEY: &'static str = "dismissed-rate-limit-notice";
fn dismissed_rate_limit_notice() -> bool {
db::kvp::KEY_VALUE_STORE
.read_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY)
.log_err()
.map_or(false, |s| s.is_some())
}
fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut App) {
db::write_and_log(cx, move || async move {
if is_dismissed {
db::kvp::KEY_VALUE_STORE
.write_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into(), "1".into())
.await
} else {
db::kvp::KEY_VALUE_STORE
.delete_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into())
.await
}
})
}
pub enum CodegenStatus {

View File

@@ -22,7 +22,7 @@ use language_model::{
ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUseId, MessageContent,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent,
ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, SelectedModel,
StopReason, TokenUsage,
};
@@ -458,7 +458,7 @@ impl Thread {
tools: Entity<ToolWorkingSet>,
prompt_builder: Arc<PromptBuilder>,
project_context: SharedProjectContext,
window: Option<&mut Window>, // None in headless mode
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let next_message_id = MessageId(
@@ -880,13 +880,7 @@ impl Thread {
}
pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
match &self.tool_use.tool_result(id)?.content {
LanguageModelToolResultContent::Text(str) => Some(str),
LanguageModelToolResultContent::Image(_) => {
// TODO: We should display image
None
}
}
Some(&self.tool_use.tool_result(id)?.content)
}
pub fn card_for_tool(&self, id: &LanguageModelToolUseId) -> Option<AnyToolCard> {
@@ -1680,43 +1674,6 @@ impl Thread {
project.set_agent_location(None, cx);
});
}
StopReason::Refusal => {
thread.project.update(cx, |project, cx| {
project.set_agent_location(None, cx);
});
// Remove the turn that was refused.
//
// https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/handle-streaming-refusals#reset-context-after-refusal
{
let mut messages_to_remove = Vec::new();
for (ix, message) in thread.messages.iter().enumerate().rev() {
messages_to_remove.push(message.id);
if message.role == Role::User {
if ix == 0 {
break;
}
if let Some(prev_message) = thread.messages.get(ix - 1) {
if prev_message.role == Role::Assistant {
break;
}
}
}
}
for message_id in messages_to_remove {
thread.delete_message(message_id, cx);
}
}
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
header: "Language model refusal".into(),
message: "Model refused to generate content for safety reasons.".into(),
}));
}
},
Err(error) => {
thread.project.update(cx, |project, cx| {
@@ -1725,6 +1682,10 @@ impl Thread {
if error.is::<PaymentRequiredError>() {
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
} else if error.is::<MaxMonthlySpendReachedError>() {
cx.emit(ThreadEvent::ShowError(
ThreadError::MaxMonthlySpendReached,
));
} else if let Some(error) =
error.downcast_ref::<ModelRequestLimitReachedError>()
{
@@ -2541,15 +2502,7 @@ impl Thread {
}
writeln!(markdown, "**\n")?;
match &tool_result.content {
LanguageModelToolResultContent::Text(str) => {
writeln!(markdown, "{}", str)?;
}
LanguageModelToolResultContent::Image(image) => {
writeln!(markdown, "![Image](data:base64,{})", image.source)?;
}
}
writeln!(markdown, "{}", tool_result.content)?;
if let Some(output) = tool_result.output.as_ref() {
writeln!(
markdown,
@@ -2739,6 +2692,8 @@ impl Thread {
pub enum ThreadError {
#[error("Payment required")]
PaymentRequired,
#[error("Max monthly spend reached")]
MaxMonthlySpendReached,
#[error("Model request limit reached")]
ModelRequestLimitReached { plan: Plan },
#[error("Message {header}: {message}")]

View File

@@ -19,7 +19,7 @@ use gpui::{
};
use heed::Database;
use heed::types::SerdeBincode;
use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage};
use language_model::{LanguageModelToolUseId, Role, TokenUsage};
use project::context_server_store::{ContextServerStatus, ContextServerStore};
use project::{Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::{
@@ -386,25 +386,6 @@ impl ThreadStore {
})
}
pub fn create_thread_from_serialized(
&mut self,
serialized: SerializedThread,
cx: &mut Context<Self>,
) -> Entity<Thread> {
cx.new(|cx| {
Thread::deserialize(
ThreadId::new(),
serialized,
self.project.clone(),
self.tools.clone(),
self.prompt_builder.clone(),
self.project_context.clone(),
None,
cx,
)
})
}
pub fn open_thread(
&self,
id: &ThreadId,
@@ -430,7 +411,7 @@ impl ThreadStore {
this.tools.clone(),
this.prompt_builder.clone(),
this.project_context.clone(),
Some(window),
window,
cx,
)
})
@@ -794,7 +775,7 @@ pub struct SerializedToolUse {
pub struct SerializedToolResult {
pub tool_use_id: LanguageModelToolUseId,
pub is_error: bool,
pub content: LanguageModelToolResultContent,
pub content: Arc<str>,
pub output: Option<serde_json::Value>,
}

View File

@@ -1,16 +1,14 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::{
AnyToolCard, Tool, ToolResultContent, ToolResultOutput, ToolUseStatus, ToolWorkingSet,
};
use assistant_tool::{AnyToolCard, Tool, ToolResultOutput, ToolUseStatus, ToolWorkingSet};
use collections::HashMap;
use futures::FutureExt as _;
use futures::future::Shared;
use gpui::{App, Entity, SharedString, Task};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelRequest, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, Role,
LanguageModelToolUse, LanguageModelToolUseId, Role,
};
use project::Project;
use ui::{IconName, Window};
@@ -54,19 +52,15 @@ impl ToolUseState {
/// Constructs a [`ToolUseState`] from the given list of [`SerializedMessage`]s.
///
/// Accepts a function to filter the tools that should be used to populate the state.
///
/// If `window` is `None` (e.g., when in headless mode or when running evals),
/// tool cards won't be deserialized
pub fn from_serialized_messages(
tools: Entity<ToolWorkingSet>,
messages: &[SerializedMessage],
project: Entity<Project>,
window: Option<&mut Window>, // None in headless mode
window: &mut Window,
cx: &mut App,
) -> Self {
let mut this = Self::new(tools);
let mut tool_names_by_id = HashMap::default();
let mut window = window;
for message in messages {
match message.role {
@@ -111,17 +105,12 @@ impl ToolUseState {
},
);
if let Some(window) = &mut window {
if let Some(tool) = this.tools.read(cx).tool(tool_use, cx) {
if let Some(output) = tool_result.output.clone() {
if let Some(card) = tool.deserialize_card(
output,
project.clone(),
window,
cx,
) {
this.tool_result_cards.insert(tool_use_id, card);
}
if let Some(tool) = this.tools.read(cx).tool(tool_use, cx) {
if let Some(output) = tool_result.output.clone() {
if let Some(card) =
tool.deserialize_card(output, project.clone(), window, cx)
{
this.tool_result_cards.insert(tool_use_id, card);
}
}
}
@@ -176,16 +165,10 @@ impl ToolUseState {
let status = (|| {
if let Some(tool_result) = tool_result {
let content = tool_result
.content
.to_str()
.map(|str| str.to_owned().into())
.unwrap_or_default();
return if tool_result.is_error {
ToolUseStatus::Error(content)
ToolUseStatus::Error(tool_result.content.clone().into())
} else {
ToolUseStatus::Finished(content)
ToolUseStatus::Finished(tool_result.content.clone().into())
};
}
@@ -416,45 +399,21 @@ impl ToolUseState {
let tool_result = output.content;
const BYTES_PER_TOKEN_ESTIMATE: usize = 3;
let old_use = self.pending_tool_uses_by_id.remove(&tool_use_id);
// Protect from overly large output
// Protect from clearly large output
let tool_output_limit = configured_model
.map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE)
.unwrap_or(usize::MAX);
let content = match tool_result {
ToolResultContent::Text(text) => {
let text = if text.len() < tool_output_limit {
text
} else {
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
format!(
"Tool result too long. The first {} bytes:\n\n{}",
truncated.len(),
truncated
)
};
LanguageModelToolResultContent::Text(text.into())
}
ToolResultContent::Image(language_model_image) => {
if language_model_image.estimate_tokens() < tool_output_limit {
LanguageModelToolResultContent::Image(language_model_image)
} else {
self.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content: "Tool responded with an image that would exceeded the remaining tokens".into(),
is_error: true,
output: None,
},
);
let tool_result = if tool_result.len() <= tool_output_limit {
tool_result
} else {
let truncated = truncate_lines_to_byte_limit(&tool_result, tool_output_limit);
return old_use;
}
}
format!(
"Tool result too long. The first {} bytes:\n\n{}",
truncated.len(),
truncated
)
};
self.tool_results.insert(
@@ -462,13 +421,12 @@ impl ToolUseState {
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content,
content: tool_result.into(),
is_error: false,
output: output.output,
},
);
old_use
self.pending_tool_uses_by_id.remove(&tool_use_id)
}
Err(err) => {
self.tool_results.insert(
@@ -476,7 +434,7 @@ impl ToolUseState {
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content: LanguageModelToolResultContent::Text(err.to_string().into()),
content: err.to_string().into(),
is_error: true,
output: None,
},

View File

@@ -34,6 +34,7 @@ pub enum AnthropicModelMode {
pub enum Model {
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
Claude3_5Sonnet,
#[default]
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
Claude3_7Sonnet,
#[serde(
@@ -41,21 +42,6 @@ pub enum Model {
alias = "claude-3-7-sonnet-thinking-latest"
)]
Claude3_7SonnetThinking,
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
ClaudeOpus4,
#[serde(
rename = "claude-opus-4-thinking",
alias = "claude-opus-4-thinking-latest"
)]
ClaudeOpus4Thinking,
#[default]
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
ClaudeSonnet4,
#[serde(
rename = "claude-sonnet-4-thinking",
alias = "claude-sonnet-4-thinking-latest"
)]
ClaudeSonnet4Thinking,
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
Claude3_5Haiku,
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
@@ -103,14 +89,6 @@ impl Model {
Ok(Self::Claude3Sonnet)
} else if id.starts_with("claude-3-haiku") {
Ok(Self::Claude3Haiku)
} else if id.starts_with("claude-opus-4-thinking") {
Ok(Self::ClaudeOpus4Thinking)
} else if id.starts_with("claude-opus-4") {
Ok(Self::ClaudeOpus4)
} else if id.starts_with("claude-sonnet-4-thinking") {
Ok(Self::ClaudeSonnet4Thinking)
} else if id.starts_with("claude-sonnet-4") {
Ok(Self::ClaudeSonnet4)
} else {
Err(anyhow!("invalid model id"))
}
@@ -118,10 +96,6 @@ impl Model {
pub fn id(&self) -> &str {
match self {
Model::ClaudeOpus4 => "claude-opus-4-latest",
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
Model::ClaudeSonnet4 => "claude-sonnet-4-latest",
Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
@@ -136,8 +110,6 @@ impl Model {
/// The id of the model that should be used for making API requests
pub fn request_id(&self) -> &str {
match self {
Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => "claude-opus-4-20250514",
Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
@@ -150,10 +122,6 @@ impl Model {
pub fn display_name(&self) -> &str {
match self {
Model::ClaudeOpus4 => "Claude Opus 4",
Model::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
Model::ClaudeSonnet4 => "Claude Sonnet 4",
Model::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
@@ -169,11 +137,7 @@ impl Model {
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
match self {
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Sonnet
Self::Claude3_5Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
@@ -192,11 +156,7 @@ impl Model {
pub fn max_token_count(&self) -> usize {
match self {
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Sonnet
Self::Claude3_5Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
@@ -213,11 +173,7 @@ impl Model {
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3_5Haiku
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking => 8_192,
| Self::Claude3_5Haiku => 8_192,
Self::Custom {
max_output_tokens, ..
} => max_output_tokens.unwrap_or(4_096),
@@ -226,11 +182,7 @@ impl Model {
pub fn default_temperature(&self) -> f32 {
match self {
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Sonnet
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3_5Haiku
@@ -249,14 +201,10 @@ impl Model {
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_5Haiku
| Self::ClaudeOpus4
| Self::ClaudeSonnet4
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3Haiku => AnthropicModelMode::Default,
Self::Claude3_7SonnetThinking
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4Thinking => AnthropicModelMode::Thinking {
Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
budget_tokens: Some(4_096),
},
Self::Custom { mode, .. } => mode.clone(),
@@ -586,26 +534,12 @@ pub enum RequestContent {
ToolResult {
tool_use_id: String,
is_error: bool,
content: ToolResultContent,
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolResultContent {
Plain(String),
Multipart(Vec<ToolResultPart>),
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ToolResultPart {
Text { text: String },
Image { source: ImageSource },
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ResponseContent {

View File

@@ -21,8 +21,8 @@ use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, P
use language_model::{
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelToolUseId, MessageContent, PaymentRequiredError, Role, StopReason,
report_assistant_event,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
Role, StopReason, report_assistant_event,
};
use open_ai::Model as OpenAiModel;
use paths::contexts_dir;
@@ -447,6 +447,7 @@ impl ContextOperation {
pub enum ContextEvent {
ShowAssistError(SharedString),
ShowPaymentRequiredError,
ShowMaxMonthlySpendReachedError,
MessagesEdited,
SummaryChanged,
SummaryGenerated,
@@ -2154,6 +2155,12 @@ impl AssistantContext {
metadata.status = MessageStatus::Canceled;
});
Some(error.to_string())
} else if error.is::<MaxMonthlySpendReachedError>() {
cx.emit(ContextEvent::ShowMaxMonthlySpendReachedError);
this.update_metadata(assistant_message_id, cx, |metadata| {
metadata.status = MessageStatus::Canceled;
});
Some(error.to_string())
} else {
let error_message = error
.chain()
@@ -2204,7 +2211,6 @@ impl AssistantContext {
StopReason::ToolUse => {}
StopReason::EndTurn => {}
StopReason::MaxTokens => {}
StopReason::Refusal => {}
}
}
})

View File

@@ -114,6 +114,7 @@ type MessageHeader = MessageMetadata;
#[derive(Clone)]
enum AssistError {
PaymentRequired,
MaxMonthlySpendReached,
Message(SharedString),
}
@@ -731,6 +732,9 @@ impl ContextEditor {
ContextEvent::ShowPaymentRequiredError => {
self.last_error = Some(AssistError::PaymentRequired);
}
ContextEvent::ShowMaxMonthlySpendReachedError => {
self.last_error = Some(AssistError::MaxMonthlySpendReached);
}
}
}
@@ -1590,7 +1594,7 @@ impl ContextEditor {
&mut self,
cx: &mut Context<Self>,
) -> (String, CopyMetadata, Vec<text::Selection<usize>>) {
let (mut selection, creases) = self.editor.update(cx, |editor, cx| {
let (selection, creases) = self.editor.update(cx, |editor, cx| {
let mut selection = editor.selections.newest_adjusted(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
@@ -1648,18 +1652,7 @@ impl ContextEditor {
} else if message.offset_range.end >= selection.range().start {
let range = cmp::max(message.offset_range.start, selection.range().start)
..cmp::min(message.offset_range.end, selection.range().end);
if range.is_empty() {
let snapshot = context.buffer().read(cx).snapshot();
let point = snapshot.offset_to_point(range.start);
selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
selection.end = snapshot.point_to_offset(cmp::min(
Point::new(point.row + 1, 0),
snapshot.max_point(),
));
for chunk in context.buffer().read(cx).text_for_range(selection.range()) {
text.push_str(chunk);
}
} else {
if !range.is_empty() {
for chunk in context.buffer().read(cx).text_for_range(range) {
text.push_str(chunk);
}
@@ -2114,6 +2107,9 @@ impl ContextEditor {
.occlude()
.child(match last_error {
AssistError::PaymentRequired => self.render_payment_required_error(cx),
AssistError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
}
AssistError::Message(error_message) => {
self.render_assist_error(error_message, cx)
}
@@ -2162,6 +2158,48 @@ impl ContextEditor {
.into_any()
}
fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(ERROR_MESSAGE)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, _window, cx| {
this.last_error = None;
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
}),
),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, _window, cx| {
this.last_error = None;
cx.notify();
},
))),
)
.into_any()
}
fn render_assist_error(
&self,
error_message: &SharedString,
@@ -3213,77 +3251,9 @@ pub fn make_lsp_adapter_delegate(
#[cfg(test)]
mod tests {
use super::*;
use fs::FakeFs;
use gpui::{App, TestAppContext, VisualTestContext};
use language::{Buffer, LanguageRegistry};
use prompt_store::PromptBuilder;
use gpui::App;
use language::Buffer;
use unindent::Unindent;
use util::path;
#[gpui::test]
async fn test_copy_paste_no_selection(cx: &mut TestAppContext) {
cx.update(init_test);
let fs = FakeFs::new(cx.executor());
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
AssistantContext::local(
registry,
None,
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
cx,
)
});
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace = window.root(cx).unwrap();
let cx = &mut VisualTestContext::from_window(*window, cx);
let context_editor = window
.update(cx, |_, window, cx| {
cx.new(|cx| {
ContextEditor::for_context(
context,
fs,
workspace.downgrade(),
project,
None,
window,
cx,
)
})
})
.unwrap();
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.set_text("abc\ndef\nghi", window, cx);
editor.move_to_beginning(&Default::default(), window, cx);
})
});
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.copy(&Default::default(), window, cx);
editor.paste(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "abc\nabc\ndef\nghi");
})
});
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.cut(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "abc\ndef\nghi");
editor.paste(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "abc\nabc\ndef\nghi");
})
});
}
#[gpui::test]
fn test_find_code_blocks(cx: &mut App) {
@@ -3358,17 +3328,4 @@ mod tests {
assert_eq!(range, expected, "unexpected result on row {:?}", row);
}
}
fn init_test(cx: &mut App) {
let settings_store = SettingsStore::test(cx);
prompt_store::init(cx);
LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
language::init(cx);
assistant_settings::init(cx);
Project::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
workspace::init_settings(cx);
editor::init_settings(cx);
}
}

View File

@@ -41,7 +41,6 @@ pub enum NotifyWhenAgentWaiting {
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")]
#[schemars(deny_unknown_fields)]
pub enum AssistantProviderContentV1 {
#[serde(rename = "zed.dev")]
ZedDotDev { default_model: Option<CloudModel> },
@@ -544,7 +543,6 @@ impl AssistantSettingsContent {
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[serde(tag = "version")]
#[schemars(deny_unknown_fields)]
pub enum VersionedAssistantSettingsContent {
#[serde(rename = "1")]
V1(AssistantSettingsContentV1),
@@ -578,7 +576,6 @@ impl Default for VersionedAssistantSettingsContent {
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
#[schemars(deny_unknown_fields)]
pub struct AssistantSettingsContentV2 {
/// Whether the Assistant is enabled.
///
@@ -737,7 +734,6 @@ pub struct ContextServerPresetContent {
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct AssistantSettingsContentV1 {
/// Whether the Assistant is enabled.
///
@@ -767,7 +763,6 @@ pub struct AssistantSettingsContentV1 {
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct LegacyAssistantSettingsContent {
/// Whether to show the assistant panel button in the status bar.
///

View File

@@ -49,37 +49,6 @@ impl ActionLog {
is_created: bool,
cx: &mut Context<Self>,
) -> &mut TrackedBuffer {
let status = if is_created {
if let Some(tracked) = self.tracked_buffers.remove(&buffer) {
match tracked.status {
TrackedBufferStatus::Created {
existing_file_content,
} => TrackedBufferStatus::Created {
existing_file_content,
},
TrackedBufferStatus::Modified | TrackedBufferStatus::Deleted => {
TrackedBufferStatus::Created {
existing_file_content: Some(tracked.diff_base),
}
}
}
} else if buffer
.read(cx)
.file()
.map_or(false, |file| file.disk_state().exists())
{
TrackedBufferStatus::Created {
existing_file_content: Some(buffer.read(cx).as_rope().clone()),
}
} else {
TrackedBufferStatus::Created {
existing_file_content: None,
}
}
} else {
TrackedBufferStatus::Modified
};
let tracked_buffer = self
.tracked_buffers
.entry(buffer.clone())
@@ -91,21 +60,36 @@ impl ActionLog {
let text_snapshot = buffer.read(cx).text_snapshot();
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
let diff_base;
let base_text;
let status;
let unreviewed_changes;
if is_created {
diff_base = Rope::default();
let existing_file_content = if buffer
.read(cx)
.file()
.map_or(false, |file| file.disk_state().exists())
{
Some(text_snapshot.as_rope().clone())
} else {
None
};
base_text = Rope::default();
status = TrackedBufferStatus::Created {
existing_file_content,
};
unreviewed_changes = Patch::new(vec![Edit {
old: 0..1,
new: 0..text_snapshot.max_point().row + 1,
}])
} else {
diff_base = buffer.read(cx).as_rope().clone();
base_text = buffer.read(cx).as_rope().clone();
status = TrackedBufferStatus::Modified;
unreviewed_changes = Patch::default();
}
TrackedBuffer {
buffer: buffer.clone(),
diff_base,
base_text,
unreviewed_changes,
snapshot: text_snapshot.clone(),
status,
@@ -200,7 +184,7 @@ impl ActionLog {
.context("buffer not tracked")?;
let rebase = cx.background_spawn({
let mut base_text = tracked_buffer.diff_base.clone();
let mut base_text = tracked_buffer.base_text.clone();
let old_snapshot = tracked_buffer.snapshot.clone();
let new_snapshot = buffer_snapshot.clone();
let unreviewed_changes = tracked_buffer.unreviewed_changes.clone();
@@ -226,7 +210,7 @@ impl ActionLog {
))
})??;
let (new_base_text, new_diff_base) = rebase.await;
let (new_base_text, new_base_text_rope) = rebase.await;
let diff_snapshot = BufferDiff::update_diff(
diff.clone(),
buffer_snapshot.clone(),
@@ -245,23 +229,24 @@ impl ActionLog {
.background_spawn({
let diff_snapshot = diff_snapshot.clone();
let buffer_snapshot = buffer_snapshot.clone();
let new_diff_base = new_diff_base.clone();
let new_base_text_rope = new_base_text_rope.clone();
async move {
let mut unreviewed_changes = Patch::default();
for hunk in diff_snapshot.hunks_intersecting_range(
Anchor::MIN..Anchor::MAX,
&buffer_snapshot,
) {
let old_range = new_diff_base
let old_range = new_base_text_rope
.offset_to_point(hunk.diff_base_byte_range.start)
..new_diff_base.offset_to_point(hunk.diff_base_byte_range.end);
..new_base_text_rope
.offset_to_point(hunk.diff_base_byte_range.end);
let new_range = hunk.range.start..hunk.range.end;
unreviewed_changes.push(point_to_row_edit(
Edit {
old: old_range,
new: new_range,
},
&new_diff_base,
&new_base_text_rope,
&buffer_snapshot.as_rope(),
));
}
@@ -279,7 +264,7 @@ impl ActionLog {
.tracked_buffers
.get_mut(&buffer)
.context("buffer not tracked")?;
tracked_buffer.diff_base = new_diff_base;
tracked_buffer.base_text = new_base_text_rope;
tracked_buffer.snapshot = buffer_snapshot;
tracked_buffer.unreviewed_changes = unreviewed_changes;
cx.notify();
@@ -298,6 +283,7 @@ impl ActionLog {
/// 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.tracked_buffers.remove(&buffer);
self.track_buffer_internal(buffer.clone(), true, cx);
}
@@ -360,11 +346,11 @@ impl ActionLog {
true
} else {
let old_range = tracked_buffer
.diff_base
.base_text
.point_to_offset(Point::new(edit.old.start, 0))
..tracked_buffer.diff_base.point_to_offset(cmp::min(
..tracked_buffer.base_text.point_to_offset(cmp::min(
Point::new(edit.old.end, 0),
tracked_buffer.diff_base.max_point(),
tracked_buffer.base_text.max_point(),
));
let new_range = tracked_buffer
.snapshot
@@ -373,7 +359,7 @@ impl ActionLog {
Point::new(edit.new.end, 0),
tracked_buffer.snapshot.max_point(),
));
tracked_buffer.diff_base.replace(
tracked_buffer.base_text.replace(
old_range,
&tracked_buffer
.snapshot
@@ -431,7 +417,7 @@ impl ActionLog {
}
TrackedBufferStatus::Deleted => {
buffer.update(cx, |buffer, cx| {
buffer.set_text(tracked_buffer.diff_base.to_string(), cx)
buffer.set_text(tracked_buffer.base_text.to_string(), cx)
});
let save = self
.project
@@ -478,14 +464,14 @@ impl ActionLog {
if revert {
let old_range = tracked_buffer
.diff_base
.base_text
.point_to_offset(Point::new(edit.old.start, 0))
..tracked_buffer.diff_base.point_to_offset(cmp::min(
..tracked_buffer.base_text.point_to_offset(cmp::min(
Point::new(edit.old.end, 0),
tracked_buffer.diff_base.max_point(),
tracked_buffer.base_text.max_point(),
));
let old_text = tracked_buffer
.diff_base
.base_text
.chunks_in_range(old_range)
.collect::<String>();
edits_to_revert.push((new_range, old_text));
@@ -506,7 +492,7 @@ impl ActionLog {
TrackedBufferStatus::Deleted => false,
_ => {
tracked_buffer.unreviewed_changes.clear();
tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone();
tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone();
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
true
}
@@ -669,7 +655,7 @@ enum TrackedBufferStatus {
struct TrackedBuffer {
buffer: Entity<Buffer>,
diff_base: Rope,
base_text: Rope,
unreviewed_changes: Patch<u32>,
status: TrackedBufferStatus,
version: clock::Global,
@@ -1108,86 +1094,6 @@ mod tests {
);
}
#[gpui::test(iterations = 10)]
async fn test_overwriting_previously_edited_files(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/dir"),
json!({
"file1": "Lorem ipsum dolor"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| buffer.append(" sit amet consecteur", cx));
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![HunkStatus {
range: Point::new(0, 0)..Point::new(0, 37),
diff_status: DiffHunkStatusKind::Modified,
old_text: "Lorem ipsum dolor".into(),
}],
)]
);
cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| buffer.set_text("rewritten", cx));
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![HunkStatus {
range: Point::new(0, 0)..Point::new(0, 9),
diff_status: DiffHunkStatusKind::Added,
old_text: "".into(),
}],
)]
);
action_log
.update(cx, |log, cx| {
log.reject_edits_in_ranges(buffer.clone(), vec![2..5], cx)
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
assert_eq!(
buffer.read_with(cx, |buffer, _cx| buffer.text()),
"Lorem ipsum dolor"
);
}
#[gpui::test(iterations = 10)]
async fn test_deleting_files(cx: &mut TestAppContext) {
init_test(cx);
@@ -1695,7 +1601,7 @@ mod tests {
cx.run_until_parked();
action_log.update(cx, |log, cx| {
let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap();
let mut old_text = tracked_buffer.diff_base.clone();
let mut old_text = tracked_buffer.base_text.clone();
let new_text = buffer.read(cx).as_rope();
for edit in tracked_buffer.unreviewed_changes.edits() {
let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));

View File

@@ -19,7 +19,6 @@ use gpui::Window;
use gpui::{App, Entity, SharedString, Task, WeakEntity};
use icons::IconName;
use language_model::LanguageModel;
use language_model::LanguageModelImage;
use language_model::LanguageModelRequest;
use language_model::LanguageModelToolSchemaFormat;
use project::Project;
@@ -66,50 +65,21 @@ impl ToolUseStatus {
#[derive(Debug)]
pub struct ToolResultOutput {
pub content: ToolResultContent,
pub content: String,
pub output: Option<serde_json::Value>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum ToolResultContent {
Text(String),
Image(LanguageModelImage),
}
impl ToolResultContent {
pub fn len(&self) -> usize {
match self {
ToolResultContent::Text(str) => str.len(),
ToolResultContent::Image(image) => image.len(),
}
}
pub fn is_empty(&self) -> bool {
match self {
ToolResultContent::Text(str) => str.is_empty(),
ToolResultContent::Image(image) => image.is_empty(),
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
ToolResultContent::Text(str) => Some(str),
ToolResultContent::Image(_) => None,
}
}
}
impl From<String> for ToolResultOutput {
fn from(value: String) -> Self {
ToolResultOutput {
content: ToolResultContent::Text(value),
content: value,
output: None,
}
}
}
impl Deref for ToolResultOutput {
type Target = ToolResultContent;
type Target = String;
fn deref(&self) -> &Self::Target {
&self.content

View File

@@ -42,7 +42,6 @@ open.workspace = true
paths.workspace = true
portable-pty.workspace = true
project.workspace = true
prompt_store.workspace = true
regex.workspace = true
rust-embed.workspace = true
schemars.workspace = true

View File

@@ -42,7 +42,7 @@ use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
use crate::thinking_tool::ThinkingTool;
pub use edit_file_tool::{EditFileMode, EditFileToolInput};
pub use edit_file_tool::EditFileToolInput;
pub use find_path_tool::FindPathToolInput;
pub use open_tool::OpenTool;
pub use read_file_tool::{ReadFileTool, ReadFileToolInput};

View File

@@ -24,7 +24,6 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{cmp, iter, mem, ops::Range, path::PathBuf, sync::Arc, task::Poll};
use streaming_diff::{CharOperation, StreamingDiff};
use util::debug_panic;
#[derive(Serialize)]
struct CreateFilePromptTemplate {
@@ -544,11 +543,6 @@ impl EditAgent {
if last_message.content.is_empty() {
conversation.messages.pop();
}
} else {
debug_panic!(
"Last message must be an Assistant tool calling! Got {:?}",
last_message.content
);
}
}

View File

@@ -1,10 +1,5 @@
use super::*;
use crate::{
ReadFileToolInput,
edit_file_tool::{EditFileMode, EditFileToolInput},
grep_tool::GrepToolInput,
list_directory_tool::ListDirectoryToolInput,
};
use crate::{ReadFileToolInput, edit_file_tool::EditFileToolInput, grep_tool::GrepToolInput};
use Role::*;
use anyhow::anyhow;
use assistant_tool::ToolRegistry;
@@ -15,11 +10,10 @@ use futures::{FutureExt, future::LocalBoxFuture};
use gpui::{AppContext, TestAppContext};
use indoc::{formatdoc, indoc};
use language_model::{
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId,
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId,
};
use project::Project;
use prompt_store::{ModelContext, ProjectContext, PromptBuilder, WorktreeContext};
use rand::prelude::*;
use reqwest_client::ReqwestClient;
use serde_json::json;
@@ -34,39 +28,21 @@ use util::path;
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_extract_handle_command_output() {
// Test how well agent generates multiple edit hunks.
//
// Model | Pass rate
// ----------------------------|----------
// claude-3.7-sonnet | 0.98
// gemini-2.5-pro | 0.86
// 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");
let possible_diffs = vec![
include_str!("evals/fixtures/extract_handle_command_output/possible-01.diff"),
include_str!("evals/fixtures/extract_handle_command_output/possible-02.diff"),
include_str!("evals/fixtures/extract_handle_command_output/possible-03.diff"),
include_str!("evals/fixtures/extract_handle_command_output/possible-04.diff"),
include_str!("evals/fixtures/extract_handle_command_output/possible-05.diff"),
include_str!("evals/fixtures/extract_handle_command_output/possible-06.diff"),
include_str!("evals/fixtures/extract_handle_command_output/possible-07.diff"),
];
let output_file_content = include_str!("evals/fixtures/extract_handle_command_output/after.rs");
let edit_description = "Extract `handle_command_output` method from `run_git_blame`.";
eval(
100,
0.7, // Taking the lower bar for Gemini
EvalInput::from_conversation(
vec![
0.95,
EvalInput {
conversation: vec![
message(
User,
[text(formatdoc! {"
Read the `{input_file_path}` file and extract a method in
the final stanza of `run_git_blame` to deal with command failures,
call it `handle_command_output` and take the std::process::Output as the only parameter.
Do not document the method and do not add any comments.
Add it right next to `run_git_blame` and copy it verbatim from `run_git_blame`.
"})],
@@ -95,14 +71,16 @@ fn eval_extract_handle_command_output() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
},
)],
),
],
Some(input_file_content.into()),
EvalAssertion::assert_diff_any(possible_diffs),
),
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::assert_eq(output_file_content),
},
);
}
@@ -116,8 +94,8 @@ fn eval_delete_run_git_blame() {
eval(
100,
0.95,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(formatdoc! {"
@@ -149,14 +127,16 @@ fn eval_delete_run_git_blame() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
},
)],
),
],
Some(input_file_content.into()),
EvalAssertion::assert_eq(output_file_content),
),
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::assert_eq(output_file_content),
},
);
}
@@ -169,8 +149,8 @@ fn eval_translate_doc_comments() {
eval(
200,
1.,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(formatdoc! {"
@@ -202,14 +182,16 @@ fn eval_translate_doc_comments() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
},
)],
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff("Doc comments were translated to Italian"),
),
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff("Doc comments were translated to Italian"),
},
);
}
@@ -223,8 +205,8 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
eval(
100,
0.95,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(formatdoc! {"
@@ -315,17 +297,19 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
},
)],
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(indoc! {"
- The compile_parser_to_wasm method has been changed to use wasi-sdk
- ureq is used to download the SDK for current platform and architecture
"}),
),
},
);
}
@@ -336,10 +320,10 @@ fn eval_disable_cursor_blinking() {
let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs");
let edit_description = "Comment out the call to `BlinkManager::enable`";
eval(
100,
200,
0.95,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(User, [text("Let's research how to cursor blinking works.")]),
message(
Assistant,
@@ -388,18 +372,20 @@ fn eval_disable_cursor_blinking() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
},
)],
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(indoc! {"
- Calls to BlinkManager in `observe_window_activation` were commented out
- The call to `blink_manager.enable` above the call to show_cursor_names was commented out
- All the edits have valid indentation
"}),
),
},
);
}
@@ -412,8 +398,8 @@ fn eval_from_pixels_constructor() {
eval(
100,
0.95,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(indoc! {"
@@ -580,17 +566,19 @@ fn eval_from_pixels_constructor() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
},
)],
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
- The diff contains a new `from_pixels` constructor
- The diff contains new tests for the `from_pixels` constructor
"}),
),
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(indoc! {"
- The diff contains a new `from_pixels` constructor
- The diff contains new tests for the `from_pixels` constructor
"}),
},
);
}
@@ -598,13 +586,12 @@ fn eval_from_pixels_constructor() {
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_zode() {
let input_file_path = "root/zode.py";
let input_content = None;
let edit_description = "Create the main Zode CLI script";
eval(
200,
1.,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(User, [text(include_str!("evals/fixtures/zode/prompt.md"))]),
message(
Assistant,
@@ -656,18 +643,20 @@ fn eval_zode() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Create,
create_or_overwrite: true,
},
),
],
),
],
input_content,
EvalAssertion::new(async move |sample, _, _cx| {
input_path: input_file_path.into(),
input_content: None,
edit_description: edit_description.into(),
assertion: EvalAssertion::new(async move |sample, _, _cx| {
let invalid_starts = [' ', '`', '\n'];
let mut message = String::new();
for start in invalid_starts {
if sample.text_after.starts_with(start) {
if sample.text.starts_with(start) {
message.push_str(&format!("The sample starts with a {:?}\n", start));
break;
}
@@ -687,7 +676,7 @@ fn eval_zode() {
})
}
}),
),
},
);
}
@@ -700,8 +689,8 @@ fn eval_add_overwrite_test() {
eval(
200,
0.5, // TODO: make this eval better
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(indoc! {"
@@ -899,99 +888,19 @@ fn eval_add_overwrite_test() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
},
),
],
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(
"A new test for overwritten files was created, without changing any previous test",
),
),
);
}
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_create_empty_file() {
// Check that Edit Agent can create a file without writing its
// thoughts into it. This issue is not specific to empty files, but
// it's easier to reproduce with them.
//
//
// Model | Pass rate
// ============================================
//
// --------------------------------------------
// Prompt version: 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:
// "data did not match any variant of untagged enum ResponseStreamResult"
//
let input_file_content = None;
let expected_output_content = String::new();
eval(
100,
0.99,
EvalInput::from_conversation(
vec![
message(User, [text("Create a second empty todo file ")]),
message(
Assistant,
[
text(formatdoc! {"
I'll help you create a second empty todo file.
First, let me examine the project structure to see if there's already a todo file, which will help me determine the appropriate name and location for the second one.
"}),
tool_use(
"toolu_01GAF8TtsgpjKxCr8fgQLDgR",
"list_directory",
ListDirectoryToolInput {
path: "root".to_string(),
},
),
],
),
message(
User,
[tool_result(
"toolu_01GAF8TtsgpjKxCr8fgQLDgR",
"list_directory",
"root/TODO\nroot/TODO2\nroot/new.txt\n",
)],
),
message(
Assistant,
[
text(formatdoc! {"
I can see there's already a `TODO` file in the `root` directory. Let me create a second empty todo file called `TODO3` in the same directory:
"}),
tool_use(
"toolu_01Tb3iQ9griqSYMmVuykQPWU",
"edit_file",
EditFileToolInput {
display_description: "Create empty TODO3 file".to_string(),
mode: EditFileMode::Create,
path: "root/TODO3".into(),
},
),
],
),
],
input_file_content,
// Bad behavior is to write something like
// "I'll create an empty TODO3 file as requested."
EvalAssertion::assert_eq(expected_output_content),
),
},
);
}
@@ -1042,7 +951,7 @@ fn tool_result(
tool_use_id: LanguageModelToolUseId::from(id.into()),
tool_name: name.into(),
is_error: false,
content: LanguageModelToolResultContent::Text(result.into()),
content: result.into(),
output: None,
})
}
@@ -1050,50 +959,15 @@ fn tool_result(
#[derive(Clone)]
struct EvalInput {
conversation: Vec<LanguageModelRequestMessage>,
edit_file_input: EditFileToolInput,
input_path: PathBuf,
input_content: Option<String>,
edit_description: String,
assertion: EvalAssertion,
}
impl EvalInput {
fn from_conversation(
conversation: Vec<LanguageModelRequestMessage>,
input_content: Option<String>,
assertion: EvalAssertion,
) -> Self {
let msg = conversation.last().expect("Conversation must not be empty");
if msg.role != Role::Assistant {
panic!("Conversation must end with an assistant message");
}
let tool_use = msg
.content
.iter()
.flat_map(|content| match content {
MessageContent::ToolUse(tool_use) if tool_use.name == "edit_file".into() => {
Some(tool_use)
}
_ => None,
})
.next()
.expect("Conversation must end with an edit_file tool use")
.clone();
let edit_file_input: EditFileToolInput =
serde_json::from_value(tool_use.input.clone()).unwrap();
EvalInput {
conversation,
edit_file_input,
input_content,
assertion,
}
}
}
#[derive(Clone)]
struct EvalSample {
text_before: String,
text_after: String,
text: String,
edit_output: EditAgentOutput,
diff: String,
}
@@ -1150,7 +1024,7 @@ impl EvalAssertion {
let expected = expected.into();
Self::new(async move |sample, _judge, _cx| {
Ok(EvalAssertionOutcome {
score: if strip_empty_lines(&sample.text_after) == strip_empty_lines(&expected) {
score: if strip_empty_lines(&sample.text) == strip_empty_lines(&expected) {
100
} else {
0
@@ -1160,22 +1034,6 @@ impl EvalAssertion {
})
}
fn assert_diff_any(expected_diffs: Vec<impl Into<String>>) -> Self {
let expected_diffs: Vec<String> = expected_diffs.into_iter().map(Into::into).collect();
Self::new(async move |sample, _judge, _cx| {
let matches = expected_diffs.iter().any(|possible_diff| {
let expected =
language::apply_diff_patch(&sample.text_before, possible_diff).unwrap();
strip_empty_lines(&expected) == strip_empty_lines(&sample.text_after)
});
Ok(EvalAssertionOutcome {
score: if matches { 100 } else { 0 },
message: None,
})
})
}
fn judge_diff(assertions: &'static str) -> Self {
Self::new(async move |sample, judge, cx| {
let prompt = DiffJudgeTemplate {
@@ -1263,7 +1121,7 @@ fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
if output.assertion.score < 80 {
failed_count += 1;
failed_evals
.entry(output.sample.text_after.clone())
.entry(output.sample.text.clone())
.or_insert(Vec::new())
.push(output);
}
@@ -1435,7 +1293,7 @@ impl EditAgentTest {
let path = self
.project
.read_with(cx, |project, cx| {
project.find_project_path(eval.edit_file_input.path, cx)
project.find_project_path(eval.input_path, cx)
})
.unwrap();
let buffer = self
@@ -1443,69 +1301,31 @@ impl EditAgentTest {
.update(cx, |project, cx| project.open_buffer(path, cx))
.await
.unwrap();
let tools = cx.update(|cx| {
ToolRegistry::default_global(cx)
.tools()
.into_iter()
.filter_map(|tool| {
let input_schema = tool
.input_schema(self.agent.model.tool_input_format())
.ok()?;
Some(LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema,
})
})
.collect::<Vec<_>>()
});
let tool_names = tools
.iter()
.map(|tool| tool.name.clone())
.collect::<Vec<_>>();
let worktrees = vec![WorktreeContext {
root_name: "root".to_string(),
rules_file: None,
}];
let prompt_builder = PromptBuilder::new(None)?;
let project_context = ProjectContext::new(worktrees, Vec::default());
let system_prompt = prompt_builder.generate_assistant_system_prompt(
&project_context,
&ModelContext {
available_tools: tool_names,
},
)?;
let has_system_prompt = eval
.conversation
.first()
.map_or(false, |msg| msg.role == Role::System);
let messages = if has_system_prompt {
eval.conversation
} else {
[LanguageModelRequestMessage {
role: Role::System,
content: vec![MessageContent::Text(system_prompt)],
cache: true,
}]
.into_iter()
.chain(eval.conversation)
.collect::<Vec<_>>()
};
let conversation = LanguageModelRequest {
messages,
tools,
messages: eval.conversation,
tools: cx.update(|cx| {
ToolRegistry::default_global(cx)
.tools()
.into_iter()
.filter_map(|tool| {
let input_schema = tool
.input_schema(self.agent.model.tool_input_format())
.ok()?;
Some(LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema,
})
})
.collect()
}),
..Default::default()
};
let edit_output = if matches!(eval.edit_file_input.mode, EditFileMode::Edit) {
if let Some(input_content) = eval.input_content.as_deref() {
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
}
let edit_output = if let Some(input_content) = eval.input_content.as_deref() {
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
let (edit_output, _) = self.agent.edit(
buffer.clone(),
eval.edit_file_input.display_description,
eval.edit_description,
&conversation,
&mut cx.to_async(),
);
@@ -1513,7 +1333,7 @@ impl EditAgentTest {
} else {
let (edit_output, _) = self.agent.overwrite(
buffer.clone(),
eval.edit_file_input.display_description,
eval.edit_description,
&conversation,
&mut cx.to_async(),
);
@@ -1527,8 +1347,7 @@ impl EditAgentTest {
eval.input_content.as_deref().unwrap_or_default(),
&buffer_text,
),
text_before: eval.input_content.unwrap_or_default(),
text_after: buffer_text,
text: buffer_text,
};
let assertion = eval
.assertion

View File

@@ -0,0 +1,378 @@
use crate::commit::get_messages;
use crate::{GitRemote, Oid};
use anyhow::{Context as _, Result, anyhow};
use collections::{HashMap, HashSet};
use futures::AsyncWriteExt;
use gpui::SharedString;
use serde::{Deserialize, Serialize};
use std::process::Stdio;
use std::{ops::Range, path::Path};
use text::Rope;
use time::OffsetDateTime;
use time::UtcOffset;
use time::macros::format_description;
pub use git2 as libgit;
#[derive(Debug, Clone, Default)]
pub struct Blame {
pub entries: Vec<BlameEntry>,
pub messages: HashMap<Oid, String>,
pub remote_url: Option<String>,
}
#[derive(Clone, Debug, Default)]
pub struct ParsedCommitMessage {
pub message: SharedString,
pub permalink: Option<url::Url>,
pub pull_request: Option<crate::hosting_provider::PullRequest>,
pub remote: Option<GitRemote>,
}
impl Blame {
pub async fn for_path(
git_binary: &Path,
working_directory: &Path,
path: &Path,
content: &Rope,
remote_url: Option<String>,
) -> Result<Self> {
let output = run_git_blame(git_binary, working_directory, path, content).await?;
let mut entries = parse_git_blame(&output)?;
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start));
let mut unique_shas = HashSet::default();
for entry in entries.iter_mut() {
unique_shas.insert(entry.sha);
}
let shas = unique_shas.into_iter().collect::<Vec<_>>();
let messages = get_messages(working_directory, &shas)
.await
.context("failed to get commit messages")?;
Ok(Self {
entries,
messages,
remote_url,
})
}
}
const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD";
const GIT_BLAME_NO_PATH: &str = "fatal: no such path";
async fn run_git_blame(
git_binary: &Path,
working_directory: &Path,
path: &Path,
contents: &Rope,
) -> Result<String> {
let mut child = util::command::new_smol_command(git_binary)
.current_dir(working_directory)
.arg("blame")
.arg("--incremental")
.arg("--contents")
.arg("-")
.arg(path.as_os_str())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
let stdin = child
.stdin
.as_mut()
.context("failed to get pipe to stdin of git blame command")?;
for chunk in contents.chunks() {
stdin.write_all(chunk.as_bytes()).await?;
}
stdin.flush().await?;
let output = child
.output()
.await
.map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
handle_command_output(output)
}
fn handle_command_output(output: std::process::Output) -> Result<String> {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let trimmed = stderr.trim();
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
return Ok(String::new());
}
return Err(anyhow!("git blame process failed: {}", stderr));
}
Ok(String::from_utf8(output.stdout)?)
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
pub struct BlameEntry {
pub sha: Oid,
pub range: Range<u32>,
pub original_line_number: u32,
pub author: Option<String>,
pub author_mail: Option<String>,
pub author_time: Option<i64>,
pub author_tz: Option<String>,
pub committer_name: Option<String>,
pub committer_email: Option<String>,
pub committer_time: Option<i64>,
pub committer_tz: Option<String>,
pub summary: Option<String>,
pub previous: Option<String>,
pub filename: String,
}
impl BlameEntry {
// Returns a BlameEntry by parsing the first line of a `git blame --incremental`
// entry. The line MUST have this format:
//
// <40-byte-hex-sha1> <sourceline> <resultline> <num-lines>
fn new_from_blame_line(line: &str) -> Result<BlameEntry> {
let mut parts = line.split_whitespace();
let sha = parts
.next()
.and_then(|line| line.parse::<Oid>().ok())
.ok_or_else(|| anyhow!("failed to parse sha"))?;
let original_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.ok_or_else(|| anyhow!("Failed to parse original line number"))?;
let final_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let line_count = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let start_line = final_line_number.saturating_sub(1);
let end_line = start_line + line_count;
let range = start_line..end_line;
Ok(Self {
sha,
range,
original_line_number,
..Default::default()
})
}
pub fn author_offset_date_time(&self) -> Result<time::OffsetDateTime> {
if let (Some(author_time), Some(author_tz)) = (self.author_time, &self.author_tz) {
let format = format_description!("[offset_hour][offset_minute]");
let offset = UtcOffset::parse(author_tz, &format)?;
let date_time_utc = OffsetDateTime::from_unix_timestamp(author_time)?;
Ok(date_time_utc.to_offset(offset))
} else {
// Directly return current time in UTC if there's no committer time or timezone
Ok(time::OffsetDateTime::now_utc())
}
}
}
// parse_git_blame parses the output of `git blame --incremental`, which returns
// all the blame-entries for a given path incrementally, as it finds them.
//
// Each entry *always* starts with:
//
// <40-byte-hex-sha1> <sourceline> <resultline> <num-lines>
//
// Each entry *always* ends with:
//
// filename <whitespace-quoted-filename-goes-here>
//
// Line numbers are 1-indexed.
//
// A `git blame --incremental` entry looks like this:
//
// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 2 2 1
// author Joe Schmoe
// author-mail <joe.schmoe@example.com>
// author-time 1709741400
// author-tz +0100
// committer Joe Schmoe
// committer-mail <joe.schmoe@example.com>
// committer-time 1709741400
// committer-tz +0100
// summary Joe's cool commit
// previous 486c2409237a2c627230589e567024a96751d475 index.js
// filename index.js
//
// If the entry has the same SHA as an entry that was already printed then no
// signature information is printed:
//
// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 3 4 1
// previous 486c2409237a2c627230589e567024a96751d475 index.js
// filename index.js
//
// More about `--incremental` output: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-blame.html
fn parse_git_blame(output: &str) -> Result<Vec<BlameEntry>> {
let mut entries: Vec<BlameEntry> = Vec::new();
let mut index: HashMap<Oid, usize> = HashMap::default();
let mut current_entry: Option<BlameEntry> = None;
for line in output.lines() {
let mut done = false;
match &mut current_entry {
None => {
let mut new_entry = BlameEntry::new_from_blame_line(line)?;
if let Some(existing_entry) = index
.get(&new_entry.sha)
.and_then(|slot| entries.get(*slot))
{
new_entry.author.clone_from(&existing_entry.author);
new_entry
.author_mail
.clone_from(&existing_entry.author_mail);
new_entry.author_time = existing_entry.author_time;
new_entry.author_tz.clone_from(&existing_entry.author_tz);
new_entry
.committer_name
.clone_from(&existing_entry.committer_name);
new_entry
.committer_email
.clone_from(&existing_entry.committer_email);
new_entry.committer_time = existing_entry.committer_time;
new_entry
.committer_tz
.clone_from(&existing_entry.committer_tz);
new_entry.summary.clone_from(&existing_entry.summary);
}
current_entry.replace(new_entry);
}
Some(entry) => {
let Some((key, value)) = line.split_once(' ') else {
continue;
};
let is_committed = !entry.sha.is_zero();
match key {
"filename" => {
entry.filename = value.into();
done = true;
}
"previous" => entry.previous = Some(value.into()),
"summary" if is_committed => entry.summary = Some(value.into()),
"author" if is_committed => entry.author = Some(value.into()),
"author-mail" if is_committed => entry.author_mail = Some(value.into()),
"author-time" if is_committed => {
entry.author_time = Some(value.parse::<i64>()?)
}
"author-tz" if is_committed => entry.author_tz = Some(value.into()),
"committer" if is_committed => entry.committer_name = Some(value.into()),
"committer-mail" if is_committed => entry.committer_email = Some(value.into()),
"committer-time" if is_committed => {
entry.committer_time = Some(value.parse::<i64>()?)
}
"committer-tz" if is_committed => entry.committer_tz = Some(value.into()),
_ => {}
}
}
};
if done {
if let Some(entry) = current_entry.take() {
index.insert(entry.sha, entries.len());
// We only want annotations that have a commit.
if !entry.sha.is_zero() {
entries.push(entry);
}
}
}
}
Ok(entries)
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::BlameEntry;
use super::parse_git_blame;
fn read_test_data(filename: &str) -> String {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("test_data");
path.push(filename);
std::fs::read_to_string(&path)
.unwrap_or_else(|_| panic!("Could not read test data at {:?}. Is it generated?", path))
}
fn assert_eq_golden(entries: &Vec<BlameEntry>, golden_filename: &str) {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("test_data");
path.push("golden");
path.push(format!("{}.json", golden_filename));
let mut have_json =
serde_json::to_string_pretty(&entries).expect("could not serialize entries to JSON");
// We always want to save with a trailing newline.
have_json.push('\n');
let update = std::env::var("UPDATE_GOLDEN")
.map(|val| val.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if update {
std::fs::create_dir_all(path.parent().unwrap())
.expect("could not create golden test data directory");
std::fs::write(&path, have_json).expect("could not write out golden data");
} else {
let want_json =
std::fs::read_to_string(&path).unwrap_or_else(|_| {
panic!("could not read golden test data file at {:?}. Did you run the test with UPDATE_GOLDEN=true before?", path);
}).replace("\r\n", "\n");
pretty_assertions::assert_eq!(have_json, want_json, "wrong blame entries");
}
}
#[test]
fn test_parse_git_blame_not_committed() {
let output = read_test_data("blame_incremental_not_committed");
let entries = parse_git_blame(&output).unwrap();
assert_eq_golden(&entries, "blame_incremental_not_committed");
}
#[test]
fn test_parse_git_blame_simple() {
let output = read_test_data("blame_incremental_simple");
let entries = parse_git_blame(&output).unwrap();
assert_eq_golden(&entries, "blame_incremental_simple");
}
#[test]
fn test_parse_git_blame_complex() {
let output = read_test_data("blame_incremental_complex");
let entries = parse_git_blame(&output).unwrap();
assert_eq_golden(&entries, "blame_incremental_complex");
}
}

View File

@@ -1,11 +0,0 @@
@@ -94,6 +94,10 @@
let output = child.output().await.context("reading git blame output")?;
+ handle_command_output(output)
+}
+
+fn handle_command_output(output: std::process::Output) -> Result<String> {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let trimmed = stderr.trim();

View File

@@ -1,26 +0,0 @@
@@ -95,15 +95,19 @@
let output = child.output().await.context("reading git blame output")?;
if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- let trimmed = stderr.trim();
- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
- return Ok(String::new());
- }
- anyhow::bail!("git blame process failed: {stderr}");
+ return handle_command_output(output);
}
Ok(String::from_utf8(output.stdout)?)
+}
+
+fn handle_command_output(output: std::process::Output) -> Result<String> {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ let trimmed = stderr.trim();
+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
+ return Ok(String::new());
+ }
+ anyhow::bail!("git blame process failed: {stderr}");
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]

View File

@@ -1,11 +0,0 @@
@@ -93,7 +93,10 @@
stdin.flush().await?;
let output = child.output().await.context("reading git blame output")?;
+ handle_command_output(output)
+}
+fn handle_command_output(output: std::process::Output) -> Result<String> {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let trimmed = stderr.trim();

View File

@@ -1,24 +0,0 @@
@@ -93,17 +93,20 @@
stdin.flush().await?;
let output = child.output().await.context("reading git blame output")?;
+ handle_command_output(&output)?;
+ Ok(String::from_utf8(output.stdout)?)
+}
+fn handle_command_output(output: &std::process::Output) -> Result<()> {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let trimmed = stderr.trim();
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
- return Ok(String::new());
+ return Ok(());
}
anyhow::bail!("git blame process failed: {stderr}");
}
-
- Ok(String::from_utf8(output.stdout)?)
+ Ok(())
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]

View File

@@ -1,26 +0,0 @@
@@ -95,15 +95,19 @@
let output = child.output().await.context("reading git blame output")?;
if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- let trimmed = stderr.trim();
- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
- return Ok(String::new());
- }
- anyhow::bail!("git blame process failed: {stderr}");
+ return handle_command_output(&output);
}
Ok(String::from_utf8(output.stdout)?)
+}
+
+fn handle_command_output(output: &std::process::Output) -> Result<String> {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ let trimmed = stderr.trim();
+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
+ return Ok(String::new());
+ }
+ anyhow::bail!("git blame process failed: {stderr}");
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]

View File

@@ -1,23 +0,0 @@
@@ -93,7 +93,12 @@
stdin.flush().await?;
let output = child.output().await.context("reading git blame output")?;
+ handle_command_output(&output)?;
+ Ok(String::from_utf8(output.stdout)?)
+}
+
+fn handle_command_output(output: &std::process::Output) -> Result<String> {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let trimmed = stderr.trim();
@@ -102,8 +107,7 @@
}
anyhow::bail!("git blame process failed: {stderr}");
}
-
- Ok(String::from_utf8(output.stdout)?)
+ Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]

View File

@@ -1,26 +0,0 @@
@@ -95,15 +95,19 @@
let output = child.output().await.context("reading git blame output")?;
if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- let trimmed = stderr.trim();
- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
- return Ok(String::new());
- }
- anyhow::bail!("git blame process failed: {stderr}");
+ return handle_command_output(output);
}
Ok(String::from_utf8(output.stdout)?)
+}
+
+fn handle_command_output(output: std::process::Output) -> Result<String> {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ let trimmed = stderr.trim();
+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
+ return Ok(String::new());
+ }
+ anyhow::bail!("git blame process failed: {stderr}");
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]

View File

@@ -1,26 +0,0 @@
@@ -95,15 +95,19 @@
let output = child.output().await.context("reading git blame output")?;
if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- let trimmed = stderr.trim();
- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
- return Ok(String::new());
- }
- anyhow::bail!("git blame process failed: {stderr}");
+ return handle_command_output(output);
}
Ok(String::from_utf8(output.stdout)?)
+}
+
+fn handle_command_output(output: std::process::Output) -> Result<String> {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ let trimmed = stderr.trim();
+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
+ return Ok(String::new());
+ }
+ anyhow::bail!("git blame process failed: {stderr}")
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]

View File

@@ -5,8 +5,7 @@ use crate::{
};
use anyhow::{Result, anyhow};
use assistant_tool::{
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput,
ToolUseStatus,
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultOutput, ToolUseStatus,
};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MultiBuffer, PathKey};
@@ -22,7 +21,7 @@ use language::{
};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use project::{Project, ProjectPath};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -38,7 +37,7 @@ use workspace::Workspace;
pub struct EditFileTool;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput {
/// A one-line, user-friendly markdown description of the edit. This will be
/// shown in the UI and also passed to another model to perform the edit.
@@ -76,22 +75,12 @@ pub struct EditFileToolInput {
/// </example>
pub path: PathBuf,
/// The mode of operation on the file. Possible values:
/// - 'edit': Make granular edits to an existing file.
/// - 'create': Create a new file if it doesn't exist.
/// - 'overwrite': Replace the entire contents of an existing file.
/// If true, this tool will recreate the file from scratch.
/// If false, this tool will produce granular edits to an existing file.
///
/// When a file already exists or you just created it, prefer editing
/// When a file already exists or you just created it, always prefer editing
/// it as opposed to recreating it from scratch.
pub mode: EditFileMode,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum EditFileMode {
Edit,
Create,
Overwrite,
pub create_or_overwrite: bool,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -171,9 +160,12 @@ impl Tool for EditFileTool {
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let project_path = match resolve_path(&input, project.clone(), cx) {
Ok(path) => path,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!(
"Path {} not found in project",
input.path.display()
)))
.into();
};
let card = window.and_then(|window| {
@@ -196,6 +188,16 @@ impl Tool for EditFileTool {
})?
.await?;
let exists = buffer.read_with(cx, |buffer, _| {
buffer
.file()
.as_ref()
.map_or(false, |file| file.disk_state().exists())
})?;
if !input.create_or_overwrite && !exists {
return Err(anyhow!("{} not found", input.path.display()));
}
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
.background_spawn({
@@ -204,15 +206,15 @@ impl Tool for EditFileTool {
})
.await;
let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
edit_agent.edit(
let (output, mut events) = if input.create_or_overwrite {
edit_agent.overwrite(
buffer.clone(),
input.display_description.clone(),
&request,
cx,
)
} else {
edit_agent.overwrite(
edit_agent.edit(
buffer.clone(),
input.display_description.clone(),
&request,
@@ -290,10 +292,7 @@ impl Tool for EditFileTool {
}
} else {
Ok(ToolResultOutput {
content: ToolResultContent::Text(format!(
"Edited {}:\n\n```diff\n{}\n```",
input_path, diff
)),
content: format!("Edited {}:\n\n```diff\n{}\n```", input_path, diff),
output: serde_json::to_value(output).ok(),
})
}
@@ -332,72 +331,6 @@ impl Tool for EditFileTool {
}
}
/// Validate that the file path is valid, meaning:
///
/// - For `edit` and `overwrite`, the path must point to an existing file.
/// - For `create`, the file must not already exist, but it's parent dir must exist.
fn resolve_path(
input: &EditFileToolInput,
project: Entity<Project>,
cx: &mut App,
) -> Result<ProjectPath> {
let project = project.read(cx);
match input.mode {
EditFileMode::Edit | EditFileMode::Overwrite => {
let path = project
.find_project_path(&input.path, cx)
.ok_or_else(|| anyhow!("Can't edit file: path not found"))?;
let entry = project
.entry_for_path(&path, cx)
.ok_or_else(|| anyhow!("Can't edit file: path not found"))?;
if !entry.is_file() {
return Err(anyhow!("Can't edit file: path is a directory"));
}
Ok(path)
}
EditFileMode::Create => {
if let Some(path) = project.find_project_path(&input.path, cx) {
if project.entry_for_path(&path, cx).is_some() {
return Err(anyhow!("Can't create file: file already exists"));
}
}
let parent_path = input
.path
.parent()
.ok_or_else(|| anyhow!("Can't create file: incorrect path"))?;
let parent_project_path = project.find_project_path(&parent_path, cx);
let parent_entry = parent_project_path
.as_ref()
.and_then(|path| project.entry_for_path(&path, cx))
.ok_or_else(|| anyhow!("Can't create file: parent directory doesn't exist"))?;
if !parent_entry.is_dir() {
return Err(anyhow!("Can't create file: parent is not a directory"));
}
let file_name = input
.path
.file_name()
.ok_or_else(|| anyhow!("Can't create file: invalid filename"))?;
let new_file_path = parent_project_path.map(|parent| ProjectPath {
path: Arc::from(parent.path.join(file_name)),
..parent
});
new_file_path.ok_or_else(|| anyhow!("Can't create file"))
}
}
}
pub struct EditFileToolCard {
path: PathBuf,
editor: Entity<Editor>,
@@ -917,10 +850,7 @@ async fn build_buffer_diff(
#[cfg(test)]
mod tests {
use std::result::Result;
use super::*;
use client::TelemetrySettings;
use fs::FakeFs;
use gpui::TestAppContext;
use language_model::fake_provider::FakeLanguageModel;
@@ -942,7 +872,7 @@ mod tests {
let input = serde_json::to_value(EditFileToolInput {
display_description: "Some edit".into(),
path: "root/nonexistent_file.txt".into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
})
.unwrap();
Arc::new(EditFileTool)
@@ -960,102 +890,10 @@ mod tests {
.await;
assert_eq!(
result.unwrap_err().to_string(),
"Can't edit file: path not found"
"root/nonexistent_file.txt not found"
);
}
#[gpui::test]
async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) {
let mode = &EditFileMode::Create;
let result = test_resolve_path(mode, "root/new.txt", cx);
assert_resolved_path_eq(result.await, "new.txt");
let result = test_resolve_path(mode, "new.txt", cx);
assert_resolved_path_eq(result.await, "new.txt");
let result = test_resolve_path(mode, "dir/new.txt", cx);
assert_resolved_path_eq(result.await, "dir/new.txt");
let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't create file: file already exists"
);
let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't create file: parent directory doesn't exist"
);
}
#[gpui::test]
async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) {
let mode = &EditFileMode::Edit;
let path_with_root = "root/dir/subdir/existing.txt";
let path_without_root = "dir/subdir/existing.txt";
let result = test_resolve_path(mode, path_with_root, cx);
assert_resolved_path_eq(result.await, path_without_root);
let result = test_resolve_path(mode, path_without_root, cx);
assert_resolved_path_eq(result.await, path_without_root);
let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't edit file: path not found"
);
let result = test_resolve_path(mode, "root/dir", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't edit file: path is a directory"
);
}
async fn test_resolve_path(
mode: &EditFileMode,
path: &str,
cx: &mut TestAppContext,
) -> Result<ProjectPath, anyhow::Error> {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"dir": {
"subdir": {
"existing.txt": "hello"
}
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let input = EditFileToolInput {
display_description: "Some edit".into(),
path: path.into(),
mode: mode.clone(),
};
let result = cx.update(|cx| resolve_path(&input, project, cx));
result
}
fn assert_resolved_path_eq(path: Result<ProjectPath, anyhow::Error>, expected: &str) {
let actual = path
.expect("Should return valid path")
.path
.to_str()
.unwrap()
.replace("\\", "/"); // Naive Windows paths normalization
assert_eq!(actual, expected);
}
#[test]
fn still_streaming_ui_text_with_path() {
let input = json!({
@@ -1128,7 +966,6 @@ mod tests {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
TelemetrySettings::register(cx);
Project::init_settings(cx);
});
}

View File

@@ -1,8 +1,6 @@
use crate::{schema::json_schema_for, ui::ToolCallCardHeader};
use anyhow::{Result, anyhow};
use assistant_tool::{
ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use editor::Editor;
use futures::channel::oneshot::{self, Receiver};
use gpui::{
@@ -40,12 +38,6 @@ pub struct FindPathToolInput {
pub offset: usize,
}
#[derive(Debug, Serialize, Deserialize)]
struct FindPathToolOutput {
glob: String,
paths: Vec<PathBuf>,
}
const RESULTS_PER_PAGE: usize = 50;
pub struct FindPathTool;
@@ -119,18 +111,10 @@ impl Tool for FindPathTool {
)
.unwrap();
}
let output = FindPathToolOutput {
glob,
paths: matches.clone(),
};
for mat in matches.into_iter().skip(offset).take(RESULTS_PER_PAGE) {
write!(&mut message, "\n{}", mat.display()).unwrap();
}
Ok(ToolResultOutput {
content: ToolResultContent::Text(message),
output: Some(serde_json::to_value(output)?),
})
Ok(message.into())
}
});
@@ -139,18 +123,6 @@ impl Tool for FindPathTool {
card: Some(card.into()),
}
}
fn deserialize_card(
self: Arc<Self>,
output: serde_json::Value,
_project: Entity<Project>,
_window: &mut Window,
cx: &mut App,
) -> Option<assistant_tool::AnyToolCard> {
let output = serde_json::from_value::<FindPathToolOutput>(output).ok()?;
let card = cx.new(|_| FindPathToolCard::from_output(output));
Some(card.into())
}
}
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
@@ -208,15 +180,6 @@ impl FindPathToolCard {
_receiver_task: Some(_receiver_task),
}
}
fn from_output(output: FindPathToolOutput) -> Self {
Self {
glob: output.glob,
paths: output.paths,
expanded: false,
_receiver_task: None,
}
}
}
impl ToolCard for FindPathToolCard {

View File

@@ -752,9 +752,9 @@ mod tests {
match task.output.await {
Ok(result) => {
if cfg!(windows) {
result.content.as_str().unwrap().replace("root\\", "root/")
result.content.replace("root\\", "root/")
} else {
result.content.as_str().unwrap().to_string()
result.content
}
}
Err(e) => panic!("Failed to run grep tool: {}", e),

View File

@@ -1,17 +1,13 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::outline;
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{ToolResultContent, outline};
use gpui::{AnyWindowHandle, App, Entity, Task};
use project::{ImageItem, image_store};
use assistant_tool::ToolResultOutput;
use indoc::formatdoc;
use itertools::Itertools;
use language::{Anchor, Point};
use language_model::{
LanguageModel, LanguageModelImage, LanguageModelRequest, LanguageModelToolSchemaFormat,
};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::{AgentLocation, Project};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -90,7 +86,7 @@ impl Tool for ReadFileTool {
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
model: Arc<dyn LanguageModel>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -104,42 +100,6 @@ impl Tool for ReadFileTool {
};
let file_path = input.path.clone();
if image_store::is_image_file(&project, &project_path, cx) {
if !model.supports_images() {
return Task::ready(Err(anyhow!(
"Attempted to read an image, but Zed doesn't currently sending images to {}.",
model.name().0
)))
.into();
}
let task = cx.spawn(async move |cx| -> Result<ToolResultOutput> {
let image_entity: Entity<ImageItem> = cx
.update(|cx| {
project.update(cx, |project, cx| {
project.open_image(project_path.clone(), cx)
})
})?
.await?;
let image =
image_entity.read_with(cx, |image_item, _| Arc::clone(&image_item.image))?;
let language_model_image = cx
.update(|cx| LanguageModelImage::from_image(image, cx))?
.await
.ok_or_else(|| anyhow!("Failed to process image"))?;
Ok(ToolResultOutput {
content: ToolResultContent::Image(language_model_image),
output: None,
})
});
return task.into();
}
cx.spawn(async move |cx| {
let buffer = cx
.update(|cx| {
@@ -322,10 +282,7 @@ mod test {
.output
})
.await;
assert_eq!(
result.unwrap().content.as_str(),
Some("This is a small file content")
);
assert_eq!(result.unwrap().content, "This is a small file content");
}
#[gpui::test]
@@ -365,7 +322,6 @@ mod test {
})
.await;
let content = result.unwrap();
let content = content.as_str().unwrap();
assert_eq!(
content.lines().skip(4).take(6).collect::<Vec<_>>(),
vec![
@@ -409,8 +365,6 @@ mod test {
.collect::<Vec<_>>();
pretty_assertions::assert_eq!(
content
.as_str()
.unwrap()
.lines()
.skip(4)
.take(expected_content.len())
@@ -454,10 +408,7 @@ mod test {
.output
})
.await;
assert_eq!(
result.unwrap().content.as_str(),
Some("Line 2\nLine 3\nLine 4")
);
assert_eq!(result.unwrap().content, "Line 2\nLine 3\nLine 4");
}
#[gpui::test]
@@ -497,7 +448,7 @@ mod test {
.output
})
.await;
assert_eq!(result.unwrap().content.as_str(), Some("Line 1\nLine 2"));
assert_eq!(result.unwrap().content, "Line 1\nLine 2");
// end_line of 0 should result in at least 1 line
let result = cx
@@ -520,7 +471,7 @@ mod test {
.output
})
.await;
assert_eq!(result.unwrap().content.as_str(), Some("Line 1"));
assert_eq!(result.unwrap().content, "Line 1");
// when start_line > end_line, should still return at least 1 line
let result = cx
@@ -543,7 +494,7 @@ mod test {
.output
})
.await;
assert_eq!(result.unwrap().content.as_str(), Some("Line 3"));
assert_eq!(result.unwrap().content, "Line 3");
}
fn init_test(cx: &mut TestAppContext) {

View File

@@ -1,13 +1,12 @@
You are an expert engineer and your task is to write a new file from scratch.
You MUST respond directly with the file's content, without explanations, additional text or triple backticks.
The text you output will be saved verbatim as the content of the file.
Tool calls have been disabled. You MUST start your response directly with the file's new content.
<file_path>
<file_to_edit>
{{path}}
</file_path>
</file_to_edit>
<edit_description>
{{edit_description}}
</edit_description>
You MUST respond directly with the file's content, without explanations, additional text or triple backticks.
The text you output will be saved verbatim as the content of the file.

View File

@@ -27,57 +27,20 @@ NEW TEXT 3 HERE
</edits>
```
# File Editing Instructions
- Use `<old_text>` and `<new_text>` tags to replace content
- `<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
- Be minimal with replacements:
- For unique lines, include only those lines
- For non-unique lines, include enough context to identify them
- Do not escape quotes, newlines, or other characters within tags
- For multiple occurrences, repeat the same tag pair for each instance
- Edits are sequential - each assumes previous edits are already applied
- Only edit the specified file
- Always close all tags properly
{{!-- This example is important for Gemini 2.5 --}}
<example>
<edits>
<old_text>
struct User {
name: String,
email: String,
}
</old_text>
<new_text>
struct User {
name: String,
email: String,
active: bool,
}
</new_text>
<old_text>
let user = User {
name: String::from("John"),
email: String::from("john@example.com"),
};
</old_text>
<new_text>
let user = User {
name: String::from("John"),
email: String::from("john@example.com"),
active: true,
};
</new_text>
</edits>
</example>
Rules for editing:
- `old_text` represents lines in the input file that will be replaced with `new_text`.
- `old_text` MUST exactly match the existing file content, character for character, including indentation.
- `old_text` MUST NEVER come from the outline, but from actual lines in the file.
- Strive to be minimal in the lines you replace in `old_text`:
- If the lines you want to replace are unique, you MUST include just those in the `old_text`.
- If the lines you want to replace are NOT unique, you MUST include enough context around them in `old_text` to distinguish them from other lines.
- If you want to replace many occurrences of the same text, repeat the same `old_text`/`new_text` pair multiple times and I will apply them sequentially, one occurrence at a time.
- When reporting multiple edits, each edit assumes the previous one has already been applied! Therefore, you must ensure `old_text` doesn't reference text that has already been modified by a previous edit.
- Don't explain the edits, just report them.
- Only edit the file specified in `<file_to_edit>` and NEVER include edits to other files!
- If you open an <old_text> tag, you MUST close it using </old_text>
- If you open an <new_text> tag, you MUST close it using </new_text>
<file_to_edit>
{{path}}

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{FutureExt as _, future::Shared};
use gpui::{
@@ -125,24 +125,14 @@ impl Tool for TerminalTool {
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let working_dir = match working_dir(&input, &project, cx) {
let input_path = Path::new(&input.cd);
let working_dir = match working_dir(&input, &project, input_path, cx) {
Ok(dir) => dir,
Err(err) => return Task::ready(Err(err)).into(),
};
let program = self.determine_shell.clone();
let command = if cfg!(windows) {
format!("$null | & {{{}}}", input.command.replace("\"", "'"))
} else if let Some(cwd) = working_dir
.as_ref()
.and_then(|cwd| cwd.as_os_str().to_str())
{
// Make sure once we're *inside* the shell, we cd into `cwd`
format!("(cd {cwd}; {}) </dev/null", input.command)
} else {
format!("({}) </dev/null", input.command)
};
let args = vec!["-c".into(), command];
let command = format!("({}) </dev/null", input.command);
let args = vec!["-c".into(), command.clone()];
let cwd = working_dir.clone();
let env = match &working_dir {
Some(dir) => project.update(cx, |project, cx| {
@@ -325,13 +315,19 @@ fn process_content(
} else {
content
};
let content = content.trim();
let is_empty = content.is_empty();
let content = format!("```\n{content}\n```");
let is_empty = content.trim().is_empty();
let content = format!(
"```\n{}{}```",
content,
if content.ends_with('\n') { "" } else { "\n" }
);
let content = if should_truncate {
format!(
"Command output too long. The first {} bytes:\n\n{content}",
"Command output too long. The first {} bytes:\n\n{}",
content.len(),
content,
)
} else {
content
@@ -371,47 +367,42 @@ fn process_content(
fn working_dir(
input: &TerminalToolInput,
project: &Entity<Project>,
input_path: &Path,
cx: &mut App,
) -> Result<Option<PathBuf>> {
let project = project.read(cx);
let cd = &input.cd;
if cd == "." || cd == "" {
// Accept "." or "" as meaning "the one worktree" if we only have one worktree.
if input.cd == "." {
// Accept "." as meaning "the one worktree" if we only have one worktree.
let mut worktrees = project.worktrees(cx);
match worktrees.next() {
Some(worktree) => {
if worktrees.next().is_none() {
Ok(Some(worktree.read(cx).abs_path().to_path_buf()))
} else {
Err(anyhow!(
if worktrees.next().is_some() {
bail!(
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.",
))
);
}
Ok(Some(worktree.read(cx).abs_path().to_path_buf()))
}
None => Ok(None),
}
} else {
let input_path = Path::new(cd);
if input_path.is_absolute() {
// Absolute paths are allowed, but only if they're in one of the project's worktrees.
if project
.worktrees(cx)
.any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))
{
return Ok(Some(input_path.into()));
}
} else {
if let Some(worktree) = project.worktree_for_root_name(cd, cx) {
return Ok(Some(worktree.read(cx).abs_path().to_path_buf()));
}
} else if input_path.is_absolute() {
// Absolute paths are allowed, but only if they're in one of the project's worktrees.
if !project
.worktrees(cx)
.any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))
{
bail!("The absolute path must be within one of the project's worktrees");
}
Err(anyhow!(
"`cd` directory {cd:?} was not in any of the project's worktrees."
))
Ok(Some(input_path.into()))
} else {
let Some(worktree) = project.worktree_for_root_name(&input.cd, cx) else {
bail!("`cd` directory {:?} not found in the project", input.cd);
};
Ok(Some(worktree.read(cx).abs_path().to_path_buf()))
}
}
@@ -732,8 +723,8 @@ mod tests {
)
});
let output = result.output.await.log_err().unwrap().content;
assert_eq!(output.as_str().unwrap(), "Command executed successfully.");
let output = result.output.await.log_err().map(|output| output.content);
assert_eq!(output, Some("Command executed successfully.".into()));
}
#[gpui::test]
@@ -766,13 +757,12 @@ mod tests {
cx,
);
cx.spawn(async move |_| {
let output = headless_result.output.await.map(|output| output.content);
assert_eq!(
output
.ok()
.and_then(|content| content.as_str().map(ToString::to_string)),
expected
);
let output = headless_result
.output
.await
.log_err()
.map(|output| output.content);
assert_eq!(output, expected);
})
};
@@ -780,7 +770,7 @@ mod tests {
check(
TerminalToolInput {
command: "pwd".into(),
cd: ".".into(),
cd: "project".into(),
},
Some(format!(
"```\n{}\n```",
@@ -795,9 +785,12 @@ mod tests {
check(
TerminalToolInput {
command: "pwd".into(),
cd: "other-project".into(),
cd: ".".into(),
},
None, // other-project is a dir, but *not* a worktree (yet)
Some(format!(
"```\n{}\n```",
tree.path().join("project").display()
)),
cx,
)
})

View File

@@ -3,9 +3,7 @@ use std::{sync::Arc, time::Duration};
use crate::schema::json_schema_for;
use crate::ui::ToolCallCardHeader;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{
ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{Future, FutureExt, TryFutureExt};
use gpui::{
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
@@ -75,13 +73,9 @@ impl Tool for WebSearchTool {
let search_task = search_task.clone();
async move {
let response = search_task.await.map_err(|err| anyhow!(err))?;
Ok(ToolResultOutput {
content: ToolResultContent::Text(
serde_json::to_string(&response)
.context("Failed to serialize search results")?,
),
output: Some(serde_json::to_value(response)?),
})
serde_json::to_string(&response)
.context("Failed to serialize search results")
.map(Into::into)
}
});
@@ -90,18 +84,6 @@ impl Tool for WebSearchTool {
card: Some(cx.new(|cx| WebSearchToolCard::new(search_task, cx)).into()),
}
}
fn deserialize_card(
self: Arc<Self>,
output: serde_json::Value,
_project: Entity<Project>,
_window: &mut Window,
cx: &mut App,
) -> Option<assistant_tool::AnyToolCard> {
let output = serde_json::from_value::<WebSearchResponse>(output).ok()?;
let card = cx.new(|cx| WebSearchToolCard::new(Task::ready(Ok(output)), cx));
Some(card.into())
}
}
#[derive(RegisterComponent)]

View File

@@ -16,20 +16,6 @@ pub enum BedrockModelMode {
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
// Anthropic models (already included)
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
ClaudeSonnet4,
#[serde(
rename = "claude-sonnet-4-thinking",
alias = "claude-sonnet-4-thinking-latest"
)]
ClaudeSonnet4Thinking,
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
ClaudeOpus4,
#[serde(
rename = "claude-opus-4-thinking",
alias = "claude-opus-4-thinking-latest"
)]
ClaudeOpus4Thinking,
#[default]
#[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")]
Claude3_5SonnetV2,
@@ -127,12 +113,6 @@ impl Model {
pub fn id(&self) -> &str {
match self {
Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => {
"anthropic.claude-sonnet-4-20250514-v1:0"
}
Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => {
"anthropic.claude-opus-4-20250514-v1:0"
}
Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0",
Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0",
Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0",
@@ -184,10 +164,6 @@ impl Model {
pub fn display_name(&self) -> &str {
match self {
Self::ClaudeSonnet4 => "Claude Sonnet 4",
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
Self::ClaudeOpus4 => "Claude Opus 4",
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3Opus => "Claude 3 Opus",
@@ -244,9 +220,7 @@ impl Model {
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
| Self::ClaudeSonnet4
| Self::ClaudeOpus4 => 200_000,
| Self::Claude3_7Sonnet => 200_000,
Self::AmazonNovaPremier => 1_000_000,
Self::PalmyraWriterX5 => 1_000_000,
Self::PalmyraWriterX4 => 128_000,
@@ -258,12 +232,7 @@ impl Model {
pub fn max_output_tokens(&self) -> u32 {
match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeOpus4
| Model::ClaudeOpus4Thinking => 128_000,
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192,
Self::Custom {
max_output_tokens, ..
@@ -278,11 +247,7 @@ impl Model {
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking => 1.0,
| Self::Claude3_7Sonnet => 1.0,
Self::Custom {
default_temperature,
..
@@ -300,10 +265,6 @@ impl Model {
| Self::Claude3_5SonnetV2
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Haiku => true,
// Amazon Nova models (all support tool use)
@@ -329,12 +290,6 @@ impl Model {
Model::Claude3_7SonnetThinking => BedrockModelMode::Thinking {
budget_tokens: Some(4096),
},
Model::ClaudeSonnet4Thinking => BedrockModelMode::Thinking {
budget_tokens: Some(4096),
},
Model::ClaudeOpus4Thinking => BedrockModelMode::Thinking {
budget_tokens: Some(4096),
},
_ => BedrockModelMode::Default,
}
}
@@ -371,10 +326,6 @@ impl Model {
(Model::Claude3Opus, "us")
| (Model::Claude3_5Haiku, "us")
| (Model::Claude3_7Sonnet, "us")
| (Model::ClaudeSonnet4, "us")
| (Model::ClaudeOpus4, "us")
| (Model::ClaudeSonnet4Thinking, "us")
| (Model::ClaudeOpus4Thinking, "us")
| (Model::Claude3_7SonnetThinking, "us")
| (Model::AmazonNovaPremier, "us")
| (Model::MistralPixtralLarge2502V1, "us") => {

View File

@@ -12,7 +12,6 @@ pub struct CallSettings {
/// Configuration of voice calls in Zed.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct CallSettingsContent {
/// Whether the microphone should be muted when joining a channel or a call.
///

View File

@@ -2517,7 +2517,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2526,7 +2526,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
@@ -2550,7 +2550,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2559,7 +2559,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
@@ -2583,7 +2583,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2592,7 +2592,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
@@ -2616,7 +2616,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2625,7 +2625,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});

View File

@@ -378,27 +378,16 @@ impl CollabPanel {
workspace: WeakEntity<Workspace>,
mut cx: AsyncWindowContext,
) -> anyhow::Result<Entity<Self>> {
let serialized_panel = match workspace
.read_with(&cx, |workspace, _| {
CollabPanel::serialization_key(workspace)
})
.ok()
let serialized_panel = cx
.background_spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
.await
.map_err(|_| anyhow::anyhow!("Failed to read collaboration panel from key value store"))
.log_err()
.flatten()
{
Some(serialization_key) => cx
.background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) })
.await
.map_err(|_| {
anyhow::anyhow!("Failed to read collaboration panel from key value store")
})
.log_err()
.flatten()
.map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))
.transpose()
.log_err()
.flatten(),
None => None,
};
.map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))
.transpose()
.log_err()
.flatten();
workspace.update_in(&mut cx, |workspace, window, cx| {
let panel = CollabPanel::new(workspace, window, cx);
@@ -418,30 +407,14 @@ impl CollabPanel {
})
}
fn serialization_key(workspace: &Workspace) -> Option<String> {
workspace
.database_id()
.map(|id| i64::from(id).to_string())
.or(workspace.session_id())
.map(|id| format!("{}-{:?}", COLLABORATION_PANEL_KEY, id))
}
fn serialize(&mut self, cx: &mut Context<Self>) {
let Some(serialization_key) = self
.workspace
.update(cx, |workspace, _| CollabPanel::serialization_key(workspace))
.ok()
.flatten()
else {
return;
};
let width = self.width;
let collapsed_channels = self.collapsed_channels.clone();
self.pending_serialization = cx.background_spawn(
async move {
KEY_VALUE_STORE
.write_kvp(
serialization_key,
COLLABORATION_PANEL_KEY.into(),
serde_json::to_string(&SerializedCollabPanel {
width,
collapsed_channels: Some(
@@ -3026,12 +2999,10 @@ impl Panel for CollabPanel {
.unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width)
}
fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
self.width = size;
self.serialize(cx);
cx.notify();
cx.defer_in(window, |this, _, cx| {
this.serialize(cx);
});
}
fn icon(&self, _window: &Window, cx: &App) -> Option<ui::IconName> {

View File

@@ -28,7 +28,6 @@ pub struct ChatPanelSettings {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct ChatPanelSettingsContent {
/// When to show the panel button in the status bar.
///
@@ -52,7 +51,6 @@ pub struct NotificationPanelSettings {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct PanelSettingsContent {
/// Whether to show the panel button in the status bar.
///
@@ -69,7 +67,6 @@ pub struct PanelSettingsContent {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct MessageEditorSettings {
/// Whether to automatically replace emoji shortcodes with emoji characters.
/// For example: typing `:wave:` gets replaced with `👋`.

View File

@@ -113,7 +113,7 @@ pub enum ModelVendor {
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
#[serde(tag = "type")]
pub enum ChatMessagePart {
pub enum ChatMessageContent {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image_url")]
@@ -194,55 +194,26 @@ pub enum ToolChoice {
None,
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(tag = "role", rename_all = "lowercase")]
pub enum ChatMessage {
Assistant {
content: ChatMessageContent,
content: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
tool_calls: Vec<ToolCall>,
},
User {
content: ChatMessageContent,
content: Vec<ChatMessageContent>,
},
System {
content: String,
},
Tool {
content: ChatMessageContent,
content: String,
tool_call_id: String,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ChatMessageContent {
Plain(String),
Multipart(Vec<ChatMessagePart>),
}
impl ChatMessageContent {
pub fn empty() -> Self {
ChatMessageContent::Multipart(vec![])
}
}
impl From<Vec<ChatMessagePart>> for ChatMessageContent {
fn from(mut parts: Vec<ChatMessagePart>) -> Self {
if let [ChatMessagePart::Text { text }] = parts.as_mut_slice() {
ChatMessageContent::Plain(std::mem::take(text))
} else {
ChatMessageContent::Multipart(parts)
}
}
}
impl From<String> for ChatMessageContent {
fn from(text: String) -> Self {
ChatMessageContent::Plain(text)
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct ToolCall {
pub id: String,
@@ -266,6 +237,7 @@ pub struct FunctionContent {
#[serde(tag = "type", rename_all = "snake_case")]
pub struct ResponseEvent {
pub choices: Vec<ResponseChoice>,
pub created: u64,
pub id: String,
}
@@ -578,15 +550,6 @@ async fn stream_completion(
api_key: String,
request: Request,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let is_vision_request = request.messages.last().map_or(false, |message| match message {
ChatMessage::User { content }
| ChatMessage::Assistant { content, .. }
| ChatMessage::Tool { content, .. } => {
matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
}
_ => false,
});
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(COPILOT_CHAT_COMPLETION_URL)
@@ -600,7 +563,7 @@ async fn stream_completion(
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.header("Copilot-Integration-Id", "vscode-chat")
.header("Copilot-Vision-Request", is_vision_request.to_string());
.header("Copilot-Vision-Request", "true");
let is_streaming = request.stream;

View File

@@ -4,11 +4,11 @@ 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 dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest};
use futures::io::BufReader;
use gpui::{AsyncApp, SharedString};
pub use http_client::{HttpClient, github::latest_github_release};
use language::{LanguageName, LanguageToolchainStore};
use language::LanguageToolchainStore;
use node_runtime::NodeRuntime;
use serde::{Deserialize, Serialize};
use settings::WorktreeId;
@@ -32,17 +32,15 @@ pub enum DapStatus {
Failed { error: String },
}
#[async_trait]
pub trait DapDelegate: Send + Sync + 'static {
#[async_trait(?Send)]
pub trait DapDelegate {
fn worktree_id(&self) -> WorktreeId;
fn worktree_root_path(&self) -> &Path;
fn http_client(&self) -> Arc<dyn HttpClient>;
fn node_runtime(&self) -> NodeRuntime;
fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>;
fn fs(&self) -> Arc<dyn Fs>;
fn output_to_console(&self, msg: String);
async fn which(&self, command: &OsStr) -> Option<PathBuf>;
async fn read_text_file(&self, path: PathBuf) -> Result<String>;
fn which(&self, command: &OsStr) -> Option<PathBuf>;
async fn shell_env(&self) -> collections::HashMap<String, String>;
}
@@ -415,16 +413,11 @@ pub trait DebugAdapter: 'static + Send + Sync {
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary>;
/// Returns the language name of an adapter if it only supports one language
fn adapter_language_name(&self) -> Option<LanguageName> {
None
}
}
#[cfg(any(test, feature = "test-support"))]
@@ -474,7 +467,7 @@ impl DebugAdapter for FakeAdapter {
async fn get_binary(
&self,
_: &Arc<dyn DapDelegate>,
_: &dyn DapDelegate,
config: &DebugTaskDefinition,
_: Option<PathBuf>,
_: &mut AsyncApp,

View File

@@ -7,14 +7,21 @@ use dap_types::{
messages::{Message, Response},
requests::Request,
};
use futures::channel::oneshot;
use gpui::{AppContext, AsyncApp};
use futures::{FutureExt as _, channel::oneshot, select};
use gpui::{AppContext, AsyncApp, BackgroundExecutor};
use smol::channel::{Receiver, Sender};
use std::{
hash::Hash,
sync::atomic::{AtomicU64, Ordering},
time::Duration,
};
#[cfg(any(test, feature = "test-support"))]
const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(2);
#[cfg(not(any(test, feature = "test-support")))]
const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(12);
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct SessionId(pub u32);
@@ -34,6 +41,7 @@ pub struct DebugAdapterClient {
id: SessionId,
sequence_count: AtomicU64,
binary: DebugAdapterBinary,
executor: BackgroundExecutor,
transport_delegate: TransportDelegate,
}
@@ -53,6 +61,7 @@ impl DebugAdapterClient {
binary,
transport_delegate,
sequence_count: AtomicU64::new(1),
executor: cx.background_executor().clone(),
};
log::info!("Successfully connected to debug adapter");
@@ -164,33 +173,40 @@ impl DebugAdapterClient {
self.send_message(Message::Request(request)).await?;
let mut timeout = self.executor.timer(DAP_REQUEST_TIMEOUT).fuse();
let command = R::COMMAND.to_string();
let response = callback_rx.await??;
log::debug!(
"Client {} received response for: `{}` sequence_id: {}",
self.id.0,
command,
sequence_id
);
match response.success {
true => {
if let Some(json) = response.body {
Ok(serde_json::from_value(json)?)
// Note: dap types configure themselves to return `None` when an empty object is received,
// which then fails here...
} else if let Ok(result) =
serde_json::from_value(serde_json::Value::Object(Default::default()))
{
Ok(result)
} else {
Ok(serde_json::from_value(Default::default())?)
select! {
response = callback_rx.fuse() => {
log::debug!(
"Client {} received response for: `{}` sequence_id: {}",
self.id.0,
command,
sequence_id
);
let response = response??;
match response.success {
true => {
if let Some(json) = response.body {
Ok(serde_json::from_value(json)?)
// Note: dap types configure themselves to return `None` when an empty object is received,
// which then fails here...
} else if let Ok(result) = serde_json::from_value(serde_json::Value::Object(Default::default())) {
Ok(result)
} else {
Ok(serde_json::from_value(Default::default())?)
}
}
false => Err(anyhow!("Request failed: {}", response.message.unwrap_or_default())),
}
}
false => Err(anyhow!(
"Request failed: {}",
response.message.unwrap_or_default()
)),
_ = timeout => {
self.transport_delegate.cancel_pending_request(&sequence_id).await;
log::error!("Cancelled DAP request for {command:?} id {sequence_id} which took over {DAP_REQUEST_TIMEOUT:?}");
anyhow::bail!("DAP request timeout");
}
}
}

View File

@@ -6,8 +6,6 @@ pub mod proto_conversions;
mod registry;
pub mod transport;
use std::net::Ipv4Addr;
pub use dap_types::*;
pub use registry::{DapLocator, DapRegistry};
pub use task::DebugRequest;
@@ -18,19 +16,3 @@ pub type StackFrameId = u64;
#[cfg(any(test, feature = "test-support"))]
pub use adapters::FakeAdapter;
use task::TcpArgumentsTemplate;
pub async fn configure_tcp_connection(
tcp_connection: TcpArgumentsTemplate,
) -> anyhow::Result<(Ipv4Addr, u16, Option<u64>)> {
let host = tcp_connection.host();
let timeout = tcp_connection.timeout;
let port = if let Some(port) = tcp_connection.port {
port
} else {
transport::TcpTransport::port(&tcp_connection).await?
};
Ok((host, port, timeout))
}

View File

@@ -29,7 +29,7 @@ pub struct InlineValueLocation {
/// during debugging sessions. Implementors must also handle variable scoping
/// themselves by traversing the syntax tree upwards to determine whether a
/// variable is local or global.
pub trait InlineValueProvider: 'static + Send + Sync {
pub trait InlineValueProvider {
/// Provides a list of inline value locations based on the given node and source code.
///
/// # Parameters

View File

@@ -2,7 +2,6 @@ use anyhow::Result;
use async_trait::async_trait;
use collections::FxHashMap;
use gpui::{App, Global, SharedString};
use language::LanguageName;
use parking_lot::RwLock;
use task::{DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate};
@@ -54,11 +53,10 @@ impl DapRegistry {
pub fn add_adapter(&self, adapter: Arc<dyn DebugAdapter>) {
let name = adapter.name();
let _previous_value = self.0.write().adapters.insert(name, adapter);
}
pub fn adapter_language(&self, adapter_name: &str) -> Option<LanguageName> {
self.adapter(adapter_name)
.and_then(|adapter| adapter.adapter_language_name())
debug_assert!(
_previous_value.is_none(),
"Attempted to insert a new debug adapter when one is already registered"
);
}
pub fn add_locator(&self, locator: Arc<dyn DapLocator>) {

View File

@@ -224,6 +224,11 @@ impl TransportDelegate {
pending_requests.insert(sequence_id, request);
}
pub(crate) async fn cancel_pending_request(&self, sequence_id: &u64) {
let mut pending_requests = self.pending_requests.lock().await;
pending_requests.remove(sequence_id);
}
pub(crate) async fn send_message(&self, message: Message) -> Result<()> {
if let Some(server_tx) = self.server_tx.lock().await.as_ref() {
server_tx

View File

@@ -61,7 +61,7 @@ impl CodeLldbDebugAdapter {
async fn fetch_latest_adapter_version(
&self,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
let release =
latest_github_release("vadimcn/codelldb", true, false, delegate.http_client()).await?;
@@ -111,7 +111,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
@@ -129,7 +129,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
self.name(),
version.clone(),
adapters::DownloadedFileType::Vsix,
delegate.as_ref(),
delegate,
)
.await?;
let version_path =

View File

@@ -6,7 +6,7 @@ mod php;
mod python;
mod ruby;
use std::sync::Arc;
use std::{net::Ipv4Addr, sync::Arc};
use anyhow::{Result, anyhow};
use async_trait::async_trait;
@@ -17,7 +17,6 @@ use dap::{
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
GithubRepo,
},
configure_tcp_connection,
inline_value::{PythonInlineValueProvider, RustInlineValueProvider},
};
use gdb::GdbDebugAdapter;
@@ -28,6 +27,7 @@ use php::PhpDebugAdapter;
use python::PythonDebugAdapter;
use ruby::RubyDebugAdapter;
use serde_json::{Value, json};
use task::TcpArgumentsTemplate;
pub fn init(cx: &mut App) {
cx.update_default_global(|registry: &mut DapRegistry, _cx| {
@@ -45,6 +45,21 @@ pub fn init(cx: &mut App) {
})
}
pub(crate) async fn configure_tcp_connection(
tcp_connection: TcpArgumentsTemplate,
) -> Result<(Ipv4Addr, u16, Option<u64>)> {
let host = tcp_connection.host();
let timeout = tcp_connection.timeout;
let port = if let Some(port) = tcp_connection.port {
port
} else {
dap::transport::TcpTransport::port(&tcp_connection).await?
};
Ok((host, port, timeout))
}
trait ToDap {
fn to_dap(&self) -> dap::StartDebuggingRequestArgumentsRequest;
}

View File

@@ -65,7 +65,7 @@ impl DebugAdapter for GdbDebugAdapter {
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
user_installed_path: Option<std::path::PathBuf>,
_: &mut AsyncApp,
@@ -76,7 +76,6 @@ impl DebugAdapter for GdbDebugAdapter {
let gdb_path = delegate
.which(OsStr::new("gdb"))
.await
.and_then(|p| p.to_str().map(|s| s.to_string()))
.ok_or(anyhow!("Could not find gdb in path"));

View File

@@ -1,6 +1,5 @@
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use gpui::AsyncApp;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
use crate::*;
@@ -44,20 +43,15 @@ impl DebugAdapter for GoDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("Go").into())
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
_user_installed_path: Option<PathBuf>,
_cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let delve_path = delegate
.which(OsStr::new("dlv"))
.await
.and_then(|p| p.to_str().map(|p| p.to_string()))
.ok_or(anyhow!("Dlv not found in path"))?;

View File

@@ -56,7 +56,7 @@ impl JsDebugAdapter {
async fn fetch_latest_adapter_version(
&self,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
let release = latest_github_release(
&format!("{}/{}", "microsoft", Self::ADAPTER_NPM_NAME),
@@ -82,7 +82,7 @@ impl JsDebugAdapter {
async fn get_installed_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
@@ -139,7 +139,7 @@ impl DebugAdapter for JsDebugAdapter {
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
@@ -151,7 +151,7 @@ impl DebugAdapter for JsDebugAdapter {
self.name(),
version,
adapters::DownloadedFileType::GzipTar,
delegate.as_ref(),
delegate,
)
.await?;
}

View File

@@ -1,7 +1,6 @@
use adapters::latest_github_release;
use dap::adapters::{DebugTaskDefinition, TcpArguments};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use gpui::AsyncApp;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use util::ResultExt;
@@ -40,7 +39,7 @@ impl PhpDebugAdapter {
async fn fetch_latest_adapter_version(
&self,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
let release = latest_github_release(
&format!("{}/{}", "xdebug", Self::ADAPTER_PACKAGE_NAME),
@@ -66,7 +65,7 @@ impl PhpDebugAdapter {
async fn get_installed_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
@@ -120,13 +119,9 @@ impl DebugAdapter for PhpDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("PHP").into())
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
@@ -138,7 +133,7 @@ impl DebugAdapter for PhpDebugAdapter {
self.name(),
version,
adapters::DownloadedFileType::Vsix,
delegate.as_ref(),
delegate,
)
.await?;
}

View File

@@ -1,7 +1,6 @@
use crate::*;
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use gpui::AsyncApp;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock};
use util::ResultExt;
@@ -52,26 +51,26 @@ impl PythonDebugAdapter {
}
async fn fetch_latest_adapter_version(
&self,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
) -> Result<AdapterVersion> {
let github_repo = GithubRepo {
repo_name: Self::ADAPTER_PACKAGE_NAME.into(),
repo_owner: "microsoft".into(),
};
adapters::fetch_latest_adapter_version_from_github(github_repo, delegate.as_ref()).await
adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await
}
async fn install_binary(
&self,
version: AdapterVersion,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
) -> Result<()> {
let version_path = adapters::download_adapter_from_github(
self.name(),
version,
adapters::DownloadedFileType::Zip,
delegate.as_ref(),
delegate,
)
.await?;
@@ -93,7 +92,7 @@ impl PythonDebugAdapter {
async fn get_installed_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
@@ -128,18 +127,14 @@ impl PythonDebugAdapter {
let python_path = if let Some(toolchain) = toolchain {
Some(toolchain.path.to_string())
} else {
let mut name = None;
for cmd in BINARY_NAMES {
name = delegate
.which(OsStr::new(cmd))
.await
.map(|path| path.to_string_lossy().to_string());
if name.is_some() {
break;
}
}
name
BINARY_NAMES
.iter()
.filter_map(|cmd| {
delegate
.which(OsStr::new(cmd))
.map(|path| path.to_string_lossy().to_string())
})
.find(|_| true)
};
Ok(DebugAdapterBinary {
@@ -170,13 +165,9 @@ impl DebugAdapter for PythonDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("Python").into())
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,

View File

@@ -6,9 +6,8 @@ use dap::{
self, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
},
};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{path::PathBuf, sync::Arc};
use gpui::AsyncApp;
use std::path::PathBuf;
use util::command::new_smol_command;
use crate::ToDap;
@@ -26,13 +25,9 @@ impl DebugAdapter for RubyDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("Ruby").into())
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
delegate: &dyn DapDelegate,
definition: &DebugTaskDefinition,
_user_installed_path: Option<PathBuf>,
_cx: &mut AsyncApp,
@@ -40,7 +35,7 @@ impl DebugAdapter for RubyDebugAdapter {
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
let mut rdbg_path = adapter_path.join("rdbg");
if !delegate.fs().is_file(&rdbg_path).await {
match delegate.which("rdbg".as_ref()).await {
match delegate.which("rdbg".as_ref()) {
Some(path) => rdbg_path = path,
None => {
delegate.output_to_console(
@@ -76,7 +71,7 @@ impl DebugAdapter for RubyDebugAdapter {
format!("--port={}", port),
format!("--host={}", host),
];
if delegate.which(launch.program.as_ref()).await.is_some() {
if delegate.which(launch.program.as_ref()).is_some() {
arguments.push("--command".to_string())
}
arguments.push(launch.program);

View File

@@ -1,8 +1,6 @@
use gpui::App;
use sqlez_macros::sql;
use util::ResultExt as _;
use crate::{define_connection, query, write_and_log};
use crate::{define_connection, query};
define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
&[sql!(
@@ -13,29 +11,6 @@ define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
)];
);
pub trait Dismissable {
const KEY: &'static str;
fn dismissed() -> bool {
KEY_VALUE_STORE
.read_kvp(Self::KEY)
.log_err()
.map_or(false, |s| s.is_some())
}
fn set_dismissed(is_dismissed: bool, cx: &mut App) {
write_and_log(cx, move || async move {
if is_dismissed {
KEY_VALUE_STORE
.write_kvp(Self::KEY.into(), "1".into())
.await
} else {
KEY_VALUE_STORE.delete_kvp(Self::KEY.into()).await
}
})
}
}
impl KeyValueStore {
query! {
pub fn read_kvp(key: &str) -> Result<Option<String>> {

View File

@@ -1,20 +0,0 @@
[package]
name = "debug_adapter_extension"
version = "0.1.0"
license = "GPL-3.0-or-later"
publish.workspace = true
edition.workspace = true
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
dap.workspace = true
extension.workspace = true
gpui.workspace = true
workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" }
[lints]
workspace = true
[lib]
path = "src/debug_adapter_extension.rs"

View File

@@ -1 +0,0 @@
../../LICENSE-GPL

View File

@@ -1,40 +0,0 @@
mod extension_dap_adapter;
use std::sync::Arc;
use dap::DapRegistry;
use extension::{ExtensionDebugAdapterProviderProxy, ExtensionHostProxy};
use extension_dap_adapter::ExtensionDapAdapter;
use gpui::App;
pub fn init(extension_host_proxy: Arc<ExtensionHostProxy>, cx: &mut App) {
let language_server_registry_proxy = DebugAdapterRegistryProxy::new(cx);
extension_host_proxy.register_debug_adapter_proxy(language_server_registry_proxy);
}
#[derive(Clone)]
struct DebugAdapterRegistryProxy {
debug_adapter_registry: DapRegistry,
}
impl DebugAdapterRegistryProxy {
fn new(cx: &mut App) -> Self {
Self {
debug_adapter_registry: DapRegistry::global(cx).clone(),
}
}
}
impl ExtensionDebugAdapterProviderProxy for DebugAdapterRegistryProxy {
fn register_debug_adapter(
&self,
extension: Arc<dyn extension::Extension>,
debug_adapter_name: Arc<str>,
) {
self.debug_adapter_registry
.add_adapter(Arc::new(ExtensionDapAdapter::new(
extension,
debug_adapter_name,
)));
}
}

View File

@@ -1,79 +0,0 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::Result;
use async_trait::async_trait;
use dap::adapters::{
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
};
use extension::{Extension, WorktreeDelegate};
use gpui::AsyncApp;
pub(crate) struct ExtensionDapAdapter {
extension: Arc<dyn Extension>,
debug_adapter_name: Arc<str>,
}
impl ExtensionDapAdapter {
pub(crate) fn new(
extension: Arc<dyn extension::Extension>,
debug_adapter_name: Arc<str>,
) -> Self {
Self {
extension,
debug_adapter_name,
}
}
}
/// An adapter that allows an [`dap::adapters::DapDelegate`] to be used as a [`WorktreeDelegate`].
struct WorktreeDelegateAdapter(pub Arc<dyn DapDelegate>);
#[async_trait]
impl WorktreeDelegate for WorktreeDelegateAdapter {
fn id(&self) -> u64 {
self.0.worktree_id().to_proto()
}
fn root_path(&self) -> String {
self.0.worktree_root_path().to_string_lossy().to_string()
}
async fn read_text_file(&self, path: PathBuf) -> Result<String> {
self.0.read_text_file(path).await
}
async fn which(&self, binary_name: String) -> Option<String> {
self.0
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string())
}
async fn shell_env(&self) -> Vec<(String, String)> {
self.0.shell_env().await.into_iter().collect()
}
}
#[async_trait(?Send)]
impl DebugAdapter for ExtensionDapAdapter {
fn name(&self) -> DebugAdapterName {
self.debug_adapter_name.as_ref().into()
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
_cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
self.extension
.get_dap_binary(
self.debug_adapter_name.clone(),
config.clone(),
user_installed_path,
Arc::new(WorktreeDelegateAdapter(delegate.clone())),
)
.await
}
}

View File

@@ -684,7 +684,7 @@ impl Render for DapLogView {
}
}
actions!(dev, [OpenDebugAdapterLogs]);
actions!(debug, [OpenDebuggerAdapterLogs]);
pub fn init(cx: &mut App) {
let log_store = cx.new(|cx| LogStore::new(cx));
@@ -702,7 +702,7 @@ pub fn init(cx: &mut App) {
}
let log_store = log_store.clone();
workspace.register_action(move |workspace, _: &OpenDebugAdapterLogs, window, cx| {
workspace.register_action(move |workspace, _: &OpenDebuggerAdapterLogs, window, cx| {
let project = workspace.project().read(cx);
if project.is_local() {
workspace.add_item_to_active_pane(

View File

@@ -36,7 +36,6 @@ 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
gpui.workspace = true

View File

@@ -1,11 +1,9 @@
use crate::persistence::DebuggerPaneItem;
use crate::session::DebugSession;
use crate::session::running::RunningState;
use crate::{
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
persistence,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart, StepBack,
StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, persistence,
};
use anyhow::{Result, anyhow};
use command_palette_hooks::CommandPaletteFilter;
@@ -31,7 +29,7 @@ use settings::Settings;
use std::any::TypeId;
use std::sync::Arc;
use task::{DebugScenario, TaskContext};
use ui::{ContextMenu, Divider, Tooltip, prelude::*};
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
use workspace::SplitDirection;
use workspace::{
Pane, Workspace,
@@ -76,13 +74,12 @@ impl DebugPanel {
) -> Entity<Self> {
cx.new(|cx| {
let project = workspace.project().clone();
let focus_handle = cx.focus_handle();
let debug_panel = Self {
size: px(300.),
sessions: vec![],
active_session: None,
focus_handle,
focus_handle: cx.focus_handle(),
project,
workspace: workspace.weak_handle(),
context_menu: None,
@@ -93,38 +90,7 @@ impl DebugPanel {
})
}
pub(crate) fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(session) = self.active_session.clone() else {
return;
};
let Some(active_pane) = session
.read(cx)
.running_state()
.read(cx)
.active_pane()
.cloned()
else {
return;
};
active_pane.update(cx, |pane, cx| {
pane.focus_active_item(window, cx);
});
}
pub(crate) fn sessions(&self) -> Vec<Entity<DebugSession>> {
self.sessions.clone()
}
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
self.active_session.clone()
}
pub(crate) fn running_state(&self, cx: &mut App) -> Option<Entity<RunningState>> {
self.active_session()
.map(|session| session.read(cx).running_state().clone())
}
pub(crate) fn filter_action_types(&self, cx: &mut App) {
fn filter_action_types(&self, cx: &mut App) {
let (has_active_session, supports_restart, support_step_back, status) = self
.active_session()
.map(|item| {
@@ -153,7 +119,6 @@ impl DebugPanel {
TypeId::of::<StepOver>(),
TypeId::of::<StepInto>(),
TypeId::of::<StepOut>(),
TypeId::of::<ShowStackTrace>(),
TypeId::of::<editor::actions::DebuggerRunToCursor>(),
TypeId::of::<editor::actions::DebuggerEvaluateSelectedText>(),
];
@@ -253,18 +218,6 @@ impl DebugPanel {
cx,
)
});
if let Some(inventory) = self
.project
.read(cx)
.task_store()
.read(cx)
.task_inventory()
.cloned()
{
inventory.update(cx, |inventory, _| {
inventory.scenario_scheduled(scenario.clone());
})
}
let task = cx.spawn_in(window, {
let session = session.clone();
async move |this, cx| {
@@ -310,7 +263,7 @@ impl DebugPanel {
.detach_and_log_err(cx);
}
pub(crate) async fn register_session(
async fn register_session(
this: WeakEntity<Self>,
session: Entity<Session>,
cx: &mut AsyncWindowContext,
@@ -379,7 +332,7 @@ impl DebugPanel {
Ok(debug_session)
}
pub(crate) fn handle_restart_request(
fn handle_restart_request(
&mut self,
mut curr_session: Entity<Session>,
window: &mut Window,
@@ -453,12 +406,10 @@ impl DebugPanel {
.detach_and_log_err(cx);
}
pub(crate) fn close_session(
&mut self,
entity_id: EntityId,
window: &mut Window,
cx: &mut Context<Self>,
) {
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
self.active_session.clone()
}
fn close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context<Self>) {
let Some(session) = self
.sessions
.iter()
@@ -512,8 +463,93 @@ impl DebugPanel {
})
.detach();
}
fn sessions_drop_down_menu(
&self,
active_session: &Entity<DebugSession>,
window: &mut Window,
cx: &mut Context<Self>,
) -> DropdownMenu {
let sessions = self.sessions.clone();
let weak = cx.weak_entity();
let label = active_session.read(cx).label_element(cx);
pub(crate) fn deploy_context_menu(
DropdownMenu::new_with_element(
"debugger-session-list",
label,
ContextMenu::build(window, cx, move |mut this, _, cx| {
let context_menu = cx.weak_entity();
for session in sessions.into_iter() {
let weak_session = session.downgrade();
let weak_session_id = weak_session.entity_id();
this = this.custom_entry(
{
let weak = weak.clone();
let context_menu = context_menu.clone();
move |_, cx| {
weak_session
.read_with(cx, |session, cx| {
let context_menu = context_menu.clone();
let id: SharedString =
format!("debug-session-{}", session.session_id(cx).0)
.into();
h_flex()
.w_full()
.group(id.clone())
.justify_between()
.child(session.label_element(cx))
.child(
IconButton::new(
"close-debug-session",
IconName::Close,
)
.visible_on_hover(id.clone())
.icon_size(IconSize::Small)
.on_click({
let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |panel, cx| {
panel.close_session(
weak_session_id,
window,
cx,
);
})
.ok();
context_menu
.update(cx, |this, cx| {
this.cancel(
&Default::default(),
window,
cx,
);
})
.ok();
}
}),
)
.into_any_element()
})
.unwrap_or_else(|_| div().into_any_element())
}
},
{
let weak = weak.clone();
move |window, cx| {
weak.update(cx, |panel, cx| {
panel.activate_session(session.clone(), window, cx);
})
.ok();
}
},
);
}
this
}),
)
}
fn deploy_context_menu(
&mut self,
position: Point<Pixels>,
window: &mut Window,
@@ -564,11 +600,7 @@ impl DebugPanel {
}
}
pub(crate) fn top_controls_strip(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Div> {
fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
let active_session = self.active_session.clone();
let focus_handle = self.focus_handle.clone();
let is_side = self.position(window, cx).axis() == gpui::Axis::Horizontal;
@@ -608,12 +640,12 @@ impl DebugPanel {
active_session
.as_ref()
.map(|session| session.read(cx).running_state()),
|this, running_state| {
|this, running_session| {
let thread_status =
running_state.read(cx).thread_status(cx).unwrap_or(
running_session.read(cx).thread_status(cx).unwrap_or(
project::debugger::session::ThreadStatus::Exited,
);
let capabilities = running_state.read(cx).capabilities(cx);
let capabilities = running_session.read(cx).capabilities(cx);
this.map(|this| {
if thread_status == ThreadStatus::Running {
this.child(
@@ -624,7 +656,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_state,
&running_session,
|this, _, _window, cx| {
this.pause_thread(cx);
},
@@ -651,7 +683,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_state,
&running_session,
|this, _, _window, cx| this.continue_thread(cx),
))
.disabled(thread_status != ThreadStatus::Stopped)
@@ -675,7 +707,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_state,
&running_session,
|this, _, _window, cx| {
this.step_over(cx);
},
@@ -699,7 +731,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_state,
&running_session,
|this, _, _window, cx| {
this.step_out(cx);
},
@@ -726,7 +758,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_state,
&running_session,
|this, _, _window, cx| {
this.step_in(cx);
},
@@ -776,7 +808,7 @@ impl DebugPanel {
|| thread_status == ThreadStatus::Ended,
)
.on_click(window.listener_for(
&running_state,
&running_session,
|this, _, _window, cx| {
this.toggle_ignore_breakpoints(cx);
},
@@ -799,7 +831,7 @@ impl DebugPanel {
IconButton::new("debug-restart", IconName::DebugRestart)
.icon_size(IconSize::XSmall)
.on_click(window.listener_for(
&running_state,
&running_session,
|this, _, _window, cx| {
this.restart_session(cx);
},
@@ -821,7 +853,7 @@ impl DebugPanel {
IconButton::new("debug-stop", IconName::Power)
.icon_size(IconSize::XSmall)
.on_click(window.listener_for(
&running_state,
&running_session,
|this, _, _window, cx| {
this.stop_thread(cx);
},
@@ -855,7 +887,7 @@ impl DebugPanel {
IconButton::new("debug-disconnect", IconName::DebugDetach)
.icon_size(IconSize::XSmall)
.on_click(window.listener_for(
&running_state,
&running_session,
|this, _, _, cx| {
this.detach_client(cx);
},
@@ -889,42 +921,30 @@ impl DebugPanel {
.as_ref()
.map(|session| session.read(cx).running_state())
.cloned(),
|this, running_state| {
this.children({
let running_state = running_state.clone();
let threads =
running_state.update(cx, |running_state, cx| {
let session = running_state.session();
session
.update(cx, |session, cx| session.threads(cx))
});
self.render_thread_dropdown(
&running_state,
threads,
window,
cx,
)
})
|this, session| {
this.child(
session.update(cx, |this, cx| {
this.thread_dropdown(window, cx)
}),
)
.when(!is_side, |this| this.gap_2().child(Divider::vertical()))
},
),
)
.child(
h_flex()
.children(self.render_session_menu(
self.active_session(),
self.running_state(cx),
window,
cx,
))
.when_some(active_session.as_ref(), |this, session| {
let context_menu =
self.sessions_drop_down_menu(session, window, cx);
this.child(context_menu).gap_2().child(Divider::vertical())
})
.when(!is_side, |this| this.child(new_session_button())),
),
),
)
}
pub(crate) fn activate_pane_in_direction(
fn activate_pane_in_direction(
&mut self,
direction: SplitDirection,
window: &mut Window,
@@ -939,7 +959,7 @@ impl DebugPanel {
}
}
pub(crate) fn activate_item(
fn activate_item(
&mut self,
item: DebuggerPaneItem,
window: &mut Window,
@@ -954,7 +974,7 @@ impl DebugPanel {
}
}
pub(crate) fn activate_session(
fn activate_session(
&mut self,
session_item: Entity<DebugSession>,
window: &mut Window,
@@ -967,7 +987,7 @@ impl DebugPanel {
this.go_to_selected_stack_frame(window, cx);
});
});
self.active_session = Some(session_item.clone());
self.active_session = Some(session_item);
cx.notify();
}
@@ -1099,7 +1119,7 @@ impl Panel for DebugPanel {
}
fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
self.size = size.unwrap_or(px(300.));
self.size = size.unwrap();
}
fn remote_id() -> Option<proto::PanelId> {

View File

@@ -7,17 +7,14 @@ use new_session_modal::NewSessionModal;
use project::debugger::{self, breakpoint_store::SourceBreakpoint};
use session::DebugSession;
use settings::Settings;
use stack_trace_view::StackTraceView;
use util::maybe;
use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
use workspace::{ShutdownDebugAdapters, Workspace};
pub mod attach_modal;
pub mod debugger_panel;
mod dropdown_menus;
mod new_session_modal;
mod persistence;
pub(crate) mod session;
mod stack_trace_view;
#[cfg(any(test, feature = "test-support"))]
pub mod tests;
@@ -44,7 +41,6 @@ actions!(
FocusModules,
FocusLoadedSources,
FocusTerminal,
ShowStackTrace,
]
);
@@ -60,16 +56,7 @@ pub fn init(cx: &mut App) {
cx.when_flag_enabled::<DebuggerFeatureFlag>(window, |workspace, _, _| {
workspace
.register_action(|workspace, _: &ToggleFocus, window, cx| {
let did_focus_panel = workspace.toggle_panel_focus::<DebugPanel>(window, cx);
if !did_focus_panel {
return;
};
let Some(panel) = workspace.panel::<DebugPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.focus_active_item(window, cx);
})
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
})
.register_action(|workspace, _: &Pause, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
@@ -159,38 +146,6 @@ pub fn init(cx: &mut App) {
})
},
)
.register_action(
|workspace: &mut 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 {
return;
};
let project = workspace.project();
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,
);
}
},
)
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
NewSessionModal::show(workspace, window, cx);
});

View File

@@ -1,186 +0,0 @@
use gpui::Entity;
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
use crate::{
debugger_panel::DebugPanel,
session::{DebugSession, running::RunningState},
};
impl DebugPanel {
fn dropdown_label(label: impl Into<SharedString>) -> Label {
Label::new(label).size(LabelSize::Small)
}
pub fn render_session_menu(
&mut self,
active_session: Option<Entity<DebugSession>>,
running_state: Option<Entity<RunningState>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
if let Some(running_state) = running_state {
let sessions = self.sessions().clone();
let weak = cx.weak_entity();
let running_state = running_state.read(cx);
let label = if let Some(active_session) = active_session {
active_session.read(cx).session(cx).read(cx).label()
} else {
SharedString::new_static("Unknown Session")
};
let is_terminated = running_state.session().read(cx).is_terminated();
let session_state_indicator = {
if is_terminated {
Some(Indicator::dot().color(Color::Error))
} else {
match running_state.thread_status(cx).unwrap_or_default() {
project::debugger::session::ThreadStatus::Stopped => {
Some(Indicator::dot().color(Color::Conflict))
}
_ => Some(Indicator::dot().color(Color::Success)),
}
}
};
let trigger = h_flex()
.gap_2()
.when_some(session_state_indicator, |this, indicator| {
this.child(indicator)
})
.justify_between()
.child(
DebugPanel::dropdown_label(label)
.when(is_terminated, |this| this.strikethrough()),
)
.into_any_element();
Some(
DropdownMenu::new_with_element(
"debugger-session-list",
trigger,
ContextMenu::build(window, cx, move |mut this, _, cx| {
let context_menu = cx.weak_entity();
for session in sessions.into_iter() {
let weak_session = session.downgrade();
let weak_session_id = weak_session.entity_id();
this = this.custom_entry(
{
let weak = weak.clone();
let context_menu = context_menu.clone();
move |_, cx| {
weak_session
.read_with(cx, |session, cx| {
let context_menu = context_menu.clone();
let id: SharedString = format!(
"debug-session-{}",
session.session_id(cx).0
)
.into();
h_flex()
.w_full()
.group(id.clone())
.justify_between()
.child(session.label_element(cx))
.child(
IconButton::new(
"close-debug-session",
IconName::Close,
)
.visible_on_hover(id.clone())
.icon_size(IconSize::Small)
.on_click({
let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |panel, cx| {
panel.close_session(
weak_session_id,
window,
cx,
);
})
.ok();
context_menu
.update(cx, |this, cx| {
this.cancel(
&Default::default(),
window,
cx,
);
})
.ok();
}
}),
)
.into_any_element()
})
.unwrap_or_else(|_| div().into_any_element())
}
},
{
let weak = weak.clone();
move |window, cx| {
weak.update(cx, |panel, cx| {
panel.activate_session(session.clone(), window, cx);
})
.ok();
}
},
);
}
this
}),
)
.style(DropdownStyle::Ghost),
)
} else {
None
}
}
pub(crate) fn render_thread_dropdown(
&self,
running_state: &Entity<RunningState>,
threads: Vec<(dap::Thread, ThreadStatus)>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<DropdownMenu> {
let running_state = running_state.clone();
let running_state_read = running_state.read(cx);
let thread_id = running_state_read.thread_id();
let session = running_state_read.session();
let session_id = session.read(cx).session_id();
let session_terminated = session.read(cx).is_terminated();
let selected_thread_name = threads
.iter()
.find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id))
.map(|(thread, _)| thread.name.clone());
if let Some(selected_thread_name) = selected_thread_name {
let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element();
Some(
DropdownMenu::new_with_element(
("thread-list", session_id.0),
trigger,
ContextMenu::build_eager(window, cx, move |mut this, _, _| {
for (thread, _) in threads {
let running_state = running_state.clone();
let thread_id = thread.id;
this = this.entry(thread.name, None, move |window, cx| {
running_state.update(cx, |running_state, cx| {
running_state.select_thread(ThreadId(thread_id), window, cx);
});
});
}
this
}),
)
.disabled(session_terminated)
.style(DropdownStyle::Ghost),
)
} else {
None
}
}
}

View File

@@ -1,5 +1,4 @@
use collections::FxHashMap;
use language::LanguageRegistry;
use std::{
borrow::Cow,
ops::Not,
@@ -51,6 +50,7 @@ pub(super) struct NewSessionModal {
attach_mode: Entity<AttachMode>,
custom_mode: Entity<CustomMode>,
debugger: Option<DebugAdapterName>,
task_contexts: Arc<TaskContexts>,
save_scenario_state: Option<SaveScenarioState>,
_subscriptions: [Subscription; 2],
}
@@ -83,9 +83,16 @@ impl NewSessionModal {
return;
};
let task_store = workspace.project().read(cx).task_store().clone();
let languages = workspace.app_state().languages.clone();
cx.spawn_in(window, async move |workspace, cx| {
let task_contexts = Arc::from(
workspace
.update_in(cx, |workspace, window, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})?
.await,
);
workspace.update_in(cx, |workspace, window, cx| {
let workspace_handle = workspace.weak_handle();
workspace.toggle_modal(window, cx, |window, cx| {
@@ -93,7 +100,12 @@ impl NewSessionModal {
let launch_picker = cx.new(|cx| {
Picker::uniform_list(
DebugScenarioDelegate::new(debug_panel.downgrade(), task_store),
DebugScenarioDelegate::new(
debug_panel.downgrade(),
workspace_handle.clone(),
task_store,
task_contexts.clone(),
),
window,
cx,
)
@@ -114,42 +126,6 @@ impl NewSessionModal {
let custom_mode = CustomMode::new(None, window, cx);
cx.spawn_in(window, {
let workspace_handle = workspace_handle.clone();
async move |this, cx| {
let task_contexts = workspace_handle
.update_in(cx, |workspace, window, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})?
.await;
this.update_in(cx, |this, window, cx| {
if let Some(active_cwd) = task_contexts
.active_context()
.and_then(|context| context.cwd.clone())
{
this.custom_mode.update(cx, |custom, cx| {
custom.load(active_cwd, window, cx);
});
this.debugger = None;
}
this.launch_picker.update(cx, |picker, cx| {
picker.delegate.task_contexts_loaded(
task_contexts,
languages,
window,
cx,
);
picker.refresh(window, cx);
cx.notify();
});
})
}
})
.detach();
Self {
launch_picker,
attach_mode,
@@ -158,6 +134,7 @@ impl NewSessionModal {
mode: NewSessionMode::Launch,
debug_panel: debug_panel.downgrade(),
workspace: workspace_handle,
task_contexts,
save_scenario_state: None,
_subscriptions,
}
@@ -169,7 +146,7 @@ impl NewSessionModal {
.detach();
}
fn render_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
fn render_mode(&self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
let dap_menu = self.adapter_drop_down_menu(window, cx);
match self.mode {
NewSessionMode::Attach => self.attach_mode.update(cx, |this, cx| {
@@ -224,6 +201,8 @@ impl NewSessionModal {
fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
let Some(debugger) = self.debugger.as_ref() else {
// todo(debugger): show in UI.
log::error!("No debugger selected");
return;
};
@@ -240,12 +219,10 @@ impl NewSessionModal {
};
let debug_panel = self.debug_panel.clone();
let Some(task_contexts) = self.task_contexts(cx) else {
return;
};
let task_context = task_contexts.active_context().cloned().unwrap_or_default();
let worktree_id = task_contexts.worktree();
let task_contexts = self.task_contexts.clone();
cx.spawn_in(window, async move |this, cx| {
let task_context = task_contexts.active_context().cloned().unwrap_or_default();
let worktree_id = task_contexts.worktree();
debug_panel.update_in(cx, |debug_panel, window, cx| {
debug_panel.start_session(config, task_context, None, worktree_id, window, cx)
})?;
@@ -279,55 +256,33 @@ impl NewSessionModal {
cx.notify();
})
}
fn task_contexts<'a>(&self, cx: &'a mut Context<Self>) -> Option<&'a TaskContexts> {
self.launch_picker.read(cx).delegate.task_contexts.as_ref()
}
fn adapter_drop_down_menu(
&mut self,
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> ui::DropdownMenu {
let workspace = self.workspace.clone();
let weak = cx.weak_entity();
let active_buffer = self.task_contexts(cx).and_then(|tc| {
tc.active_item_context
.as_ref()
.and_then(|aic| aic.1.as_ref().map(|l| l.buffer.clone()))
});
let active_buffer_language = active_buffer
.and_then(|buffer| buffer.read(cx).language())
.cloned();
let mut available_adapters = workspace
.update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
.unwrap_or_default();
if let Some(language) = active_buffer_language {
available_adapters.sort_by_key(|adapter| {
language
.config()
.debuggers
.get_index_of(adapter.0.as_ref())
.unwrap_or(usize::MAX)
});
}
if self.debugger.is_none() {
self.debugger = available_adapters.first().cloned();
}
let label = self
.debugger
.as_ref()
.map(|d| d.0.clone())
.unwrap_or_else(|| SELECT_DEBUGGER_LABEL.clone());
let active_buffer_language = self
.task_contexts
.active_item_context
.as_ref()
.and_then(|item| {
item.1
.as_ref()
.and_then(|location| location.buffer.read(cx).language())
})
.cloned();
DropdownMenu::new(
"dap-adapter-picker",
label,
ContextMenu::build(window, cx, move |mut menu, _, _| {
ContextMenu::build(window, cx, move |mut menu, _, cx| {
let setter_for_name = |name: DebugAdapterName| {
let weak = weak.clone();
move |window: &mut Window, cx: &mut App| {
@@ -342,10 +297,22 @@ impl NewSessionModal {
}
};
let mut available_adapters = workspace
.update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
.unwrap_or_default();
if let Some(language) = active_buffer_language {
available_adapters.sort_by_key(|adapter| {
language
.config()
.debuggers
.get_index_of(adapter.0.as_ref())
.unwrap_or(usize::MAX)
});
}
for adapter in available_adapters.into_iter() {
menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.clone()));
}
menu
}),
)
@@ -538,10 +505,7 @@ impl Render for NewSessionModal {
.debugger
.as_ref()
.and_then(|debugger| this.debug_scenario(&debugger, cx))
.zip(
this.task_contexts(cx)
.and_then(|tcx| tcx.worktree()),
)
.zip(this.task_contexts.worktree())
.and_then(|(scenario, worktree_id)| {
this.debug_panel
.update(cx, |panel, cx| {
@@ -770,24 +734,8 @@ impl CustomMode {
})
}
fn load(&mut self, cwd: PathBuf, window: &mut Window, cx: &mut App) {
self.cwd.update(cx, |editor, cx| {
if editor.is_empty(cx) {
editor.set_text(cwd.to_string_lossy(), window, cx);
}
});
}
pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest {
let path = self.cwd.read(cx).text(cx);
if cfg!(windows) {
return task::LaunchRequest {
program: self.program.read(cx).text(cx),
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
args: Default::default(),
env: Default::default(),
};
}
let command = self.program.read(cx).text(cx);
let mut args = shlex::split(&command).into_iter().flatten().peekable();
let mut env = FxHashMap::default();
@@ -806,8 +754,6 @@ impl CustomMode {
let args = args.collect::<Vec<_>>();
let (program, path) = resolve_paths(program, path);
task::LaunchRequest {
program,
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
@@ -827,6 +773,14 @@ impl CustomMode {
.w_full()
.gap_3()
.track_focus(&self.program.focus_handle(cx))
.child(
div().child(
Label::new("Program")
.size(ui::LabelSize::Small)
.color(Color::Muted),
),
)
.child(render_editor(&self.program, window, cx))
.child(
h_flex()
.child(
@@ -837,14 +791,10 @@ impl CustomMode {
.gap(ui::DynamicSpacing::Base08.rems(cx))
.child(adapter_menu),
)
.child(render_editor(&self.program, window, cx))
.child(render_editor(&self.cwd, window, cx))
.child(
CheckboxWithLabel::new(
"debugger-stop-on-entry",
Label::new("Stop on Entry")
.size(ui::LabelSize::Small)
.color(Color::Muted),
Label::new("Stop on Entry").size(ui::LabelSize::Small),
self.stop_on_entry,
{
let this = cx.weak_entity();
@@ -901,106 +851,33 @@ impl AttachMode {
pub(super) struct DebugScenarioDelegate {
task_store: Entity<TaskStore>,
candidates: Vec<(Option<TaskSourceKind>, DebugScenario)>,
candidates: Option<Vec<(TaskSourceKind, DebugScenario)>>,
selected_index: usize,
matches: Vec<StringMatch>,
prompt: String,
debug_panel: WeakEntity<DebugPanel>,
task_contexts: Option<TaskContexts>,
divider_index: Option<usize>,
last_used_candidate_index: Option<usize>,
workspace: WeakEntity<Workspace>,
task_contexts: Arc<TaskContexts>,
}
impl DebugScenarioDelegate {
pub(super) fn new(debug_panel: WeakEntity<DebugPanel>, task_store: Entity<TaskStore>) -> Self {
pub(super) fn new(
debug_panel: WeakEntity<DebugPanel>,
workspace: WeakEntity<Workspace>,
task_store: Entity<TaskStore>,
task_contexts: Arc<TaskContexts>,
) -> Self {
Self {
task_store,
candidates: Vec::default(),
candidates: None,
selected_index: 0,
matches: Vec::new(),
prompt: String::new(),
debug_panel,
task_contexts: None,
divider_index: None,
last_used_candidate_index: None,
workspace,
task_contexts,
}
}
fn get_scenario_kind(
languages: &Arc<LanguageRegistry>,
dap_registry: &DapRegistry,
scenario: DebugScenario,
) -> (Option<TaskSourceKind>, DebugScenario) {
let language_names = languages.language_names();
let language = dap_registry
.adapter_language(&scenario.adapter)
.map(|language| TaskSourceKind::Language {
name: language.into(),
});
let language = language.or_else(|| {
scenario
.request
.as_ref()
.and_then(|request| match request {
DebugRequest::Launch(launch) => launch
.program
.rsplit_once(".")
.and_then(|split| languages.language_name_for_extension(split.1))
.map(|name| TaskSourceKind::Language { name: name.into() }),
_ => None,
})
.or_else(|| {
scenario.label.split_whitespace().find_map(|word| {
language_names
.iter()
.find(|name| name.eq_ignore_ascii_case(word))
.map(|name| TaskSourceKind::Language {
name: name.to_owned().into(),
})
})
})
});
(language, scenario)
}
pub fn task_contexts_loaded(
&mut self,
task_contexts: TaskContexts,
languages: Arc<LanguageRegistry>,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
self.task_contexts = Some(task_contexts);
let (recent, scenarios) = self
.task_store
.update(cx, |task_store, cx| {
task_store.task_inventory().map(|inventory| {
inventory.update(cx, |inventory, cx| {
inventory.list_debug_scenarios(self.task_contexts.as_ref().unwrap(), cx)
})
})
})
.unwrap_or_default();
if !recent.is_empty() {
self.last_used_candidate_index = Some(recent.len() - 1);
}
let dap_registry = cx.global::<DapRegistry>();
self.candidates = recent
.into_iter()
.map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario))
.chain(scenarios.into_iter().map(|(kind, scenario)| {
let (language, scenario) =
Self::get_scenario_kind(&languages, &dap_registry, scenario);
(language.or(Some(kind)), scenario)
}))
.collect();
}
}
impl PickerDelegate for DebugScenarioDelegate {
@@ -1034,15 +911,53 @@ impl PickerDelegate for DebugScenarioDelegate {
cx: &mut Context<picker::Picker<Self>>,
) -> gpui::Task<()> {
let candidates = self.candidates.clone();
let workspace = self.workspace.clone();
let task_store = self.task_store.clone();
cx.spawn_in(window, async move |picker, cx| {
let candidates: Vec<_> = candidates
.into_iter()
.enumerate()
.map(|(index, (_, candidate))| {
StringMatchCandidate::new(index, candidate.label.as_ref())
})
.collect();
let candidates: Vec<_> = match &candidates {
Some(candidates) => candidates
.into_iter()
.enumerate()
.map(|(index, (_, candidate))| {
StringMatchCandidate::new(index, candidate.label.as_ref())
})
.collect(),
None => {
let worktree_ids: Vec<_> = workspace
.update(cx, |this, cx| {
this.visible_worktrees(cx)
.map(|tree| tree.read(cx).id())
.collect()
})
.ok()
.unwrap_or_default();
let scenarios: Vec<_> = task_store
.update(cx, |task_store, cx| {
task_store.task_inventory().map(|item| {
item.read(cx).list_debug_scenarios(worktree_ids.into_iter())
})
})
.ok()
.flatten()
.unwrap_or_default();
picker
.update(cx, |picker, _| {
picker.delegate.candidates = Some(scenarios.clone());
})
.ok();
scenarios
.into_iter()
.enumerate()
.map(|(index, (_, candidate))| {
StringMatchCandidate::new(index, candidate.label.as_ref())
})
.collect()
}
};
let matches = fuzzy::match_strings(
&candidates,
@@ -1061,13 +976,6 @@ impl PickerDelegate for DebugScenarioDelegate {
delegate.matches = matches;
delegate.prompt = query;
delegate.divider_index = delegate.last_used_candidate_index.and_then(|index| {
let index = delegate
.matches
.partition_point(|matching_task| matching_task.candidate_id <= index);
Some(index).and_then(|index| (index != 0).then(|| index - 1))
});
if delegate.matches.is_empty() {
delegate.selected_index = 0;
} else {
@@ -1079,47 +987,34 @@ impl PickerDelegate for DebugScenarioDelegate {
})
}
fn separators_after_indices(&self) -> Vec<usize> {
if let Some(i) = self.divider_index {
vec![i]
} else {
Vec::new()
}
}
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
let debug_scenario = self
.matches
.get(self.selected_index())
.and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
.and_then(|match_candidate| {
self.candidates
.as_ref()
.map(|candidates| candidates[match_candidate.candidate_id].clone())
});
let Some((_, mut debug_scenario)) = debug_scenario else {
let Some((task_source_kind, debug_scenario)) = debug_scenario else {
return;
};
let (task_context, worktree_id) = self
.task_contexts
.as_ref()
.and_then(|task_contexts| {
Some((
task_contexts.active_context().cloned()?,
task_contexts.worktree(),
))
})
.unwrap_or_default();
if let Some(launch_config) =
debug_scenario
.request
.as_mut()
.and_then(|request| match request {
DebugRequest::Launch(launch) => Some(launch),
_ => None,
})
let (task_context, worktree_id) = if let TaskSourceKind::Worktree {
id: worktree_id,
directory_in_worktree: _,
id_base: _,
} = task_source_kind
{
let (program, _) = resolve_paths(launch_config.program.clone(), String::new());
launch_config.program = program;
self.task_contexts
.task_context_for_worktree_id(worktree_id)
.cloned()
.map(|context| (context, Some(worktree_id)))
} else {
None
}
.unwrap_or_default();
self.debug_panel
.update(cx, |panel, cx| {
@@ -1149,19 +1044,10 @@ impl PickerDelegate for DebugScenarioDelegate {
char_count: hit.string.chars().count(),
color: Color::Default,
};
let task_kind = &self.candidates[hit.candidate_id].0;
let icon = match task_kind {
Some(TaskSourceKind::Lsp(..)) => Some(Icon::new(IconName::Bolt)),
Some(TaskSourceKind::UserInput) => Some(Icon::new(IconName::Terminal)),
Some(TaskSourceKind::AbsPath { .. }) => Some(Icon::new(IconName::Settings)),
Some(TaskSourceKind::Worktree { .. }) => Some(Icon::new(IconName::FileTree)),
Some(TaskSourceKind::Language { name }) => file_icons::FileIcons::get(cx)
.get_icon_for_type(&name.to_lowercase(), cx)
.map(Icon::from_path),
None => Some(Icon::new(IconName::HistoryRerun)),
}
.map(|icon| icon.color(Color::Muted).size(ui::IconSize::Small));
let icon = Icon::new(IconName::FileTree)
.color(Color::Muted)
.size(ui::IconSize::Small);
Some(
ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
@@ -1173,35 +1059,3 @@ impl PickerDelegate for DebugScenarioDelegate {
)
}
}
fn resolve_paths(program: String, path: String) -> (String, String) {
let program = if let Some(program) = program.strip_prefix('~') {
format!(
"$ZED_WORKTREE_ROOT{}{}",
std::path::MAIN_SEPARATOR,
&program
)
} else if !program.starts_with(std::path::MAIN_SEPARATOR) {
format!(
"$ZED_WORKTREE_ROOT{}{}",
std::path::MAIN_SEPARATOR,
&program
)
} else {
program
};
let path = if path.starts_with('~') && !path.is_empty() {
format!(
"$ZED_WORKTREE_ROOT{}{}",
std::path::MAIN_SEPARATOR,
&path[1..]
)
} else if !path.starts_with(std::path::MAIN_SEPARATOR) && !path.is_empty() {
format!("$ZED_WORKTREE_ROOT{}{}", std::path::MAIN_SEPARATOR, &path)
} else {
path
};
(program, path)
}

View File

@@ -278,7 +278,7 @@ pub(crate) fn deserialize_pane_layout(
cx,
)),
DebuggerPaneItem::Console => Box::new(SubView::new(
console.focus_handle(cx),
pane.focus_handle(cx),
console.clone().into(),
DebuggerPaneItem::Console,
Some(Box::new({
@@ -292,7 +292,7 @@ pub(crate) fn deserialize_pane_layout(
cx,
)),
DebuggerPaneItem::Terminal => Box::new(SubView::new(
terminal.focus_handle(cx),
pane.focus_handle(cx),
terminal.clone().into(),
DebuggerPaneItem::Terminal,
None,

View File

@@ -1,6 +1,7 @@
pub mod running;
use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
use std::sync::OnceLock;
use dap::client::SessionId;
use gpui::{
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
@@ -10,21 +11,21 @@ use project::debugger::session::Session;
use project::worktree_store::WorktreeStore;
use rpc::proto;
use running::RunningState;
use std::{cell::OnceCell, sync::OnceLock};
use ui::{Indicator, prelude::*};
use workspace::{
CollaboratorId, FollowableItem, ViewId, Workspace,
item::{self, Item},
};
use crate::{debugger_panel::DebugPanel, persistence::SerializedLayout};
pub struct DebugSession {
remote_id: Option<workspace::ViewId>,
running_state: Entity<RunningState>,
label: OnceLock<SharedString>,
stack_trace_view: OnceCell<Entity<StackTraceView>>,
_debug_panel: WeakEntity<DebugPanel>,
_worktree_store: WeakEntity<WorktreeStore>,
workspace: WeakEntity<Workspace>,
_workspace: WeakEntity<Workspace>,
_subscriptions: [Subscription; 1],
}
@@ -65,9 +66,8 @@ impl DebugSession {
running_state,
label: OnceLock::new(),
_debug_panel,
stack_trace_view: OnceCell::new(),
_worktree_store: project.read(cx).worktree_store().downgrade(),
workspace,
_workspace: workspace,
})
}
@@ -75,32 +75,6 @@ impl DebugSession {
self.running_state.read(cx).session_id()
}
pub(crate) fn stack_trace_view(
&mut self,
project: &Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> &Entity<StackTraceView> {
let workspace = self.workspace.clone();
let running_state = self.running_state.clone();
self.stack_trace_view.get_or_init(|| {
let stackframe_list = running_state.read(cx).stack_frame_list().clone();
let stack_frame_view = cx.new(|cx| {
StackTraceView::new(
workspace.clone(),
project.clone(),
stackframe_list,
window,
cx,
)
});
stack_frame_view
})
}
pub fn session(&self, cx: &App) -> Entity<Session> {
self.running_state.read(cx).session().clone()
}
@@ -157,11 +131,7 @@ impl DebugSession {
.gap_2()
.when_some(icon, |this, indicator| this.child(indicator))
.justify_between()
.child(
Label::new(label)
.size(LabelSize::Small)
.when(is_terminated, |this| this.strikethrough()),
)
.child(Label::new(label).when(is_terminated, |this| this.strikethrough()))
.into_any_element()
}
}

View File

@@ -43,10 +43,11 @@ use task::{
};
use terminal_view::TerminalView;
use ui::{
ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, FluentBuilder,
IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon as _,
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Tab, Tooltip,
VisibleOnHover, VisualContext, Window, div, h_flex, v_flex,
ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, ContextMenu,
Disableable, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, InteractiveElement,
IntoElement, Label, LabelCommon as _, ParentElement, Render, SharedString,
StatefulInteractiveElement, Styled, Tab, Tooltip, VisibleOnHover, VisualContext, Window, div,
h_flex, v_flex,
};
use util::ResultExt;
use variable_list::VariableList;
@@ -77,16 +78,6 @@ pub struct RunningState {
_schedule_serialize: Option<Task<()>>,
}
impl RunningState {
pub(crate) fn thread_id(&self) -> Option<ThreadId> {
self.thread_id
}
pub(crate) fn active_pane(&self) -> Option<&Entity<Pane>> {
self.active_pane.as_ref()
}
}
impl Render for RunningState {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let zoomed_pane = self
@@ -128,7 +119,7 @@ impl Render for RunningState {
pub(crate) struct SubView {
inner: AnyView,
item_focus_handle: FocusHandle,
pane_focus_handle: FocusHandle,
kind: DebuggerPaneItem,
show_indicator: Box<dyn Fn(&App) -> bool>,
hovered: bool,
@@ -136,7 +127,7 @@ pub(crate) struct SubView {
impl SubView {
pub(crate) fn new(
item_focus_handle: FocusHandle,
pane_focus_handle: FocusHandle,
view: AnyView,
kind: DebuggerPaneItem,
show_indicator: Option<Box<dyn Fn(&App) -> bool>>,
@@ -145,7 +136,7 @@ impl SubView {
cx.new(|_| Self {
kind,
inner: view,
item_focus_handle,
pane_focus_handle,
show_indicator: show_indicator.unwrap_or(Box::new(|_| false)),
hovered: false,
})
@@ -157,7 +148,7 @@ impl SubView {
}
impl Focusable for SubView {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.item_focus_handle.clone()
self.pane_focus_handle.clone()
}
}
impl EventEmitter<()> for SubView {}
@@ -208,7 +199,7 @@ impl Render for SubView {
.size_full()
// Add border unconditionally to prevent layout shifts on focus changes.
.border_1()
.when(self.item_focus_handle.contains_focused(window, cx), |el| {
.when(self.pane_focus_handle.contains_focused(window, cx), |el| {
el.border_color(cx.theme().colors().pane_focused_border)
})
.child(self.inner.clone())
@@ -506,20 +497,25 @@ impl DebugTerminal {
impl gpui::Render for DebugTerminal {
fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.track_focus(&self.focus_handle)
.children(self.terminal.clone())
if let Some(terminal) = self.terminal.clone() {
terminal.into_any_element()
} else {
div().track_focus(&self.focus_handle).into_any_element()
}
}
}
impl Focusable for DebugTerminal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
fn focus_handle(&self, cx: &App) -> FocusHandle {
if let Some(terminal) = self.terminal.as_ref() {
return terminal.focus_handle(cx);
} else {
self.focus_handle.clone()
}
}
}
impl RunningState {
pub(crate) fn new(
pub fn new(
session: Entity<Session>,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
@@ -1206,9 +1202,7 @@ impl RunningState {
.as_ref()
.and_then(|pane| self.panes.find_pane_in_direction(pane, direction, cx))
{
pane.update(cx, |pane, cx| {
pane.focus_active_item(window, cx);
})
window.focus(&pane.focus_handle(cx));
} else {
self.workspace
.update(cx, |workspace, cx| {
@@ -1218,16 +1212,10 @@ impl RunningState {
}
}
pub(crate) fn go_to_selected_stack_frame(&self, window: &mut Window, cx: &mut Context<Self>) {
pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
if self.thread_id.is_some() {
self.stack_frame_list
.update(cx, |list, cx| {
let Some(stack_frame_id) = list.opened_stack_frame_id() else {
return Task::ready(Ok(()));
};
list.go_to_stack_frame(stack_frame_id, window, cx)
})
.detach();
.update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
}
}
@@ -1244,10 +1232,11 @@ impl RunningState {
}
pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
self.stack_frame_list.read(cx).opened_stack_frame_id()
self.stack_frame_list.read(cx).selected_stack_frame_id()
}
pub(crate) fn stack_frame_list(&self) -> &Entity<StackFrameList> {
#[cfg(test)]
pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
&self.stack_frame_list
}
@@ -1321,12 +1310,7 @@ impl RunningState {
.map(|id| self.session().read(cx).thread_status(id))
}
pub(crate) fn select_thread(
&mut self,
thread_id: ThreadId,
window: &mut Window,
cx: &mut Context<Self>,
) {
fn select_thread(&mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context<Self>) {
if self.thread_id.is_some_and(|id| id == thread_id) {
return;
}
@@ -1463,6 +1447,38 @@ impl RunningState {
});
}
pub(crate) fn thread_dropdown(
&self,
window: &mut Window,
cx: &mut Context<'_, RunningState>,
) -> DropdownMenu {
let state = cx.entity();
let session_terminated = self.session.read(cx).is_terminated();
let threads = self.session.update(cx, |this, cx| this.threads(cx));
let selected_thread_name = threads
.iter()
.find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
.map(|(thread, _)| thread.name.clone())
.unwrap_or("Threads".to_owned());
DropdownMenu::new(
("thread-list", self.session_id.0),
selected_thread_name,
ContextMenu::build_eager(window, cx, move |mut this, _, _| {
for (thread, _) in threads {
let state = state.clone();
let thread_id = thread.id;
this = this.entry(thread.name, None, move |window, cx| {
state.update(cx, |state, cx| {
state.select_thread(ThreadId(thread_id), window, cx);
});
});
}
this
}),
)
.disabled(session_terminated)
}
fn default_pane_layout(
project: Entity<Project>,
workspace: &WeakEntity<Workspace>,

View File

@@ -21,8 +21,8 @@ use project::{
use ui::{
App, Clickable, Color, Context, Div, Icon, IconButton, IconName, Indicator, InteractiveElement,
IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Tooltip, Window,
div, h_flex, px, v_flex,
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Window, div,
h_flex, px, v_flex,
};
use util::{ResultExt, maybe};
use workspace::Workspace;
@@ -148,7 +148,7 @@ impl Render for BreakpointList {
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
let old_len = self.breakpoints.len();
let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
let breakpoints = self.breakpoint_store.read(cx).all_breakpoints(cx);
self.breakpoints.clear();
let weak = cx.weak_entity();
let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
@@ -259,11 +259,6 @@ impl LineBreakpoint {
dir, name, line
)))
.cursor_pointer()
.tooltip(Tooltip::text(if breakpoint.state.is_enabled() {
"Disable Breakpoint"
} else {
"Enable Breakpoint"
}))
.on_click({
let weak = weak.clone();
let path = path.clone();
@@ -295,9 +290,6 @@ impl LineBreakpoint {
)))
.start_slot(indicator)
.rounded()
.on_secondary_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.end_hover_slot(
IconButton::new(
SharedString::from(format!(
@@ -431,20 +423,12 @@ impl ExceptionBreakpoint {
self.id
)))
.rounded()
.on_secondary_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.start_slot(
div()
.id(SharedString::from(format!(
"exception-breakpoint-ui-item-{}-click-handler",
self.id
)))
.tooltip(Tooltip::text(if self.is_enabled {
"Disable Exception Breakpoint"
} else {
"Enable Exception Breakpoint"
}))
.on_click(move |_, _, cx| {
list.update(cx, |this, cx| {
this.session.update(cx, |this, cx| {

View File

@@ -45,7 +45,6 @@ impl Console {
let mut editor = Editor::multi_line(window, cx);
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
editor.set_read_only(true);
editor.disable_scrollbars_and_minimap(window, cx);
editor.set_show_gutter(false, cx);
editor.set_show_runnables(false, cx);
editor.set_show_breakpoints(false, cx);
@@ -77,14 +76,8 @@ impl Console {
editor
});
let _subscriptions = vec![
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
cx.on_focus_in(&focus_handle, window, |console, window, cx| {
if console.is_running(cx) {
console.query_bar.focus_handle(cx).focus(window);
}
}),
];
let _subscriptions =
vec![cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events)];
Self {
session,
@@ -104,7 +97,7 @@ impl Console {
&self.console
}
fn is_running(&self, cx: &Context<Self>) -> bool {
fn is_local(&self, cx: &Context<Self>) -> bool {
self.session.read(cx).is_local()
}
@@ -116,7 +109,6 @@ impl Console {
) {
match event {
StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(),
StackFrameListEvent::BuiltEntries => {}
}
}
@@ -150,9 +142,8 @@ impl Console {
pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
let expression = self.query_bar.update(cx, |editor, cx| {
let expression = editor.text(cx);
cx.defer_in(window, |editor, window, cx| {
editor.clear(window, cx);
});
editor.clear(window, cx);
expression
});
@@ -162,7 +153,7 @@ impl Console {
.evaluate(
expression,
Some(dap::EvaluateArgumentsContext::Repl),
self.stack_frame_list.read(cx).opened_stack_frame_id(),
self.stack_frame_list.read(cx).selected_stack_frame_id(),
None,
cx,
)
@@ -227,7 +218,7 @@ impl Render for Console {
.on_action(cx.listener(Self::evaluate))
.size_full()
.child(self.render_console(cx))
.when(self.is_running(cx), |this| {
.when(self.is_local(cx), |this| {
this.child(Divider::horizontal())
.child(self.render_query_bar(cx))
})
@@ -389,7 +380,7 @@ impl ConsoleQueryBarCompletionProvider {
) -> Task<Result<Option<Vec<Completion>>>> {
let completion_task = console.update(cx, |console, cx| {
console.session.update(cx, |state, cx| {
let frame_id = console.stack_frame_list.read(cx).opened_stack_frame_id();
let frame_id = console.stack_frame_list.read(cx).selected_stack_frame_id();
state.completions(
CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),

View File

@@ -1,8 +1,7 @@
use anyhow::anyhow;
use dap::Module;
use gpui::{
AnyElement, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful,
Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, MouseButton, Stateful,
Subscription, WeakEntity, list,
};
use project::{
ProjectItem as _, ProjectPath,
@@ -10,17 +9,16 @@ use project::{
};
use std::{path::Path, sync::Arc};
use ui::{Scrollbar, ScrollbarState, prelude::*};
use util::maybe;
use workspace::Workspace;
pub struct ModuleList {
scroll_handle: UniformListScrollHandle,
selected_ix: Option<usize>,
list: ListState,
invalidate: bool,
session: Entity<Session>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
scrollbar_state: ScrollbarState,
entries: Vec<Module>,
_rebuild_task: Task<()>,
_subscription: Subscription,
}
@@ -30,43 +28,38 @@ impl ModuleList {
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> Self {
let weak_entity = cx.weak_entity();
let focus_handle = cx.focus_handle();
let list = ListState::new(
0,
gpui::ListAlignment::Top,
px(1000.),
move |ix, _window, cx| {
weak_entity
.upgrade()
.map(|module_list| module_list.update(cx, |this, cx| this.render_entry(ix, cx)))
.unwrap_or(div().into_any())
},
);
let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
SessionEvent::Stopped(_) | SessionEvent::Modules => {
this.schedule_rebuild(cx);
this.invalidate = true;
cx.notify();
}
_ => {}
});
let scroll_handle = UniformListScrollHandle::new();
let mut this = Self {
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
Self {
scrollbar_state: ScrollbarState::new(list.clone()),
list,
session,
workspace,
focus_handle,
entries: Vec::new(),
selected_ix: None,
_subscription,
_rebuild_task: Task::ready(()),
};
this.schedule_rebuild(cx);
this
}
fn schedule_rebuild(&mut self, cx: &mut Context<Self>) {
self._rebuild_task = cx.spawn(async move |this, cx| {
this.update(cx, |this, cx| {
let modules = this
.session
.update(cx, |session, cx| session.modules(cx).to_owned());
this.entries = modules;
cx.notify();
})
.ok();
});
invalidate: true,
}
}
fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
@@ -118,40 +111,36 @@ impl ModuleList {
anyhow::Ok(())
})
.detach();
.detach_and_log_err(cx);
}
fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
let module = self.entries[ix].clone();
let Some(module) = maybe!({
self.session
.update(cx, |state, cx| state.modules(cx).get(ix).cloned())
}) else {
return Empty.into_any();
};
v_flex()
.rounded_md()
.w_full()
.group("")
.id(("module-list", ix))
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.when(module.path.is_some(), |this| {
this.on_click({
let path = module
.path
.as_deref()
.map(|path| Arc::<Path>::from(Path::new(path)));
let path = module.path.as_deref().map(|path| Arc::<Path>::from(Path::new(path)));
cx.listener(move |this, _, window, cx| {
this.selected_ix = Some(ix);
if let Some(path) = path.as_ref() {
this.open_module(path.clone(), window, cx);
} else {
log::error!("Wasn't able to find module path, but was still able to click on module list entry");
}
cx.notify();
})
})
})
.p_1()
.hover(|s| s.bg(cx.theme().colors().element_hover))
.when(Some(ix) == self.selected_ix, |s| {
s.bg(cx.theme().colors().element_hover)
})
.child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone()))
.child(
h_flex()
@@ -199,96 +188,6 @@ impl ModuleList {
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let Some(ix) = self.selected_ix else { return };
let Some(entry) = self.entries.get(ix) else {
return;
};
let Some(path) = entry.path.as_deref() else {
return;
};
let path = Arc::from(Path::new(path));
self.open_module(path, window, cx);
}
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
self.selected_ix = ix;
if let Some(ix) = ix {
self.scroll_handle
.scroll_to_item(ix, ScrollStrategy::Center);
}
cx.notify();
}
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
let ix = match self.selected_ix {
_ if self.entries.len() == 0 => None,
None => Some(0),
Some(ix) => {
if ix == self.entries.len() - 1 {
Some(0)
} else {
Some(ix + 1)
}
}
};
self.select_ix(ix, cx);
}
fn select_previous(
&mut self,
_: &menu::SelectPrevious,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let ix = match self.selected_ix {
_ if self.entries.len() == 0 => None,
None => Some(self.entries.len() - 1),
Some(ix) => {
if ix == 0 {
Some(self.entries.len() - 1)
} else {
Some(ix - 1)
}
}
};
self.select_ix(ix, cx);
}
fn select_first(
&mut self,
_: &menu::SelectFirst,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let ix = if self.entries.len() > 0 {
Some(0)
} else {
None
};
self.select_ix(ix, cx);
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
let ix = if self.entries.len() > 0 {
Some(self.entries.len() - 1)
} else {
None
};
self.select_ix(ix, cx);
}
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
uniform_list(
cx.entity(),
"module-list",
self.entries.len(),
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
)
.track_scroll(self.scroll_handle.clone())
.size_full()
}
}
impl Focusable for ModuleList {
@@ -298,17 +197,21 @@ impl Focusable for ModuleList {
}
impl Render for ModuleList {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.invalidate {
let len = self
.session
.update(cx, |session, cx| session.modules(cx).len());
self.list.reset(len);
self.invalidate = false;
cx.notify();
}
div()
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::confirm))
.size_full()
.p_1()
.child(self.render_list(window, cx))
.child(list(self.list.clone()).size_full())
.child(self.render_vertical_scrollbar(cx))
}
}

View File

@@ -5,37 +5,35 @@ use std::time::Duration;
use anyhow::{Result, anyhow};
use dap::StackFrameId;
use gpui::{
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, MouseButton, ScrollStrategy,
Stateful, Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, MouseButton, Stateful,
Subscription, Task, WeakEntity, list,
};
use crate::StackTraceView;
use language::PointUtf16;
use project::debugger::breakpoint_store::ActiveStackFrame;
use project::debugger::session::{Session, SessionEvent, StackFrame};
use project::{ProjectItem, ProjectPath};
use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
use workspace::{ItemHandle, Workspace};
use util::ResultExt;
use workspace::Workspace;
use super::RunningState;
#[derive(Debug)]
pub enum StackFrameListEvent {
SelectedStackFrameChanged(StackFrameId),
BuiltEntries,
}
pub struct StackFrameList {
list: ListState,
focus_handle: FocusHandle,
_subscription: Subscription,
session: Entity<Session>,
state: WeakEntity<RunningState>,
entries: Vec<StackFrameEntry>,
workspace: WeakEntity<Workspace>,
selected_ix: Option<usize>,
opened_stack_frame_id: Option<StackFrameId>,
selected_stack_frame_id: Option<StackFrameId>,
scrollbar_state: ScrollbarState,
scroll_handle: UniformListScrollHandle,
_refresh_task: Task<()>,
}
@@ -54,8 +52,22 @@ impl StackFrameList {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let weak_entity = cx.weak_entity();
let focus_handle = cx.focus_handle();
let scroll_handle = UniformListScrollHandle::new();
let list = ListState::new(
0,
gpui::ListAlignment::Top,
px(1000.),
move |ix, _window, cx| {
weak_entity
.upgrade()
.map(|stack_frame_list| {
stack_frame_list.update(cx, |this, cx| this.render_entry(ix, cx))
})
.unwrap_or(div().into_any())
},
);
let _subscription =
cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
@@ -69,16 +81,15 @@ impl StackFrameList {
});
let mut this = Self {
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scrollbar_state: ScrollbarState::new(list.clone()),
list,
session,
workspace,
focus_handle,
state,
_subscription,
entries: Default::default(),
selected_ix: None,
opened_stack_frame_id: None,
scroll_handle,
selected_stack_frame_id: None,
_refresh_task: Task::ready(()),
};
this.schedule_refresh(true, window, cx);
@@ -90,18 +101,13 @@ impl StackFrameList {
&self.entries
}
pub(crate) fn flatten_entries(&self, show_collapsed: bool) -> Vec<dap::StackFrame> {
#[cfg(test)]
pub(crate) fn flatten_entries(&self) -> Vec<dap::StackFrame> {
self.entries
.iter()
.flat_map(|frame| match frame {
StackFrameEntry::Normal(frame) => vec![frame.clone()],
StackFrameEntry::Collapsed(frames) => {
if show_collapsed {
frames.clone()
} else {
vec![]
}
}
StackFrameEntry::Collapsed(frames) => frames.clone(),
})
.collect::<Vec<_>>()
}
@@ -109,7 +115,7 @@ impl StackFrameList {
fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> {
self.state
.read_with(cx, |state, _| state.thread_id)
.ok()
.log_err()
.flatten()
.map(|thread_id| {
self.session
@@ -126,8 +132,8 @@ impl StackFrameList {
.collect()
}
pub fn opened_stack_frame_id(&self) -> Option<StackFrameId> {
self.opened_stack_frame_id
pub fn selected_stack_frame_id(&self) -> Option<StackFrameId> {
self.selected_stack_frame_id
}
pub(super) fn schedule_refresh(
@@ -160,22 +166,13 @@ impl StackFrameList {
pub fn build_entries(
&mut self,
open_first_stack_frame: bool,
select_first_stack_frame: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let old_selected_frame_id = self
.selected_ix
.and_then(|ix| self.entries.get(ix))
.and_then(|entry| match entry {
StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id),
StackFrameEntry::Collapsed(stack_frames) => {
stack_frames.first().map(|stack_frame| stack_frame.id)
}
});
let mut entries = Vec::new();
let mut collapsed_entries = Vec::new();
let mut first_stack_frame = None;
let mut current_stack_frame = None;
let stack_frames = self.stack_frames(cx);
for stack_frame in &stack_frames {
@@ -189,7 +186,7 @@ impl StackFrameList {
entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
}
first_stack_frame.get_or_insert(entries.len());
current_stack_frame.get_or_insert(&stack_frame.dap);
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
}
}
@@ -201,60 +198,68 @@ impl StackFrameList {
}
std::mem::swap(&mut self.entries, &mut entries);
self.list.reset(self.entries.len());
if let Some(ix) = first_stack_frame.filter(|_| open_first_stack_frame) {
self.select_ix(Some(ix), cx);
self.activate_selected_entry(window, cx);
} else if let Some(old_selected_frame_id) = old_selected_frame_id {
let ix = self.entries.iter().position(|entry| match entry {
StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id,
StackFrameEntry::Collapsed(frames) => {
frames.iter().any(|frame| frame.id == old_selected_frame_id)
}
});
self.selected_ix = ix;
if let Some(current_stack_frame) = current_stack_frame.filter(|_| select_first_stack_frame)
{
self.select_stack_frame(current_stack_frame, true, window, cx)
.detach_and_log_err(cx);
}
cx.emit(StackFrameListEvent::BuiltEntries);
cx.notify();
}
pub fn go_to_stack_frame(
&mut self,
stack_frame_id: StackFrameId,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(stack_frame) = self
.entries
.iter()
.flat_map(|entry| match entry {
StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame),
StackFrameEntry::Collapsed(stack_frames) => stack_frames.as_slice(),
})
.find(|stack_frame| stack_frame.id == stack_frame_id)
.cloned()
else {
return Task::ready(Err(anyhow!("No stack frame for ID")));
};
self.go_to_stack_frame_inner(stack_frame, window, cx)
pub fn go_to_selected_stack_frame(&mut self, window: &Window, cx: &mut Context<Self>) {
if let Some(selected_stack_frame_id) = self.selected_stack_frame_id {
let frame = self
.entries
.iter()
.find_map(|entry| match entry {
StackFrameEntry::Normal(dap) => {
if dap.id == selected_stack_frame_id {
Some(dap)
} else {
None
}
}
StackFrameEntry::Collapsed(daps) => {
daps.iter().find(|dap| dap.id == selected_stack_frame_id)
}
})
.cloned();
if let Some(frame) = frame.as_ref() {
self.select_stack_frame(frame, true, window, cx)
.detach_and_log_err(cx);
}
}
}
fn go_to_stack_frame_inner(
pub fn select_stack_frame(
&mut self,
stack_frame: dap::StackFrame,
window: &mut Window,
stack_frame: &dap::StackFrame,
go_to_stack_frame: bool,
window: &Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let stack_frame_id = stack_frame.id;
self.opened_stack_frame_id = Some(stack_frame_id);
let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
self.selected_stack_frame_id = Some(stack_frame.id);
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
stack_frame.id,
));
cx.notify();
if !go_to_stack_frame {
return Task::ready(Ok(()));
};
let row = (stack_frame.line.saturating_sub(1)) as u32;
let Some(abs_path) = self.abs_path_from_stack_frame(&stack_frame) else {
return Task::ready(Err(anyhow!("Project path not found")));
};
let row = stack_frame.line.saturating_sub(1) as u32;
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
stack_frame_id,
));
let stack_frame_id = stack_frame.id;
cx.spawn_in(window, async move |this, cx| {
let (worktree, relative_path) = this
.update(cx, |this, cx| {
@@ -289,22 +294,12 @@ impl StackFrameList {
let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
anyhow!("Could not select a stack frame for unnamed buffer")
})?;
let open_preview = !workspace
.item_of_type::<StackTraceView>(cx)
.map(|viewer| {
workspace
.active_item(cx)
.is_some_and(|item| item.item_id() == viewer.item_id())
})
.unwrap_or_default();
anyhow::Ok(workspace.open_path_preview(
project_path,
None,
false,
true,
true,
open_preview,
window,
cx,
))
@@ -337,7 +332,7 @@ impl StackFrameList {
})
}
pub(crate) fn abs_path_from_stack_frame(stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
fn abs_path_from_stack_frame(&self, stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
stack_frame.source.as_ref().and_then(|s| {
s.path
.as_deref()
@@ -353,12 +348,11 @@ impl StackFrameList {
fn render_normal_entry(
&self,
ix: usize,
stack_frame: &dap::StackFrame,
cx: &mut Context<Self>,
) -> AnyElement {
let source = stack_frame.source.clone();
let is_selected_frame = Some(ix) == self.selected_ix;
let is_selected_frame = Some(stack_frame.id) == self.selected_stack_frame_id;
let path = source.clone().and_then(|s| s.path.or(s.name));
let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,));
@@ -394,12 +388,12 @@ impl StackFrameList {
.when(is_selected_frame, |this| {
this.bg(cx.theme().colors().element_hover)
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_click(cx.listener(move |this, _, window, cx| {
this.selected_ix = Some(ix);
this.activate_selected_entry(window, cx);
.on_click(cx.listener({
let stack_frame = stack_frame.clone();
move |this, _, window, cx| {
this.select_stack_frame(&stack_frame, true, window, cx)
.detach_and_log_err(cx);
}
}))
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
.child(
@@ -454,15 +448,20 @@ impl StackFrameList {
.into_any()
}
pub(crate) fn expand_collapsed_entry(&mut self, ix: usize) {
let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else {
return;
};
let entries = std::mem::take(stack_frames)
.into_iter()
.map(StackFrameEntry::Normal);
self.entries.splice(ix..ix + 1, entries);
self.selected_ix = Some(ix);
pub fn expand_collapsed_entry(
&mut self,
ix: usize,
stack_frames: &Vec<dap::StackFrame>,
cx: &mut Context<Self>,
) {
self.entries.splice(
ix..ix + 1,
stack_frames
.iter()
.map(|frame| StackFrameEntry::Normal(frame.clone())),
);
self.list.reset(self.entries.len());
cx.notify();
}
fn render_collapsed_entry(
@@ -472,7 +471,6 @@ impl StackFrameList {
cx: &mut Context<Self>,
) -> AnyElement {
let first_stack_frame = &stack_frames[0];
let is_selected = Some(ix) == self.selected_ix;
h_flex()
.rounded_md()
@@ -481,15 +479,11 @@ impl StackFrameList {
.group("")
.id(("stack-frame", first_stack_frame.id))
.p_1()
.when(is_selected, |this| {
this.bg(cx.theme().colors().element_hover)
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_click(cx.listener(move |this, _, window, cx| {
this.selected_ix = Some(ix);
this.activate_selected_entry(window, cx);
.on_click(cx.listener({
let stack_frames = stack_frames.clone();
move |this, _, _window, cx| {
this.expand_collapsed_entry(ix, &stack_frames, cx);
}
}))
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
.child(
@@ -512,7 +506,7 @@ impl StackFrameList {
fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
match &self.entries[ix] {
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(stack_frame, cx),
StackFrameEntry::Collapsed(stack_frames) => {
self.render_collapsed_entry(ix, stack_frames, cx)
}
@@ -551,120 +545,15 @@ impl StackFrameList {
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
self.selected_ix = ix;
if let Some(ix) = self.selected_ix {
self.scroll_handle
.scroll_to_item(ix, ScrollStrategy::Center);
}
cx.notify();
}
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
let ix = match self.selected_ix {
_ if self.entries.len() == 0 => None,
None => Some(0),
Some(ix) => {
if ix == self.entries.len() - 1 {
Some(0)
} else {
Some(ix + 1)
}
}
};
self.select_ix(ix, cx);
}
fn select_previous(
&mut self,
_: &menu::SelectPrevious,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let ix = match self.selected_ix {
_ if self.entries.len() == 0 => None,
None => Some(self.entries.len() - 1),
Some(ix) => {
if ix == 0 {
Some(self.entries.len() - 1)
} else {
Some(ix - 1)
}
}
};
self.select_ix(ix, cx);
}
fn select_first(
&mut self,
_: &menu::SelectFirst,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let ix = if self.entries.len() > 0 {
Some(0)
} else {
None
};
self.select_ix(ix, cx);
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
let ix = if self.entries.len() > 0 {
Some(self.entries.len() - 1)
} else {
None
};
self.select_ix(ix, cx);
}
fn activate_selected_entry(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(ix) = self.selected_ix else {
return;
};
let Some(entry) = self.entries.get_mut(ix) else {
return;
};
match entry {
StackFrameEntry::Normal(stack_frame) => {
let stack_frame = stack_frame.clone();
self.go_to_stack_frame_inner(stack_frame, window, cx)
.detach_and_log_err(cx)
}
StackFrameEntry::Collapsed(_) => self.expand_collapsed_entry(ix),
}
cx.notify();
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
self.activate_selected_entry(window, cx);
}
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
uniform_list(
cx.entity(),
"stack-frame-list",
self.entries.len(),
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
)
.track_scroll(self.scroll_handle.clone())
.size_full()
}
}
impl Render for StackFrameList {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.size_full()
.p_1()
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::confirm))
.child(self.render_list(window, cx))
.child(list(self.list.clone()).size_full())
.child(self.render_vertical_scrollbar(cx))
}
}

View File

@@ -302,7 +302,6 @@ impl VariableList {
self.selected_stack_frame_id = Some(*stack_frame_id);
cx.notify();
}
StackFrameListEvent::BuiltEntries => {}
}
}

View File

@@ -1,453 +0,0 @@
use std::any::{Any, TypeId};
use collections::HashMap;
use dap::StackFrameId;
use editor::{
Anchor, Bias, DebugStackFrameLine, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer,
RowHighlightOptions, ToPoint, scroll::Autoscroll,
};
use gpui::{
AnyView, App, AppContext, Entity, EventEmitter, Focusable, IntoElement, Render, SharedString,
Subscription, Task, WeakEntity, Window,
};
use language::{BufferSnapshot, Capability, Point, Selection, SelectionGoal, TreeSitterOptions};
use project::{Project, ProjectPath};
use ui::{ActiveTheme as _, Context, ParentElement as _, Styled as _, div};
use util::ResultExt as _;
use workspace::{
Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
item::{BreadcrumbText, ItemEvent},
searchable::SearchableItemHandle,
};
use crate::session::running::stack_frame_list::{StackFrameList, StackFrameListEvent};
use anyhow::Result;
pub(crate) struct StackTraceView {
editor: Entity<Editor>,
multibuffer: Entity<MultiBuffer>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
stack_frame_list: Entity<StackFrameList>,
selected_stack_frame_id: Option<StackFrameId>,
highlights: Vec<(StackFrameId, Anchor)>,
excerpt_for_frames: collections::HashMap<ExcerptId, StackFrameId>,
refresh_task: Option<Task<Result<()>>>,
_subscription: Option<Subscription>,
}
impl StackTraceView {
pub(crate) fn new(
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
stack_frame_list: Entity<StackFrameList>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let editor = cx.new(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
editor.set_vertical_scroll_margin(5, cx);
editor
});
cx.subscribe_in(&editor, window, |this, editor, event, window, cx| {
if let EditorEvent::SelectionsChanged { local: true } = event {
let excerpt_id = editor.update(cx, |editor, cx| {
let position: Point = editor.selections.newest(cx).head();
editor
.snapshot(window, cx)
.buffer_snapshot
.excerpt_containing(position..position)
.map(|excerpt| excerpt.id())
});
if let Some(stack_frame_id) = excerpt_id
.and_then(|id| this.excerpt_for_frames.get(&id))
.filter(|id| Some(**id) != this.selected_stack_frame_id)
{
this.stack_frame_list.update(cx, |list, cx| {
list.go_to_stack_frame(*stack_frame_id, window, cx).detach();
});
}
}
})
.detach();
cx.subscribe_in(
&stack_frame_list,
window,
|this, stack_frame_list, event, window, cx| match event {
StackFrameListEvent::BuiltEntries => {
this.selected_stack_frame_id =
stack_frame_list.read(cx).opened_stack_frame_id();
this.update_excerpts(window, cx);
}
StackFrameListEvent::SelectedStackFrameChanged(selected_frame_id) => {
this.selected_stack_frame_id = Some(*selected_frame_id);
this.update_highlights(window, cx);
if let Some(frame_anchor) = this
.highlights
.iter()
.find(|(frame_id, _)| frame_id == selected_frame_id)
.map(|highlight| highlight.1)
{
this.editor.update(cx, |editor, cx| {
if frame_anchor.excerpt_id
!= editor.selections.newest_anchor().head().excerpt_id
{
let auto_scroll =
Some(Autoscroll::center().for_anchor(frame_anchor));
editor.change_selections(auto_scroll, window, cx, |selections| {
let selection_id = selections.new_selection_id();
let selection = Selection {
id: selection_id,
start: frame_anchor,
end: frame_anchor,
goal: SelectionGoal::None,
reversed: false,
};
selections.select_anchors(vec![selection]);
})
}
});
}
}
},
)
.detach();
let mut this = Self {
editor,
multibuffer,
workspace,
project,
excerpt_for_frames: HashMap::default(),
highlights: Vec::default(),
stack_frame_list,
selected_stack_frame_id: None,
refresh_task: None,
_subscription: None,
};
this.update_excerpts(window, cx);
this
}
fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.refresh_task.take();
self.editor.update(cx, |editor, cx| {
editor.clear_highlights::<DebugStackFrameLine>(cx)
});
let stack_frames = self
.stack_frame_list
.update(cx, |list, _| list.flatten_entries(false));
let frames_to_open: Vec<_> = stack_frames
.into_iter()
.filter_map(|frame| {
Some((
frame.id,
frame.line as u32 - 1,
StackFrameList::abs_path_from_stack_frame(&frame)?,
))
})
.collect();
self.multibuffer
.update(cx, |multi_buffer, cx| multi_buffer.clear(cx));
let task = cx.spawn_in(window, async move |this, cx| {
let mut to_highlights = Vec::default();
for (stack_frame_id, line, abs_path) in frames_to_open {
let (worktree, relative_path) = this
.update(cx, |this, cx| {
this.workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |this, cx| {
this.find_or_create_worktree(&abs_path, false, cx)
})
})
})??
.await?;
let project_path = ProjectPath {
worktree_id: worktree.read_with(cx, |tree, _| tree.id())?,
path: relative_path.into(),
};
if let Some(buffer) = this
.read_with(cx, |this, _| this.project.clone())?
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
.await
.log_err()
{
this.update(cx, |this, cx| {
this.multibuffer.update(cx, |multi_buffer, cx| {
let line_point = Point::new(line, 0);
let start_context = Self::heuristic_syntactic_expand(
&buffer.read(cx).snapshot(),
line_point,
);
// Users will want to see what happened before an active debug line in most cases
let range = ExcerptRange {
context: start_context..Point::new(line.saturating_add(1), 0),
primary: line_point..line_point,
};
multi_buffer.push_excerpts(buffer.clone(), vec![range], cx);
let line_anchor =
multi_buffer.buffer_point_to_anchor(&buffer, line_point, cx);
if let Some(line_anchor) = line_anchor {
this.excerpt_for_frames
.insert(line_anchor.excerpt_id, stack_frame_id);
to_highlights.push((stack_frame_id, line_anchor));
}
});
})
.ok();
}
}
this.update_in(cx, |this, window, cx| {
this.highlights = to_highlights;
this.update_highlights(window, cx);
})
.ok();
anyhow::Ok(())
});
self.refresh_task = Some(task);
}
fn update_highlights(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, _| {
editor.clear_row_highlights::<DebugStackFrameLine>()
});
let stack_frames = self
.stack_frame_list
.update(cx, |session, _| session.flatten_entries(false));
let active_idx = self
.selected_stack_frame_id
.and_then(|id| {
stack_frames
.iter()
.enumerate()
.find_map(|(idx, frame)| if frame.id == id { Some(idx) } else { None })
})
.unwrap_or(0);
self.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx).display_snapshot;
let first_color = cx.theme().colors().editor_debugger_active_line_background;
let color = first_color.opacity(0.5);
let mut is_first = true;
for (_, highlight) in self.highlights.iter().skip(active_idx) {
let position = highlight.to_point(&snapshot.buffer_snapshot);
let color = if is_first {
is_first = false;
first_color
} else {
color
};
let start = snapshot
.buffer_snapshot
.clip_point(Point::new(position.row, 0), Bias::Left);
let end = start + Point::new(1, 0);
let start = snapshot.buffer_snapshot.anchor_before(start);
let end = snapshot.buffer_snapshot.anchor_before(end);
editor.highlight_rows::<DebugStackFrameLine>(
start..end,
color,
RowHighlightOptions::default(),
cx,
);
}
})
}
fn heuristic_syntactic_expand(snapshot: &BufferSnapshot, selected_point: Point) -> Point {
let mut text_objects = snapshot.text_object_ranges(
selected_point..selected_point,
TreeSitterOptions::max_start_depth(4),
);
let mut start_position = text_objects
.find(|(_, obj)| matches!(obj, language::TextObject::AroundFunction))
.map(|(range, _)| snapshot.offset_to_point(range.start))
.map(|point| Point::new(point.row.max(selected_point.row.saturating_sub(8)), 0))
.unwrap_or(selected_point);
if start_position.row == selected_point.row {
start_position.row = start_position.row.saturating_sub(1);
}
start_position
}
}
impl Render for StackTraceView {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().size_full().child(self.editor.clone())
}
}
impl EventEmitter<EditorEvent> for StackTraceView {}
impl Focusable for StackTraceView {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.editor.focus_handle(cx)
}
}
impl Item for StackTraceView {
type Event = EditorEvent;
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
Editor::to_item_events(event, f)
}
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.editor
.update(cx, |editor, cx| editor.deactivated(window, cx));
}
fn navigate(
&mut self,
data: Box<dyn Any>,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
self.editor
.update(cx, |editor, cx| editor.navigate(data, window, cx))
}
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
Some("Stack Frame Viewer".into())
}
fn tab_content_text(&self, _detail: usize, _: &App) -> SharedString {
"Stack Frames".into()
}
fn for_each_project_item(
&self,
cx: &App,
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
) {
self.editor.for_each_project_item(cx, f)
}
fn is_singleton(&self, _: &App) -> bool {
false
}
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
});
}
fn is_dirty(&self, cx: &App) -> bool {
self.multibuffer.read(cx).is_dirty(cx)
}
fn has_deleted_file(&self, cx: &App) -> bool {
self.multibuffer.read(cx).has_deleted_file(cx)
}
fn has_conflict(&self, cx: &App) -> bool {
self.multibuffer.read(cx).has_conflict(cx)
}
fn can_save(&self, _: &App) -> bool {
true
}
fn save(
&mut self,
format: bool,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.save(format, project, window, cx)
}
fn save_as(
&mut self,
_: Entity<Project>,
_: ProjectPath,
_window: &mut Window,
_: &mut Context<Self>,
) -> Task<Result<()>> {
unreachable!()
}
fn reload(
&mut self,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.reload(project, window, cx)
}
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
} else {
None
}
}
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft
}
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
self.editor.breadcrumbs(theme, cx)
}
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.added_to_workspace(workspace, window, cx)
});
}
}

View File

@@ -1,7 +1,7 @@
use std::{path::Path, sync::Arc};
use dap::{Scope, StackFrame, Variable, requests::Variables};
use editor::{Editor, EditorMode, MultiBuffer};
use editor::{Editor, EditorMode, MultiBuffer, actions::ToggleInlineValues};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_python, tree_sitter_rust};
use project::{FakeFs, Project};
@@ -239,7 +239,11 @@ fn main() {
});
cx.run_until_parked();
editor.update(cx, |editor, cx| editor.refresh_inline_values(cx));
editor.update_in(cx, |editor, window, cx| {
if !editor.inline_values_enabled() {
editor.toggle_inline_values(&ToggleInlineValues, window, cx);
}
});
cx.run_until_parked();
@@ -1600,7 +1604,11 @@ def process_data(untyped_param, typed_param: int, another_typed: str):
)
});
editor.update(cx, |editor, cx| editor.refresh_inline_values(cx));
editor.update_in(cx, |editor, window, cx| {
if !editor.inline_values_enabled() {
editor.toggle_inline_values(&ToggleInlineValues, window, cx);
}
});
client.on_request::<dap::requests::Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {

View File

@@ -168,7 +168,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
.update(cx, |state, _| state.stack_frame_list().clone());
stack_frame_list.update(cx, |stack_frame_list, cx| {
assert_eq!(Some(1), stack_frame_list.opened_stack_frame_id());
assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
});
});
@@ -373,14 +373,14 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
.unwrap();
stack_frame_list.update(cx, |stack_frame_list, cx| {
assert_eq!(Some(1), stack_frame_list.opened_stack_frame_id());
assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
});
// select second stack frame
stack_frame_list
.update_in(cx, |stack_frame_list, window, cx| {
stack_frame_list.go_to_stack_frame(stack_frames[1].id, window, cx)
stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
})
.await
.unwrap();
@@ -388,7 +388,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
cx.run_until_parked();
stack_frame_list.update(cx, |stack_frame_list, cx| {
assert_eq!(Some(2), stack_frame_list.opened_stack_frame_id());
assert_eq!(Some(2), stack_frame_list.selected_stack_frame_id());
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
});
@@ -718,7 +718,11 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
stack_frame_list.entries()
);
stack_frame_list.expand_collapsed_entry(1);
stack_frame_list.expand_collapsed_entry(
1,
&vec![stack_frames[1].clone(), stack_frames[2].clone()],
cx,
);
assert_eq!(
&vec![
@@ -735,7 +739,11 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
stack_frame_list.entries()
);
stack_frame_list.expand_collapsed_entry(4);
stack_frame_list.expand_collapsed_entry(
4,
&vec![stack_frames[4].clone(), stack_frames[5].clone()],
cx,
);
assert_eq!(
&vec![

View File

@@ -190,7 +190,7 @@ async fn test_basic_fetch_initial_scope_and_variables(
running_state.update(cx, |running_state, cx| {
let (stack_frame_list, stack_frame_id) =
running_state.stack_frame_list().update(cx, |list, _| {
(list.flatten_entries(true), list.opened_stack_frame_id())
(list.flatten_entries(), list.selected_stack_frame_id())
});
assert_eq!(stack_frames, stack_frame_list);
@@ -431,7 +431,7 @@ async fn test_fetch_variables_for_multiple_scopes(
running_state.update(cx, |running_state, cx| {
let (stack_frame_list, stack_frame_id) =
running_state.stack_frame_list().update(cx, |list, _| {
(list.flatten_entries(true), list.opened_stack_frame_id())
(list.flatten_entries(), list.selected_stack_frame_id())
});
assert_eq!(Some(1), stack_frame_id);
@@ -1452,7 +1452,7 @@ async fn test_variable_list_only_sends_requests_when_rendering(
running_state.update(cx, |running_state, cx| {
let (stack_frame_list, stack_frame_id) =
running_state.stack_frame_list().update(cx, |list, _| {
(list.flatten_entries(true), list.opened_stack_frame_id())
(list.flatten_entries(), list.selected_stack_frame_id())
});
assert_eq!(Some(1), stack_frame_id);
@@ -1734,7 +1734,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
running_state.update(cx, |running_state, cx| {
let (stack_frame_list, stack_frame_id) =
running_state.stack_frame_list().update(cx, |list, _| {
(list.flatten_entries(true), list.opened_stack_frame_id())
(list.flatten_entries(), list.selected_stack_frame_id())
});
let variable_list = running_state.variable_list().read(cx);
@@ -1745,7 +1745,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
running_state
.stack_frame_list()
.read(cx)
.opened_stack_frame_id(),
.selected_stack_frame_id(),
Some(1)
);
@@ -1778,7 +1778,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
running_state
.stack_frame_list()
.update(cx, |stack_frame_list, cx| {
stack_frame_list.go_to_stack_frame(stack_frames[1].id, window, cx)
stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
})
})
.await
@@ -1789,7 +1789,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
running_state.update(cx, |running_state, cx| {
let (stack_frame_list, stack_frame_id) =
running_state.stack_frame_list().update(cx, |list, _| {
(list.flatten_entries(true), list.opened_stack_frame_id())
(list.flatten_entries(), list.selected_stack_frame_id())
});
let variable_list = running_state.variable_list().read(cx);

View File

@@ -450,7 +450,29 @@ impl CompletionsMenu {
window: &mut Window,
cx: &mut Context<Editor>,
) -> AnyElement {
let completions = self.completions.borrow_mut();
let show_completion_documentation = self.show_completion_documentation;
let widest_completion_ix = self
.entries
.borrow()
.iter()
.enumerate()
.max_by_key(|(_, mat)| {
let completion = &completions[mat.candidate_id];
let documentation = &completion.documentation;
let mut len = completion.label.text.chars().count();
if let Some(CompletionDocumentation::SingleLine(text)) = documentation {
if show_completion_documentation {
len += text.chars().count();
}
}
len
})
.map(|(ix, _)| ix);
drop(completions);
let selected_item = self.selected_item;
let completions = self.completions.clone();
let entries = self.entries.clone();
@@ -510,25 +532,22 @@ impl CompletionsMenu {
let completion_label = StyledText::new(completion.label.text.clone())
.with_default_highlights(&style.text, highlights);
let documentation_label = match documentation {
Some(CompletionDocumentation::SingleLine(text))
| Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
single_line: text,
..
}) => {
if text.trim().is_empty() {
None
} else {
Some(
Label::new(text.clone())
.ml_4()
.size(LabelSize::Small)
.color(Color::Muted),
)
}
let documentation_label = if let Some(
CompletionDocumentation::SingleLine(text),
) = documentation
{
if text.trim().is_empty() {
None
} else {
Some(
Label::new(text.clone())
.ml_4()
.size(LabelSize::Small)
.color(Color::Muted),
)
}
_ => None,
} else {
None
};
let start_slot = completion
@@ -577,8 +596,8 @@ impl CompletionsMenu {
.occlude()
.max_h(max_height_in_lines as f32 * window.line_height())
.track_scroll(self.scroll_handle.clone())
.with_sizing_behavior(ListSizingBehavior::Infer)
.w(rems(34.));
.with_width_from_item(widest_completion_ix)
.with_sizing_behavior(ListSizingBehavior::Infer);
Popover::new().child(list).into_any_element()
}
@@ -600,10 +619,6 @@ impl CompletionsMenu {
.as_ref()?
{
CompletionDocumentation::MultiLinePlainText(text) => div().child(text.clone()),
CompletionDocumentation::SingleLineAndMultiLinePlainText {
plain_text: Some(text),
..
} => div().child(text.clone()),
CompletionDocumentation::MultiLineMarkdown(parsed) if !parsed.is_empty() => {
let markdown = self.markdown_element.get_or_insert_with(|| {
cx.new(|cx| {
@@ -634,11 +649,6 @@ impl CompletionsMenu {
CompletionDocumentation::MultiLineMarkdown(_) => return None,
CompletionDocumentation::SingleLine(_) => return None,
CompletionDocumentation::Undocumented => return None,
CompletionDocumentation::SingleLineAndMultiLinePlainText {
plain_text: None, ..
} => {
return None;
}
};
Some(
@@ -1103,7 +1113,6 @@ impl CodeActionsMenu {
this.child(
h_flex()
.overflow_hidden()
.child("debug: ")
.child(scenario.label.clone())
.when(selected, |this| {
this.text_color(colors.text_accent)
@@ -1139,9 +1148,7 @@ impl CodeActionsMenu {
CodeActionsItem::CodeAction { action, .. } => {
action.lsp_action.title().chars().count()
}
CodeActionsItem::DebugScenario(scenario) => {
format!("debug: {}", scenario.label).chars().count()
}
CodeActionsItem::DebugScenario(scenario) => scenario.label.chars().count(),
})
.map(|(ix, _)| ix),
)

View File

@@ -122,11 +122,10 @@ use markdown::Markdown;
use mouse_context_menu::MouseContextMenu;
use persistence::DB;
use project::{
BreakpointWithPosition, ProjectPath,
ProjectPath,
debugger::{
breakpoint_store::{
BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
BreakpointStoreEvent,
BreakpointEditAction, BreakpointState, BreakpointStore, BreakpointStoreEvent,
},
session::{Session, SessionEvent},
},
@@ -199,7 +198,7 @@ use theme::{
};
use ui::{
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
IconSize, Indicator, Key, Tooltip, h_flex, prelude::*,
IconSize, Key, Tooltip, h_flex, prelude::*,
};
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
use workspace::{
@@ -290,7 +289,6 @@ impl InlayId {
}
pub enum ActiveDebugLine {}
pub enum DebugStackFrameLine {}
enum DocumentHighlightRead {}
enum DocumentHighlightWrite {}
enum InputComposition {}
@@ -5333,9 +5331,9 @@ impl Editor {
.map(SharedString::from)
})?;
dap_store.update(cx, |dap_store, cx| {
dap_store.update(cx, |this, cx| {
for (_, task) in &resolved_tasks.templates {
if let Some(scenario) = dap_store
if let Some(scenario) = this
.debug_scenario_for_build_task(
task.original_task().clone(),
debug_adapter.clone().into(),
@@ -5760,22 +5758,10 @@ impl Editor {
let cursor_position = newest_selection.head();
let (cursor_buffer, cursor_buffer_position) =
buffer.text_anchor_for_position(cursor_position, cx)?;
let (tail_buffer, tail_buffer_position) =
buffer.text_anchor_for_position(newest_selection.tail(), cx)?;
let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?;
if cursor_buffer != tail_buffer {
return None;
}
let snapshot = cursor_buffer.read(cx).snapshot();
let (start_word_range, _) = snapshot.surrounding_word(cursor_buffer_position);
let (end_word_range, _) = snapshot.surrounding_word(tail_buffer_position);
if start_word_range != end_word_range {
self.document_highlights_task.take();
self.clear_background_highlights::<DocumentHighlightRead>(cx);
self.clear_background_highlights::<DocumentHighlightWrite>(cx);
return None;
}
let debounce = EditorSettings::get_global(cx).lsp_highlight_debounce;
self.document_highlights_task = Some(cx.spawn(async move |this, cx| {
cx.background_executor()
@@ -6849,7 +6835,7 @@ impl Editor {
range: Range<DisplayRow>,
window: &mut Window,
cx: &mut Context<Self>,
) -> HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)> {
) -> HashMap<DisplayRow, (Anchor, Breakpoint)> {
let mut breakpoint_display_points = HashMap::default();
let Some(breakpoint_store) = self.breakpoint_store.clone() else {
@@ -6883,17 +6869,15 @@ impl Editor {
buffer_snapshot,
cx,
);
for (breakpoint, state) in breakpoints {
for (anchor, breakpoint) in breakpoints {
let multi_buffer_anchor =
Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), breakpoint.position);
Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), *anchor);
let position = multi_buffer_anchor
.to_point(&multi_buffer_snapshot)
.to_display_point(&snapshot);
breakpoint_display_points.insert(
position.row(),
(multi_buffer_anchor, breakpoint.bp.clone(), state),
);
breakpoint_display_points
.insert(position.row(), (multi_buffer_anchor, breakpoint.clone()));
}
}
@@ -7068,10 +7052,8 @@ impl Editor {
position: Anchor,
row: DisplayRow,
breakpoint: &Breakpoint,
state: Option<BreakpointSessionState>,
cx: &mut Context<Self>,
) -> IconButton {
let is_rejected = state.is_some_and(|s| !s.verified);
// Is it a breakpoint that shows up when hovering over gutter?
let (is_phantom, collides_with_existing) = self.gutter_breakpoint_indicator.0.map_or(
(false, false),
@@ -7097,8 +7079,6 @@ impl Editor {
let color = if is_phantom {
Color::Hint
} else if is_rejected {
Color::Disabled
} else {
Color::Debugger
};
@@ -7126,18 +7106,9 @@ impl Editor {
}
let primary_text = SharedString::from(primary_text);
let focus_handle = self.focus_handle.clone();
let meta = if is_rejected {
"No executable code is associated with this line."
} else {
"Right-click for more options."
};
IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
.icon_size(IconSize::XSmall)
.size(ui::ButtonSize::None)
.when(is_rejected, |this| {
this.indicator(Indicator::icon(Icon::new(IconName::Warning)).color(Color::Warning))
})
.icon_color(color)
.style(ButtonStyle::Transparent)
.on_click(cx.listener({
@@ -7169,7 +7140,14 @@ impl Editor {
);
}))
.tooltip(move |window, cx| {
Tooltip::with_meta_in(primary_text.clone(), None, meta, &focus_handle, window, cx)
Tooltip::with_meta_in(
primary_text.clone(),
None,
"Right-click for more options",
&focus_handle,
window,
cx,
)
})
}
@@ -7309,11 +7287,11 @@ impl Editor {
_style: &EditorStyle,
is_active: bool,
row: DisplayRow,
breakpoint: Option<(Anchor, Breakpoint, Option<BreakpointSessionState>)>,
breakpoint: Option<(Anchor, Breakpoint)>,
cx: &mut Context<Self>,
) -> IconButton {
let color = Color::Muted;
let position = breakpoint.as_ref().map(|(anchor, _, _)| *anchor);
let position = breakpoint.as_ref().map(|(anchor, _)| *anchor);
IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play)
.shape(ui::IconButtonShape::Square)
@@ -9493,16 +9471,16 @@ impl Editor {
cx,
)
.next()
.and_then(|(bp, _)| {
.and_then(|(anchor, bp)| {
let breakpoint_row = buffer_snapshot
.summary_for_anchor::<text::PointUtf16>(&bp.position)
.summary_for_anchor::<text::PointUtf16>(anchor)
.row;
if breakpoint_row == row {
snapshot
.buffer_snapshot
.anchor_in_excerpt(enclosing_excerpt, bp.position)
.map(|position| (position, bp.bp.clone()))
.anchor_in_excerpt(enclosing_excerpt, *anchor)
.map(|anchor| (anchor, bp.clone()))
} else {
None
}
@@ -9665,10 +9643,7 @@ impl Editor {
breakpoint_store.update(cx, |breakpoint_store, cx| {
breakpoint_store.toggle_breakpoint(
buffer,
BreakpointWithPosition {
position: breakpoint_position.text_anchor,
bp: breakpoint,
},
(breakpoint_position.text_anchor, breakpoint),
edit_action,
cx,
);
@@ -13893,10 +13868,7 @@ impl Editor {
Default::default(),
cx,
);
if self.buffer.read(cx).is_singleton() {
self.request_autoscroll(Autoscroll::center().for_anchor(start), cx);
}
self.request_autoscroll(Autoscroll::center().for_anchor(start), cx);
}
pub fn go_to_definition(
@@ -16902,7 +16874,6 @@ impl Editor {
handled = true;
self.clear_row_highlights::<ActiveDebugLine>();
self.go_to_line::<ActiveDebugLine>(
multibuffer_anchor,
Some(cx.theme().colors().editor_debugger_active_line_background),
@@ -17917,7 +17888,9 @@ impl Editor {
let Some(project) = self.project.clone() else {
return;
};
let Some(buffer) = self.buffer.read(cx).as_singleton() else {
return;
};
if !self.inline_value_cache.enabled {
let inlays = std::mem::take(&mut self.inline_value_cache.inlays);
self.splice_inlays(&inlays, Vec::new(), cx);
@@ -17930,25 +17903,20 @@ impl Editor {
.and_then(|lines| lines.last().map(|line| line.range.start));
self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| {
let snapshot = editor
.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
.ok()?;
let inline_values = editor
.update(cx, |editor, cx| {
.update(cx, |_, cx| {
let Some(current_execution_position) = current_execution_position else {
return Some(Task::ready(Ok(Vec::new())));
};
let buffer = editor.buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
let excerpt = snapshot.excerpt_containing(
current_execution_position..current_execution_position,
)?;
editor.buffer.read(cx).buffer(excerpt.buffer_id())
})?;
// todo(debugger) when introducing multi buffer inline values check execution position's buffer id to make sure the text
// anchor is in the same buffer
let range =
buffer.read(cx).anchor_before(0)..current_execution_position.text_anchor;
project.inline_values(buffer, range, cx)
})
.ok()
@@ -17957,40 +17925,22 @@ impl Editor {
.context("refreshing debugger inlays")
.log_err()?;
let mut buffer_inline_values: HashMap<BufferId, Vec<InlayHint>> = HashMap::default();
for (buffer_id, inline_value) in inline_values
.into_iter()
.filter_map(|hint| Some((hint.position.buffer_id?, hint)))
{
buffer_inline_values
.entry(buffer_id)
.or_default()
.push(inline_value);
}
let (excerpt_id, buffer_id) = snapshot
.excerpts()
.next()
.map(|excerpt| (excerpt.0, excerpt.1.remote_id()))?;
editor
.update(cx, |editor, cx| {
let snapshot = editor.buffer.read(cx).snapshot(cx);
let mut new_inlays = Vec::default();
for (excerpt_id, buffer_snapshot, _) in snapshot.excerpts() {
let buffer_id = buffer_snapshot.remote_id();
buffer_inline_values
.get(&buffer_id)
.into_iter()
.flatten()
.for_each(|hint| {
let inlay = Inlay::debugger_hint(
post_inc(&mut editor.next_inlay_id),
Anchor::in_buffer(excerpt_id, buffer_id, hint.position),
hint.text(),
);
new_inlays.push(inlay);
});
}
let new_inlays = inline_values
.into_iter()
.map(|debugger_value| {
Inlay::debugger_hint(
post_inc(&mut editor.next_inlay_id),
Anchor::in_buffer(excerpt_id, buffer_id, debugger_value.position),
debugger_value.text(),
)
})
.collect::<Vec<_>>();
let mut inlay_ids = new_inlays.iter().map(|inlay| inlay.id).collect();
std::mem::swap(&mut editor.inline_value_cache.inlays, &mut inlay_ids);
@@ -19910,15 +19860,9 @@ fn snippet_completions(
filter_range: 0..matching_prefix.len(),
},
icon_path: None,
documentation: Some(
CompletionDocumentation::SingleLineAndMultiLinePlainText {
single_line: snippet.name.clone().into(),
plain_text: snippet
.description
.clone()
.map(|description| description.into()),
},
),
documentation: snippet.description.clone().map(|description| {
CompletionDocumentation::SingleLine(description.into())
}),
insert_text_mode: None,
confirm: None,
})

View File

@@ -6,8 +6,6 @@ use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, VsCodeSettings};
use util::serde::default_true;
/// Imports from the VSCode settings at
/// https://code.visualstudio.com/docs/reference/default-settings
#[derive(Deserialize, Clone)]
pub struct EditorSettings {
pub cursor_blink: bool,
@@ -334,7 +332,6 @@ pub enum SnippetSortOrder {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct EditorSettingsContent {
/// Whether the cursor blinks in the editor.
///
@@ -371,9 +368,9 @@ pub struct EditorSettingsContent {
///
/// Default: true
pub hover_popover_enabled: Option<bool>,
/// Time to wait in milliseconds before showing the informational hover box.
/// Time to wait before showing the informational hover box
///
/// Default: 300
/// Default: 350
pub hover_popover_delay: Option<u64>,
/// Toolbar related settings
pub toolbar: Option<ToolbarContent>,
@@ -541,7 +538,7 @@ pub struct ScrollbarContent {
}
/// Minimap related settings
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct MinimapContent {
/// When to show the minimap in the editor.
///
@@ -772,32 +769,5 @@ impl Settings for EditorSettings {
let search = current.search.get_or_insert_default();
search.include_ignored = use_ignored;
}
let mut minimap = MinimapContent::default();
let minimap_enabled = vscode.read_bool("editor.minimap.enabled").unwrap_or(true);
let autohide = vscode.read_bool("editor.minimap.autohide");
if minimap_enabled {
if let Some(false) = autohide {
minimap.show = Some(ShowMinimap::Always);
} else {
minimap.show = Some(ShowMinimap::Auto);
}
} else {
minimap.show = Some(ShowMinimap::Never);
}
vscode.enum_setting(
"editor.minimap.showSlider",
&mut minimap.thumb,
|s| match s {
"always" => Some(MinimapThumb::Always),
"mouseover" => Some(MinimapThumb::Hover),
_ => None,
},
);
if minimap != MinimapContent::default() {
current.minimap = Some(minimap)
}
}
}

View File

@@ -16520,7 +16520,7 @@ async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
assert_indent_guides(
0..6,
vec![
indent_guide(buffer_id, 1, 5, 0),
indent_guide(buffer_id, 1, 6, 0),
indent_guide(buffer_id, 3, 4, 1),
],
None,
@@ -18522,7 +18522,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
@@ -18547,7 +18547,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
@@ -18569,7 +18569,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
@@ -18636,7 +18636,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
@@ -18657,7 +18657,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
@@ -18677,7 +18677,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
@@ -18700,7 +18700,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
@@ -18723,7 +18723,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
@@ -18816,7 +18816,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
@@ -18848,7 +18848,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});
@@ -18884,7 +18884,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
.all_breakpoints(cx)
.clone()
});

View File

@@ -62,7 +62,7 @@ use multi_buffer::{
use project::{
ProjectPath,
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
debugger::breakpoint_store::Breakpoint,
project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings},
};
use settings::Settings;
@@ -2320,7 +2320,7 @@ impl EditorElement {
gutter_hitbox: &Hitbox,
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
snapshot: &EditorSnapshot,
breakpoints: HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
breakpoints: HashMap<DisplayRow, (Anchor, Breakpoint)>,
row_infos: &[RowInfo],
window: &mut Window,
cx: &mut App,
@@ -2328,7 +2328,7 @@ impl EditorElement {
self.editor.update(cx, |editor, cx| {
breakpoints
.into_iter()
.filter_map(|(display_row, (text_anchor, bp, state))| {
.filter_map(|(display_row, (text_anchor, bp))| {
if row_infos
.get((display_row.0.saturating_sub(range.start.0)) as usize)
.is_some_and(|row_info| {
@@ -2351,7 +2351,7 @@ impl EditorElement {
return None;
}
let button = editor.render_breakpoint(text_anchor, display_row, &bp, state, cx);
let button = editor.render_breakpoint(text_anchor, display_row, &bp, cx);
let button = prepaint_gutter_button(
button,
@@ -2381,7 +2381,7 @@ impl EditorElement {
gutter_hitbox: &Hitbox,
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
snapshot: &EditorSnapshot,
breakpoints: &mut HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
breakpoints: &mut HashMap<DisplayRow, (Anchor, Breakpoint)>,
window: &mut Window,
cx: &mut App,
) -> Vec<AnyElement> {
@@ -4451,7 +4451,7 @@ impl EditorElement {
let target_y = selection_row.as_f32() * line_height - scroll_pixel_position.y;
let target_point = content_origin + point(target_x, target_y);
let actual_size = element.layout_as_root(Size::<AvailableSpace>::default(), window, cx);
let actual_size = element.layout_as_root(max_size.into(), window, cx);
let overall_height = actual_size.height + HOVER_POPOVER_GAP;
let popover_origin = if target_point.y > overall_height {
@@ -7454,10 +7454,8 @@ impl Element for EditorElement {
editor.active_breakpoints(start_row..end_row, window, cx)
});
if cx.has_flag::<DebuggerFeatureFlag>() {
for (display_row, (_, bp, state)) in &breakpoint_rows {
if bp.is_enabled() && state.is_none_or(|s| s.verified) {
active_rows.entry(*display_row).or_default().breakpoint = true;
}
for display_row in breakpoint_rows.keys() {
active_rows.entry(*display_row).or_default().breakpoint = true;
}
}
@@ -7497,7 +7495,7 @@ impl Element for EditorElement {
let breakpoint = Breakpoint::new_standard();
phantom_breakpoint.collides_with_existing_breakpoint =
false;
(position, breakpoint, None)
(position, breakpoint)
});
}
})

View File

@@ -30,7 +30,6 @@ chrono.workspace = true
clap.workspace = true
client.workspace = true
collections.workspace = true
debug_adapter_extension.workspace = true
dirs.workspace = true
dotenv.workspace = true
env_logger.workspace = true

View File

@@ -422,7 +422,6 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
let extension_host_proxy = ExtensionHostProxy::global(cx);
language::init(cx);
debug_adapter_extension::init(extension_host_proxy.clone(), cx);
language_extension::init(extension_host_proxy.clone(), languages.clone());
language_model::init(client.clone(), cx);
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
@@ -712,9 +711,9 @@ fn print_report(
.values()
.flat_map(|results| {
results.iter().map(|(example, _)| {
let absolute_path = run_dir.join(example.run_directory.join("last.messages.json"));
let cwd = std::env::current_dir().expect("Can't get current dir");
pathdiff::diff_paths(&absolute_path, cwd).unwrap_or_else(|| absolute_path.clone())
let absolute_path = example.run_directory.join("last.messages.json");
pathdiff::diff_paths(&absolute_path, run_dir)
.unwrap_or_else(|| absolute_path.clone())
})
})
.collect::<Vec<_>>();

View File

@@ -48,7 +48,6 @@ pub struct ExampleMetadata {
pub language_server: Option<LanguageServer>,
pub max_assertions: Option<usize>,
pub profile_id: AgentProfileId,
pub existing_thread_json: Option<String>,
}
#[derive(Clone, Debug)]
@@ -233,10 +232,6 @@ impl ExampleContext {
Ok(StopReason::MaxTokens) => {
tx.try_send(Err(anyhow!("Exceeded maximum tokens"))).ok();
}
Ok(StopReason::Refusal) => {
tx.try_send(Err(anyhow!("Model refused to generate content")))
.ok();
}
Err(err) => {
tx.try_send(Err(anyhow!(err.clone()))).ok();
}
@@ -482,16 +477,12 @@ impl Response {
tool_name: &'static str,
cx: &mut ExampleContext,
) -> Result<&ToolUse> {
let result = self.find_tool_call(tool_name);
cx.assert_some(result, format!("called `{}`", tool_name))
}
pub fn find_tool_call(&self, tool_name: &str) -> Option<&ToolUse> {
self.messages.iter().rev().find_map(|msg| {
let result = self.messages.iter().find_map(|msg| {
msg.tool_use
.iter()
.find(|tool_use| tool_use.name == tool_name)
})
});
cx.assert_some(result, format!("called `{}`", tool_name))
}
#[allow(dead_code)]

View File

@@ -21,7 +21,6 @@ impl Example for AddArgToTraitMethod {
}),
max_assertions: None,
profile_id: AgentProfileId::default(),
existing_thread_json: None,
}
}

View File

@@ -22,7 +22,6 @@ impl Example for CodeBlockCitations {
}),
max_assertions: None,
profile_id: AgentProfileId::default(),
existing_thread_json: None,
}
}

Some files were not shown because too many files have changed in this diff Show More