debugger: Move breakpoint management to the pane strip (#33062)

Closes #ISSUE

Release Notes:

- debugger: Moved "remove breakpoint" button to the top of a breakpoint
list"
This commit is contained in:
Piotr Osiewicz 2025-06-19 22:29:15 +02:00 committed by GitHub
parent bca1a9145c
commit 00fe195416
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 198 additions and 135 deletions

View file

@ -1472,8 +1472,10 @@ impl Render for DebugPanel {
h_flex().size_full()
.items_start()
.child(v_flex().items_start().min_w_1_3().h_full().p_1()
.child(h_flex().px_1().child(Label::new("Breakpoints").size(LabelSize::Small)))
.child(v_flex().group("base-breakpoint-list").items_start().min_w_1_3().h_full().p_1()
.child(h_flex().pl_1().w_full().justify_between()
.child(Label::new("Breakpoints").size(LabelSize::Small))
.child(h_flex().visible_on_hover("base-breakpoint-list").child(self.breakpoint_list.read(cx).render_control_strip())))
.child(Divider::horizontal())
.child(self.breakpoint_list.clone()))
.child(Divider::vertical())

View file

@ -265,56 +265,37 @@ pub(crate) fn deserialize_pane_layout(
stack_frame_list.focus_handle(cx),
stack_frame_list.clone().into(),
DebuggerPaneItem::Frames,
None,
cx,
)),
DebuggerPaneItem::Variables => Box::new(SubView::new(
variable_list.focus_handle(cx),
variable_list.clone().into(),
DebuggerPaneItem::Variables,
None,
cx,
)),
DebuggerPaneItem::BreakpointList => Box::new(SubView::new(
breakpoint_list.focus_handle(cx),
breakpoint_list.clone().into(),
DebuggerPaneItem::BreakpointList,
None,
cx,
)),
DebuggerPaneItem::BreakpointList => {
Box::new(SubView::breakpoint_list(breakpoint_list.clone(), cx))
}
DebuggerPaneItem::Modules => Box::new(SubView::new(
module_list.focus_handle(cx),
module_list.clone().into(),
DebuggerPaneItem::Modules,
None,
cx,
)),
DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
loaded_sources.focus_handle(cx),
loaded_sources.clone().into(),
DebuggerPaneItem::LoadedSources,
None,
cx,
)),
DebuggerPaneItem::Console => Box::new(SubView::new(
console.focus_handle(cx),
console.clone().into(),
DebuggerPaneItem::Console,
Some(Box::new({
let console = console.clone().downgrade();
move |cx| {
console
.read_with(cx, |console, cx| console.show_indicator(cx))
.unwrap_or_default()
}
})),
cx,
)),
DebuggerPaneItem::Console => {
let view = SubView::console(console.clone(), cx);
Box::new(view)
}
DebuggerPaneItem::Terminal => Box::new(SubView::new(
terminal.focus_handle(cx),
terminal.clone().into(),
DebuggerPaneItem::Terminal,
None,
cx,
)),
})

View file

