diff --git a/Cargo.lock b/Cargo.lock index 19e105e9a3..ef2f698d0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4310,6 +4310,7 @@ version = "0.1.0" dependencies = [ "alacritty_terminal", "anyhow", + "bitflags 2.9.0", "client", "collections", "command_palette_hooks", diff --git a/assets/icons/arrow_down10.svg b/assets/icons/arrow_down10.svg new file mode 100644 index 0000000000..97ce967a8b --- /dev/null +++ b/assets/icons/arrow_down10.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/scroll_text.svg b/assets/icons/scroll_text.svg new file mode 100644 index 0000000000..f066c8a84e --- /dev/null +++ b/assets/icons/scroll_text.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/split_alt.svg b/assets/icons/split_alt.svg new file mode 100644 index 0000000000..3f7622701d --- /dev/null +++ b/assets/icons/split_alt.svg @@ -0,0 +1 @@ + diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 525907a71a..ca94fd4853 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -919,7 +919,9 @@ "context": "BreakpointList", "bindings": { "space": "debugger::ToggleEnableBreakpoint", - "backspace": "debugger::UnsetBreakpoint" + "backspace": "debugger::UnsetBreakpoint", + "left": "debugger::PreviousBreakpointProperty", + "right": "debugger::NextBreakpointProperty" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 121dbe93e0..fa38480c37 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -980,7 +980,9 @@ "context": "BreakpointList", "bindings": { "space": "debugger::ToggleEnableBreakpoint", - "backspace": "debugger::UnsetBreakpoint" + "backspace": "debugger::UnsetBreakpoint", + "left": "debugger::PreviousBreakpointProperty", + "right": "debugger::NextBreakpointProperty" } }, { diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 91f9acad3c..ba71e50a08 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -28,6 +28,7 @@ test-support = [ [dependencies] alacritty_terminal.workspace = true anyhow.workspace = true +bitflags.workspace = true client.workspace = true collections.workspace = true command_palette_hooks.workspace = true diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index b7f3be0426..8ced5d1eea 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -100,7 +100,13 @@ impl DebugPanel { sessions: vec![], active_session: None, focus_handle, - breakpoint_list: BreakpointList::new(None, workspace.weak_handle(), &project, cx), + breakpoint_list: BreakpointList::new( + None, + workspace.weak_handle(), + &project, + window, + cx, + ), project, workspace: workspace.weak_handle(), context_menu: None, diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 6a3535fe0e..58001ce11d 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -697,8 +697,13 @@ impl RunningState { ) }); - let breakpoint_list = - BreakpointList::new(Some(session.clone()), workspace.clone(), &project, cx); + let breakpoint_list = BreakpointList::new( + Some(session.clone()), + workspace.clone(), + &project, + window, + cx, + ); let _subscriptions = vec![ cx.on_app_quit(move |this, cx| { diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 8077b289a7..d19eb8c777 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -5,11 +5,11 @@ use std::{ time::Duration, }; -use dap::ExceptionBreakpointsFilter; +use dap::{Capabilities, ExceptionBreakpointsFilter}; use editor::Editor; use gpui::{ - Action, AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful, - Task, UniformListScrollHandle, WeakEntity, uniform_list, + Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, + Stateful, Task, UniformListScrollHandle, WeakEntity, actions, uniform_list, }; use language::Point; use project::{ @@ -21,16 +21,20 @@ use project::{ worktree_store::WorktreeStore, }; use ui::{ - 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, + ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div, + Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, Indicator, + InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, + Render, RenderOnce, 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}; +actions!( + debugger, + [PreviousBreakpointProperty, NextBreakpointProperty] +); #[derive(Clone, Copy, PartialEq)] pub(crate) enum SelectedBreakpointKind { Source, @@ -48,6 +52,8 @@ pub(crate) struct BreakpointList { focus_handle: FocusHandle, scroll_handle: UniformListScrollHandle, selected_ix: Option, + input: Entity, + strip_mode: Option, } impl Focusable for BreakpointList { @@ -56,11 +62,19 @@ impl Focusable for BreakpointList { } } +#[derive(Clone, Copy, PartialEq)] +enum ActiveBreakpointStripMode { + Log, + Condition, + HitCondition, +} + impl BreakpointList { pub(crate) fn new( session: Option>, workspace: WeakEntity, project: &Entity, + window: &mut Window, cx: &mut App, ) -> Entity { let project = project.read(cx); @@ -70,7 +84,7 @@ impl BreakpointList { let scroll_handle = UniformListScrollHandle::new(); let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); - cx.new(|_| Self { + cx.new(|cx| Self { breakpoint_store, worktree_store, scrollbar_state, @@ -82,17 +96,28 @@ impl BreakpointList { focus_handle, scroll_handle, selected_ix: None, + input: cx.new(|cx| Editor::single_line(window, cx)), + strip_mode: None, }) } fn edit_line_breakpoint( - &mut self, + &self, path: Arc, row: u32, action: BreakpointEditAction, - cx: &mut Context, + cx: &mut App, ) { - self.breakpoint_store.update(cx, |breakpoint_store, cx| { + Self::edit_line_breakpoint_inner(&self.breakpoint_store, path, row, action, cx); + } + fn edit_line_breakpoint_inner( + breakpoint_store: &Entity, + path: Arc, + row: u32, + action: BreakpointEditAction, + cx: &mut App, + ) { + 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 { @@ -148,16 +173,63 @@ impl BreakpointList { }) } - fn select_ix(&mut self, ix: Option, cx: &mut Context) { + fn set_active_breakpoint_property( + &mut self, + prop: ActiveBreakpointStripMode, + window: &mut Window, + cx: &mut App, + ) { + self.strip_mode = Some(prop); + let placeholder = match prop { + ActiveBreakpointStripMode::Log => "Set Log Message", + ActiveBreakpointStripMode::Condition => "Set Condition", + ActiveBreakpointStripMode::HitCondition => "Set Hit Condition", + }; + let mut is_exception_breakpoint = true; + let active_value = self.selected_ix.and_then(|ix| { + self.breakpoints.get(ix).and_then(|bp| { + if let BreakpointEntryKind::LineBreakpoint(bp) = &bp.kind { + is_exception_breakpoint = false; + match prop { + ActiveBreakpointStripMode::Log => bp.breakpoint.message.clone(), + ActiveBreakpointStripMode::Condition => bp.breakpoint.condition.clone(), + ActiveBreakpointStripMode::HitCondition => { + bp.breakpoint.hit_condition.clone() + } + } + } else { + None + } + }) + }); + + self.input.update(cx, |this, cx| { + this.set_placeholder_text(placeholder, cx); + this.set_read_only(is_exception_breakpoint); + this.set_text(active_value.as_deref().unwrap_or(""), window, cx); + }); + } + + fn select_ix(&mut self, ix: Option, window: &mut Window, cx: &mut Context) { self.selected_ix = ix; if let Some(ix) = ix { self.scroll_handle .scroll_to_item(ix, ScrollStrategy::Center); } + if let Some(mode) = self.strip_mode { + self.set_active_breakpoint_property(mode, window, cx); + } + cx.notify(); } - fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context) { + fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context) { + if self.strip_mode.is_some() { + if self.input.focus_handle(cx).contains_focused(window, cx) { + cx.propagate(); + return; + } + } let ix = match self.selected_ix { _ if self.breakpoints.len() == 0 => None, None => Some(0), @@ -169,15 +241,21 @@ impl BreakpointList { } } }; - self.select_ix(ix, cx); + self.select_ix(ix, window, cx); } fn select_previous( &mut self, _: &menu::SelectPrevious, - _window: &mut Window, + window: &mut Window, cx: &mut Context, ) { + if self.strip_mode.is_some() { + if self.input.focus_handle(cx).contains_focused(window, cx) { + cx.propagate(); + return; + } + } let ix = match self.selected_ix { _ if self.breakpoints.len() == 0 => None, None => Some(self.breakpoints.len() - 1), @@ -189,37 +267,105 @@ impl BreakpointList { } } }; - self.select_ix(ix, cx); + self.select_ix(ix, window, cx); } - fn select_first( - &mut self, - _: &menu::SelectFirst, - _window: &mut Window, - cx: &mut Context, - ) { + fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context) { + if self.strip_mode.is_some() { + if self.input.focus_handle(cx).contains_focused(window, cx) { + cx.propagate(); + return; + } + } let ix = if self.breakpoints.len() > 0 { Some(0) } else { None }; - self.select_ix(ix, cx); + self.select_ix(ix, window, cx); } - fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { + fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context) { + if self.strip_mode.is_some() { + if self.input.focus_handle(cx).contains_focused(window, cx) { + cx.propagate(); + return; + } + } let ix = if self.breakpoints.len() > 0 { Some(self.breakpoints.len() - 1) } else { None }; - self.select_ix(ix, cx); + self.select_ix(ix, window, cx); } + fn dismiss(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { + if self.input.focus_handle(cx).contains_focused(window, cx) { + self.focus_handle.focus(window); + } else if self.strip_mode.is_some() { + self.strip_mode.take(); + cx.notify(); + } else { + cx.propagate(); + } + } fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else { return; }; + if let Some(mode) = self.strip_mode { + let handle = self.input.focus_handle(cx); + if handle.is_focused(window) { + // Go back to the main strip. Save the result as well. + let text = self.input.read(cx).text(cx); + + match mode { + ActiveBreakpointStripMode::Log => match &entry.kind { + BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { + Self::edit_line_breakpoint_inner( + &self.breakpoint_store, + line_breakpoint.breakpoint.path.clone(), + line_breakpoint.breakpoint.row, + BreakpointEditAction::EditLogMessage(Arc::from(text)), + cx, + ); + } + _ => {} + }, + ActiveBreakpointStripMode::Condition => match &entry.kind { + BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { + Self::edit_line_breakpoint_inner( + &self.breakpoint_store, + line_breakpoint.breakpoint.path.clone(), + line_breakpoint.breakpoint.row, + BreakpointEditAction::EditCondition(Arc::from(text)), + cx, + ); + } + _ => {} + }, + ActiveBreakpointStripMode::HitCondition => match &entry.kind { + BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { + Self::edit_line_breakpoint_inner( + &self.breakpoint_store, + line_breakpoint.breakpoint.path.clone(), + line_breakpoint.breakpoint.row, + BreakpointEditAction::EditHitCondition(Arc::from(text)), + cx, + ); + } + _ => {} + }, + } + self.focus_handle.focus(window); + } else { + handle.focus(window); + } + + return; + } match &mut entry.kind { BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { let path = line_breakpoint.breakpoint.path.clone(); @@ -233,12 +379,18 @@ impl BreakpointList { fn toggle_enable_breakpoint( &mut self, _: &ToggleEnableBreakpoint, - _window: &mut Window, + window: &mut Window, cx: &mut Context, ) { let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else { return; }; + if self.strip_mode.is_some() { + if self.input.focus_handle(cx).contains_focused(window, cx) { + cx.propagate(); + return; + } + } match &mut entry.kind { BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { @@ -279,6 +431,50 @@ impl BreakpointList { cx.notify(); } + fn previous_breakpoint_property( + &mut self, + _: &PreviousBreakpointProperty, + window: &mut Window, + cx: &mut Context, + ) { + let next_mode = match self.strip_mode { + Some(ActiveBreakpointStripMode::Log) => None, + Some(ActiveBreakpointStripMode::Condition) => Some(ActiveBreakpointStripMode::Log), + Some(ActiveBreakpointStripMode::HitCondition) => { + Some(ActiveBreakpointStripMode::Condition) + } + None => Some(ActiveBreakpointStripMode::HitCondition), + }; + if let Some(mode) = next_mode { + self.set_active_breakpoint_property(mode, window, cx); + } else { + self.strip_mode.take(); + } + + cx.notify(); + } + fn next_breakpoint_property( + &mut self, + _: &NextBreakpointProperty, + window: &mut Window, + cx: &mut Context, + ) { + let next_mode = match self.strip_mode { + Some(ActiveBreakpointStripMode::Log) => Some(ActiveBreakpointStripMode::Condition), + Some(ActiveBreakpointStripMode::Condition) => { + Some(ActiveBreakpointStripMode::HitCondition) + } + Some(ActiveBreakpointStripMode::HitCondition) => None, + None => Some(ActiveBreakpointStripMode::Log), + }; + if let Some(mode) = next_mode { + self.set_active_breakpoint_property(mode, window, cx); + } else { + self.strip_mode.take(); + } + cx.notify(); + } + fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context) { const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| { @@ -294,20 +490,31 @@ impl BreakpointList { })) } - fn render_list(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render_list(&mut self, cx: &mut Context) -> impl IntoElement { let selected_ix = self.selected_ix; let focus_handle = self.focus_handle.clone(); + let supported_breakpoint_properties = self + .session + .as_ref() + .map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities())) + .unwrap_or_else(SupportedBreakpointProperties::empty); + let strip_mode = self.strip_mode; uniform_list( "breakpoint-list", self.breakpoints.len(), - cx.processor(move |this, range: Range, window, cx| { + cx.processor(move |this, range: Range, _, _| { range .clone() .zip(&mut this.breakpoints[range]) .map(|(ix, breakpoint)| { breakpoint - .render(ix, focus_handle.clone(), window, cx) - .toggle_state(Some(ix) == selected_ix) + .render( + strip_mode, + supported_breakpoint_properties, + ix, + Some(ix) == selected_ix, + focus_handle.clone(), + ) .into_any_element() }) .collect() @@ -443,7 +650,6 @@ impl BreakpointList { impl Render for BreakpointList { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { - // let old_len = self.breakpoints.len(); let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx); self.breakpoints.clear(); let weak = cx.weak_entity(); @@ -523,15 +729,46 @@ impl Render for BreakpointList { .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::dismiss)) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::toggle_enable_breakpoint)) .on_action(cx.listener(Self::unset_breakpoint)) + .on_action(cx.listener(Self::next_breakpoint_property)) + .on_action(cx.listener(Self::previous_breakpoint_property)) .size_full() .m_0p5() - .child(self.render_list(window, cx)) - .children(self.render_vertical_scrollbar(cx)) + .child( + v_flex() + .size_full() + .child(self.render_list(cx)) + .children(self.render_vertical_scrollbar(cx)), + ) + .when_some(self.strip_mode, |this, _| { + this.child(Divider::horizontal()).child( + h_flex() + // .w_full() + .m_0p5() + .p_0p5() + .border_1() + .rounded_sm() + .when( + self.input.focus_handle(cx).contains_focused(window, cx), + |this| { + let colors = cx.theme().colors(); + let border = if self.input.read(cx).read_only(cx) { + colors.border_disabled + } else { + colors.border_focused + }; + this.border_color(border) + }, + ) + .child(self.input.clone()), + ) + }) } } + #[derive(Clone, Debug)] struct LineBreakpoint { name: SharedString, @@ -543,7 +780,10 @@ struct LineBreakpoint { impl LineBreakpoint { fn render( &mut self, + props: SupportedBreakpointProperties, + strip_mode: Option, ix: usize, + is_selected: bool, focus_handle: FocusHandle, weak: WeakEntity, ) -> ListItem { @@ -594,15 +834,16 @@ impl LineBreakpoint { }) .child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger)) .on_mouse_down(MouseButton::Left, move |_, _, _| {}); + ListItem::new(SharedString::from(format!( "breakpoint-ui-item-{:?}/{}:{}", self.dir, self.name, self.line ))) .on_click({ let weak = weak.clone(); - move |_, _, cx| { + move |_, window, cx| { weak.update(cx, |breakpoint_list, cx| { - breakpoint_list.select_ix(Some(ix), cx); + breakpoint_list.select_ix(Some(ix), window, cx); }) .ok(); } @@ -613,21 +854,26 @@ impl LineBreakpoint { cx.stop_propagation(); }) .child( - v_flex() - .py_1() + h_flex() + .w_full() + .mr_4() + .py_0p5() .gap_1() .min_h(px(26.)) - .justify_center() + .justify_between() .id(SharedString::from(format!( "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}", self.dir, self.name, self.line ))) - .on_click(move |_, window, cx| { - weak.update(cx, |breakpoint_list, cx| { - breakpoint_list.select_ix(Some(ix), cx); - breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx); - }) - .ok(); + .on_click({ + let weak = weak.clone(); + move |_, window, cx| { + weak.update(cx, |breakpoint_list, cx| { + breakpoint_list.select_ix(Some(ix), window, cx); + breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx); + }) + .ok(); + } }) .cursor_pointer() .child( @@ -644,8 +890,20 @@ impl LineBreakpoint { .size(LabelSize::Small) .line_height_style(ui::LineHeightStyle::UiLabel) })), - ), + ) + .child(BreakpointOptionsStrip { + props, + breakpoint: BreakpointEntry { + kind: BreakpointEntryKind::LineBreakpoint(self.clone()), + weak: weak, + }, + is_selected, + focus_handle, + strip_mode, + index: ix, + }), ) + .toggle_state(is_selected) } } #[derive(Clone, Debug)] @@ -658,7 +916,10 @@ struct ExceptionBreakpoint { impl ExceptionBreakpoint { fn render( &mut self, + props: SupportedBreakpointProperties, + strip_mode: Option, ix: usize, + is_selected: bool, focus_handle: FocusHandle, list: WeakEntity, ) -> ListItem { @@ -669,15 +930,15 @@ impl ExceptionBreakpoint { }; let id = SharedString::from(&self.id); let is_enabled = self.is_enabled; - + let weak = list.clone(); ListItem::new(SharedString::from(format!( "exception-breakpoint-ui-item-{}", self.id ))) .on_click({ let list = list.clone(); - move |_, _, cx| { - list.update(cx, |list, cx| list.select_ix(Some(ix), cx)) + move |_, window, cx| { + list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx)) .ok(); } }) @@ -691,18 +952,21 @@ impl ExceptionBreakpoint { "exception-breakpoint-ui-item-{}-click-handler", self.id ))) - .tooltip(move |window, cx| { - Tooltip::for_action_in( - if is_enabled { - "Disable Exception Breakpoint" - } else { - "Enable Exception Breakpoint" - }, - &ToggleEnableBreakpoint, - &focus_handle, - window, - cx, - ) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + if is_enabled { + "Disable Exception Breakpoint" + } else { + "Enable Exception Breakpoint" + }, + &ToggleEnableBreakpoint, + &focus_handle, + window, + cx, + ) + } }) .on_click({ let list = list.clone(); @@ -722,21 +986,40 @@ impl ExceptionBreakpoint { .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)), ) .child( - v_flex() - .py_1() - .gap_1() - .min_h(px(26.)) - .justify_center() - .id(("exception-breakpoint-label", ix)) + h_flex() + .w_full() + .mr_4() + .py_0p5() + .justify_between() .child( - Label::new(self.data.label.clone()) - .size(LabelSize::Small) - .line_height_style(ui::LineHeightStyle::UiLabel), + v_flex() + .py_1() + .gap_1() + .min_h(px(26.)) + .justify_center() + .id(("exception-breakpoint-label", ix)) + .child( + Label::new(self.data.label.clone()) + .size(LabelSize::Small) + .line_height_style(ui::LineHeightStyle::UiLabel), + ) + .when_some(self.data.description.clone(), |el, description| { + el.tooltip(Tooltip::text(description)) + }), ) - .when_some(self.data.description.clone(), |el, description| { - el.tooltip(Tooltip::text(description)) + .child(BreakpointOptionsStrip { + props, + breakpoint: BreakpointEntry { + kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()), + weak: weak, + }, + is_selected, + focus_handle, + strip_mode, + index: ix, }), ) + .toggle_state(is_selected) } } #[derive(Clone, Debug)] @@ -754,18 +1037,264 @@ struct BreakpointEntry { impl BreakpointEntry { fn render( &mut self, + strip_mode: Option, + props: SupportedBreakpointProperties, ix: usize, + is_selected: bool, focus_handle: FocusHandle, - _: &mut Window, - _: &mut App, ) -> ListItem { match &mut self.kind { + BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render( + props, + strip_mode, + ix, + is_selected, + focus_handle, + self.weak.clone(), + ), + BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint + .render( + props.for_exception_breakpoints(), + strip_mode, + ix, + is_selected, + focus_handle, + self.weak.clone(), + ), + } + } + + fn id(&self) -> SharedString { + match &self.kind { + BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!( + "source-breakpoint-control-strip-{:?}:{}", + line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row + ) + .into(), + BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!( + "exception-breakpoint-control-strip--{}", + exception_breakpoint.id + ) + .into(), + } + } + + fn has_log(&self) -> bool { + match &self.kind { BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { - line_breakpoint.render(ix, focus_handle, self.weak.clone()) + line_breakpoint.breakpoint.message.is_some() } - BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => { - exception_breakpoint.render(ix, focus_handle, self.weak.clone()) + _ => false, + } + } + + fn has_condition(&self) -> bool { + match &self.kind { + BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { + line_breakpoint.breakpoint.condition.is_some() + } + // We don't support conditions on exception breakpoints + BreakpointEntryKind::ExceptionBreakpoint(_) => false, + } + } + + fn has_hit_condition(&self) -> bool { + match &self.kind { + BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { + line_breakpoint.breakpoint.hit_condition.is_some() + } + _ => false, + } + } +} +bitflags::bitflags! { + #[derive(Clone, Copy)] + pub struct SupportedBreakpointProperties: u32 { + const LOG = 1 << 0; + const CONDITION = 1 << 1; + const HIT_CONDITION = 1 << 2; + // Conditions for exceptions can be set only when exception filters are supported. + const EXCEPTION_FILTER_OPTIONS = 1 << 3; + } +} + +impl From<&Capabilities> for SupportedBreakpointProperties { + fn from(caps: &Capabilities) -> Self { + let mut this = Self::empty(); + for (prop, offset) in [ + (caps.supports_log_points, Self::LOG), + (caps.supports_conditional_breakpoints, Self::CONDITION), + ( + caps.supports_hit_conditional_breakpoints, + Self::HIT_CONDITION, + ), + ( + caps.supports_exception_options, + Self::EXCEPTION_FILTER_OPTIONS, + ), + ] { + if prop.unwrap_or_default() { + this.insert(offset); + } + } + this + } +} + +impl SupportedBreakpointProperties { + fn for_exception_breakpoints(self) -> Self { + // TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here. + Self::empty() + } +} +#[derive(IntoElement)] +struct BreakpointOptionsStrip { + props: SupportedBreakpointProperties, + breakpoint: BreakpointEntry, + is_selected: bool, + focus_handle: FocusHandle, + strip_mode: Option, + index: usize, +} + +impl BreakpointOptionsStrip { + fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool { + self.is_selected && self.strip_mode == Some(expected_mode) + } + fn on_click_callback( + &self, + mode: ActiveBreakpointStripMode, + ) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> { + let list = self.breakpoint.weak.clone(); + let ix = self.index; + move |_, window, cx| { + list.update(cx, |this, cx| { + if this.strip_mode != Some(mode) { + this.set_active_breakpoint_property(mode, window, cx); + } else if this.selected_ix == Some(ix) { + this.strip_mode.take(); + } else { + cx.propagate(); + } + }) + .ok(); + } + } + fn add_border( + &self, + kind: ActiveBreakpointStripMode, + available: bool, + window: &Window, + cx: &App, + ) -> impl Fn(Div) -> Div { + move |this: Div| { + // Avoid layout shifts in case there's no colored border + let this = this.border_2().rounded_sm(); + if self.is_selected && self.strip_mode == Some(kind) { + let theme = cx.theme().colors(); + if self.focus_handle.is_focused(window) { + this.border_color(theme.border_selected) + } else { + this.border_color(theme.border_disabled) + } + } else if !available { + this.border_color(cx.theme().colors().border_disabled) + } else { + this } } } } +impl RenderOnce for BreakpointOptionsStrip { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let id = self.breakpoint.id(); + let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG); + let supports_condition = self + .props + .contains(SupportedBreakpointProperties::CONDITION); + let supports_hit_condition = self + .props + .contains(SupportedBreakpointProperties::HIT_CONDITION); + let has_logs = self.breakpoint.has_log(); + let has_condition = self.breakpoint.has_condition(); + let has_hit_condition = self.breakpoint.has_hit_condition(); + let style_for_toggle = |mode, is_enabled| { + if is_enabled && self.strip_mode == Some(mode) && self.is_selected { + ui::ButtonStyle::Filled + } else { + ui::ButtonStyle::Subtle + } + }; + let color_for_toggle = |is_enabled| { + if is_enabled { + ui::Color::Default + } else { + ui::Color::Muted + } + }; + + h_flex() + .gap_2() + .child( + div() .map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx)) + .child( + IconButton::new( + SharedString::from(format!("{id}-log-toggle")), + IconName::ScrollText, + ) + .style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs)) + .icon_color(color_for_toggle(has_logs)) + .disabled(!supports_logs) + .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log)) + .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)).tooltip(|window, cx| Tooltip::with_meta("Set Log Message", None, "Set log message to display (instead of stopping) when a breakpoint is hit", window, cx)) + ) + .when(!has_logs && !self.is_selected, |this| this.invisible()), + ) + .child( + div().map(self.add_border( + ActiveBreakpointStripMode::Condition, + supports_condition, + window, cx + )) + .child( + IconButton::new( + SharedString::from(format!("{id}-condition-toggle")), + IconName::SplitAlt, + ) + .style(style_for_toggle( + ActiveBreakpointStripMode::Condition, + has_condition + )) + .icon_color(color_for_toggle(has_condition)) + .disabled(!supports_condition) + .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition)) + .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition)) + .tooltip(|window, cx| Tooltip::with_meta("Set Condition", None, "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met", window, cx)) + ) + .when(!has_condition && !self.is_selected, |this| this.invisible()), + ) + .child( + div() .map(self.add_border( + ActiveBreakpointStripMode::HitCondition, + supports_hit_condition,window, cx + )) + .child( + IconButton::new( + SharedString::from(format!("{id}-hit-condition-toggle")), + IconName::ArrowDown10, + ) + .style(style_for_toggle( + ActiveBreakpointStripMode::HitCondition, + has_hit_condition, + )) + .icon_color(color_for_toggle(has_hit_condition)) + .disabled(!supports_hit_condition) + .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition)) + .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)).tooltip(|window, cx| Tooltip::with_meta("Set Hit Condition", None, "Set expression that controls how many hits of the breakpoint are ignored.", window, cx)) + ) + .when(!has_hit_condition && !self.is_selected, |this| { + this.invisible() + }), + ) + } +} diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index ffbe148a3b..332e38b038 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -23,6 +23,7 @@ pub enum IconName { AiZed, ArrowCircle, ArrowDown, + ArrowDown10, ArrowDownFromLine, ArrowDownRight, ArrowLeft, @@ -212,6 +213,7 @@ pub enum IconName { Save, Scissors, Screen, + ScrollText, SearchCode, SearchSelection, SelectAll, @@ -231,6 +233,7 @@ pub enum IconName { SparkleFilled, Spinner, Split, + SplitAlt, SquareDot, SquareMinus, SquarePlus,