Cargo.lock 🔗
@@ -4310,6 +4310,7 @@ version = "0.1.0"
dependencies = [
"alacritty_terminal",
"anyhow",
+ "bitflags 2.9.0",
"client",
"collections",
"command_palette_hooks",
Piotr Osiewicz created
Release Notes:
- debugger: Breakpoint properties (log/hit condition/condition) can now
be set directly from breakpoint list.
Cargo.lock | 1
assets/icons/arrow_down10.svg | 1
assets/icons/scroll_text.svg | 1
assets/icons/split_alt.svg | 1
assets/keymaps/default-linux.json | 4
assets/keymaps/default-macos.json | 4
crates/debugger_ui/Cargo.toml | 1
crates/debugger_ui/src/debugger_panel.rs | 8
crates/debugger_ui/src/session/running.rs | 9
crates/debugger_ui/src/session/running/breakpoint_list.rs | 683 +++++++-
crates/icons/src/icons.rs | 3
11 files changed, 634 insertions(+), 82 deletions(-)
@@ -4310,6 +4310,7 @@ version = "0.1.0"
dependencies = [
"alacritty_terminal",
"anyhow",
+ "bitflags 2.9.0",
"client",
"collections",
"command_palette_hooks",
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down10-icon lucide-arrow-down-1-0"><path d="m3 16 4 4 4-4"/><path d="M7 20V4"/><path d="M17 10V4h-2"/><path d="M15 10h4"/><rect x="15" y="14" width="4" height="6" ry="2"/></svg>
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scroll-text-icon lucide-scroll-text"><path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/></svg>
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-split-icon lucide-split"><path d="M16 3h5v5"/><path d="M8 3H3v5"/><path d="M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3"/><path d="m15 9 6-6"/></svg>
@@ -919,7 +919,9 @@
"context": "BreakpointList",
"bindings": {
"space": "debugger::ToggleEnableBreakpoint",
- "backspace": "debugger::UnsetBreakpoint"
+ "backspace": "debugger::UnsetBreakpoint",
+ "left": "debugger::PreviousBreakpointProperty",
+ "right": "debugger::NextBreakpointProperty"
}
},
{
@@ -980,7 +980,9 @@
"context": "BreakpointList",
"bindings": {
"space": "debugger::ToggleEnableBreakpoint",
- "backspace": "debugger::UnsetBreakpoint"
+ "backspace": "debugger::UnsetBreakpoint",
+ "left": "debugger::PreviousBreakpointProperty",
+ "right": "debugger::NextBreakpointProperty"
}
},
{
@@ -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
@@ -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,
@@ -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| {
@@ -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<usize>,
+ input: Entity<Editor>,
+ strip_mode: Option<ActiveBreakpointStripMode>,
}
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<Entity<Session>>,
workspace: WeakEntity<Workspace>,
project: &Entity<Project>,
+ window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
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<Path>,
row: u32,
action: BreakpointEditAction,
- cx: &mut Context<Self>,
+ cx: &mut App,
+ ) {
+ Self::edit_line_breakpoint_inner(&self.breakpoint_store, path, row, action, cx);
+ }
+ fn edit_line_breakpoint_inner(
+ breakpoint_store: &Entity<BreakpointStore>,
+ path: Arc<Path>,
+ row: u32,
+ action: BreakpointEditAction,
+ cx: &mut App,
) {
- self.breakpoint_store.update(cx, |breakpoint_store, cx| {
+ 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<usize>, cx: &mut Context<Self>) {
+ 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<usize>, window: &mut Window, cx: &mut Context<Self>) {
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<Self>) {
+ fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
+ 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<Self>,
) {
+ 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<Self>,
- ) {
+ fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
+ 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<Self>) {
+ fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
+ 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<Self>) {
+ 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<Self>) {
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<Self>,
) {
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<Self>,
+ ) {
+ 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<Self>,
+ ) {
+ 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<Self>) {
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<Self>) -> impl IntoElement {
+ fn render_list(&mut self, cx: &mut Context<Self>) -> 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<usize>, window, cx| {
+ cx.processor(move |this, range: Range<usize>, _, _| {
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<Self>) -> 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<ActiveBreakpointStripMode>,
ix: usize,
+ is_selected: bool,
focus_handle: FocusHandle,
weak: WeakEntity<BreakpointList>,
) -> 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<ActiveBreakpointStripMode>,
ix: usize,
+ is_selected: bool,
focus_handle: FocusHandle,
list: WeakEntity<BreakpointList>,
) -> 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<ActiveBreakpointStripMode>,
+ 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<ActiveBreakpointStripMode>,
+ 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()
+ }),
+ )
}
}
@@ -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,