@ -135,6 +135,7 @@ pub(crate) struct SubView {
item_focus_handle: FocusHandle,
kind: DebuggerPaneItem,
show_indicator: Box<dyn Fn(&App) -> bool>,
actions: Option<Box<dyn FnMut(&mut Window, &mut App) -> AnyElement>>,
hovered: bool,
}
@ -143,21 +144,68 @@ impl SubView {
item_focus_handle: FocusHandle,
view: AnyView,
kind: DebuggerPaneItem,
show_indicator: Option<Box<dyn Fn(&App) -> bool>>,
cx: &mut App,
) -> Entity<Self> {
cx.new(|_| Self {
kind,
inner: view,
item_focus_handle,
show_indicator: show_indicator.unwrap_or(Box::new(|_| false)),
show_indicator: Box::new(|_| false),
actions: None,
hovered: false,
})
}
pub(crate) fn console(console: Entity<Console>, cx: &mut App) -> Entity<Self> {
let weak_console = console.downgrade();
let this = Self::new(
console.focus_handle(cx),
console.into(),
DebuggerPaneItem::Console,
cx,
);
this.update(cx, |this, _| {
this.with_indicator(Box::new(move |cx| {
weak_console
.read_with(cx, |console, cx| console.show_indicator(cx))
.unwrap_or_default()
}))
});
this
}
pub(crate) fn breakpoint_list(list: Entity<BreakpointList>, cx: &mut App) -> Entity<Self> {
let weak_list = list.downgrade();
let focus_handle = list.focus_handle(cx);
let this = Self::new(
focus_handle.clone(),
list.into(),
DebuggerPaneItem::BreakpointList,
cx,
);
this.update(cx, |this, _| {
this.with_actions(Box::new(move |_, cx| {
weak_list
.update(cx, |this, _| this.render_control_strip())
.unwrap_or_else(|_| div().into_any_element())
}));
});
this
}
pub(crate) fn view_kind(&self) -> DebuggerPaneItem {
self.kind
}
pub(crate) fn with_indicator(&mut self, indicator: Box<dyn Fn(&App) -> bool>) {
self.show_indicator = indicator;
}
pub(crate) fn with_actions(
&mut self,
actions: Box<dyn FnMut(&mut Window, &mut App) -> AnyElement>,
) {
self.actions = Some(actions);
}
}
impl Focusable for SubView {
fn focus_handle(&self, _: &App) -> FocusHandle {
@ -359,10 +407,13 @@ pub(crate) fn new_debugger_pane(
let active_pane_item = pane.active_item();
let pane_group_id: SharedString =
format!("pane-zoom-button-hover-{}", cx.entity_id()).into();
let is_hovered = active_pane_item.as_ref().map_or(false, |item| {
item.downcast::<SubView>()
.map_or(false, |this| this.read(cx).hovered)
});
let as_subview = active_pane_item
.as_ref()
.and_then(|item| item.downcast::<SubView>());
let is_hovered = as_subview
.as_ref()
.map_or(false, |item| item.read(cx).hovered);
h_flex()
.group(pane_group_id.clone())
.justify_between()
@ -459,9 +510,17 @@ pub(crate) fn new_debugger_pane(
)
.child({
let zoomed = pane.is_zoomed();
div()
h_flex()
.visible_on_hover(pane_group_id)
.when(is_hovered, |this| this.visible())
.when_some(as_subview.as_ref(), |this, subview| {
subview.update(cx, |view, cx| {
let Some(additional_actions) = view.actions.as_mut() else {
return this;
};
this.child(additional_actions(window, cx))
})
})
.child(
IconButton::new(
SharedString::from(format!(
@ -1095,61 +1154,38 @@ impl RunningState {
cx: &mut Context<Self>,
) -> Box<dyn ItemHandle> {
match item_kind {
DebuggerPaneItem::Console => {
let weak_console = self.console.clone().downgrade();
Box::new(SubView::new(
self.console.focus_handle(cx),
self.console.clone().into(),
item_kind,
Some(Box::new(move |cx| {
weak_console
.read_with(cx, |console, cx| console.show_indicator(cx))
.unwrap_or_default()
})),
cx,
))
}
DebuggerPaneItem::Console => Box::new(SubView::console(self.console.clone(), cx)),
DebuggerPaneItem::Variables => Box::new(SubView::new(
self.variable_list.focus_handle(cx),
self.variable_list.clone().into(),
item_kind,
None,
cx,
)),
DebuggerPaneItem::BreakpointList => Box::new(SubView::new(
self.breakpoint_list.focus_handle(cx),
self.breakpoint_list.clone().into(),
item_kind,
None,
cx,
)),
DebuggerPaneItem::BreakpointList => {
Box::new(SubView::breakpoint_list(self.breakpoint_list.clone(), cx))
}
DebuggerPaneItem::Frames => Box::new(SubView::new(
self.stack_frame_list.focus_handle(cx),
self.stack_frame_list.clone().into(),
item_kind,
None,
cx,
)),
DebuggerPaneItem::Modules => Box::new(SubView::new(
self.module_list.focus_handle(cx),
self.module_list.clone().into(),
item_kind,
None,
cx,
)),
DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
self.loaded_sources_list.focus_handle(cx),
self.loaded_sources_list.clone().into(),
item_kind,
None,
cx,
)),
DebuggerPaneItem::Terminal => Box::new(SubView::new(
self.debug_terminal.focus_handle(cx),
self.debug_terminal.clone().into(),
item_kind,
None,
cx,
)),
}
@ -1558,7 +1594,6 @@ impl RunningState {
this.focus_handle(cx),
stack_frame_list.clone().into(),
DebuggerPaneItem::Frames,
None,
cx,
)),
true,
@ -1568,13 +1603,7 @@ impl RunningState {
cx,
);
this.add_item(
Box::new(SubView::new(
breakpoints.focus_handle(cx),
breakpoints.clone().into(),
DebuggerPaneItem::BreakpointList,
None,
cx,
)),
Box::new(SubView::breakpoint_list(breakpoints.clone(), cx)),
true,
false,
None,
@ -1586,32 +1615,15 @@ impl RunningState {
let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
center_pane.update(cx, |this, cx| {
let weak_console = console.downgrade();
this.add_item(
Box::new(SubView::new(
console.focus_handle(cx),
console.clone().into(),
DebuggerPaneItem::Console,
Some(Box::new(move |cx| {
weak_console
.read_with(cx, |console, cx| console.show_indicator(cx))
.unwrap_or_default()
})),
cx,
)),
true,
false,
None,
window,
cx,
);
let view = SubView::console(console.clone(), cx);
this.add_item(Box::new(view), true, false, None, window, cx);
this.add_item(
Box::new(SubView::new(
variable_list.focus_handle(cx),
variable_list.clone().into(),
DebuggerPaneItem::Variables,
None,
cx,
)),
true,
@ -1630,7 +1642,6 @@ impl RunningState {
debug_terminal.focus_handle(cx),
debug_terminal.clone().into(),
DebuggerPaneItem::Terminal,
None,
cx,
)),
false,

View file

@ -8,8 +8,8 @@ use std::{
use dap::ExceptionBreakpointsFilter;
use editor::Editor;
use gpui::{
AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful, Task,
UniformListScrollHandle, WeakEntity, uniform_list,
Action, AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful,
Task, UniformListScrollHandle, WeakEntity, uniform_list,
};
use language::Point;
use project::{
@ -21,15 +21,21 @@ use project::{
worktree_store::WorktreeStore,
};
use ui::{
App, ButtonCommon, Clickable, Color, Context, Div, FluentBuilder as _, Icon, IconButton,
IconName, Indicator, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem,
ParentElement, Render, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div, FluentBuilder as _,
Icon, IconButton, IconName, IconSize, Indicator, InteractiveElement, IntoElement, Label,
LabelCommon, LabelSize, ListItem, ParentElement, Render, Scrollbar, ScrollbarState,
SharedString, StatefulInteractiveElement, Styled, Toggleable, Tooltip, Window, div, h_flex, px,
v_flex,
};
use util::ResultExt;
use workspace::Workspace;
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum SelectedBreakpointKind {
Source,
Exception,
}
pub(crate) struct BreakpointList {
workspace: WeakEntity<Workspace>,
breakpoint_store: Entity<BreakpointStore>,
@ -127,6 +133,21 @@ impl BreakpointList {
.detach();
}
pub(crate) fn selection_kind(&self) -> Option<(SelectedBreakpointKind, bool)> {
self.selected_ix.and_then(|ix| {
self.breakpoints.get(ix).map(|bp| match &bp.kind {
BreakpointEntryKind::LineBreakpoint(bp) => (
SelectedBreakpointKind::Source,
bp.breakpoint.state
== project::debugger::breakpoint_store::BreakpointState::Enabled,
),
BreakpointEntryKind::ExceptionBreakpoint(bp) => {
(SelectedBreakpointKind::Exception, bp.is_enabled)
}
})
})
}
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
self.selected_ix = ix;
if let Some(ix) = ix {
@ -333,7 +354,93 @@ impl BreakpointList {
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
)
}
pub(crate) fn render_control_strip(&self) -> AnyElement {
let selection_kind = self.selection_kind();
let focus_handle = self.focus_handle.clone();
let remove_breakpoint_tooltip = selection_kind.map(|(kind, _)| match kind {
SelectedBreakpointKind::Source => "Remove breakpoint from a breakpoint list",
SelectedBreakpointKind::Exception => {
"Exception Breakpoints cannot be removed from the breakpoint list"
}
});
let toggle_label = selection_kind.map(|(_, is_enabled)| {
if is_enabled {
(
"Disable Breakpoint",
"Disable a breakpoint without removing it from the list",
)
} else {
("Enable Breakpoint", "Re-enable a breakpoint")
}
});
h_flex()
.gap_2()
.child(
IconButton::new(
"disable-breakpoint-breakpoint-list",
IconName::DebugDisabledBreakpoint,
)
.icon_size(IconSize::XSmall)
.when_some(toggle_label, |this, (label, meta)| {
this.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::with_meta_in(
label,
Some(&ToggleEnableBreakpoint),
meta,
&focus_handle,
window,
cx,
)
}
})
})
.disabled(selection_kind.is_none())
.on_click({
let focus_handle = focus_handle.clone();
move |_, window, cx| {
focus_handle.focus(window);
window.dispatch_action(ToggleEnableBreakpoint.boxed_clone(), cx)
}
}),
)
.child(
IconButton::new("remove-breakpoint-breakpoint-list", IconName::X)
.icon_size(IconSize::XSmall)
.icon_color(ui::Color::Error)
.when_some(remove_breakpoint_tooltip, |this, tooltip| {
this.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::with_meta_in(
"Remove Breakpoint",
Some(&UnsetBreakpoint),
tooltip,
&focus_handle,
window,
cx,
)
}
})
})
.disabled(
selection_kind.map(|kind| kind.0) != Some(SelectedBreakpointKind::Source),
)
.on_click({
let focus_handle = focus_handle.clone();
move |_, window, cx| {
focus_handle.focus(window);
window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx)
}
}),
)
.mr_2()
.into_any_element()
}
}
impl Render for BreakpointList {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
// let old_len = self.breakpoints.len();
@ -505,44 +612,6 @@ impl LineBreakpoint {
.on_secondary_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.end_hover_slot(
h_flex()
.child(
IconButton::new(
SharedString::from(format!(
"breakpoint-ui-on-click-go-to-line-remove-{:?}/{}:{}",
self.dir, self.name, self.line
)),
IconName::Close,
)
.on_click({
let weak = weak.clone();
let path = path.clone();
move |_, _, cx| {
weak.update(cx, |breakpoint_list, cx| {
breakpoint_list.edit_line_breakpoint(
path.clone(),
row,
BreakpointEditAction::Toggle,
cx,
);
})
.ok();
}
})
.tooltip(move |window, cx| {
Tooltip::for_action_in(
"Unset Breakpoint",
&UnsetBreakpoint,
&focus_handle,
window,
cx,
)
})
.icon_size(ui::IconSize::XSmall),
)
.right_4(),
)
.child(
v_flex()
.py_1()