debugger: Add keyboard navigation for breakpoint list (#31221)

Release Notes:

- Debugger Beta: made it possible to navigate the breakpoint list using
menu keybindings.
This commit is contained in:
Cole Miller 2025-05-26 15:40:07 -04:00 committed by GitHub
parent 4acb4730a5
commit ee415de45f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 385 additions and 171 deletions

1
Cargo.lock generated
View file

@ -4248,6 +4248,7 @@ dependencies = [
"util", "util",
"workspace", "workspace",
"workspace-hack", "workspace-hack",
"zed_actions",
"zlog", "zlog",
] ]

View file

@ -872,6 +872,13 @@
"ctrl-i": "debugger::ToggleSessionPicker" "ctrl-i": "debugger::ToggleSessionPicker"
} }
}, },
{
"context": "BreakpointList",
"bindings": {
"space": "debugger::ToggleEnableBreakpoint",
"backspace": "debugger::UnsetBreakpoint"
}
},
{ {
"context": "CollabPanel && not_editing", "context": "CollabPanel && not_editing",
"bindings": { "bindings": {

View file

@ -932,6 +932,13 @@
"cmd-i": "debugger::ToggleSessionPicker" "cmd-i": "debugger::ToggleSessionPicker"
} }
}, },
{
"context": "BreakpointList",
"bindings": {
"space": "debugger::ToggleEnableBreakpoint",
"backspace": "debugger::UnsetBreakpoint"
}
},
{ {
"context": "CollabPanel && not_editing", "context": "CollabPanel && not_editing",
"use_key_equivalents": true, "use_key_equivalents": true,

View file

@ -63,6 +63,7 @@ workspace.workspace = true
workspace-hack.workspace = true workspace-hack.workspace = true
debugger_tools = { workspace = true, optional = true } debugger_tools = { workspace = true, optional = true }
unindent = { workspace = true, optional = true } unindent = { workspace = true, optional = true }
zed_actions.workspace = true
[dev-dependencies] [dev-dependencies]
dap = { workspace = true, features = ["test-support"] } dap = { workspace = true, features = ["test-support"] }

View file

@ -1,13 +1,14 @@
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc,
time::Duration, time::Duration,
}; };
use dap::ExceptionBreakpointsFilter; use dap::ExceptionBreakpointsFilter;
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
AppContext, Entity, FocusHandle, Focusable, ListState, MouseButton, Stateful, Task, WeakEntity, AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful, Task,
list, UniformListScrollHandle, WeakEntity, uniform_list,
}; };
use language::Point; use language::Point;
use project::{ use project::{
@ -19,25 +20,27 @@ use project::{
worktree_store::WorktreeStore, worktree_store::WorktreeStore,
}; };
use ui::{ use ui::{
App, Clickable, Color, Context, Div, Icon, IconButton, IconName, Indicator, InteractiveElement, App, ButtonCommon, Clickable, Color, Context, Div, FluentBuilder as _, Icon, IconButton,
IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce, IconName, Indicator, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem,
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Tooltip, Window, ParentElement, Render, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
div, h_flex, px, v_flex, Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
}; };
use util::{ResultExt, maybe}; use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
pub(crate) struct BreakpointList { pub(crate) struct BreakpointList {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
breakpoint_store: Entity<BreakpointStore>, breakpoint_store: Entity<BreakpointStore>,
worktree_store: Entity<WorktreeStore>, worktree_store: Entity<WorktreeStore>,
list_state: ListState,
scrollbar_state: ScrollbarState, scrollbar_state: ScrollbarState,
breakpoints: Vec<BreakpointEntry>, breakpoints: Vec<BreakpointEntry>,
session: Entity<Session>, session: Entity<Session>,
hide_scrollbar_task: Option<Task<()>>, hide_scrollbar_task: Option<Task<()>>,
show_scrollbar: bool, show_scrollbar: bool,
focus_handle: FocusHandle, focus_handle: FocusHandle,
scroll_handle: UniformListScrollHandle,
selected_ix: Option<usize>,
} }
impl Focusable for BreakpointList { impl Focusable for BreakpointList {
@ -56,38 +59,205 @@ impl BreakpointList {
let project = project.read(cx); let project = project.read(cx);
let breakpoint_store = project.breakpoint_store(); let breakpoint_store = project.breakpoint_store();
let worktree_store = project.worktree_store(); let worktree_store = project.worktree_store();
let focus_handle = cx.focus_handle();
let scroll_handle = UniformListScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
cx.new(|cx| { cx.new(|_| {
let weak: gpui::WeakEntity<Self> = cx.weak_entity();
let list_state = ListState::new(
0,
gpui::ListAlignment::Top,
px(1000.),
move |ix, window, cx| {
let Ok(Some(breakpoint)) =
weak.update(cx, |this, _| this.breakpoints.get(ix).cloned())
else {
return div().into_any_element();
};
breakpoint.render(window, cx).into_any_element()
},
);
Self { Self {
breakpoint_store, breakpoint_store,
worktree_store, worktree_store,
scrollbar_state: ScrollbarState::new(list_state.clone()), scrollbar_state,
list_state, // list_state,
breakpoints: Default::default(), breakpoints: Default::default(),
hide_scrollbar_task: None, hide_scrollbar_task: None,
show_scrollbar: false, show_scrollbar: false,
workspace, workspace,
session, session,
focus_handle: cx.focus_handle(), focus_handle,
scroll_handle,
selected_ix: None,
} }
}) })
} }
fn edit_line_breakpoint(
&mut self,
path: Arc<Path>,
row: u32,
action: BreakpointEditAction,
cx: &mut Context<Self>,
) {
self.breakpoint_store.update(cx, |breakpoint_store, cx| {
if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
} else {
log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
}
})
}
fn go_to_line_breakpoint(
&mut self,
path: Arc<Path>,
row: u32,
window: &mut Window,
cx: &mut Context<Self>,
) {
let task = self
.worktree_store
.update(cx, |this, cx| this.find_or_create_worktree(path, false, cx));
cx.spawn_in(window, async move |this, cx| {
let (worktree, relative_path) = task.await?;
let worktree_id = worktree.update(cx, |this, _| this.id())?;
let item = this
.update_in(cx, |this, window, cx| {
this.workspace.update(cx, |this, cx| {
this.open_path((worktree_id, relative_path), None, true, window, cx)
})
})??
.await?;
if let Some(editor) = item.downcast::<Editor>() {
editor
.update_in(cx, |this, window, cx| {
this.go_to_singleton_buffer_point(Point { row, column: 0 }, window, cx);
})
.ok();
}
anyhow::Ok(())
})
.detach();
}
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.breakpoints.len() == 0 => None,
None => Some(0),
Some(ix) => {
if ix == self.breakpoints.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.breakpoints.len() == 0 => None,
None => Some(self.breakpoints.len() - 1),
Some(ix) => {
if ix == 0 {
Some(self.breakpoints.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.breakpoints.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.breakpoints.len() > 0 {
Some(self.breakpoints.len() - 1)
} else {
None
};
self.select_ix(ix, cx);
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
return;
};
match &mut entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
let path = line_breakpoint.breakpoint.path.clone();
let row = line_breakpoint.breakpoint.row;
self.go_to_line_breakpoint(path, row, window, cx);
}
BreakpointEntryKind::ExceptionBreakpoint(_) => {}
}
}
fn toggle_enable_breakpoint(
&mut self,
_: &ToggleEnableBreakpoint,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
return;
};
match &mut entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
let path = line_breakpoint.breakpoint.path.clone();
let row = line_breakpoint.breakpoint.row;
self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
}
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
let id = exception_breakpoint.id.clone();
self.session.update(cx, |session, cx| {
session.toggle_exception_breakpoint(&id, cx);
});
}
}
cx.notify();
}
fn unset_breakpoint(
&mut self,
_: &UnsetBreakpoint,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
return;
};
match &mut entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
let path = line_breakpoint.breakpoint.path.clone();
let row = line_breakpoint.breakpoint.row;
self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
}
BreakpointEntryKind::ExceptionBreakpoint(_) => {}
}
cx.notify();
}
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| { self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
@ -103,6 +273,30 @@ impl BreakpointList {
})) }))
} }
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let selected_ix = self.selected_ix;
let focus_handle = self.focus_handle.clone();
uniform_list(
cx.entity(),
"breakpoint-list",
self.breakpoints.len(),
move |this, range, window, cx| {
range
.clone()
.zip(&mut this.breakpoints[range])
.map(|(ix, breakpoint)| {
breakpoint
.render(ix, focus_handle.clone(), window, cx)
.toggle_state(Some(ix) == selected_ix)
.into_any_element()
})
.collect()
},
)
.track_scroll(self.scroll_handle.clone())
.flex_grow()
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> { fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) { if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) {
return None; return None;
@ -142,12 +336,8 @@ impl BreakpointList {
} }
} }
impl Render for BreakpointList { impl Render for BreakpointList {
fn render( fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
&mut self, // let old_len = self.breakpoints.len();
_window: &mut ui::Window,
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
let old_len = self.breakpoints.len();
let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx); let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
self.breakpoints.clear(); self.breakpoints.clear();
let weak = cx.weak_entity(); let weak = cx.weak_entity();
@ -183,7 +373,7 @@ impl Render for BreakpointList {
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
.map(SharedString::from)?; .map(SharedString::from)?;
let weak = weak.clone(); let weak = weak.clone();
let line = format!("Line {}", breakpoint.row + 1).into(); let line = breakpoint.row + 1;
Some(BreakpointEntry { Some(BreakpointEntry {
kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint { kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
name, name,
@ -209,11 +399,9 @@ impl Render for BreakpointList {
}); });
self.breakpoints self.breakpoints
.extend(breakpoints.chain(exception_breakpoints)); .extend(breakpoints.chain(exception_breakpoints));
if self.breakpoints.len() != old_len {
self.list_state.reset(self.breakpoints.len());
}
v_flex() v_flex()
.id("breakpoint-list") .id("breakpoint-list")
.key_context("BreakpointList")
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.on_hover(cx.listener(|this, hovered, window, cx| { .on_hover(cx.listener(|this, hovered, window, cx| {
if *hovered { if *hovered {
@ -224,9 +412,16 @@ impl Render for BreakpointList {
this.hide_scrollbar(window, cx); this.hide_scrollbar(window, cx);
} }
})) }))
.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))
.on_action(cx.listener(Self::toggle_enable_breakpoint))
.on_action(cx.listener(Self::unset_breakpoint))
.size_full() .size_full()
.m_0p5() .m_0p5()
.child(list(self.list_state.clone()).flex_grow()) .child(self.render_list(window, cx))
.children(self.render_vertical_scrollbar(cx)) .children(self.render_vertical_scrollbar(cx))
} }
} }
@ -234,55 +429,58 @@ impl Render for BreakpointList {
struct LineBreakpoint { struct LineBreakpoint {
name: SharedString, name: SharedString,
dir: Option<SharedString>, dir: Option<SharedString>,
line: SharedString, line: u32,
breakpoint: SourceBreakpoint, breakpoint: SourceBreakpoint,
} }
impl LineBreakpoint { impl LineBreakpoint {
fn render(self, weak: WeakEntity<BreakpointList>) -> ListItem { fn render(
let LineBreakpoint { &mut self,
name, ix: usize,
dir, focus_handle: FocusHandle,
line, weak: WeakEntity<BreakpointList>,
breakpoint, ) -> ListItem {
} = self; let icon_name = if self.breakpoint.state.is_enabled() {
let icon_name = if breakpoint.state.is_enabled() {
IconName::DebugBreakpoint IconName::DebugBreakpoint
} else { } else {
IconName::DebugDisabledBreakpoint IconName::DebugDisabledBreakpoint
}; };
let path = breakpoint.path; let path = self.breakpoint.path.clone();
let row = breakpoint.row; let row = self.breakpoint.row;
let is_enabled = self.breakpoint.state.is_enabled();
let indicator = div() let indicator = div()
.id(SharedString::from(format!( .id(SharedString::from(format!(
"breakpoint-ui-toggle-{:?}/{}:{}", "breakpoint-ui-toggle-{:?}/{}:{}",
dir, name, line self.dir, self.name, self.line
))) )))
.cursor_pointer() .cursor_pointer()
.tooltip(Tooltip::text(if breakpoint.state.is_enabled() { .tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
if is_enabled {
"Disable Breakpoint" "Disable Breakpoint"
} else { } else {
"Enable Breakpoint" "Enable Breakpoint"
})) },
&ToggleEnableBreakpoint,
&focus_handle,
window,
cx,
)
}
})
.on_click({ .on_click({
let weak = weak.clone(); let weak = weak.clone();
let path = path.clone(); let path = path.clone();
move |_, _, cx| { move |_, _, cx| {
weak.update(cx, |this, cx| { weak.update(cx, |breakpoint_list, cx| {
this.breakpoint_store.update(cx, |this, cx| { breakpoint_list.edit_line_breakpoint(
if let Some((buffer, breakpoint)) = path.clone(),
this.breakpoint_at_row(&path, row, cx) row,
{
this.toggle_breakpoint(
buffer,
breakpoint,
BreakpointEditAction::InvertState, BreakpointEditAction::InvertState,
cx, cx,
); );
} else {
log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
}
})
}) })
.ok(); .ok();
} }
@ -291,8 +489,17 @@ impl LineBreakpoint {
.on_mouse_down(MouseButton::Left, move |_, _, _| {}); .on_mouse_down(MouseButton::Left, move |_, _, _| {});
ListItem::new(SharedString::from(format!( ListItem::new(SharedString::from(format!(
"breakpoint-ui-item-{:?}/{}:{}", "breakpoint-ui-item-{:?}/{}:{}",
dir, name, line self.dir, self.name, self.line
))) )))
.on_click({
let weak = weak.clone();
move |_, _, cx| {
weak.update(cx, |breakpoint_list, cx| {
breakpoint_list.select_ix(Some(ix), cx);
})
.ok();
}
})
.start_slot(indicator) .start_slot(indicator)
.rounded() .rounded()
.on_secondary_mouse_down(|_, _, cx| { .on_secondary_mouse_down(|_, _, cx| {
@ -302,7 +509,7 @@ impl LineBreakpoint {
IconButton::new( IconButton::new(
SharedString::from(format!( SharedString::from(format!(
"breakpoint-ui-on-click-go-to-line-remove-{:?}/{}:{}", "breakpoint-ui-on-click-go-to-line-remove-{:?}/{}:{}",
dir, name, line self.dir, self.name, self.line
)), )),
IconName::Close, IconName::Close,
) )
@ -310,103 +517,60 @@ impl LineBreakpoint {
let weak = weak.clone(); let weak = weak.clone();
let path = path.clone(); let path = path.clone();
move |_, _, cx| { move |_, _, cx| {
weak.update(cx, |this, cx| { weak.update(cx, |breakpoint_list, cx| {
this.breakpoint_store.update(cx, |this, cx| { breakpoint_list.edit_line_breakpoint(
if let Some((buffer, breakpoint)) = path.clone(),
this.breakpoint_at_row(&path, row, cx) row,
{
this.toggle_breakpoint(
buffer,
breakpoint,
BreakpointEditAction::Toggle, BreakpointEditAction::Toggle,
cx, cx,
); );
} else {
log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
}
})
}) })
.ok(); .ok();
} }
}) })
.icon_size(ui::IconSize::XSmall), .tooltip(move |window, cx| {
Tooltip::for_action_in(
"Unset Breakpoint",
&UnsetBreakpoint,
&focus_handle,
window,
cx,
)
})
.icon_size(ui::IconSize::Indicator),
) )
.child( .child(
v_flex() v_flex()
.py_1()
.gap_1()
.min_h(px(22.))
.justify_center()
.id(SharedString::from(format!( .id(SharedString::from(format!(
"breakpoint-ui-on-click-go-to-line-{:?}/{}:{}", "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
dir, name, line self.dir, self.name, self.line
))) )))
.on_click(move |_, window, cx| { .on_click(move |_, window, cx| {
let path = path.clone(); weak.update(cx, |breakpoint_list, cx| {
let weak = weak.clone(); breakpoint_list.select_ix(Some(ix), cx);
let row = breakpoint.row; breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
maybe!({
let task = weak
.update(cx, |this, cx| {
this.worktree_store.update(cx, |this, cx| {
this.find_or_create_worktree(path, false, cx)
})
})
.ok()?;
window
.spawn(cx, async move |cx| {
let (worktree, relative_path) = task.await?;
let worktree_id = worktree.update(cx, |this, _| this.id())?;
let item = weak
.update_in(cx, |this, window, cx| {
this.workspace.update(cx, |this, cx| {
this.open_path(
(worktree_id, relative_path),
None,
true,
window,
cx,
)
})
})??
.await?;
if let Some(editor) = item.downcast::<Editor>() {
editor
.update_in(cx, |this, window, cx| {
this.go_to_singleton_buffer_point(
Point { row, column: 0 },
window,
cx,
);
}) })
.ok(); .ok();
}
anyhow::Ok(())
})
.detach();
Some(())
});
}) })
.cursor_pointer() .cursor_pointer()
.py_1()
.items_center()
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
.child( .child(
Label::new(name) Label::new(format!("{}:{}", self.name, self.line))
.size(LabelSize::Small) .size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel), .line_height_style(ui::LineHeightStyle::UiLabel),
) )
.children(dir.map(|dir| { .children(self.dir.clone().map(|dir| {
Label::new(dir) Label::new(dir)
.color(Color::Muted) .color(Color::Muted)
.size(LabelSize::Small) .size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel) .line_height_style(ui::LineHeightStyle::UiLabel)
})), })),
)
.child(
Label::new(line)
.size(LabelSize::XSmall)
.color(Color::Muted)
.line_height_style(ui::LineHeightStyle::UiLabel),
), ),
) )
} }
@ -419,17 +583,31 @@ struct ExceptionBreakpoint {
} }
impl ExceptionBreakpoint { impl ExceptionBreakpoint {
fn render(self, list: WeakEntity<BreakpointList>) -> ListItem { fn render(
&mut self,
ix: usize,
focus_handle: FocusHandle,
list: WeakEntity<BreakpointList>,
) -> ListItem {
let color = if self.is_enabled { let color = if self.is_enabled {
Color::Debugger Color::Debugger
} else { } else {
Color::Muted Color::Muted
}; };
let id = SharedString::from(&self.id); let id = SharedString::from(&self.id);
let is_enabled = self.is_enabled;
ListItem::new(SharedString::from(format!( ListItem::new(SharedString::from(format!(
"exception-breakpoint-ui-item-{}", "exception-breakpoint-ui-item-{}",
self.id self.id
))) )))
.on_click({
let list = list.clone();
move |_, _, cx| {
list.update(cx, |list, cx| list.select_ix(Some(ix), cx))
.ok();
}
})
.rounded() .rounded()
.on_secondary_mouse_down(|_, _, cx| { .on_secondary_mouse_down(|_, _, cx| {
cx.stop_propagation(); cx.stop_propagation();
@ -440,12 +618,22 @@ impl ExceptionBreakpoint {
"exception-breakpoint-ui-item-{}-click-handler", "exception-breakpoint-ui-item-{}-click-handler",
self.id self.id
))) )))
.tooltip(Tooltip::text(if self.is_enabled { .tooltip(move |window, cx| {
Tooltip::for_action_in(
if is_enabled {
"Disable Exception Breakpoint" "Disable Exception Breakpoint"
} else { } else {
"Enable Exception Breakpoint" "Enable Exception Breakpoint"
})) },
.on_click(move |_, _, cx| { &ToggleEnableBreakpoint,
&focus_handle,
window,
cx,
)
})
.on_click({
let list = list.clone();
move |_, _, cx| {
list.update(cx, |this, cx| { list.update(cx, |this, cx| {
this.session.update(cx, |this, cx| { this.session.update(cx, |this, cx| {
this.toggle_exception_breakpoint(&id, cx); this.toggle_exception_breakpoint(&id, cx);
@ -453,25 +641,26 @@ impl ExceptionBreakpoint {
cx.notify(); cx.notify();
}) })
.ok(); .ok();
}
}) })
.cursor_pointer() .cursor_pointer()
.child(Indicator::icon(Icon::new(IconName::Flame)).color(color)), .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
) )
.child( .child(
div() v_flex()
.py_1() .py_1()
.gap_1() .gap_1()
.min_h(px(22.))
.justify_center()
.id(("exception-breakpoint-label", ix))
.child( .child(
Label::new(self.data.label) Label::new(self.data.label.clone())
.size(LabelSize::Small) .size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel), .line_height_style(ui::LineHeightStyle::UiLabel),
) )
.children(self.data.description.map(|description| { .when_some(self.data.description.clone(), |el, description| {
Label::new(description) el.tooltip(Tooltip::text(description))
.size(LabelSize::XSmall) }),
.line_height_style(ui::LineHeightStyle::UiLabel)
.color(Color::Muted)
})),
) )
} }
} }
@ -486,14 +675,21 @@ struct BreakpointEntry {
kind: BreakpointEntryKind, kind: BreakpointEntryKind,
weak: WeakEntity<BreakpointList>, weak: WeakEntity<BreakpointList>,
} }
impl RenderOnce for BreakpointEntry {
fn render(self, _: &mut ui::Window, _: &mut App) -> impl ui::IntoElement { impl BreakpointEntry {
match self.kind { fn render(
&mut self,
ix: usize,
focus_handle: FocusHandle,
_: &mut Window,
_: &mut App,
) -> ListItem {
match &mut self.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
line_breakpoint.render(self.weak) line_breakpoint.render(ix, focus_handle, self.weak.clone())
} }
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => { BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
exception_breakpoint.render(self.weak) exception_breakpoint.render(ix, focus_handle, self.weak.clone())
} }
} }
} }

View file

@ -339,3 +339,5 @@ pub mod outline {
actions!(zed_predict_onboarding, [OpenZedPredictOnboarding]); actions!(zed_predict_onboarding, [OpenZedPredictOnboarding]);
actions!(git_onboarding, [OpenGitIntegrationOnboarding]); actions!(git_onboarding, [OpenGitIntegrationOnboarding]);
actions!(debugger, [ToggleEnableBreakpoint, UnsetBreakpoint]);