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,