Compare commits
53 Commits
simplify-e
...
v0.187.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18d383fcbb | ||
|
|
2a36b0dfd0 | ||
|
|
69f5d524b2 | ||
|
|
9743d2aa9c | ||
|
|
db270f5cbe | ||
|
|
eb412076f5 | ||
|
|
a4cc0941cf | ||
|
|
9b3808b842 | ||
|
|
c64c6dfd4b | ||
|
|
60e570ee44 | ||
|
|
abb7309d45 | ||
|
|
a4056fdf93 | ||
|
|
aa87bd4b58 | ||
|
|
66bd1f0ca8 | ||
|
|
b4c6508aa7 | ||
|
|
f6f18968b8 | ||
|
|
7aefcf0554 | ||
|
|
3c4cb93d72 | ||
|
|
ff435e1b2f | ||
|
|
339c1d0f34 | ||
|
|
2529cc7a16 | ||
|
|
2c4b30faee | ||
|
|
80651c4cff | ||
|
|
a6e579db72 | ||
|
|
a01127904f | ||
|
|
9b75288558 | ||
|
|
b2edefd6e1 | ||
|
|
f810584d19 | ||
|
|
ac2afad2cb | ||
|
|
a69b020339 | ||
|
|
5570248fa7 | ||
|
|
1c999287e2 | ||
|
|
31e3cd3726 | ||
|
|
96c3fb7d67 | ||
|
|
af4d39efce | ||
|
|
b2f32c53c9 | ||
|
|
186660ed20 | ||
|
|
6b0d58da9d | ||
|
|
853b70687d | ||
|
|
75b8203566 | ||
|
|
1d0b4df24f | ||
|
|
2c861184fa | ||
|
|
7c0c5bd516 | ||
|
|
8f9b217f79 | ||
|
|
de30643e74 | ||
|
|
235fd06adc | ||
|
|
c408200f9b | ||
|
|
53faf0d78d | ||
|
|
f2050dfe2b | ||
|
|
4cfc49e2d4 | ||
|
|
72426a9608 | ||
|
|
1c638a1309 | ||
|
|
249597a4a8 |
@@ -2,16 +2,14 @@
|
||||
{
|
||||
"label": "Debug Zed (CodeLLDB)",
|
||||
"adapter": "CodeLLDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
"program": "target/debug/zed",
|
||||
"request": "launch"
|
||||
},
|
||||
{
|
||||
"label": "Debug Zed (GDB)",
|
||||
"adapter": "GDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"program": "target/debug/zed",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"initialize_args": {
|
||||
"stopAtBeginningOfMainSubprogram": true
|
||||
}
|
||||
|
||||
31
Cargo.lock
generated
31
Cargo.lock
generated
@@ -682,6 +682,7 @@ dependencies = [
|
||||
"portable-pty",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest_client",
|
||||
@@ -4366,6 +4367,15 @@ 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"
|
||||
@@ -4953,6 +4963,7 @@ dependencies = [
|
||||
"clap",
|
||||
"client",
|
||||
"collections",
|
||||
"debug_adapter_extension",
|
||||
"dirs 4.0.0",
|
||||
"dotenv",
|
||||
"env_logger 0.11.8",
|
||||
@@ -7699,6 +7710,7 @@ dependencies = [
|
||||
"clock",
|
||||
"collections",
|
||||
"ctor",
|
||||
"diffy",
|
||||
"ec4rs",
|
||||
"env_logger 0.11.8",
|
||||
"fs",
|
||||
@@ -9169,6 +9181,15 @@ 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"
|
||||
@@ -11861,6 +11882,7 @@ dependencies = [
|
||||
"clock",
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
"debug_adapter_extension",
|
||||
"env_logger 0.11.8",
|
||||
"extension",
|
||||
"extension_host",
|
||||
@@ -15302,7 +15324,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"nu-ansi-term 0.46.0",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -18540,7 +18562,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.187.0"
|
||||
version = "0.187.8"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
@@ -18572,6 +18594,7 @@ dependencies = [
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
"db",
|
||||
"debug_adapter_extension",
|
||||
"debugger_tools",
|
||||
"debugger_ui",
|
||||
"diagnostics",
|
||||
@@ -18733,9 +18756,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16d993fc42f9ec43ab76fa46c6eb579a66e116bb08cd2bc9a67f3afcaa05d39d"
|
||||
checksum = "9be71e2f9b271e1eb8eb3e0d986075e770d1a0a299fb036abc3f1fc13a2fa7eb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
|
||||
@@ -608,7 +608,7 @@ wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.8.1"
|
||||
zed_llm_client = "0.8.2"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
|
||||
@@ -538,7 +538,6 @@
|
||||
"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",
|
||||
@@ -929,6 +928,7 @@
|
||||
"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"],
|
||||
|
||||
@@ -608,7 +608,6 @@
|
||||
"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 }],
|
||||
@@ -1012,7 +1011,7 @@
|
||||
"alt-right": ["terminal::SendText", "\u001bf"],
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
"alt-.": ["terminal::SendText", "\u001b."],
|
||||
"ctrl-delete": ["terminal::SendText", "\u001bd"],
|
||||
// There are conflicting bindings for these keys in the global context.
|
||||
// these bindings override them, remove at your own risk:
|
||||
"up": ["terminal::SendKeystroke", "up"],
|
||||
|
||||
@@ -51,9 +51,7 @@
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"f3": "editor::FindNextMatch",
|
||||
"shift-f3": "editor::FindPreviousMatch"
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -53,9 +53,7 @@
|
||||
"cmd-shift-j": "editor::JoinLines",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"cmd-g": "editor::FindNextMatch",
|
||||
"cmd-shift-g": "editor::FindPreviousMatch"
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -230,11 +230,11 @@
|
||||
// Possible values:
|
||||
// - "off" — no diagnostics are allowed
|
||||
// - "error"
|
||||
// - "warning" (default)
|
||||
// - "warning"
|
||||
// - "info"
|
||||
// - "hint"
|
||||
// - null — allow all diagnostics
|
||||
"diagnostics_max_severity": "warning",
|
||||
// - null — allow all diagnostics (default)
|
||||
"diagnostics_max_severity": null,
|
||||
// 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
|
||||
|
||||
@@ -383,18 +383,25 @@ 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.to_string_lossy().to_string())
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new(parent.to_string_lossy().to_string())
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.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,
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(path_range.path.to_string_lossy().to_string())
|
||||
@@ -404,7 +411,7 @@ fn render_markdown_code_block(
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id(("code-block-header-label", ix))
|
||||
.id(("code-block-header-button", ix))
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.px_1()
|
||||
@@ -412,7 +419,6 @@ 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()
|
||||
@@ -462,10 +468,87 @@ 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()
|
||||
.py_1()
|
||||
.pl_1p5()
|
||||
.pr_1()
|
||||
.relative()
|
||||
.p_1()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
@@ -473,79 +556,7 @@ fn render_markdown_code_block(
|
||||
.bg(codeblock_header_bg)
|
||||
.rounded_t_md()
|
||||
.children(label)
|
||||
.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();
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
);
|
||||
.child(control_buttons);
|
||||
|
||||
v_flex()
|
||||
.group(CODEBLOCK_CONTAINER_GROUP)
|
||||
|
||||
@@ -85,6 +85,7 @@ actions!(
|
||||
KeepAll,
|
||||
Follow,
|
||||
ResetTrialUpsell,
|
||||
ResetTrialEndUpsell,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -1348,6 +1348,7 @@ 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 => {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::kvp::{Dismissable, 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, ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleContextPicker,
|
||||
ToggleNavigationMenu, ToggleOptionsMenu,
|
||||
OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, TextThreadStore, ThreadEvent,
|
||||
ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
|
||||
};
|
||||
|
||||
const AGENT_PANEL_KEY: &str = "agent_panel";
|
||||
@@ -157,7 +157,10 @@ pub fn init(cx: &mut App) {
|
||||
window.refresh();
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
|
||||
set_trial_upsell_dismissed(false, cx);
|
||||
TrialUpsell::set_dismissed(false, cx);
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
|
||||
TrialEndUpsell::set_dismissed(false, cx);
|
||||
});
|
||||
},
|
||||
)
|
||||
@@ -1911,12 +1914,23 @@ 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 || dismissed_trial_upsell() {
|
||||
if self.hide_trial_upsell || TrialUpsell::dismissed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1962,125 +1976,115 @@ impl AgentPanel {
|
||||
move |toggle_state, _window, cx| {
|
||||
let toggle_state_bool = toggle_state.selected();
|
||||
|
||||
set_trial_upsell_dismissed(toggle_state_bool, cx);
|
||||
TrialUpsell::set_dismissed(toggle_state_bool, cx);
|
||||
},
|
||||
);
|
||||
|
||||
Some(
|
||||
div().p_2().child(
|
||||
v_flex()
|
||||
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()
|
||||
.w_full()
|
||||
.elevation_2(cx)
|
||||
.rounded(px(8.))
|
||||
.bg(cx.theme().colors().background.alpha(0.5))
|
||||
.p(px(3.))
|
||||
|
||||
.px_neg_1()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(h_flex().items_center().gap_1().child(checkbox))
|
||||
.child(
|
||||
div()
|
||||
h_flex()
|
||||
.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)))
|
||||
)
|
||||
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();
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.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(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))
|
||||
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())
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.px_neg_1()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(h_flex().items_center().gap_1().child(checkbox))
|
||||
.gap_2()
|
||||
.child(
|
||||
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))
|
||||
}),
|
||||
),
|
||||
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))
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -2088,6 +2092,91 @@ 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,
|
||||
@@ -2805,6 +2894,7 @@ 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()
|
||||
@@ -2992,25 +3082,14 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
const DISMISSED_TRIAL_UPSELL_KEY: &str = "dismissed-trial-upsell";
|
||||
struct TrialUpsell;
|
||||
|
||||
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())
|
||||
impl Dismissable for TrialUpsell {
|
||||
const KEY: &'static str = "dismissed-trial-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
|
||||
}
|
||||
})
|
||||
struct TrialEndUpsell;
|
||||
|
||||
impl Dismissable for TrialEndUpsell {
|
||||
const KEY: &'static str = "dismissed-trial-end-upsell";
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ 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,
|
||||
@@ -33,7 +34,6 @@ 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 dismissed_rate_limit_notice() {
|
||||
if RateLimitNotice::dismissed() {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
@@ -734,7 +734,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
ui::ToggleState::Selected => true,
|
||||
};
|
||||
|
||||
set_rate_limit_notice_dismissed(is_dismissed, cx)
|
||||
RateLimitNotice::set_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
|
||||
&& !dismissed_rate_limit_notice()
|
||||
&& !RateLimitNotice::dismissed()
|
||||
{
|
||||
self.show_rate_limit_notice = true;
|
||||
cx.notify();
|
||||
@@ -1180,27 +1180,10 @@ impl PromptEditor<TerminalCodegen> {
|
||||
}
|
||||
}
|
||||
|
||||
const DISMISSED_RATE_LIMIT_NOTICE_KEY: &str = "dismissed-rate-limit-notice";
|
||||
struct RateLimitNotice;
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
impl Dismissable for RateLimitNotice {
|
||||
const KEY: &'static str = "dismissed-rate-limit-notice";
|
||||
}
|
||||
|
||||
pub enum CodegenStatus {
|
||||
|
||||
@@ -1680,6 +1680,43 @@ 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| {
|
||||
|
||||
@@ -425,16 +425,17 @@ impl ToolUseState {
|
||||
|
||||
let content = match tool_result {
|
||||
ToolResultContent::Text(text) => {
|
||||
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
|
||||
|
||||
LanguageModelToolResultContent::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
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
};
|
||||
LanguageModelToolResultContent::Text(text.into())
|
||||
}
|
||||
ToolResultContent::Image(language_model_image) => {
|
||||
if language_model_image.estimate_tokens() < tool_output_limit {
|
||||
|
||||
@@ -34,7 +34,6 @@ 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(
|
||||
@@ -42,6 +41,21 @@ 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")]
|
||||
@@ -89,6 +103,14 @@ 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"))
|
||||
}
|
||||
@@ -96,6 +118,10 @@ 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",
|
||||
@@ -110,6 +136,8 @@ 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",
|
||||
@@ -122,6 +150,10 @@ 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",
|
||||
@@ -137,7 +169,11 @@ impl Model {
|
||||
|
||||
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
@@ -156,7 +192,11 @@ impl Model {
|
||||
|
||||
pub fn max_token_count(&self) -> usize {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
@@ -173,7 +213,11 @@ impl Model {
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
| Self::Claude3_5Haiku => 8_192,
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking => 8_192,
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
} => max_output_tokens.unwrap_or(4_096),
|
||||
@@ -182,7 +226,11 @@ impl Model {
|
||||
|
||||
pub fn default_temperature(&self) -> f32 {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
| Self::Claude3_5Haiku
|
||||
@@ -201,10 +249,14 @@ 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 => AnthropicModelMode::Thinking {
|
||||
Self::Claude3_7SonnetThinking
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4Thinking => AnthropicModelMode::Thinking {
|
||||
budget_tokens: Some(4_096),
|
||||
},
|
||||
Self::Custom { mode, .. } => mode.clone(),
|
||||
|
||||
@@ -2204,6 +2204,7 @@ impl AssistantContext {
|
||||
StopReason::ToolUse => {}
|
||||
StopReason::EndTurn => {}
|
||||
StopReason::MaxTokens => {}
|
||||
StopReason::Refusal => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -49,6 +49,37 @@ 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())
|
||||
@@ -60,36 +91,21 @@ 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 base_text;
|
||||
let status;
|
||||
let diff_base;
|
||||
let unreviewed_changes;
|
||||
if is_created {
|
||||
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,
|
||||
};
|
||||
diff_base = Rope::default();
|
||||
unreviewed_changes = Patch::new(vec![Edit {
|
||||
old: 0..1,
|
||||
new: 0..text_snapshot.max_point().row + 1,
|
||||
}])
|
||||
} else {
|
||||
base_text = buffer.read(cx).as_rope().clone();
|
||||
status = TrackedBufferStatus::Modified;
|
||||
diff_base = buffer.read(cx).as_rope().clone();
|
||||
unreviewed_changes = Patch::default();
|
||||
}
|
||||
TrackedBuffer {
|
||||
buffer: buffer.clone(),
|
||||
base_text,
|
||||
diff_base,
|
||||
unreviewed_changes,
|
||||
snapshot: text_snapshot.clone(),
|
||||
status,
|
||||
@@ -184,7 +200,7 @@ impl ActionLog {
|
||||
.context("buffer not tracked")?;
|
||||
|
||||
let rebase = cx.background_spawn({
|
||||
let mut base_text = tracked_buffer.base_text.clone();
|
||||
let mut base_text = tracked_buffer.diff_base.clone();
|
||||
let old_snapshot = tracked_buffer.snapshot.clone();
|
||||
let new_snapshot = buffer_snapshot.clone();
|
||||
let unreviewed_changes = tracked_buffer.unreviewed_changes.clone();
|
||||
@@ -210,7 +226,7 @@ impl ActionLog {
|
||||
))
|
||||
})??;
|
||||
|
||||
let (new_base_text, new_base_text_rope) = rebase.await;
|
||||
let (new_base_text, new_diff_base) = rebase.await;
|
||||
let diff_snapshot = BufferDiff::update_diff(
|
||||
diff.clone(),
|
||||
buffer_snapshot.clone(),
|
||||
@@ -229,24 +245,23 @@ impl ActionLog {
|
||||
.background_spawn({
|
||||
let diff_snapshot = diff_snapshot.clone();
|
||||
let buffer_snapshot = buffer_snapshot.clone();
|
||||
let new_base_text_rope = new_base_text_rope.clone();
|
||||
let new_diff_base = new_diff_base.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_base_text_rope
|
||||
let old_range = new_diff_base
|
||||
.offset_to_point(hunk.diff_base_byte_range.start)
|
||||
..new_base_text_rope
|
||||
.offset_to_point(hunk.diff_base_byte_range.end);
|
||||
..new_diff_base.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_base_text_rope,
|
||||
&new_diff_base,
|
||||
&buffer_snapshot.as_rope(),
|
||||
));
|
||||
}
|
||||
@@ -264,7 +279,7 @@ impl ActionLog {
|
||||
.tracked_buffers
|
||||
.get_mut(&buffer)
|
||||
.context("buffer not tracked")?;
|
||||
tracked_buffer.base_text = new_base_text_rope;
|
||||
tracked_buffer.diff_base = new_diff_base;
|
||||
tracked_buffer.snapshot = buffer_snapshot;
|
||||
tracked_buffer.unreviewed_changes = unreviewed_changes;
|
||||
cx.notify();
|
||||
@@ -283,7 +298,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -346,11 +360,11 @@ impl ActionLog {
|
||||
true
|
||||
} else {
|
||||
let old_range = tracked_buffer
|
||||
.base_text
|
||||
.diff_base
|
||||
.point_to_offset(Point::new(edit.old.start, 0))
|
||||
..tracked_buffer.base_text.point_to_offset(cmp::min(
|
||||
..tracked_buffer.diff_base.point_to_offset(cmp::min(
|
||||
Point::new(edit.old.end, 0),
|
||||
tracked_buffer.base_text.max_point(),
|
||||
tracked_buffer.diff_base.max_point(),
|
||||
));
|
||||
let new_range = tracked_buffer
|
||||
.snapshot
|
||||
@@ -359,7 +373,7 @@ impl ActionLog {
|
||||
Point::new(edit.new.end, 0),
|
||||
tracked_buffer.snapshot.max_point(),
|
||||
));
|
||||
tracked_buffer.base_text.replace(
|
||||
tracked_buffer.diff_base.replace(
|
||||
old_range,
|
||||
&tracked_buffer
|
||||
.snapshot
|
||||
@@ -417,7 +431,7 @@ impl ActionLog {
|
||||
}
|
||||
TrackedBufferStatus::Deleted => {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_text(tracked_buffer.base_text.to_string(), cx)
|
||||
buffer.set_text(tracked_buffer.diff_base.to_string(), cx)
|
||||
});
|
||||
let save = self
|
||||
.project
|
||||
@@ -464,14 +478,14 @@ impl ActionLog {
|
||||
|
||||
if revert {
|
||||
let old_range = tracked_buffer
|
||||
.base_text
|
||||
.diff_base
|
||||
.point_to_offset(Point::new(edit.old.start, 0))
|
||||
..tracked_buffer.base_text.point_to_offset(cmp::min(
|
||||
..tracked_buffer.diff_base.point_to_offset(cmp::min(
|
||||
Point::new(edit.old.end, 0),
|
||||
tracked_buffer.base_text.max_point(),
|
||||
tracked_buffer.diff_base.max_point(),
|
||||
));
|
||||
let old_text = tracked_buffer
|
||||
.base_text
|
||||
.diff_base
|
||||
.chunks_in_range(old_range)
|
||||
.collect::<String>();
|
||||
edits_to_revert.push((new_range, old_text));
|
||||
@@ -492,7 +506,7 @@ impl ActionLog {
|
||||
TrackedBufferStatus::Deleted => false,
|
||||
_ => {
|
||||
tracked_buffer.unreviewed_changes.clear();
|
||||
tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone();
|
||||
tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone();
|
||||
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
|
||||
true
|
||||
}
|
||||
@@ -655,7 +669,7 @@ enum TrackedBufferStatus {
|
||||
|
||||
struct TrackedBuffer {
|
||||
buffer: Entity<Buffer>,
|
||||
base_text: Rope,
|
||||
diff_base: Rope,
|
||||
unreviewed_changes: Patch<u32>,
|
||||
status: TrackedBufferStatus,
|
||||
version: clock::Global,
|
||||
@@ -1094,6 +1108,86 @@ 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);
|
||||
@@ -1601,7 +1695,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.base_text.clone();
|
||||
let mut old_text = tracked_buffer.diff_base.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));
|
||||
|
||||
@@ -42,6 +42,7 @@ 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
|
||||
|
||||
@@ -24,6 +24,7 @@ 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 {
|
||||
@@ -543,6 +544,11 @@ 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::{
|
||||
ReadFileToolInput,
|
||||
edit_file_tool::{EditFileMode, EditFileToolInput},
|
||||
grep_tool::GrepToolInput,
|
||||
list_directory_tool::ListDirectoryToolInput,
|
||||
};
|
||||
use Role::*;
|
||||
use anyhow::anyhow;
|
||||
@@ -18,6 +19,7 @@ use language_model::{
|
||||
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_store::{ModelContext, ProjectContext, PromptBuilder, WorktreeContext};
|
||||
use rand::prelude::*;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use serde_json::json;
|
||||
@@ -32,21 +34,39 @@ 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 output_file_content = include_str!("evals/fixtures/extract_handle_command_output/after.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 edit_description = "Extract `handle_command_output` method from `run_git_blame`.";
|
||||
eval(
|
||||
100,
|
||||
0.95,
|
||||
EvalInput {
|
||||
conversation: vec![
|
||||
0.7, // Taking the lower bar for Gemini
|
||||
EvalInput::from_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`.
|
||||
"})],
|
||||
@@ -80,11 +100,9 @@ fn eval_extract_handle_command_output() {
|
||||
)],
|
||||
),
|
||||
],
|
||||
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),
|
||||
},
|
||||
Some(input_file_content.into()),
|
||||
EvalAssertion::assert_diff_any(possible_diffs),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,8 +116,8 @@ fn eval_delete_run_git_blame() {
|
||||
eval(
|
||||
100,
|
||||
0.95,
|
||||
EvalInput {
|
||||
conversation: vec![
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
message(
|
||||
User,
|
||||
[text(formatdoc! {"
|
||||
@@ -136,11 +154,9 @@ fn eval_delete_run_git_blame() {
|
||||
)],
|
||||
),
|
||||
],
|
||||
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),
|
||||
},
|
||||
Some(input_file_content.into()),
|
||||
EvalAssertion::assert_eq(output_file_content),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,8 +169,8 @@ fn eval_translate_doc_comments() {
|
||||
eval(
|
||||
200,
|
||||
1.,
|
||||
EvalInput {
|
||||
conversation: vec![
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
message(
|
||||
User,
|
||||
[text(formatdoc! {"
|
||||
@@ -191,11 +207,9 @@ fn eval_translate_doc_comments() {
|
||||
)],
|
||||
),
|
||||
],
|
||||
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"),
|
||||
},
|
||||
Some(input_file_content.into()),
|
||||
EvalAssertion::judge_diff("Doc comments were translated to Italian"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,8 +223,8 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
|
||||
eval(
|
||||
100,
|
||||
0.95,
|
||||
EvalInput {
|
||||
conversation: vec![
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
message(
|
||||
User,
|
||||
[text(formatdoc! {"
|
||||
@@ -306,14 +320,12 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
|
||||
)],
|
||||
),
|
||||
],
|
||||
input_path: input_file_path.into(),
|
||||
input_content: Some(input_file_content.into()),
|
||||
edit_description: edit_description.into(),
|
||||
assertion: EvalAssertion::judge_diff(indoc! {"
|
||||
Some(input_file_content.into()),
|
||||
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
|
||||
"}),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -324,10 +336,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(
|
||||
200,
|
||||
100,
|
||||
0.95,
|
||||
EvalInput {
|
||||
conversation: vec![
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
message(User, [text("Let's research how to cursor blinking works.")]),
|
||||
message(
|
||||
Assistant,
|
||||
@@ -381,15 +393,13 @@ fn eval_disable_cursor_blinking() {
|
||||
)],
|
||||
),
|
||||
],
|
||||
input_path: input_file_path.into(),
|
||||
input_content: Some(input_file_content.into()),
|
||||
edit_description: edit_description.into(),
|
||||
assertion: EvalAssertion::judge_diff(indoc! {"
|
||||
Some(input_file_content.into()),
|
||||
EvalAssertion::judge_diff(indoc! {"
|
||||
- Calls to BlinkManager in `observe_window_activation` were commented out
|
||||
- The call to `blink_manager.enable` above the call to show_cursor_names was commented out
|
||||
- All the edits have valid indentation
|
||||
"}),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -402,8 +412,8 @@ fn eval_from_pixels_constructor() {
|
||||
eval(
|
||||
100,
|
||||
0.95,
|
||||
EvalInput {
|
||||
conversation: vec![
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
message(
|
||||
User,
|
||||
[text(indoc! {"
|
||||
@@ -575,14 +585,12 @@ fn eval_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
|
||||
"}),
|
||||
},
|
||||
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
|
||||
"}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -590,12 +598,13 @@ 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 {
|
||||
conversation: vec![
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
message(User, [text(include_str!("evals/fixtures/zode/prompt.md"))]),
|
||||
message(
|
||||
Assistant,
|
||||
@@ -653,14 +662,12 @@ fn eval_zode() {
|
||||
],
|
||||
),
|
||||
],
|
||||
input_path: input_file_path.into(),
|
||||
input_content: None,
|
||||
edit_description: edit_description.into(),
|
||||
assertion: EvalAssertion::new(async move |sample, _, _cx| {
|
||||
input_content,
|
||||
EvalAssertion::new(async move |sample, _, _cx| {
|
||||
let invalid_starts = [' ', '`', '\n'];
|
||||
let mut message = String::new();
|
||||
for start in invalid_starts {
|
||||
if sample.text.starts_with(start) {
|
||||
if sample.text_after.starts_with(start) {
|
||||
message.push_str(&format!("The sample starts with a {:?}\n", start));
|
||||
break;
|
||||
}
|
||||
@@ -680,7 +687,7 @@ fn eval_zode() {
|
||||
})
|
||||
}
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -693,8 +700,8 @@ fn eval_add_overwrite_test() {
|
||||
eval(
|
||||
200,
|
||||
0.5, // TODO: make this eval better
|
||||
EvalInput {
|
||||
conversation: vec![
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
message(
|
||||
User,
|
||||
[text(indoc! {"
|
||||
@@ -898,13 +905,93 @@ fn eval_add_overwrite_test() {
|
||||
],
|
||||
),
|
||||
],
|
||||
input_path: input_file_path.into(),
|
||||
input_content: Some(input_file_content.into()),
|
||||
edit_description: edit_description.into(),
|
||||
assertion: EvalAssertion::judge_diff(
|
||||
Some(input_file_content.into()),
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -963,15 +1050,50 @@ fn tool_result(
|
||||
#[derive(Clone)]
|
||||
struct EvalInput {
|
||||
conversation: Vec<LanguageModelRequestMessage>,
|
||||
input_path: PathBuf,
|
||||
edit_file_input: EditFileToolInput,
|
||||
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: String,
|
||||
text_before: String,
|
||||
text_after: String,
|
||||
edit_output: EditAgentOutput,
|
||||
diff: String,
|
||||
}
|
||||
@@ -1028,7 +1150,7 @@ impl EvalAssertion {
|
||||
let expected = expected.into();
|
||||
Self::new(async move |sample, _judge, _cx| {
|
||||
Ok(EvalAssertionOutcome {
|
||||
score: if strip_empty_lines(&sample.text) == strip_empty_lines(&expected) {
|
||||
score: if strip_empty_lines(&sample.text_after) == strip_empty_lines(&expected) {
|
||||
100
|
||||
} else {
|
||||
0
|
||||
@@ -1038,6 +1160,22 @@ 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 {
|
||||
@@ -1125,7 +1263,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.clone())
|
||||
.entry(output.sample.text_after.clone())
|
||||
.or_insert(Vec::new())
|
||||
.push(output);
|
||||
}
|
||||
@@ -1297,7 +1435,7 @@ impl EditAgentTest {
|
||||
let path = self
|
||||
.project
|
||||
.read_with(cx, |project, cx| {
|
||||
project.find_project_path(eval.input_path, cx)
|
||||
project.find_project_path(eval.edit_file_input.path, cx)
|
||||
})
|
||||
.unwrap();
|
||||
let buffer = self
|
||||
@@ -1305,31 +1443,69 @@ impl EditAgentTest {
|
||||
.update(cx, |project, cx| project.open_buffer(path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let conversation = LanguageModelRequest {
|
||||
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,
|
||||
})
|
||||
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()
|
||||
}),
|
||||
})
|
||||
.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,
|
||||
..Default::default()
|
||||
};
|
||||
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 = 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, _) = self.agent.edit(
|
||||
buffer.clone(),
|
||||
eval.edit_description,
|
||||
eval.edit_file_input.display_description,
|
||||
&conversation,
|
||||
&mut cx.to_async(),
|
||||
);
|
||||
@@ -1337,7 +1513,7 @@ impl EditAgentTest {
|
||||
} else {
|
||||
let (edit_output, _) = self.agent.overwrite(
|
||||
buffer.clone(),
|
||||
eval.edit_description,
|
||||
eval.edit_file_input.display_description,
|
||||
&conversation,
|
||||
&mut cx.to_async(),
|
||||
);
|
||||
@@ -1351,7 +1527,8 @@ impl EditAgentTest {
|
||||
eval.input_content.as_deref().unwrap_or_default(),
|
||||
&buffer_text,
|
||||
),
|
||||
text: buffer_text,
|
||||
text_before: eval.input_content.unwrap_or_default(),
|
||||
text_after: buffer_text,
|
||||
};
|
||||
let assertion = eval
|
||||
.assertion
|
||||
|
||||
@@ -1,378 +0,0 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
@@ -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();
|
||||
@@ -0,0 +1,26 @@
|
||||
@@ -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)]
|
||||
@@ -0,0 +1,11 @@
|
||||
@@ -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();
|
||||
@@ -0,0 +1,24 @@
|
||||
@@ -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)]
|
||||
@@ -0,0 +1,26 @@
|
||||
@@ -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)]
|
||||
@@ -0,0 +1,23 @@
|
||||
@@ -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)]
|
||||
@@ -0,0 +1,26 @@
|
||||
@@ -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)]
|
||||
@@ -0,0 +1,26 @@
|
||||
@@ -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)]
|
||||
@@ -22,7 +22,7 @@ use language::{
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
use project::Project;
|
||||
use project::{Project, ProjectPath};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -38,7 +38,7 @@ use workspace::Workspace;
|
||||
|
||||
pub struct EditFileTool;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, 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.
|
||||
@@ -86,7 +86,7 @@ pub struct EditFileToolInput {
|
||||
pub mode: EditFileMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum EditFileMode {
|
||||
Edit,
|
||||
@@ -171,12 +171,9 @@ impl Tool for EditFileTool {
|
||||
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 project_path = match resolve_path(&input, project.clone(), cx) {
|
||||
Ok(path) => path,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let card = window.and_then(|window| {
|
||||
@@ -199,20 +196,6 @@ impl Tool for EditFileTool {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let exists = buffer.read_with(cx, |buffer, _| {
|
||||
buffer
|
||||
.file()
|
||||
.as_ref()
|
||||
.map_or(false, |file| file.disk_state().exists())
|
||||
})?;
|
||||
let create_or_overwrite = match input.mode {
|
||||
EditFileMode::Create | EditFileMode::Overwrite => true,
|
||||
_ => false,
|
||||
};
|
||||
if !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({
|
||||
@@ -221,15 +204,15 @@ impl Tool for EditFileTool {
|
||||
})
|
||||
.await;
|
||||
|
||||
let (output, mut events) = if create_or_overwrite {
|
||||
edit_agent.overwrite(
|
||||
let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
|
||||
edit_agent.edit(
|
||||
buffer.clone(),
|
||||
input.display_description.clone(),
|
||||
&request,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
edit_agent.edit(
|
||||
edit_agent.overwrite(
|
||||
buffer.clone(),
|
||||
input.display_description.clone(),
|
||||
&request,
|
||||
@@ -349,6 +332,72 @@ 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>,
|
||||
@@ -868,7 +917,10 @@ 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;
|
||||
@@ -908,10 +960,102 @@ mod tests {
|
||||
.await;
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
"root/nonexistent_file.txt not found"
|
||||
"Can't edit file: path 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!({
|
||||
@@ -984,6 +1128,7 @@ mod tests {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
TelemetrySettings::register(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
You are an expert engineer and your task is to write a new file from scratch.
|
||||
|
||||
<file_to_edit>
|
||||
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>
|
||||
{{path}}
|
||||
</file_to_edit>
|
||||
</file_path>
|
||||
|
||||
<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.
|
||||
|
||||
@@ -27,20 +27,57 @@ NEW TEXT 3 HERE
|
||||
</edits>
|
||||
```
|
||||
|
||||
Rules for editing:
|
||||
# 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>
|
||||
|
||||
- `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}}
|
||||
|
||||
@@ -16,6 +16,20 @@ 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,
|
||||
@@ -113,6 +127,12 @@ 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",
|
||||
@@ -164,6 +184,10 @@ 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",
|
||||
@@ -220,7 +244,9 @@ impl Model {
|
||||
| Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3_7Sonnet => 200_000,
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeOpus4 => 200_000,
|
||||
Self::AmazonNovaPremier => 1_000_000,
|
||||
Self::PalmyraWriterX5 => 1_000_000,
|
||||
Self::PalmyraWriterX4 => 128_000,
|
||||
@@ -232,7 +258,12 @@ 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 => 128_000,
|
||||
Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeOpus4
|
||||
| Model::ClaudeOpus4Thinking => 128_000,
|
||||
Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192,
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
@@ -247,7 +278,11 @@ impl Model {
|
||||
| Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3_7Sonnet => 1.0,
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking => 1.0,
|
||||
Self::Custom {
|
||||
default_temperature,
|
||||
..
|
||||
@@ -265,6 +300,10 @@ 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)
|
||||
@@ -290,6 +329,12 @@ 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,
|
||||
}
|
||||
}
|
||||
@@ -326,6 +371,10 @@ 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") => {
|
||||
|
||||
@@ -922,7 +922,15 @@ impl Client {
|
||||
}
|
||||
|
||||
futures::select_biased! {
|
||||
result = self.set_connection(conn, cx).fuse() => ConnectionResult::Result(result.context("client auth and connect")),
|
||||
result = self.set_connection(conn, cx).fuse() => {
|
||||
match result.context("client auth and connect") {
|
||||
Ok(()) => ConnectionResult::Result(Ok(())),
|
||||
Err(err) => {
|
||||
self.set_status(Status::ConnectionError, cx);
|
||||
ConnectionResult::Result(Err(err))
|
||||
},
|
||||
}
|
||||
},
|
||||
_ = timeout => {
|
||||
self.set_status(Status::ConnectionError, cx);
|
||||
ConnectionResult::Timeout
|
||||
|
||||
@@ -56,6 +56,12 @@ pub use sea_orm::ConnectOptions;
|
||||
pub use tables::user::Model as User;
|
||||
pub use tables::*;
|
||||
|
||||
#[cfg(test)]
|
||||
pub struct DatabaseTestOptions {
|
||||
pub runtime: tokio::runtime::Runtime,
|
||||
pub query_failure_probability: parking_lot::Mutex<f64>,
|
||||
}
|
||||
|
||||
/// Database gives you a handle that lets you access the database.
|
||||
/// It handles pooling internally.
|
||||
pub struct Database {
|
||||
@@ -68,7 +74,7 @@ pub struct Database {
|
||||
notification_kinds_by_id: HashMap<NotificationKindId, &'static str>,
|
||||
notification_kinds_by_name: HashMap<String, NotificationKindId>,
|
||||
#[cfg(test)]
|
||||
runtime: Option<tokio::runtime::Runtime>,
|
||||
test_options: Option<DatabaseTestOptions>,
|
||||
}
|
||||
|
||||
// The `Database` type has so many methods that its impl blocks are split into
|
||||
@@ -87,7 +93,7 @@ impl Database {
|
||||
notification_kinds_by_name: HashMap::default(),
|
||||
executor,
|
||||
#[cfg(test)]
|
||||
runtime: None,
|
||||
test_options: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -359,11 +365,16 @@ impl Database {
|
||||
{
|
||||
#[cfg(test)]
|
||||
{
|
||||
let test_options = self.test_options.as_ref().unwrap();
|
||||
if let Executor::Deterministic(executor) = &self.executor {
|
||||
executor.simulate_random_delay().await;
|
||||
let fail_probability = *test_options.query_failure_probability.lock();
|
||||
if executor.rng().gen_bool(fail_probability) {
|
||||
return Err(anyhow!("simulated query failure"))?;
|
||||
}
|
||||
}
|
||||
|
||||
self.runtime.as_ref().unwrap().block_on(future)
|
||||
test_options.runtime.block_on(future)
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
|
||||
@@ -30,7 +30,7 @@ pub struct TestDb {
|
||||
}
|
||||
|
||||
impl TestDb {
|
||||
pub fn sqlite(background: BackgroundExecutor) -> Self {
|
||||
pub fn sqlite(executor: BackgroundExecutor) -> Self {
|
||||
let url = "sqlite::memory:";
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
@@ -41,7 +41,7 @@ impl TestDb {
|
||||
let mut db = runtime.block_on(async {
|
||||
let mut options = ConnectOptions::new(url);
|
||||
options.max_connections(5);
|
||||
let mut db = Database::new(options, Executor::Deterministic(background))
|
||||
let mut db = Database::new(options, Executor::Deterministic(executor.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
let sql = include_str!(concat!(
|
||||
@@ -59,7 +59,10 @@ impl TestDb {
|
||||
db
|
||||
});
|
||||
|
||||
db.runtime = Some(runtime);
|
||||
db.test_options = Some(DatabaseTestOptions {
|
||||
runtime,
|
||||
query_failure_probability: parking_lot::Mutex::new(0.0),
|
||||
});
|
||||
|
||||
Self {
|
||||
db: Some(Arc::new(db)),
|
||||
@@ -67,7 +70,7 @@ impl TestDb {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn postgres(background: BackgroundExecutor) -> Self {
|
||||
pub fn postgres(executor: BackgroundExecutor) -> Self {
|
||||
static LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
let _guard = LOCK.lock();
|
||||
@@ -90,7 +93,7 @@ impl TestDb {
|
||||
options
|
||||
.max_connections(5)
|
||||
.idle_timeout(Duration::from_secs(0));
|
||||
let mut db = Database::new(options, Executor::Deterministic(background))
|
||||
let mut db = Database::new(options, Executor::Deterministic(executor.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations");
|
||||
@@ -101,7 +104,10 @@ impl TestDb {
|
||||
db
|
||||
});
|
||||
|
||||
db.runtime = Some(runtime);
|
||||
db.test_options = Some(DatabaseTestOptions {
|
||||
runtime,
|
||||
query_failure_probability: parking_lot::Mutex::new(0.0),
|
||||
});
|
||||
|
||||
Self {
|
||||
db: Some(Arc::new(db)),
|
||||
@@ -112,6 +118,12 @@ impl TestDb {
|
||||
pub fn db(&self) -> &Arc<Database> {
|
||||
self.db.as_ref().unwrap()
|
||||
}
|
||||
|
||||
pub fn set_query_failure_probability(&self, probability: f64) {
|
||||
let database = self.db.as_ref().unwrap();
|
||||
let test_options = database.test_options.as_ref().unwrap();
|
||||
*test_options.query_failure_probability.lock() = probability;
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
@@ -136,7 +148,7 @@ impl Drop for TestDb {
|
||||
fn drop(&mut self) {
|
||||
let db = self.db.take().unwrap();
|
||||
if let sea_orm::DatabaseBackend::Postgres = db.pool.get_database_backend() {
|
||||
db.runtime.as_ref().unwrap().block_on(async {
|
||||
db.test_options.as_ref().unwrap().runtime.block_on(async {
|
||||
use util::ResultExt;
|
||||
let query = "
|
||||
SELECT pg_terminate_backend(pg_stat_activity.pid)
|
||||
|
||||
@@ -2517,7 +2517,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
|
||||
.clone()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.all_breakpoints(cx)
|
||||
.all_source_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_breakpoints(cx)
|
||||
.all_source_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_breakpoints(cx)
|
||||
.all_source_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_breakpoints(cx)
|
||||
.all_source_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_breakpoints(cx)
|
||||
.all_source_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_breakpoints(cx)
|
||||
.all_source_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_breakpoints(cx)
|
||||
.all_source_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_breakpoints(cx)
|
||||
.all_source_breakpoints(cx)
|
||||
.clone()
|
||||
});
|
||||
|
||||
|
||||
@@ -61,6 +61,35 @@ fn init_logger() {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_database_failure_during_client_reconnection(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client = server.create_client(cx, "user_a").await;
|
||||
|
||||
// Keep disconnecting the client until a database failure prevents it from
|
||||
// reconnecting.
|
||||
server.test_db.set_query_failure_probability(0.3);
|
||||
loop {
|
||||
server.disconnect_client(client.peer_id().unwrap());
|
||||
executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
|
||||
if !client.status().borrow().is_connected() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Make the database healthy again and ensure the client can finally connect.
|
||||
server.test_db.set_query_failure_probability(0.);
|
||||
executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
|
||||
assert!(
|
||||
matches!(*client.status().borrow(), client::Status::Connected { .. }),
|
||||
"status was {:?}",
|
||||
*client.status().borrow()
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_basic_calls(
|
||||
executor: BackgroundExecutor,
|
||||
|
||||
@@ -52,11 +52,11 @@ use livekit_client::test::TestServer as LivekitTestServer;
|
||||
pub struct TestServer {
|
||||
pub app_state: Arc<AppState>,
|
||||
pub test_livekit_server: Arc<LivekitTestServer>,
|
||||
pub test_db: TestDb,
|
||||
server: Arc<Server>,
|
||||
next_github_user_id: i32,
|
||||
connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
|
||||
forbid_connections: Arc<AtomicBool>,
|
||||
_test_db: TestDb,
|
||||
}
|
||||
|
||||
pub struct TestClient {
|
||||
@@ -117,7 +117,7 @@ impl TestServer {
|
||||
connection_killers: Default::default(),
|
||||
forbid_connections: Default::default(),
|
||||
next_github_user_id: 0,
|
||||
_test_db: test_db,
|
||||
test_db,
|
||||
test_livekit_server: livekit_server,
|
||||
}
|
||||
}
|
||||
@@ -241,7 +241,12 @@ impl TestServer {
|
||||
let user = db
|
||||
.get_user_by_id(user_id)
|
||||
.await
|
||||
.expect("retrieving user failed")
|
||||
.map_err(|e| {
|
||||
EstablishConnectionError::Other(anyhow!(
|
||||
"retrieving user failed: {}",
|
||||
e
|
||||
))
|
||||
})?
|
||||
.unwrap();
|
||||
cx.background_spawn(server.handle_connection(
|
||||
server_conn,
|
||||
|
||||
@@ -578,6 +578,15 @@ 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)
|
||||
@@ -591,7 +600,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", "true");
|
||||
.header("Copilot-Vision-Request", is_vision_request.to_string());
|
||||
|
||||
let is_streaming = request.stream;
|
||||
|
||||
|
||||
@@ -32,15 +32,17 @@ pub enum DapStatus {
|
||||
Failed { error: String },
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait DapDelegate {
|
||||
#[async_trait]
|
||||
pub trait DapDelegate: Send + Sync + 'static {
|
||||
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);
|
||||
fn which(&self, command: &OsStr) -> Option<PathBuf>;
|
||||
async fn which(&self, command: &OsStr) -> Option<PathBuf>;
|
||||
async fn read_text_file(&self, path: PathBuf) -> Result<String>;
|
||||
async fn shell_env(&self) -> collections::HashMap<String, String>;
|
||||
}
|
||||
|
||||
@@ -413,7 +415,7 @@ pub trait DebugAdapter: 'static + Send + Sync {
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
config: &DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
@@ -472,7 +474,7 @@ impl DebugAdapter for FakeAdapter {
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
_: &dyn DapDelegate,
|
||||
_: &Arc<dyn DapDelegate>,
|
||||
config: &DebugTaskDefinition,
|
||||
_: Option<PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
|
||||
@@ -6,6 +6,8 @@ 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;
|
||||
@@ -16,3 +18,19 @@ 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))
|
||||
}
|
||||
|
||||
@@ -54,10 +54,6 @@ 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);
|
||||
debug_assert!(
|
||||
_previous_value.is_none(),
|
||||
"Attempted to insert a new debug adapter when one is already registered"
|
||||
);
|
||||
}
|
||||
|
||||
pub fn adapter_language(&self, adapter_name: &str) -> Option<LanguageName> {
|
||||
|
||||
@@ -61,7 +61,7 @@ impl CodeLldbDebugAdapter {
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
delegate: &Arc<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: &dyn DapDelegate,
|
||||
delegate: &Arc<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,
|
||||
delegate.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
let version_path =
|
||||
|
||||
@@ -6,7 +6,7 @@ mod php;
|
||||
mod python;
|
||||
mod ruby;
|
||||
|
||||
use std::{net::Ipv4Addr, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
@@ -17,6 +17,7 @@ use dap::{
|
||||
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
|
||||
GithubRepo,
|
||||
},
|
||||
configure_tcp_connection,
|
||||
inline_value::{PythonInlineValueProvider, RustInlineValueProvider},
|
||||
};
|
||||
use gdb::GdbDebugAdapter;
|
||||
@@ -27,7 +28,6 @@ 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,21 +45,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ impl DebugAdapter for GdbDebugAdapter {
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
config: &DebugTaskDefinition,
|
||||
user_installed_path: Option<std::path::PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
@@ -76,6 +76,7 @@ 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"));
|
||||
|
||||
|
||||
@@ -50,13 +50,14 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
delegate: &Arc<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"))?;
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ impl JsDebugAdapter {
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
delegate: &Arc<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: &dyn DapDelegate,
|
||||
delegate: &Arc<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: &dyn DapDelegate,
|
||||
delegate: &Arc<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,
|
||||
delegate.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ impl PhpDebugAdapter {
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
) -> Result<AdapterVersion> {
|
||||
let release = latest_github_release(
|
||||
&format!("{}/{}", "xdebug", Self::ADAPTER_PACKAGE_NAME),
|
||||
@@ -66,7 +66,7 @@ impl PhpDebugAdapter {
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
config: &DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
@@ -126,7 +126,7 @@ impl DebugAdapter for PhpDebugAdapter {
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
config: &DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
@@ -138,7 +138,7 @@ impl DebugAdapter for PhpDebugAdapter {
|
||||
self.name(),
|
||||
version,
|
||||
adapters::DownloadedFileType::Vsix,
|
||||
delegate,
|
||||
delegate.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -52,26 +52,26 @@ impl PythonDebugAdapter {
|
||||
}
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
delegate: &Arc<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).await
|
||||
adapters::fetch_latest_adapter_version_from_github(github_repo, delegate.as_ref()).await
|
||||
}
|
||||
|
||||
async fn install_binary(
|
||||
&self,
|
||||
version: AdapterVersion,
|
||||
delegate: &dyn DapDelegate,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
) -> Result<()> {
|
||||
let version_path = adapters::download_adapter_from_github(
|
||||
self.name(),
|
||||
version,
|
||||
adapters::DownloadedFileType::Zip,
|
||||
delegate,
|
||||
delegate.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -93,7 +93,7 @@ impl PythonDebugAdapter {
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
config: &DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
@@ -128,14 +128,18 @@ impl PythonDebugAdapter {
|
||||
let python_path = if let Some(toolchain) = toolchain {
|
||||
Some(toolchain.path.to_string())
|
||||
} else {
|
||||
BINARY_NAMES
|
||||
.iter()
|
||||
.filter_map(|cmd| {
|
||||
delegate
|
||||
.which(OsStr::new(cmd))
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
})
|
||||
.find(|_| true)
|
||||
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
|
||||
};
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
@@ -172,7 +176,7 @@ impl DebugAdapter for PythonDebugAdapter {
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
config: &DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
|
||||
@@ -8,7 +8,7 @@ use dap::{
|
||||
};
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use language::LanguageName;
|
||||
use std::path::PathBuf;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use util::command::new_smol_command;
|
||||
|
||||
use crate::ToDap;
|
||||
@@ -32,7 +32,7 @@ impl DebugAdapter for RubyDebugAdapter {
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
definition: &DebugTaskDefinition,
|
||||
_user_installed_path: Option<PathBuf>,
|
||||
_cx: &mut AsyncApp,
|
||||
@@ -40,7 +40,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()) {
|
||||
match delegate.which("rdbg".as_ref()).await {
|
||||
Some(path) => rdbg_path = path,
|
||||
None => {
|
||||
delegate.output_to_console(
|
||||
@@ -76,7 +76,7 @@ impl DebugAdapter for RubyDebugAdapter {
|
||||
format!("--port={}", port),
|
||||
format!("--host={}", host),
|
||||
];
|
||||
if delegate.which(launch.program.as_ref()).is_some() {
|
||||
if delegate.which(launch.program.as_ref()).await.is_some() {
|
||||
arguments.push("--command".to_string())
|
||||
}
|
||||
arguments.push(launch.program);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use gpui::App;
|
||||
use sqlez_macros::sql;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{define_connection, query};
|
||||
use crate::{define_connection, query, write_and_log};
|
||||
|
||||
define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
|
||||
&[sql!(
|
||||
@@ -11,6 +13,29 @@ 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>> {
|
||||
|
||||
@@ -5,7 +5,7 @@ use async_trait::async_trait;
|
||||
use dap::adapters::{
|
||||
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
|
||||
};
|
||||
use extension::Extension;
|
||||
use extension::{Extension, WorktreeDelegate};
|
||||
use gpui::AsyncApp;
|
||||
|
||||
pub(crate) struct ExtensionDapAdapter {
|
||||
@@ -25,6 +25,35 @@ impl ExtensionDapAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -33,7 +62,7 @@ impl DebugAdapter for ExtensionDapAdapter {
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
_: &dyn DapDelegate,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
config: &DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
_cx: &mut AsyncApp,
|
||||
@@ -43,6 +72,7 @@ impl DebugAdapter for ExtensionDapAdapter {
|
||||
self.debug_adapter_name.clone(),
|
||||
config.clone(),
|
||||
user_installed_path,
|
||||
Arc::new(WorktreeDelegateAdapter(delegate.clone())),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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,
|
||||
@@ -30,7 +31,7 @@ use settings::Settings;
|
||||
use std::any::TypeId;
|
||||
use std::sync::Arc;
|
||||
use task::{DebugScenario, TaskContext};
|
||||
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
|
||||
use ui::{ContextMenu, Divider, Tooltip, prelude::*};
|
||||
use workspace::SplitDirection;
|
||||
use workspace::{
|
||||
Pane, Workspace,
|
||||
@@ -68,15 +69,20 @@ pub struct DebugPanel {
|
||||
}
|
||||
|
||||
impl DebugPanel {
|
||||
pub fn new(workspace: &Workspace, cx: &mut Context<Workspace>) -> Entity<Self> {
|
||||
pub fn new(
|
||||
workspace: &Workspace,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> 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: cx.focus_handle(),
|
||||
focus_handle,
|
||||
project,
|
||||
workspace: workspace.weak_handle(),
|
||||
context_menu: None,
|
||||
@@ -87,7 +93,38 @@ impl DebugPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn filter_action_types(&self, cx: &mut App) {
|
||||
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) {
|
||||
let (has_active_session, supports_restart, support_step_back, status) = self
|
||||
.active_session()
|
||||
.map(|item| {
|
||||
@@ -168,8 +205,8 @@ impl DebugPanel {
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
cx.spawn(async move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let debug_panel = DebugPanel::new(workspace, cx);
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
let debug_panel = DebugPanel::new(workspace, window, cx);
|
||||
|
||||
workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
|
||||
workspace.project().read(cx).breakpoint_store().update(
|
||||
@@ -273,7 +310,7 @@ impl DebugPanel {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
async fn register_session(
|
||||
pub(crate) async fn register_session(
|
||||
this: WeakEntity<Self>,
|
||||
session: Entity<Session>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
@@ -342,7 +379,7 @@ impl DebugPanel {
|
||||
Ok(debug_session)
|
||||
}
|
||||
|
||||
fn handle_restart_request(
|
||||
pub(crate) fn handle_restart_request(
|
||||
&mut self,
|
||||
mut curr_session: Entity<Session>,
|
||||
window: &mut Window,
|
||||
@@ -416,11 +453,12 @@ impl DebugPanel {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
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>) {
|
||||
pub(crate) fn close_session(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(session) = self
|
||||
.sessions
|
||||
.iter()
|
||||
@@ -474,93 +512,8 @@ 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);
|
||||
|
||||
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(
|
||||
pub(crate) fn deploy_context_menu(
|
||||
&mut self,
|
||||
position: Point<Pixels>,
|
||||
window: &mut Window,
|
||||
@@ -611,7 +564,11 @@ impl DebugPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
|
||||
pub(crate) fn top_controls_strip(
|
||||
&mut 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;
|
||||
@@ -651,12 +608,12 @@ impl DebugPanel {
|
||||
active_session
|
||||
.as_ref()
|
||||
.map(|session| session.read(cx).running_state()),
|
||||
|this, running_session| {
|
||||
|this, running_state| {
|
||||
let thread_status =
|
||||
running_session.read(cx).thread_status(cx).unwrap_or(
|
||||
running_state.read(cx).thread_status(cx).unwrap_or(
|
||||
project::debugger::session::ThreadStatus::Exited,
|
||||
);
|
||||
let capabilities = running_session.read(cx).capabilities(cx);
|
||||
let capabilities = running_state.read(cx).capabilities(cx);
|
||||
this.map(|this| {
|
||||
if thread_status == ThreadStatus::Running {
|
||||
this.child(
|
||||
@@ -667,7 +624,7 @@ impl DebugPanel {
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.pause_thread(cx);
|
||||
},
|
||||
@@ -694,7 +651,7 @@ impl DebugPanel {
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| this.continue_thread(cx),
|
||||
))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
@@ -718,7 +675,7 @@ impl DebugPanel {
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.step_over(cx);
|
||||
},
|
||||
@@ -742,7 +699,7 @@ impl DebugPanel {
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.step_out(cx);
|
||||
},
|
||||
@@ -769,7 +726,7 @@ impl DebugPanel {
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.step_in(cx);
|
||||
},
|
||||
@@ -819,7 +776,7 @@ impl DebugPanel {
|
||||
|| thread_status == ThreadStatus::Ended,
|
||||
)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.toggle_ignore_breakpoints(cx);
|
||||
},
|
||||
@@ -842,7 +799,7 @@ impl DebugPanel {
|
||||
IconButton::new("debug-restart", IconName::DebugRestart)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.restart_session(cx);
|
||||
},
|
||||
@@ -864,7 +821,7 @@ impl DebugPanel {
|
||||
IconButton::new("debug-stop", IconName::Power)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.stop_thread(cx);
|
||||
},
|
||||
@@ -898,7 +855,7 @@ impl DebugPanel {
|
||||
IconButton::new("debug-disconnect", IconName::DebugDetach)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(window.listener_for(
|
||||
&running_session,
|
||||
&running_state,
|
||||
|this, _, _, cx| {
|
||||
this.detach_client(cx);
|
||||
},
|
||||
@@ -932,30 +889,42 @@ impl DebugPanel {
|
||||
.as_ref()
|
||||
.map(|session| session.read(cx).running_state())
|
||||
.cloned(),
|
||||
|this, session| {
|
||||
this.child(
|
||||
session.update(cx, |this, cx| {
|
||||
this.thread_dropdown(window, cx)
|
||||
}),
|
||||
)
|
||||
|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,
|
||||
)
|
||||
})
|
||||
.when(!is_side, |this| this.gap_2().child(Divider::vertical()))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.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())
|
||||
})
|
||||
.children(self.render_session_menu(
|
||||
self.active_session(),
|
||||
self.running_state(cx),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.when(!is_side, |this| this.child(new_session_button())),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn activate_pane_in_direction(
|
||||
pub(crate) fn activate_pane_in_direction(
|
||||
&mut self,
|
||||
direction: SplitDirection,
|
||||
window: &mut Window,
|
||||
@@ -970,7 +939,7 @@ impl DebugPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_item(
|
||||
pub(crate) fn activate_item(
|
||||
&mut self,
|
||||
item: DebuggerPaneItem,
|
||||
window: &mut Window,
|
||||
@@ -985,7 +954,7 @@ impl DebugPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_session(
|
||||
pub(crate) fn activate_session(
|
||||
&mut self,
|
||||
session_item: Entity<DebugSession>,
|
||||
window: &mut Window,
|
||||
|
||||
@@ -13,6 +13,7 @@ use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
|
||||
|
||||
pub mod attach_modal;
|
||||
pub mod debugger_panel;
|
||||
mod dropdown_menus;
|
||||
mod new_session_modal;
|
||||
mod persistence;
|
||||
pub(crate) mod session;
|
||||
@@ -59,7 +60,16 @@ pub fn init(cx: &mut App) {
|
||||
cx.when_flag_enabled::<DebuggerFeatureFlag>(window, |workspace, _, _| {
|
||||
workspace
|
||||
.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
workspace.toggle_panel_focus::<DebugPanel>(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);
|
||||
})
|
||||
})
|
||||
.register_action(|workspace, _: &Pause, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
|
||||
186
crates/debugger_ui/src/dropdown_menus.rs
Normal file
186
crates/debugger_ui/src/dropdown_menus.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
pub mod running;
|
||||
|
||||
use std::{cell::OnceCell, sync::OnceLock};
|
||||
|
||||
use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
|
||||
use dap::client::SessionId;
|
||||
use gpui::{
|
||||
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
|
||||
@@ -11,14 +10,13 @@ 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::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
|
||||
|
||||
pub struct DebugSession {
|
||||
remote_id: Option<workspace::ViewId>,
|
||||
running_state: Entity<RunningState>,
|
||||
@@ -159,7 +157,11 @@ impl DebugSession {
|
||||
.gap_2()
|
||||
.when_some(icon, |this, indicator| this.child(indicator))
|
||||
.justify_between()
|
||||
.child(Label::new(label).when(is_terminated, |this| this.strikethrough()))
|
||||
.child(
|
||||
Label::new(label)
|
||||
.size(LabelSize::Small)
|
||||
.when(is_terminated, |this| this.strikethrough()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,11 +43,10 @@ use task::{
|
||||
};
|
||||
use terminal_view::TerminalView;
|
||||
use ui::{
|
||||
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,
|
||||
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,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use variable_list::VariableList;
|
||||
@@ -78,6 +77,16 @@ 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
|
||||
@@ -497,25 +506,20 @@ impl DebugTerminal {
|
||||
|
||||
impl gpui::Render for DebugTerminal {
|
||||
fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
if let Some(terminal) = self.terminal.clone() {
|
||||
terminal.into_any_element()
|
||||
} else {
|
||||
div().track_focus(&self.focus_handle).into_any_element()
|
||||
}
|
||||
div()
|
||||
.size_full()
|
||||
.track_focus(&self.focus_handle)
|
||||
.children(self.terminal.clone())
|
||||
}
|
||||
}
|
||||
impl Focusable for DebugTerminal {
|
||||
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()
|
||||
}
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl RunningState {
|
||||
pub fn new(
|
||||
pub(crate) fn new(
|
||||
session: Entity<Session>,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
@@ -1214,10 +1218,16 @@ impl RunningState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
|
||||
pub(crate) fn go_to_selected_stack_frame(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.thread_id.is_some() {
|
||||
self.stack_frame_list
|
||||
.update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1234,7 +1244,7 @@ impl RunningState {
|
||||
}
|
||||
|
||||
pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
|
||||
self.stack_frame_list.read(cx).selected_stack_frame_id()
|
||||
self.stack_frame_list.read(cx).opened_stack_frame_id()
|
||||
}
|
||||
|
||||
pub(crate) fn stack_frame_list(&self) -> &Entity<StackFrameList> {
|
||||
@@ -1311,7 +1321,12 @@ impl RunningState {
|
||||
.map(|id| self.session().read(cx).thread_status(id))
|
||||
}
|
||||
|
||||
fn select_thread(&mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub(crate) 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;
|
||||
}
|
||||
@@ -1448,38 +1463,6 @@ 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>,
|
||||
|
||||
@@ -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, Window, div,
|
||||
h_flex, px, v_flex,
|
||||
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Tooltip, 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_breakpoints(cx);
|
||||
let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
|
||||
self.breakpoints.clear();
|
||||
let weak = cx.weak_entity();
|
||||
let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
|
||||
@@ -259,6 +259,11 @@ 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();
|
||||
@@ -290,6 +295,9 @@ impl LineBreakpoint {
|
||||
)))
|
||||
.start_slot(indicator)
|
||||
.rounded()
|
||||
.on_secondary_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.end_hover_slot(
|
||||
IconButton::new(
|
||||
SharedString::from(format!(
|
||||
@@ -423,12 +431,20 @@ 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| {
|
||||
|
||||
@@ -162,7 +162,7 @@ impl Console {
|
||||
.evaluate(
|
||||
expression,
|
||||
Some(dap::EvaluateArgumentsContext::Repl),
|
||||
self.stack_frame_list.read(cx).selected_stack_frame_id(),
|
||||
self.stack_frame_list.read(cx).opened_stack_frame_id(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
@@ -389,7 +389,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).selected_stack_frame_id();
|
||||
let frame_id = console.stack_frame_list.read(cx).opened_stack_frame_id();
|
||||
|
||||
state.completions(
|
||||
CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use anyhow::anyhow;
|
||||
use dap::Module;
|
||||
use gpui::{
|
||||
AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, MouseButton, Stateful,
|
||||
Subscription, WeakEntity, list,
|
||||
AnyElement, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful,
|
||||
Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
|
||||
};
|
||||
use project::{
|
||||
ProjectItem as _, ProjectPath,
|
||||
@@ -9,16 +10,17 @@ use project::{
|
||||
};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use ui::{Scrollbar, ScrollbarState, prelude::*};
|
||||
use util::maybe;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct ModuleList {
|
||||
list: ListState,
|
||||
invalidate: bool,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_ix: Option<usize>,
|
||||
session: Entity<Session>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
scrollbar_state: ScrollbarState,
|
||||
entries: Vec<Module>,
|
||||
_rebuild_task: Task<()>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
@@ -28,38 +30,43 @@ 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.invalidate = true;
|
||||
cx.notify();
|
||||
this.schedule_rebuild(cx);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
Self {
|
||||
scrollbar_state: ScrollbarState::new(list.clone()),
|
||||
list,
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
|
||||
let mut this = Self {
|
||||
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
|
||||
scroll_handle,
|
||||
session,
|
||||
workspace,
|
||||
focus_handle,
|
||||
entries: Vec::new(),
|
||||
selected_ix: None,
|
||||
_subscription,
|
||||
invalidate: true,
|
||||
}
|
||||
_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();
|
||||
});
|
||||
}
|
||||
|
||||
fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -111,36 +118,40 @@ impl ModuleList {
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(module) = maybe!({
|
||||
self.session
|
||||
.update(cx, |state, cx| state.modules(cx).get(ix).cloned())
|
||||
}) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
let module = self.entries[ix].clone();
|
||||
|
||||
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()
|
||||
@@ -188,6 +199,96 @@ 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 {
|
||||
@@ -197,21 +298,17 @@ impl Focusable for ModuleList {
|
||||
}
|
||||
|
||||
impl Render for ModuleList {
|
||||
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();
|
||||
}
|
||||
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
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(list(self.list.clone()).size_full())
|
||||
.child(self.render_list(window, cx))
|
||||
.child(self.render_vertical_scrollbar(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,20 +5,18 @@ use std::time::Duration;
|
||||
use anyhow::{Result, anyhow};
|
||||
use dap::StackFrameId;
|
||||
use gpui::{
|
||||
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, MouseButton, Stateful,
|
||||
Subscription, Task, WeakEntity, list,
|
||||
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, MouseButton, ScrollStrategy,
|
||||
Stateful, Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_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 util::ResultExt;
|
||||
use workspace::{ItemHandle, Workspace};
|
||||
|
||||
use crate::StackTraceView;
|
||||
|
||||
use super::RunningState;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -28,15 +26,16 @@ pub enum StackFrameListEvent {
|
||||
}
|
||||
|
||||
pub struct StackFrameList {
|
||||
list: ListState,
|
||||
focus_handle: FocusHandle,
|
||||
_subscription: Subscription,
|
||||
session: Entity<Session>,
|
||||
state: WeakEntity<RunningState>,
|
||||
entries: Vec<StackFrameEntry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
selected_stack_frame_id: Option<StackFrameId>,
|
||||
selected_ix: Option<usize>,
|
||||
opened_stack_frame_id: Option<StackFrameId>,
|
||||
scrollbar_state: ScrollbarState,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
_refresh_task: Task<()>,
|
||||
}
|
||||
|
||||
@@ -55,22 +54,8 @@ impl StackFrameList {
|
||||
window: &mut Window,
|
||||
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(|stack_frame_list| {
|
||||
stack_frame_list.update(cx, |this, cx| this.render_entry(ix, cx))
|
||||
})
|
||||
.unwrap_or(div().into_any())
|
||||
},
|
||||
);
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
|
||||
let _subscription =
|
||||
cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
|
||||
@@ -84,15 +69,16 @@ impl StackFrameList {
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
scrollbar_state: ScrollbarState::new(list.clone()),
|
||||
list,
|
||||
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
|
||||
session,
|
||||
workspace,
|
||||
focus_handle,
|
||||
state,
|
||||
_subscription,
|
||||
entries: Default::default(),
|
||||
selected_stack_frame_id: None,
|
||||
selected_ix: None,
|
||||
opened_stack_frame_id: None,
|
||||
scroll_handle,
|
||||
_refresh_task: Task::ready(()),
|
||||
};
|
||||
this.schedule_refresh(true, window, cx);
|
||||
@@ -123,7 +109,7 @@ impl StackFrameList {
|
||||
fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> {
|
||||
self.state
|
||||
.read_with(cx, |state, _| state.thread_id)
|
||||
.log_err()
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|thread_id| {
|
||||
self.session
|
||||
@@ -140,27 +126,8 @@ impl StackFrameList {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn selected_stack_frame_id(&self) -> Option<StackFrameId> {
|
||||
self.selected_stack_frame_id
|
||||
}
|
||||
|
||||
pub(crate) fn select_stack_frame_id(
|
||||
&mut self,
|
||||
id: StackFrameId,
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if !self.entries.iter().any(|entry| match entry {
|
||||
StackFrameEntry::Normal(entry) => entry.id == id,
|
||||
StackFrameEntry::Collapsed(stack_frames) => {
|
||||
stack_frames.iter().any(|frame| frame.id == id)
|
||||
}
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.selected_stack_frame_id = Some(id);
|
||||
self.go_to_selected_stack_frame(window, cx);
|
||||
pub fn opened_stack_frame_id(&self) -> Option<StackFrameId> {
|
||||
self.opened_stack_frame_id
|
||||
}
|
||||
|
||||
pub(super) fn schedule_refresh(
|
||||
@@ -193,13 +160,22 @@ impl StackFrameList {
|
||||
|
||||
pub fn build_entries(
|
||||
&mut self,
|
||||
select_first_stack_frame: bool,
|
||||
open_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 current_stack_frame = None;
|
||||
let mut first_stack_frame = None;
|
||||
|
||||
let stack_frames = self.stack_frames(cx);
|
||||
for stack_frame in &stack_frames {
|
||||
@@ -213,7 +189,7 @@ impl StackFrameList {
|
||||
entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
|
||||
}
|
||||
|
||||
current_stack_frame.get_or_insert(&stack_frame.dap);
|
||||
first_stack_frame.get_or_insert(entries.len());
|
||||
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
|
||||
}
|
||||
}
|
||||
@@ -225,69 +201,60 @@ impl StackFrameList {
|
||||
}
|
||||
|
||||
std::mem::swap(&mut self.entries, &mut entries);
|
||||
self.list.reset(self.entries.len());
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
cx.emit(StackFrameListEvent::BuiltEntries);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_stack_frame(
|
||||
pub fn go_to_stack_frame(
|
||||
&mut self,
|
||||
stack_frame: &dap::StackFrame,
|
||||
go_to_stack_frame: bool,
|
||||
window: &Window,
|
||||
stack_frame_id: StackFrameId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
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 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)
|
||||
}
|
||||
|
||||
let row = (stack_frame.line.saturating_sub(1)) as u32;
|
||||
|
||||
fn go_to_stack_frame_inner(
|
||||
&mut self,
|
||||
stack_frame: dap::StackFrame,
|
||||
window: &mut 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 {
|
||||
return Task::ready(Err(anyhow!("Project path not found")));
|
||||
};
|
||||
|
||||
let stack_frame_id = stack_frame.id;
|
||||
let row = stack_frame.line.saturating_sub(1) as u32;
|
||||
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
|
||||
stack_frame_id,
|
||||
));
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let (worktree, relative_path) = this
|
||||
.update(cx, |this, cx| {
|
||||
@@ -386,11 +353,12 @@ 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(stack_frame.id) == self.selected_stack_frame_id;
|
||||
let is_selected_frame = Some(ix) == self.selected_ix;
|
||||
|
||||
let path = source.clone().and_then(|s| s.path.or(s.name));
|
||||
let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,));
|
||||
@@ -426,12 +394,12 @@ impl StackFrameList {
|
||||
.when(is_selected_frame, |this| {
|
||||
this.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
.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);
|
||||
}
|
||||
.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);
|
||||
}))
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(
|
||||
@@ -486,20 +454,15 @@ impl StackFrameList {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
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();
|
||||
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);
|
||||
}
|
||||
|
||||
fn render_collapsed_entry(
|
||||
@@ -509,6 +472,7 @@ 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()
|
||||
@@ -517,11 +481,15 @@ impl StackFrameList {
|
||||
.group("")
|
||||
.id(("stack-frame", first_stack_frame.id))
|
||||
.p_1()
|
||||
.on_click(cx.listener({
|
||||
let stack_frames = stack_frames.clone();
|
||||
move |this, _, _window, cx| {
|
||||
this.expand_collapsed_entry(ix, &stack_frames, cx);
|
||||
}
|
||||
.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);
|
||||
}))
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(
|
||||
@@ -544,7 +512,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(stack_frame, cx),
|
||||
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
|
||||
StackFrameEntry::Collapsed(stack_frames) => {
|
||||
self.render_collapsed_entry(ix, stack_frames, cx)
|
||||
}
|
||||
@@ -583,15 +551,120 @@ 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()
|
||||
.child(list(self.list.clone()).size_full())
|
||||
.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(self.render_vertical_scrollbar(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ impl StackTraceView {
|
||||
.filter(|id| Some(**id) != this.selected_stack_frame_id)
|
||||
{
|
||||
this.stack_frame_list.update(cx, |list, cx| {
|
||||
list.select_stack_frame_id(*stack_frame_id, window, cx);
|
||||
list.go_to_stack_frame(*stack_frame_id, window, cx).detach();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ impl StackTraceView {
|
||||
|this, stack_frame_list, event, window, cx| match event {
|
||||
StackFrameListEvent::BuiltEntries => {
|
||||
this.selected_stack_frame_id =
|
||||
stack_frame_list.read(cx).selected_stack_frame_id();
|
||||
stack_frame_list.read(cx).opened_stack_frame_id();
|
||||
this.update_excerpts(window, cx);
|
||||
}
|
||||
StackFrameListEvent::SelectedStackFrameChanged(selected_frame_id) => {
|
||||
|
||||
@@ -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.selected_stack_frame_id());
|
||||
assert_eq!(Some(1), stack_frame_list.opened_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.selected_stack_frame_id());
|
||||
assert_eq!(Some(1), stack_frame_list.opened_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.select_stack_frame(&stack_frames[1], true, window, cx)
|
||||
stack_frame_list.go_to_stack_frame(stack_frames[1].id, 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.selected_stack_frame_id());
|
||||
assert_eq!(Some(2), stack_frame_list.opened_stack_frame_id());
|
||||
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
|
||||
});
|
||||
|
||||
@@ -718,11 +718,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
|
||||
stack_frame_list.entries()
|
||||
);
|
||||
|
||||
stack_frame_list.expand_collapsed_entry(
|
||||
1,
|
||||
&vec![stack_frames[1].clone(), stack_frames[2].clone()],
|
||||
cx,
|
||||
);
|
||||
stack_frame_list.expand_collapsed_entry(1);
|
||||
|
||||
assert_eq!(
|
||||
&vec![
|
||||
@@ -739,11 +735,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
|
||||
stack_frame_list.entries()
|
||||
);
|
||||
|
||||
stack_frame_list.expand_collapsed_entry(
|
||||
4,
|
||||
&vec![stack_frames[4].clone(), stack_frames[5].clone()],
|
||||
cx,
|
||||
);
|
||||
stack_frame_list.expand_collapsed_entry(4);
|
||||
|
||||
assert_eq!(
|
||||
&vec![
|
||||
|
||||
@@ -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.selected_stack_frame_id())
|
||||
(list.flatten_entries(true), list.opened_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.selected_stack_frame_id())
|
||||
(list.flatten_entries(true), list.opened_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.selected_stack_frame_id())
|
||||
(list.flatten_entries(true), list.opened_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.selected_stack_frame_id())
|
||||
(list.flatten_entries(true), list.opened_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)
|
||||
.selected_stack_frame_id(),
|
||||
.opened_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.select_stack_frame(&stack_frames[1], true, window, cx)
|
||||
stack_frame_list.go_to_stack_frame(stack_frames[1].id, 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.selected_stack_frame_id())
|
||||
(list.flatten_entries(true), list.opened_stack_frame_id())
|
||||
});
|
||||
|
||||
let variable_list = running_state.variable_list().read(cx);
|
||||
|
||||
@@ -122,10 +122,11 @@ use markdown::Markdown;
|
||||
use mouse_context_menu::MouseContextMenu;
|
||||
use persistence::DB;
|
||||
use project::{
|
||||
ProjectPath,
|
||||
BreakpointWithPosition, ProjectPath,
|
||||
debugger::{
|
||||
breakpoint_store::{
|
||||
BreakpointEditAction, BreakpointState, BreakpointStore, BreakpointStoreEvent,
|
||||
BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
|
||||
BreakpointStoreEvent,
|
||||
},
|
||||
session::{Session, SessionEvent},
|
||||
},
|
||||
@@ -198,7 +199,7 @@ use theme::{
|
||||
};
|
||||
use ui::{
|
||||
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
|
||||
IconSize, Key, Tooltip, h_flex, prelude::*,
|
||||
IconSize, Indicator, Key, Tooltip, h_flex, prelude::*,
|
||||
};
|
||||
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
|
||||
use workspace::{
|
||||
@@ -6848,7 +6849,7 @@ impl Editor {
|
||||
range: Range<DisplayRow>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> HashMap<DisplayRow, (Anchor, Breakpoint)> {
|
||||
) -> HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)> {
|
||||
let mut breakpoint_display_points = HashMap::default();
|
||||
|
||||
let Some(breakpoint_store) = self.breakpoint_store.clone() else {
|
||||
@@ -6882,15 +6883,17 @@ impl Editor {
|
||||
buffer_snapshot,
|
||||
cx,
|
||||
);
|
||||
for (anchor, breakpoint) in breakpoints {
|
||||
for (breakpoint, state) in breakpoints {
|
||||
let multi_buffer_anchor =
|
||||
Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), *anchor);
|
||||
Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), breakpoint.position);
|
||||
let position = multi_buffer_anchor
|
||||
.to_point(&multi_buffer_snapshot)
|
||||
.to_display_point(&snapshot);
|
||||
|
||||
breakpoint_display_points
|
||||
.insert(position.row(), (multi_buffer_anchor, breakpoint.clone()));
|
||||
breakpoint_display_points.insert(
|
||||
position.row(),
|
||||
(multi_buffer_anchor, breakpoint.bp.clone(), state),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7065,8 +7068,10 @@ 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),
|
||||
@@ -7092,6 +7097,8 @@ impl Editor {
|
||||
|
||||
let color = if is_phantom {
|
||||
Color::Hint
|
||||
} else if is_rejected {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Debugger
|
||||
};
|
||||
@@ -7119,9 +7126,18 @@ 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({
|
||||
@@ -7153,14 +7169,7 @@ impl Editor {
|
||||
);
|
||||
}))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta_in(
|
||||
primary_text.clone(),
|
||||
None,
|
||||
"Right-click for more options",
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
Tooltip::with_meta_in(primary_text.clone(), None, meta, &focus_handle, window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7300,11 +7309,11 @@ impl Editor {
|
||||
_style: &EditorStyle,
|
||||
is_active: bool,
|
||||
row: DisplayRow,
|
||||
breakpoint: Option<(Anchor, Breakpoint)>,
|
||||
breakpoint: Option<(Anchor, Breakpoint, Option<BreakpointSessionState>)>,
|
||||
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)
|
||||
@@ -9484,16 +9493,16 @@ impl Editor {
|
||||
cx,
|
||||
)
|
||||
.next()
|
||||
.and_then(|(anchor, bp)| {
|
||||
.and_then(|(bp, _)| {
|
||||
let breakpoint_row = buffer_snapshot
|
||||
.summary_for_anchor::<text::PointUtf16>(anchor)
|
||||
.summary_for_anchor::<text::PointUtf16>(&bp.position)
|
||||
.row;
|
||||
|
||||
if breakpoint_row == row {
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(enclosing_excerpt, *anchor)
|
||||
.map(|anchor| (anchor, bp.clone()))
|
||||
.anchor_in_excerpt(enclosing_excerpt, bp.position)
|
||||
.map(|position| (position, bp.bp.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -9656,7 +9665,10 @@ impl Editor {
|
||||
breakpoint_store.update(cx, |breakpoint_store, cx| {
|
||||
breakpoint_store.toggle_breakpoint(
|
||||
buffer,
|
||||
(breakpoint_position.text_anchor, breakpoint),
|
||||
BreakpointWithPosition {
|
||||
position: breakpoint_position.text_anchor,
|
||||
bp: breakpoint,
|
||||
},
|
||||
edit_action,
|
||||
cx,
|
||||
);
|
||||
@@ -17918,10 +17930,6 @@ 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| {
|
||||
let Some(current_execution_position) = current_execution_position else {
|
||||
@@ -17949,22 +17957,40 @@ impl Editor {
|
||||
.context("refreshing debugger inlays")
|
||||
.log_err()?;
|
||||
|
||||
let (excerpt_id, buffer_id) = snapshot
|
||||
.excerpts()
|
||||
.next()
|
||||
.map(|excerpt| (excerpt.0, excerpt.1.remote_id()))?;
|
||||
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);
|
||||
}
|
||||
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
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 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 mut inlay_ids = new_inlays.iter().map(|inlay| inlay.id).collect();
|
||||
std::mem::swap(&mut editor.inline_value_cache.inlays, &mut inlay_ids);
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ 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,
|
||||
@@ -539,7 +541,7 @@ pub struct ScrollbarContent {
|
||||
}
|
||||
|
||||
/// Minimap related settings
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct MinimapContent {
|
||||
/// When to show the minimap in the editor.
|
||||
///
|
||||
@@ -770,5 +772,32 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18522,7 +18522,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.all_breakpoints(cx)
|
||||
.all_source_breakpoints(cx)
|
||||
.clone()
|
||||
});
|
||||
|
||||
@@ -18547,7 +18547,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.all_breakpoints(cx)
|
||||
.all_source_breakpoints(cx)
|
||||
.clone()
|
||||
});
|
||||
|
||||
@@ -18569,7 +18569,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.all_breakpoints(cx)
|
||||
.all_source_breakpoints(cx)
|
||||
.clone()
|
||||
});
|
||||
|
||||
@@ -18636,7 +18636,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.all_breakpoints(cx)
|
||||
.all_source_breakpoints(cx)
|
||||
.clone()
|
||||
});
|
||||
|
||||
@@ -18657,7 +18657,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.all_breakpoints(cx)
|
||||
.all_source_breakpoints(cx)
|
||||
.clone()
|
||||
});
|
||||
|
||||
@@ -18677,7 +18677,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.all_breakpoints(cx)
|
||||
.all_source_breakpoints(cx)
|
||||
.clone()
|
||||
});
|
||||
|
||||
@@ -18700,7 +18700,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.all_breakpoints(cx)
|
||||
.all_source_breakpoints(cx)
|
||||
.clone()
|
||||
});
|
||||
|
||||
@@ -18723,7 +18723,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.all_breakpoints(cx)
|
||||
.all_source_breakpoints(cx)
|
||||
.clone()
|
||||
});
|
||||
|
||||
@@ -18816,7 +18816,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.all_breakpoints(cx)
|
||||
.all_source_breakpoints(cx)
|
||||
.clone()
|
||||
});
|
||||
|
||||
@@ -18848,7 +18848,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.all_breakpoints(cx)
|
||||
.all_source_breakpoints(cx)
|
||||
.clone()
|
||||
});
|
||||
|
||||
@@ -18884,7 +18884,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.all_breakpoints(cx)
|
||||
.all_source_breakpoints(cx)
|
||||
.clone()
|
||||
});
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ use multi_buffer::{
|
||||
|
||||
use project::{
|
||||
ProjectPath,
|
||||
debugger::breakpoint_store::Breakpoint,
|
||||
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
|
||||
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)>,
|
||||
breakpoints: HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
|
||||
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))| {
|
||||
.filter_map(|(display_row, (text_anchor, bp, state))| {
|
||||
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, cx);
|
||||
let button = editor.render_breakpoint(text_anchor, display_row, &bp, state, 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)>,
|
||||
breakpoints: &mut HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Vec<AnyElement> {
|
||||
@@ -7454,8 +7454,10 @@ impl Element for EditorElement {
|
||||
editor.active_breakpoints(start_row..end_row, window, cx)
|
||||
});
|
||||
if cx.has_flag::<DebuggerFeatureFlag>() {
|
||||
for display_row in breakpoint_rows.keys() {
|
||||
active_rows.entry(*display_row).or_default().breakpoint = true;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7495,7 +7497,7 @@ impl Element for EditorElement {
|
||||
let breakpoint = Breakpoint::new_standard();
|
||||
phantom_breakpoint.collides_with_existing_breakpoint =
|
||||
false;
|
||||
(position, breakpoint)
|
||||
(position, breakpoint, None)
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -30,6 +30,7 @@ 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
|
||||
|
||||
@@ -422,6 +422,7 @@ 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);
|
||||
|
||||
@@ -233,6 +233,10 @@ 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();
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ pub trait Extension: Send + Sync + 'static {
|
||||
dap_name: Arc<str>,
|
||||
config: DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<DebugAdapterBinary>;
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,8 @@ pub struct ExtensionManifest {
|
||||
pub snippets: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub capabilities: Vec<ExtensionCapability>,
|
||||
#[serde(default)]
|
||||
pub debug_adapters: Vec<Arc<str>>,
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
@@ -274,6 +276,7 @@ fn manifest_from_old_manifest(
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +304,7 @@ mod tests {
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: vec![],
|
||||
debug_adapters: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ pub use wit::{
|
||||
KeyValueStore, LanguageServerInstallationStatus, Project, Range, Worktree, download_file,
|
||||
make_file_executable,
|
||||
zed::extension::context_server::ContextServerConfiguration,
|
||||
zed::extension::dap::{
|
||||
DebugAdapterBinary, DebugRequest, DebugTaskDefinition, StartDebuggingRequestArguments,
|
||||
StartDebuggingRequestArgumentsRequest, TcpArguments, TcpArgumentsTemplate,
|
||||
resolve_tcp_template,
|
||||
},
|
||||
zed::extension::github::{
|
||||
GithubRelease, GithubReleaseAsset, GithubReleaseOptions, github_release_by_tag_name,
|
||||
latest_github_release,
|
||||
@@ -194,6 +199,7 @@ pub trait Extension: Send + Sync {
|
||||
_adapter_name: String,
|
||||
_config: DebugTaskDefinition,
|
||||
_user_provided_path: Option<String>,
|
||||
_worktree: &Worktree,
|
||||
) -> Result<DebugAdapterBinary, String> {
|
||||
Err("`get_dap_binary` not implemented".to_string())
|
||||
}
|
||||
@@ -386,8 +392,9 @@ impl wit::Guest for Component {
|
||||
adapter_name: String,
|
||||
config: DebugTaskDefinition,
|
||||
user_installed_path: Option<String>,
|
||||
) -> Result<DebugAdapterBinary, String> {
|
||||
extension().get_dap_binary(adapter_name, config, user_installed_path)
|
||||
worktree: &Worktree,
|
||||
) -> Result<wit::DebugAdapterBinary, String> {
|
||||
extension().get_dap_binary(adapter_name, config, user_installed_path, worktree)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
interface dap {
|
||||
use common.{env-vars};
|
||||
|
||||
/// Resolves a specified TcpArgumentsTemplate into TcpArguments
|
||||
resolve-tcp-template: func(template: tcp-arguments-template) -> result<tcp-arguments, string>;
|
||||
|
||||
record launch-request {
|
||||
program: string,
|
||||
cwd: option<string>,
|
||||
@@ -27,6 +31,7 @@ interface dap {
|
||||
host: option<u32>,
|
||||
timeout: option<u64>,
|
||||
}
|
||||
|
||||
record debug-task-definition {
|
||||
label: string,
|
||||
adapter: string,
|
||||
@@ -40,11 +45,12 @@ interface dap {
|
||||
launch,
|
||||
attach,
|
||||
}
|
||||
|
||||
record start-debugging-request-arguments {
|
||||
configuration: string,
|
||||
request: start-debugging-request-arguments-request,
|
||||
|
||||
}
|
||||
|
||||
record debug-adapter-binary {
|
||||
command: string,
|
||||
arguments: list<string>,
|
||||
|
||||
@@ -11,7 +11,7 @@ world extension {
|
||||
|
||||
use common.{env-vars, range};
|
||||
use context-server.{context-server-configuration};
|
||||
use dap.{debug-adapter-binary, debug-task-definition};
|
||||
use dap.{debug-adapter-binary, debug-task-definition, debug-request};
|
||||
use lsp.{completion, symbol};
|
||||
use process.{command};
|
||||
use slash-command.{slash-command, slash-command-argument-completion, slash-command-output};
|
||||
@@ -157,5 +157,5 @@ world extension {
|
||||
export index-docs: func(provider-name: string, package-name: string, database: borrow<key-value-store>) -> result<_, string>;
|
||||
|
||||
/// Returns a configured debug adapter binary for a given debug task.
|
||||
export get-dap-binary: func(adapter-name: string, config: debug-task-definition, user-installed-path: option<string>) -> result<debug-adapter-binary, string>;
|
||||
export get-dap-binary: func(adapter-name: string, config: debug-task-definition, user-installed-path: option<string>, worktree: borrow<worktree>) -> result<debug-adapter-binary, string>;
|
||||
}
|
||||
|
||||
@@ -14,9 +14,10 @@ use collections::{BTreeMap, BTreeSet, HashMap, HashSet, btree_map};
|
||||
pub use extension::ExtensionManifest;
|
||||
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
|
||||
use extension::{
|
||||
ExtensionContextServerProxy, ExtensionEvents, ExtensionGrammarProxy, ExtensionHostProxy,
|
||||
ExtensionIndexedDocsProviderProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy,
|
||||
ExtensionSlashCommandProxy, ExtensionSnippetProxy, ExtensionThemeProxy,
|
||||
ExtensionContextServerProxy, ExtensionDebugAdapterProviderProxy, ExtensionEvents,
|
||||
ExtensionGrammarProxy, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy,
|
||||
ExtensionLanguageProxy, ExtensionLanguageServerProxy, ExtensionSlashCommandProxy,
|
||||
ExtensionSnippetProxy, ExtensionThemeProxy,
|
||||
};
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::{
|
||||
@@ -1328,6 +1329,11 @@ impl ExtensionStore {
|
||||
this.proxy
|
||||
.register_indexed_docs_provider(extension.clone(), provider_id.clone());
|
||||
}
|
||||
|
||||
for debug_adapter in &manifest.debug_adapters {
|
||||
this.proxy
|
||||
.register_debug_adapter(extension.clone(), debug_adapter.clone());
|
||||
}
|
||||
}
|
||||
|
||||
this.wasm_extensions.extend(wasm_extensions);
|
||||
|
||||
@@ -164,6 +164,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
@@ -193,6 +194,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
@@ -367,6 +369,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
|
||||
@@ -379,11 +379,13 @@ impl extension::Extension for WasmExtension {
|
||||
dap_name: Arc<str>,
|
||||
config: DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
self.call(|extension, store| {
|
||||
async move {
|
||||
let resource = store.data_mut().table().push(worktree)?;
|
||||
let dap_binary = extension
|
||||
.call_get_dap_binary(store, dap_name, config, user_installed_path)
|
||||
.call_get_dap_binary(store, dap_name, config, user_installed_path, resource)
|
||||
.await?
|
||||
.map_err(|err| anyhow!("{err:?}"))?;
|
||||
let dap_binary = dap_binary.try_into()?;
|
||||
|
||||
@@ -903,6 +903,7 @@ impl Extension {
|
||||
adapter_name: Arc<str>,
|
||||
task: DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
resource: Resource<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<Result<DebugAdapterBinary, String>> {
|
||||
match self {
|
||||
Extension::V0_6_0(ext) => {
|
||||
@@ -912,6 +913,7 @@ impl Extension {
|
||||
&adapter_name,
|
||||
&task.try_into()?,
|
||||
user_installed_path.as_ref().and_then(|p| p.to_str()),
|
||||
resource,
|
||||
)
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{e:?}"))?;
|
||||
|
||||
@@ -48,7 +48,7 @@ wasmtime::component::bindgen!({
|
||||
pub use self::zed::extension::*;
|
||||
|
||||
mod settings {
|
||||
include!(concat!(env!("OUT_DIR"), "/since_v0.5.0/settings.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/since_v0.6.0/settings.rs"));
|
||||
}
|
||||
|
||||
pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
|
||||
@@ -729,8 +729,29 @@ impl slash_command::Host for WasmState {}
|
||||
#[async_trait]
|
||||
impl context_server::Host for WasmState {}
|
||||
|
||||
#[async_trait]
|
||||
impl dap::Host for WasmState {}
|
||||
impl dap::Host for WasmState {
|
||||
async fn resolve_tcp_template(
|
||||
&mut self,
|
||||
template: TcpArgumentsTemplate,
|
||||
) -> wasmtime::Result<Result<TcpArguments, String>> {
|
||||
maybe!(async {
|
||||
let (host, port, timeout) =
|
||||
::dap::configure_tcp_connection(task::TcpArgumentsTemplate {
|
||||
port: template.port,
|
||||
host: template.host.map(Ipv4Addr::from_bits),
|
||||
timeout: template.timeout,
|
||||
})
|
||||
.await?;
|
||||
Ok(TcpArguments {
|
||||
port,
|
||||
host: host.to_bits(),
|
||||
timeout,
|
||||
})
|
||||
})
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionImports for WasmState {
|
||||
async fn get_settings(
|
||||
|
||||
@@ -748,7 +748,6 @@ impl GitRepository for RealGitRepository {
|
||||
"--no-optional-locks",
|
||||
"cat-file",
|
||||
"--batch-check=%(objectname)",
|
||||
"-z",
|
||||
])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
@@ -761,7 +760,7 @@ impl GitRepository for RealGitRepository {
|
||||
.ok_or_else(|| anyhow!("no stdin for git cat-file subprocess"))?;
|
||||
let mut stdin = BufWriter::new(stdin);
|
||||
for rev in &revs {
|
||||
write!(&mut stdin, "{rev}\0")?;
|
||||
write!(&mut stdin, "{rev}\n")?;
|
||||
}
|
||||
drop(stdin);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
mod client;
|
||||
mod clipboard;
|
||||
mod display;
|
||||
mod event;
|
||||
mod window;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::platform::scap_screen_capture::scap_screen_sources;
|
||||
use core::str;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
@@ -40,9 +41,8 @@ use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSIO
|
||||
use xkbcommon::xkb::{self as xkbc, LayoutIndex, ModMask, STATE_LAYOUT_EFFECTIVE};
|
||||
|
||||
use super::{
|
||||
ButtonOrScroll, ScrollDirection, button_or_scroll_from_event_detail,
|
||||
clipboard::{self, Clipboard},
|
||||
get_valuator_axis_index, modifiers_from_state, pressed_button_from_mask,
|
||||
ButtonOrScroll, ScrollDirection, button_or_scroll_from_event_detail, get_valuator_axis_index,
|
||||
modifiers_from_state, pressed_button_from_mask,
|
||||
};
|
||||
use super::{X11Display, X11WindowStatePtr, XcbAtoms};
|
||||
use super::{XimCallbackEvent, XimHandler};
|
||||
@@ -56,7 +56,6 @@ use crate::platform::{
|
||||
reveal_path_internal,
|
||||
xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
|
||||
},
|
||||
scap_screen_capture::scap_screen_sources,
|
||||
};
|
||||
use crate::{
|
||||
AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke,
|
||||
@@ -202,7 +201,7 @@ pub struct X11ClientState {
|
||||
pointer_device_states: BTreeMap<xinput::DeviceId, PointerDeviceState>,
|
||||
|
||||
pub(crate) common: LinuxCommon,
|
||||
pub(crate) clipboard: Clipboard,
|
||||
pub(crate) clipboard: x11_clipboard::Clipboard,
|
||||
pub(crate) clipboard_item: Option<ClipboardItem>,
|
||||
pub(crate) xdnd_state: Xdnd,
|
||||
}
|
||||
@@ -389,7 +388,7 @@ impl X11Client {
|
||||
.reply()
|
||||
.unwrap();
|
||||
|
||||
let clipboard = Clipboard::new().unwrap();
|
||||
let clipboard = x11_clipboard::Clipboard::new().unwrap();
|
||||
|
||||
let xcb_connection = Rc::new(xcb_connection);
|
||||
|
||||
@@ -1497,36 +1496,39 @@ impl LinuxClient for X11Client {
|
||||
let state = self.0.borrow_mut();
|
||||
state
|
||||
.clipboard
|
||||
.set_text(
|
||||
std::borrow::Cow::Owned(item.text().unwrap_or_default()),
|
||||
clipboard::ClipboardKind::Primary,
|
||||
clipboard::WaitConfig::None,
|
||||
.store(
|
||||
state.clipboard.setter.atoms.primary,
|
||||
state.clipboard.setter.atoms.utf8_string,
|
||||
item.text().unwrap_or_default().as_bytes(),
|
||||
)
|
||||
.context("Failed to write to clipboard (primary)")
|
||||
.log_with_level(log::Level::Debug);
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
state
|
||||
.clipboard
|
||||
.set_text(
|
||||
std::borrow::Cow::Owned(item.text().unwrap_or_default()),
|
||||
clipboard::ClipboardKind::Clipboard,
|
||||
clipboard::WaitConfig::None,
|
||||
.store(
|
||||
state.clipboard.setter.atoms.clipboard,
|
||||
state.clipboard.setter.atoms.utf8_string,
|
||||
item.text().unwrap_or_default().as_bytes(),
|
||||
)
|
||||
.context("Failed to write to clipboard (clipboard)")
|
||||
.log_with_level(log::Level::Debug);
|
||||
.ok();
|
||||
state.clipboard_item.replace(item);
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
let state = self.0.borrow_mut();
|
||||
return state
|
||||
state
|
||||
.clipboard
|
||||
.get_any(clipboard::ClipboardKind::Primary)
|
||||
.context("Failed to read from clipboard (primary)")
|
||||
.log_with_level(log::Level::Debug);
|
||||
.load(
|
||||
state.clipboard.getter.atoms.primary,
|
||||
state.clipboard.getter.atoms.utf8_string,
|
||||
state.clipboard.getter.atoms.property,
|
||||
Duration::from_secs(3),
|
||||
)
|
||||
.map(|text| crate::ClipboardItem::new_string(String::from_utf8(text).unwrap()))
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
||||
@@ -1535,15 +1537,26 @@ impl LinuxClient for X11Client {
|
||||
// which has metadata attached.
|
||||
if state
|
||||
.clipboard
|
||||
.is_owner(clipboard::ClipboardKind::Clipboard)
|
||||
.setter
|
||||
.connection
|
||||
.get_selection_owner(state.clipboard.setter.atoms.clipboard)
|
||||
.ok()
|
||||
.and_then(|r| r.reply().ok())
|
||||
.map(|reply| reply.owner == state.clipboard.setter.window)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return state.clipboard_item.clone();
|
||||
}
|
||||
return state
|
||||
state
|
||||
.clipboard
|
||||
.get_any(clipboard::ClipboardKind::Clipboard)
|
||||
.context("Failed to read from clipboard (clipboard)")
|
||||
.log_with_level(log::Level::Debug);
|
||||
.load(
|
||||
state.clipboard.getter.atoms.clipboard,
|
||||
state.clipboard.getter.atoms.utf8_string,
|
||||
state.clipboard.getter.atoms.property,
|
||||
Duration::from_secs(3),
|
||||
)
|
||||
.map(|text| crate::ClipboardItem::new_string(String::from_utf8(text).unwrap()))
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn run(&self) {
|
||||
|
||||
@@ -27,6 +27,19 @@ pub trait FluentBuilder {
|
||||
self.map(|this| if condition { then(this) } else { this })
|
||||
}
|
||||
|
||||
/// Conditionally modify self with the given closure.
|
||||
fn when_else(
|
||||
self,
|
||||
condition: bool,
|
||||
then: impl FnOnce(Self) -> Self,
|
||||
else_fn: impl FnOnce(Self) -> Self,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.map(|this| if condition { then(this) } else { else_fn(this) })
|
||||
}
|
||||
|
||||
/// Conditionally unwrap and modify self with the given closure, if the given option is Some.
|
||||
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
|
||||
where
|
||||
|
||||
@@ -83,6 +83,13 @@ impl EditPredictionUsage {
|
||||
|
||||
Ok(Self { limit, amount })
|
||||
}
|
||||
|
||||
pub fn over_limit(&self) -> bool {
|
||||
match self.limit {
|
||||
UsageLimit::Limited(limit) => self.amount >= limit,
|
||||
UsageLimit::Unlimited => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EditPredictionProvider: 'static + Sized {
|
||||
|
||||
@@ -33,7 +33,7 @@ use workspace::{
|
||||
StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle,
|
||||
notifications::NotificationId,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
use zed_actions::{OpenBrowser, OpenZedUrl};
|
||||
use zed_llm_client::UsageLimit;
|
||||
use zeta::RateCompletions;
|
||||
|
||||
@@ -277,14 +277,31 @@ impl Render for InlineCompletionButton {
|
||||
);
|
||||
}
|
||||
|
||||
let mut over_limit = false;
|
||||
|
||||
if let Some(usage) = self
|
||||
.edit_prediction_provider
|
||||
.as_ref()
|
||||
.and_then(|provider| provider.usage(cx))
|
||||
{
|
||||
over_limit = usage.over_limit()
|
||||
}
|
||||
|
||||
let show_editor_predictions = self.editor_show_predictions;
|
||||
|
||||
let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
|
||||
.shape(IconButtonShape::Square)
|
||||
.when(enabled && !show_editor_predictions, |this| {
|
||||
this.indicator(Indicator::dot().color(Color::Muted))
|
||||
.when(
|
||||
enabled && (!show_editor_predictions || over_limit),
|
||||
|this| {
|
||||
this.indicator(Indicator::dot().when_else(
|
||||
over_limit,
|
||||
|dot| dot.color(Color::Error),
|
||||
|dot| dot.color(Color::Muted),
|
||||
))
|
||||
.indicator_border_color(Some(cx.theme().colors().status_bar_background))
|
||||
})
|
||||
},
|
||||
)
|
||||
.when(!self.popover_menu_handle.is_deployed(), |element| {
|
||||
element.tooltip(move |window, cx| {
|
||||
if enabled {
|
||||
@@ -440,6 +457,16 @@ impl InlineCompletionButton {
|
||||
},
|
||||
move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
|
||||
)
|
||||
.when(usage.over_limit(), |menu| -> ContextMenu {
|
||||
menu.entry("Subscribe to increase your limit", None, |window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(OpenZedUrl {
|
||||
url: zed_urls::account_url(cx),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.separator();
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ tree-sitter.workspace = true
|
||||
unicase = "2.6"
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
diffy = "0.4.2"
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -65,7 +65,9 @@ use std::{num::NonZeroU32, sync::OnceLock};
|
||||
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
|
||||
use task::RunnableTag;
|
||||
pub use task_context::{ContextProvider, RunnableRange};
|
||||
pub use text_diff::{DiffOptions, line_diff, text_diff, text_diff_with_options, unified_diff};
|
||||
pub use text_diff::{
|
||||
DiffOptions, apply_diff_patch, line_diff, text_diff, text_diff_with_options, unified_diff,
|
||||
};
|
||||
use theme::SyntaxTheme;
|
||||
pub use toolchain::{LanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister};
|
||||
use tree_sitter::{self, Query, QueryCursor, WasmStore, wasmtime};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{CharClassifier, CharKind, LanguageScope};
|
||||
use anyhow::{Context, anyhow};
|
||||
use imara_diff::{
|
||||
Algorithm, UnifiedDiffBuilder, diff,
|
||||
intern::{InternedInput, Token},
|
||||
@@ -119,6 +120,12 @@ pub fn text_diff_with_options(
|
||||
edits
|
||||
}
|
||||
|
||||
pub fn apply_diff_patch(base_text: &str, patch: &str) -> Result<String, anyhow::Error> {
|
||||
let patch = diffy::Patch::from_str(patch).context("Failed to parse patch")?;
|
||||
let result = diffy::apply(base_text, &patch);
|
||||
result.map_err(|err| anyhow!(err))
|
||||
}
|
||||
|
||||
fn should_perform_word_diff_within_hunk(
|
||||
old_row_range: &Range<u32>,
|
||||
old_byte_range: &Range<usize>,
|
||||
@@ -270,4 +277,12 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_diff_patch() {
|
||||
let old_text = "one two\nthree four five\nsix seven eight nine\nten\n";
|
||||
let new_text = "one two\nthree FOUR five\nsix SEVEN eight nine\nten\nELEVEN\n";
|
||||
let patch = unified_diff(old_text, new_text);
|
||||
assert_eq!(apply_diff_patch(old_text, &patch).unwrap(), new_text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ pub trait ToolchainLister: Send + Sync {
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait LanguageToolchainStore {
|
||||
pub trait LanguageToolchainStore: Send + Sync + 'static {
|
||||
async fn active_toolchain(
|
||||
self: Arc<Self>,
|
||||
worktree_id: WorktreeId,
|
||||
|
||||
@@ -16,7 +16,6 @@ use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window};
|
||||
use http_client::http::{HeaderMap, HeaderValue};
|
||||
use icons::IconName;
|
||||
use parking_lot::Mutex;
|
||||
use proto::Plan;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use std::fmt;
|
||||
@@ -48,15 +47,6 @@ pub fn init_settings(cx: &mut App) {
|
||||
registry::init(cx);
|
||||
}
|
||||
|
||||
/// The availability of a [`LanguageModel`].
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum LanguageModelAvailability {
|
||||
/// The language model is available to the general public.
|
||||
Public,
|
||||
/// The language model is available to users on the indicated plan.
|
||||
RequiresPlan(Plan),
|
||||
}
|
||||
|
||||
/// Configuration for caching language model messages.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct LanguageModelCacheConfiguration {
|
||||
@@ -110,6 +100,7 @@ pub enum StopReason {
|
||||
EndTurn,
|
||||
MaxTokens,
|
||||
ToolUse,
|
||||
Refusal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -238,11 +229,6 @@ pub trait LanguageModel: Send + Sync {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the availability of this language model.
|
||||
fn availability(&self) -> LanguageModelAvailability {
|
||||
LanguageModelAvailability::Public
|
||||
}
|
||||
|
||||
/// Whether this model supports images
|
||||
fn supports_images(&self) -> bool;
|
||||
|
||||
@@ -259,6 +245,10 @@ pub trait LanguageModel: Send + Sync {
|
||||
}
|
||||
|
||||
const MAX_MODE_CAPABLE_MODELS: &[CloudModel] = &[
|
||||
CloudModel::Anthropic(anthropic::Model::ClaudeOpus4),
|
||||
CloudModel::Anthropic(anthropic::Model::ClaudeOpus4Thinking),
|
||||
CloudModel::Anthropic(anthropic::Model::ClaudeSonnet4),
|
||||
CloudModel::Anthropic(anthropic::Model::ClaudeSonnet4Thinking),
|
||||
CloudModel::Anthropic(anthropic::Model::Claude3_7Sonnet),
|
||||
CloudModel::Anthropic(anthropic::Model::Claude3_7SonnetThinking),
|
||||
];
|
||||
|
||||
@@ -13,7 +13,7 @@ use smol::lock::{RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard};
|
||||
use strum::EnumIter;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{LanguageModelAvailability, LanguageModelToolSchemaFormat};
|
||||
use crate::LanguageModelToolSchemaFormat;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "provider", rename_all = "lowercase")]
|
||||
@@ -60,59 +60,6 @@ impl CloudModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the availability of this model.
|
||||
pub fn availability(&self) -> LanguageModelAvailability {
|
||||
match self {
|
||||
Self::Anthropic(model) => match model {
|
||||
anthropic::Model::Claude3_5Sonnet
|
||||
| anthropic::Model::Claude3_7Sonnet
|
||||
| anthropic::Model::Claude3_7SonnetThinking => {
|
||||
LanguageModelAvailability::RequiresPlan(Plan::Free)
|
||||
}
|
||||
anthropic::Model::Claude3Opus
|
||||
| anthropic::Model::Claude3Sonnet
|
||||
| anthropic::Model::Claude3Haiku
|
||||
| anthropic::Model::Claude3_5Haiku
|
||||
| anthropic::Model::Custom { .. } => {
|
||||
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
|
||||
}
|
||||
},
|
||||
Self::OpenAi(model) => match model {
|
||||
open_ai::Model::ThreePointFiveTurbo
|
||||
| open_ai::Model::Four
|
||||
| open_ai::Model::FourTurbo
|
||||
| open_ai::Model::FourOmni
|
||||
| open_ai::Model::FourOmniMini
|
||||
| open_ai::Model::FourPointOne
|
||||
| open_ai::Model::FourPointOneMini
|
||||
| open_ai::Model::FourPointOneNano
|
||||
| open_ai::Model::O1Mini
|
||||
| open_ai::Model::O1Preview
|
||||
| open_ai::Model::O1
|
||||
| open_ai::Model::O3Mini
|
||||
| open_ai::Model::O3
|
||||
| open_ai::Model::O4Mini
|
||||
| open_ai::Model::Custom { .. } => {
|
||||
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
|
||||
}
|
||||
},
|
||||
Self::Google(model) => match model {
|
||||
google_ai::Model::Gemini15Pro
|
||||
| google_ai::Model::Gemini15Flash
|
||||
| google_ai::Model::Gemini20Pro
|
||||
| google_ai::Model::Gemini20Flash
|
||||
| google_ai::Model::Gemini20FlashThinking
|
||||
| google_ai::Model::Gemini20FlashLite
|
||||
| google_ai::Model::Gemini25ProExp0325
|
||||
| google_ai::Model::Gemini25ProPreview0325
|
||||
| google_ai::Model::Gemini25FlashPreview0417
|
||||
| google_ai::Model::Custom { .. } => {
|
||||
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
|
||||
match self {
|
||||
Self::Anthropic(_) | Self::OpenAi(_) => LanguageModelToolSchemaFormat::JsonSchema,
|
||||
|
||||
@@ -326,8 +326,14 @@ struct GroupedModels {
|
||||
|
||||
impl GroupedModels {
|
||||
pub fn new(other: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
|
||||
let recommended_ids: HashSet<_> = recommended.iter().map(|info| info.model.id()).collect();
|
||||
|
||||
let mut other_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
|
||||
for model in other {
|
||||
if recommended_ids.contains(&model.model.id()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let provider = model.model.provider_id();
|
||||
if let Some(models) = other_by_provider.get_mut(&provider) {
|
||||
models.push(model);
|
||||
@@ -889,4 +895,26 @@ mod tests {
|
||||
let results = matcher.fuzzy_search("z4n");
|
||||
assert_models_eq(results, vec!["zed/gpt-4.1-nano"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_exclude_recommended_models(_cx: &mut TestAppContext) {
|
||||
let recommended_models = create_models(vec![("zed", "claude")]);
|
||||
let all_models = create_models(vec![
|
||||
("zed", "claude"), // Should be filtered out from "other"
|
||||
("zed", "gemini"),
|
||||
("copilot", "o3"),
|
||||
]);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
|
||||
let actual_other_models = grouped_models
|
||||
.other
|
||||
.values()
|
||||
.flatten()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Recommended models should not appear in "other"
|
||||
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/o3"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,8 +240,8 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
|
||||
|
||||
fn recommended_models(&self, _cx: &App) -> Vec<Arc<dyn LanguageModel>> {
|
||||
[
|
||||
anthropic::Model::Claude3_7Sonnet,
|
||||
anthropic::Model::Claude3_7SonnetThinking,
|
||||
anthropic::Model::ClaudeSonnet4,
|
||||
anthropic::Model::ClaudeSonnet4Thinking,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|model| self.create_language_model(model))
|
||||
@@ -820,6 +820,7 @@ impl AnthropicEventMapper {
|
||||
"end_turn" => StopReason::EndTurn,
|
||||
"max_tokens" => StopReason::MaxTokens,
|
||||
"tool_use" => StopReason::ToolUse,
|
||||
"refusal" => StopReason::Refusal,
|
||||
_ => {
|
||||
log::error!("Unexpected anthropic stop_reason: {stop_reason}");
|
||||
StopReason::EndTurn
|
||||
|
||||
@@ -19,8 +19,8 @@ use language_model::{
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_model::{
|
||||
LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider, LlmApiToken,
|
||||
PaymentRequiredError, RefreshLlmTokenListener,
|
||||
LanguageModelCompletionEvent, LanguageModelProvider, LlmApiToken, PaymentRequiredError,
|
||||
RefreshLlmTokenListener,
|
||||
};
|
||||
use proto::Plan;
|
||||
use release_channel::AppVersion;
|
||||
@@ -278,7 +278,7 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
||||
|
||||
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
let llm_api_token = self.state.read(cx).llm_api_token.clone();
|
||||
let model = CloudModel::Anthropic(anthropic::Model::Claude3_7Sonnet);
|
||||
let model = CloudModel::Anthropic(anthropic::Model::ClaudeSonnet4);
|
||||
Some(self.create_language_model(model, llm_api_token))
|
||||
}
|
||||
|
||||
@@ -291,8 +291,8 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
||||
fn recommended_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
|
||||
let llm_api_token = self.state.read(cx).llm_api_token.clone();
|
||||
[
|
||||
CloudModel::Anthropic(anthropic::Model::Claude3_7Sonnet),
|
||||
CloudModel::Anthropic(anthropic::Model::Claude3_7SonnetThinking),
|
||||
CloudModel::Anthropic(anthropic::Model::ClaudeSonnet4),
|
||||
CloudModel::Anthropic(anthropic::Model::ClaudeSonnet4Thinking),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|model| self.create_language_model(model, llm_api_token.clone()))
|
||||
@@ -331,6 +331,14 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
||||
anthropic::Model::Claude3_7SonnetThinking.id().to_string(),
|
||||
CloudModel::Anthropic(anthropic::Model::Claude3_7SonnetThinking),
|
||||
);
|
||||
models.insert(
|
||||
anthropic::Model::ClaudeSonnet4.id().to_string(),
|
||||
CloudModel::Anthropic(anthropic::Model::ClaudeSonnet4),
|
||||
);
|
||||
models.insert(
|
||||
anthropic::Model::ClaudeSonnet4Thinking.id().to_string(),
|
||||
CloudModel::Anthropic(anthropic::Model::ClaudeSonnet4Thinking),
|
||||
);
|
||||
}
|
||||
|
||||
let llm_closed_beta_models = if cx.has_flag::<LlmClosedBetaFeatureFlag>() {
|
||||
@@ -699,10 +707,6 @@ impl LanguageModel for CloudLanguageModel {
|
||||
format!("zed.dev/{}", self.model.id())
|
||||
}
|
||||
|
||||
fn availability(&self) -> LanguageModelAvailability {
|
||||
self.model.availability()
|
||||
}
|
||||
|
||||
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
|
||||
self.model.tool_input_format()
|
||||
}
|
||||
|
||||
@@ -628,6 +628,7 @@ impl GoogleEventMapper {
|
||||
// responds with `finish_reason: STOP`
|
||||
if wants_to_use_tool {
|
||||
self.stop_reason = StopReason::ToolUse;
|
||||
events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
@@ -477,14 +477,14 @@ fn add_message_content_part(
|
||||
_ => {
|
||||
messages.push(match role {
|
||||
Role::User => open_ai::RequestMessage::User {
|
||||
content: open_ai::MessageContent::empty(),
|
||||
content: open_ai::MessageContent::from(vec![new_part]),
|
||||
},
|
||||
Role::Assistant => open_ai::RequestMessage::Assistant {
|
||||
content: open_ai::MessageContent::empty(),
|
||||
content: open_ai::MessageContent::from(vec![new_part]),
|
||||
tool_calls: Vec::new(),
|
||||
},
|
||||
Role::System => open_ai::RequestMessage::System {
|
||||
content: open_ai::MessageContent::empty(),
|
||||
content: open_ai::MessageContent::from(vec![new_part]),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user