Allow enabling/disabling breakpoints (#27280)

This PR adds the ability to enable/disable breakpoints. It also fixes a
bug where toggling a log breakpoint from the breakpoint context menu
would add a standard breakpoint on top of the log breakpoint instead of
deleting it.

todo: 
- [x] Add `BreakpointState` field Breakpoint that manages if a
breakpoint is active or not
- [x] Don't send disabled breakpoints to DAP servers - in progress
- [x] Half the opacity of disabled breakpoints - in progress
- [x] Add `BreakpointState` to database
- [x] Editor test for enabling/disabling breakpoints
- [ ] Integration Test to make sure we don't send disabled breakpoints
to DAP servers
- [x] Database test to make sure we properly serialize/deserialize
BreakpointState

Release Notes:

- N/A

---------

Co-authored-by: Piotr <piotr@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
This commit is contained in:
Anthony Eid 2025-03-26 02:06:08 -04:00 committed by GitHub
parent df583d73b9
commit d70ac64fe4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 583 additions and 172 deletions

View file

@ -414,6 +414,8 @@ actions!(
Tab, Tab,
Backtab, Backtab,
ToggleBreakpoint, ToggleBreakpoint,
DisableBreakpoint,
EnableBreakpoint,
EditLogBreakpoint, EditLogBreakpoint,
ToggleAutoSignatureHelp, ToggleAutoSignatureHelp,
ToggleGitBlameInline, ToggleGitBlameInline,

View file

@ -116,7 +116,9 @@ use linked_editing_ranges::refresh_linked_ranges;
use mouse_context_menu::MouseContextMenu; use mouse_context_menu::MouseContextMenu;
use persistence::DB; use persistence::DB;
use project::{ use project::{
debugger::breakpoint_store::{BreakpointEditAction, BreakpointStore, BreakpointStoreEvent}, debugger::breakpoint_store::{
BreakpointEditAction, BreakpointState, BreakpointStore, BreakpointStoreEvent,
},
ProjectPath, ProjectPath,
}; };
@ -6019,13 +6021,7 @@ impl Editor {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Option<IconButton> { ) -> Option<IconButton> {
let color = Color::Muted; let color = Color::Muted;
let position = breakpoint.as_ref().map(|(anchor, _)| *anchor); let position = breakpoint.as_ref().map(|(anchor, _)| *anchor);
let bp_kind = Arc::new(
breakpoint
.map(|(_, bp)| bp.kind.clone())
.unwrap_or(BreakpointKind::Standard),
);
if self.available_code_actions.is_some() { if self.available_code_actions.is_some() {
Some( Some(
@ -6062,7 +6058,6 @@ impl Editor {
editor.set_breakpoint_context_menu( editor.set_breakpoint_context_menu(
row, row,
position, position,
bp_kind.clone(),
event.down.position, event.down.position,
window, window,
cx, cx,
@ -6115,7 +6110,7 @@ impl Editor {
for breakpoint in for breakpoint in
breakpoint_store breakpoint_store
.read(cx) .read(cx)
.breakpoints(&buffer, None, buffer_snapshot.clone(), cx) .breakpoints(&buffer, None, &buffer_snapshot, cx)
{ {
let point = buffer_snapshot.summary_for_anchor::<Point>(&breakpoint.0); let point = buffer_snapshot.summary_for_anchor::<Point>(&breakpoint.0);
let mut anchor = multi_buffer_snapshot.anchor_before(point); let mut anchor = multi_buffer_snapshot.anchor_before(point);
@ -6140,49 +6135,33 @@ impl Editor {
let range = snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left) let range = snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left)
..snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right); ..snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right);
for excerpt_boundary in multi_buffer_snapshot.excerpt_boundaries_in_range(range) {
let info = excerpt_boundary.next;
let Some(excerpt_ranges) = multi_buffer_snapshot.range_for_excerpt(info.id) else { for (buffer_snapshot, range, excerpt_id) in
multi_buffer_snapshot.range_to_buffer_ranges(range)
{
let Some(buffer) = project.read_with(cx, |this, cx| {
this.buffer_for_id(buffer_snapshot.remote_id(), cx)
}) else {
continue; continue;
}; };
let Some(buffer) =
project.read_with(cx, |this, cx| this.buffer_for_id(info.buffer_id, cx))
else {
continue;
};
if buffer.read(cx).file().is_none() {
continue;
}
let breakpoints = breakpoint_store.read(cx).breakpoints( let breakpoints = breakpoint_store.read(cx).breakpoints(
&buffer, &buffer,
Some(info.range.context.start..info.range.context.end), Some(
info.buffer.clone(), buffer_snapshot.anchor_before(range.start)
..buffer_snapshot.anchor_after(range.end),
),
buffer_snapshot,
cx, cx,
); );
// To translate a breakpoint's position within a singular buffer to a multi buffer
// position we need to know it's excerpt starting location, it's position within
// the singular buffer, and if that position is within the excerpt's range.
let excerpt_head = excerpt_ranges
.start
.to_display_point(&snapshot.display_snapshot);
let buffer_start = info
.buffer
.summary_for_anchor::<Point>(&info.range.context.start);
for (anchor, breakpoint) in breakpoints { for (anchor, breakpoint) in breakpoints {
let as_row = info.buffer.summary_for_anchor::<Point>(&anchor).row; let multi_buffer_anchor =
let delta = as_row - buffer_start.row; Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), *anchor);
let position = multi_buffer_anchor
.to_point(&multi_buffer_snapshot)
.to_display_point(&snapshot);
let position = excerpt_head + DisplayPoint::new(DisplayRow(delta), 0); breakpoint_display_points
.insert(position.row(), (multi_buffer_anchor, breakpoint.clone()));
let anchor = snapshot.display_point_to_anchor(position, Bias::Left);
breakpoint_display_points.insert(position.row(), (anchor, breakpoint.clone()));
} }
} }
@ -6192,30 +6171,80 @@ impl Editor {
fn breakpoint_context_menu( fn breakpoint_context_menu(
&self, &self,
anchor: Anchor, anchor: Anchor,
kind: Arc<BreakpointKind>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Entity<ui::ContextMenu> { ) -> Entity<ui::ContextMenu> {
let weak_editor = cx.weak_entity(); let weak_editor = cx.weak_entity();
let focus_handle = self.focus_handle(cx); let focus_handle = self.focus_handle(cx);
let second_entry_msg = if kind.log_message().is_some() { let row = self
.buffer
.read(cx)
.snapshot(cx)
.summary_for_anchor::<Point>(&anchor)
.row;
let breakpoint = self
.breakpoint_at_row(row, window, cx)
.map(|(_, bp)| Arc::from(bp));
let log_breakpoint_msg = if breakpoint
.as_ref()
.is_some_and(|bp| bp.kind.log_message().is_some())
{
"Edit Log Breakpoint" "Edit Log Breakpoint"
} else { } else {
"Add Log Breakpoint" "Set Log Breakpoint"
}; };
let set_breakpoint_msg = if breakpoint.as_ref().is_some() {
"Unset Breakpoint"
} else {
"Set Breakpoint"
};
let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.state {
BreakpointState::Enabled => Some("Disable"),
BreakpointState::Disabled => Some("Enable"),
});
let breakpoint = breakpoint.unwrap_or_else(|| {
Arc::new(Breakpoint {
state: BreakpointState::Enabled,
kind: BreakpointKind::Standard,
})
});
ui::ContextMenu::build(window, cx, |menu, _, _cx| { ui::ContextMenu::build(window, cx, |menu, _, _cx| {
menu.on_blur_subscription(Subscription::new(|| {})) menu.on_blur_subscription(Subscription::new(|| {}))
.context(focus_handle) .context(focus_handle)
.entry("Toggle Breakpoint", None, { .when_some(toggle_state_msg, |this, msg| {
this.entry(msg, None, {
let weak_editor = weak_editor.clone(); let weak_editor = weak_editor.clone();
let breakpoint = breakpoint.clone();
move |_window, cx| { move |_window, cx| {
weak_editor weak_editor
.update(cx, |this, cx| { .update(cx, |this, cx| {
this.edit_breakpoint_at_anchor( this.edit_breakpoint_at_anchor(
anchor, anchor,
BreakpointKind::Standard, breakpoint.as_ref().clone(),
BreakpointEditAction::InvertState,
cx,
);
})
.log_err();
}
})
})
.entry(set_breakpoint_msg, None, {
let weak_editor = weak_editor.clone();
let breakpoint = breakpoint.clone();
move |_window, cx| {
weak_editor
.update(cx, |this, cx| {
this.edit_breakpoint_at_anchor(
anchor,
breakpoint.as_ref().clone(),
BreakpointEditAction::Toggle, BreakpointEditAction::Toggle,
cx, cx,
); );
@ -6223,10 +6252,10 @@ impl Editor {
.log_err(); .log_err();
} }
}) })
.entry(second_entry_msg, None, move |window, cx| { .entry(log_breakpoint_msg, None, move |window, cx| {
weak_editor weak_editor
.update(cx, |this, cx| { .update(cx, |this, cx| {
this.add_edit_breakpoint_block(anchor, kind.as_ref(), window, cx); this.add_edit_breakpoint_block(anchor, breakpoint.as_ref(), window, cx);
}) })
.log_err(); .log_err();
}) })
@ -6237,44 +6266,51 @@ impl Editor {
&self, &self,
position: Anchor, position: Anchor,
row: DisplayRow, row: DisplayRow,
kind: &BreakpointKind, breakpoint: &Breakpoint,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> IconButton { ) -> IconButton {
let (color, icon) = {
let color = if self let color = if self
.gutter_breakpoint_indicator .gutter_breakpoint_indicator
.is_some_and(|gutter_bp| gutter_bp.row() == row) .is_some_and(|point| point.row() == row)
{ {
Color::Hint Color::Hint
} else if breakpoint.is_disabled() {
Color::Custom(Color::Debugger.color(cx).opacity(0.5))
} else { } else {
Color::Debugger Color::Debugger
}; };
let icon = match &breakpoint.kind {
let icon = match &kind {
BreakpointKind::Standard => ui::IconName::DebugBreakpoint, BreakpointKind::Standard => ui::IconName::DebugBreakpoint,
BreakpointKind::Log(_) => ui::IconName::DebugLogBreakpoint, BreakpointKind::Log(_) => ui::IconName::DebugLogBreakpoint,
}; };
let arc_kind = Arc::new(kind.clone()); (color, icon)
let arc_kind2 = arc_kind.clone(); };
let breakpoint = Arc::from(breakpoint.clone());
IconButton::new(("breakpoint_indicator", row.0 as usize), icon) IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.size(ui::ButtonSize::None) .size(ui::ButtonSize::None)
.icon_color(color) .icon_color(color)
.style(ButtonStyle::Transparent) .style(ButtonStyle::Transparent)
.on_click(cx.listener(move |editor, _e, window, cx| { .on_click(cx.listener({
let breakpoint = breakpoint.clone();
move |editor, _e, window, cx| {
window.focus(&editor.focus_handle(cx)); window.focus(&editor.focus_handle(cx));
editor.edit_breakpoint_at_anchor( editor.edit_breakpoint_at_anchor(
position, position,
arc_kind.as_ref().clone(), breakpoint.as_ref().clone(),
BreakpointEditAction::Toggle, BreakpointEditAction::Toggle,
cx, cx,
); );
}
})) }))
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
editor.set_breakpoint_context_menu( editor.set_breakpoint_context_menu(
row, row,
Some(position), Some(position),
arc_kind2.clone(),
event.down.position, event.down.position,
window, window,
cx, cx,
@ -6422,13 +6458,7 @@ impl Editor {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> IconButton { ) -> IconButton {
let color = Color::Muted; let color = Color::Muted;
let position = breakpoint.as_ref().map(|(anchor, _)| *anchor); let position = breakpoint.as_ref().map(|(anchor, _)| *anchor);
let bp_kind = Arc::new(
breakpoint
.map(|(_, bp)| bp.kind)
.unwrap_or(BreakpointKind::Standard),
);
IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play) IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play)
.shape(ui::IconButtonShape::Square) .shape(ui::IconButtonShape::Square)
@ -6446,14 +6476,7 @@ impl Editor {
); );
})) }))
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
editor.set_breakpoint_context_menu( editor.set_breakpoint_context_menu(row, position, event.down.position, window, cx);
row,
position,
bp_kind.clone(),
event.down.position,
window,
cx,
);
})) }))
} }
@ -8430,9 +8453,8 @@ impl Editor {
fn set_breakpoint_context_menu( fn set_breakpoint_context_menu(
&mut self, &mut self,
row: DisplayRow, display_row: DisplayRow,
position: Option<Anchor>, position: Option<Anchor>,
kind: Arc<BreakpointKind>,
clicked_point: gpui::Point<Pixels>, clicked_point: gpui::Point<Pixels>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@ -8444,10 +8466,9 @@ impl Editor {
.buffer .buffer
.read(cx) .read(cx)
.snapshot(cx) .snapshot(cx)
.anchor_before(Point::new(row.0, 0u32)); .anchor_before(Point::new(display_row.0, 0u32));
let context_menu = let context_menu = self.breakpoint_context_menu(position.unwrap_or(source), window, cx);
self.breakpoint_context_menu(position.unwrap_or(source), kind, window, cx);
self.mouse_context_menu = MouseContextMenu::pinned_to_editor( self.mouse_context_menu = MouseContextMenu::pinned_to_editor(
self, self,
@ -8462,13 +8483,14 @@ impl Editor {
fn add_edit_breakpoint_block( fn add_edit_breakpoint_block(
&mut self, &mut self,
anchor: Anchor, anchor: Anchor,
kind: &BreakpointKind, breakpoint: &Breakpoint,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let weak_editor = cx.weak_entity(); let weak_editor = cx.weak_entity();
let bp_prompt = let bp_prompt = cx.new(|cx| {
cx.new(|cx| BreakpointPromptEditor::new(weak_editor, anchor, kind.clone(), window, cx)); BreakpointPromptEditor::new(weak_editor, anchor, breakpoint.clone(), window, cx)
});
let height = bp_prompt.update(cx, |this, cx| { let height = bp_prompt.update(cx, |this, cx| {
this.prompt this.prompt
@ -8495,36 +8517,45 @@ impl Editor {
}); });
} }
pub(crate) fn breakpoint_at_cursor_head( fn breakpoint_at_cursor_head(
&self, &self,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Option<(Anchor, Breakpoint)> { ) -> Option<(Anchor, Breakpoint)> {
let cursor_position: Point = self.selections.newest(cx).head(); let cursor_position: Point = self.selections.newest(cx).head();
self.breakpoint_at_row(cursor_position.row, window, cx)
}
pub(crate) fn breakpoint_at_row(
&self,
row: u32,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<(Anchor, Breakpoint)> {
let snapshot = self.snapshot(window, cx); let snapshot = self.snapshot(window, cx);
// We Set the column position to zero so this function interacts correctly let breakpoint_position = snapshot.buffer_snapshot.anchor_before(Point::new(row, 0));
// between calls by clicking on the gutter & using an action to toggle a
// breakpoint. Otherwise, toggling a breakpoint through an action wouldn't
// untoggle a breakpoint that was added through clicking on the gutter
let cursor_position = snapshot
.display_snapshot
.buffer_snapshot
.anchor_before(Point::new(cursor_position.row, 0));
let project = self.project.clone(); let project = self.project.clone()?;
let buffer_id = cursor_position.text_anchor.buffer_id?; let buffer_id = breakpoint_position.buffer_id.or_else(|| {
let enclosing_excerpt = snapshot snapshot
.buffer_snapshot .buffer_snapshot
.excerpt_ids_for_range(cursor_position..cursor_position) .buffer_id_for_excerpt(breakpoint_position.excerpt_id)
.next()?; })?;
let buffer = project?.read_with(cx, |project, cx| project.buffer_for_id(buffer_id, cx))?;
let enclosing_excerpt = breakpoint_position.excerpt_id;
let buffer = project.read_with(cx, |project, cx| project.buffer_for_id(buffer_id, cx))?;
let buffer_snapshot = buffer.read(cx).snapshot(); let buffer_snapshot = buffer.read(cx).snapshot();
let row = buffer_snapshot let row = buffer_snapshot
.summary_for_anchor::<text::PointUtf16>(&cursor_position.text_anchor) .summary_for_anchor::<text::PointUtf16>(&breakpoint_position.text_anchor)
.row; .row;
let line_len = snapshot.buffer_snapshot.line_len(MultiBufferRow(row));
let anchor_end = snapshot
.buffer_snapshot
.anchor_before(Point::new(row, line_len));
let bp = self let bp = self
.breakpoint_store .breakpoint_store
.as_ref()? .as_ref()?
@ -8532,12 +8563,12 @@ impl Editor {
breakpoint_store breakpoint_store
.breakpoints( .breakpoints(
&buffer, &buffer,
Some(cursor_position.text_anchor..(text::Anchor::MAX)), Some(breakpoint_position.text_anchor..anchor_end.text_anchor),
buffer_snapshot.clone(), &buffer_snapshot,
cx, cx,
) )
.next() .next()
.and_then(move |(anchor, bp)| { .and_then(|(anchor, bp)| {
let breakpoint_row = buffer_snapshot let breakpoint_row = buffer_snapshot
.summary_for_anchor::<text::PointUtf16>(anchor) .summary_for_anchor::<text::PointUtf16>(anchor)
.row; .row;
@ -8576,11 +8607,48 @@ impl Editor {
breakpoint_position, breakpoint_position,
Breakpoint { Breakpoint {
kind: BreakpointKind::Standard, kind: BreakpointKind::Standard,
state: BreakpointState::Enabled,
}, },
) )
}); });
self.add_edit_breakpoint_block(anchor, &bp.kind, window, cx); self.add_edit_breakpoint_block(anchor, &bp, window, cx);
}
pub fn enable_breakpoint(
&mut self,
_: &crate::actions::EnableBreakpoint,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some((anchor, breakpoint)) = self.breakpoint_at_cursor_head(window, cx) {
if breakpoint.is_disabled() {
self.edit_breakpoint_at_anchor(
anchor,
breakpoint,
BreakpointEditAction::InvertState,
cx,
);
}
}
}
pub fn disable_breakpoint(
&mut self,
_: &crate::actions::DisableBreakpoint,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some((anchor, breakpoint)) = self.breakpoint_at_cursor_head(window, cx) {
if breakpoint.is_enabled() {
self.edit_breakpoint_at_anchor(
anchor,
breakpoint,
BreakpointEditAction::InvertState,
cx,
);
}
}
} }
pub fn toggle_breakpoint( pub fn toggle_breakpoint(
@ -8592,7 +8660,7 @@ impl Editor {
let edit_action = BreakpointEditAction::Toggle; let edit_action = BreakpointEditAction::Toggle;
if let Some((anchor, breakpoint)) = self.breakpoint_at_cursor_head(window, cx) { if let Some((anchor, breakpoint)) = self.breakpoint_at_cursor_head(window, cx) {
self.edit_breakpoint_at_anchor(anchor, breakpoint.kind, edit_action, cx); self.edit_breakpoint_at_anchor(anchor, breakpoint, edit_action, cx);
} else { } else {
let cursor_position: Point = self.selections.newest(cx).head(); let cursor_position: Point = self.selections.newest(cx).head();
@ -8604,7 +8672,7 @@ impl Editor {
self.edit_breakpoint_at_anchor( self.edit_breakpoint_at_anchor(
breakpoint_position, breakpoint_position,
BreakpointKind::Standard, Breakpoint::new_standard(),
edit_action, edit_action,
cx, cx,
); );
@ -8614,7 +8682,7 @@ impl Editor {
pub fn edit_breakpoint_at_anchor( pub fn edit_breakpoint_at_anchor(
&mut self, &mut self,
breakpoint_position: Anchor, breakpoint_position: Anchor,
kind: BreakpointKind, breakpoint: Breakpoint,
edit_action: BreakpointEditAction, edit_action: BreakpointEditAction,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
@ -8643,7 +8711,7 @@ impl Editor {
breakpoint_store.update(cx, |breakpoint_store, cx| { breakpoint_store.update(cx, |breakpoint_store, cx| {
breakpoint_store.toggle_breakpoint( breakpoint_store.toggle_breakpoint(
buffer, buffer,
(breakpoint_position.text_anchor, Breakpoint { kind }), (breakpoint_position.text_anchor, breakpoint),
edit_action, edit_action,
cx, cx,
); );
@ -19605,7 +19673,7 @@ struct BreakpointPromptEditor {
pub(crate) prompt: Entity<Editor>, pub(crate) prompt: Entity<Editor>,
editor: WeakEntity<Editor>, editor: WeakEntity<Editor>,
breakpoint_anchor: Anchor, breakpoint_anchor: Anchor,
kind: BreakpointKind, breakpoint: Breakpoint,
block_ids: HashSet<CustomBlockId>, block_ids: HashSet<CustomBlockId>,
gutter_dimensions: Arc<Mutex<GutterDimensions>>, gutter_dimensions: Arc<Mutex<GutterDimensions>>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
@ -19617,13 +19685,15 @@ impl BreakpointPromptEditor {
fn new( fn new(
editor: WeakEntity<Editor>, editor: WeakEntity<Editor>,
breakpoint_anchor: Anchor, breakpoint_anchor: Anchor,
kind: BreakpointKind, breakpoint: Breakpoint,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let buffer = cx.new(|cx| { let buffer = cx.new(|cx| {
Buffer::local( Buffer::local(
kind.log_message() breakpoint
.kind
.log_message()
.map(|msg| msg.to_string()) .map(|msg| msg.to_string())
.unwrap_or_default(), .unwrap_or_default(),
cx, cx,
@ -19655,7 +19725,7 @@ impl BreakpointPromptEditor {
prompt, prompt,
editor, editor,
breakpoint_anchor, breakpoint_anchor,
kind, breakpoint,
gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())), gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())),
block_ids: Default::default(), block_ids: Default::default(),
_subscriptions: vec![], _subscriptions: vec![],
@ -19682,7 +19752,7 @@ impl BreakpointPromptEditor {
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.edit_breakpoint_at_anchor( editor.edit_breakpoint_at_anchor(
self.breakpoint_anchor, self.breakpoint_anchor,
self.kind.clone(), self.breakpoint.clone(),
BreakpointEditAction::EditLogMessage(log_message.into()), BreakpointEditAction::EditLogMessage(log_message.into()),
cx, cx,
); );

View file

@ -32,7 +32,7 @@ use multi_buffer::{IndentGuide, PathKey};
use parking_lot::Mutex; use parking_lot::Mutex;
use pretty_assertions::{assert_eq, assert_ne}; use pretty_assertions::{assert_eq, assert_ne};
use project::{ use project::{
debugger::breakpoint_store::{BreakpointKind, SerializedBreakpoint}, debugger::breakpoint_store::{BreakpointKind, BreakpointState, SerializedBreakpoint},
project_settings::{LspSettings, ProjectSettings}, project_settings::{LspSettings, ProjectSettings},
FakeFs, FakeFs,
}; };
@ -17392,7 +17392,7 @@ async fn assert_highlighted_edits(
fn assert_breakpoint( fn assert_breakpoint(
breakpoints: &BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>>, breakpoints: &BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>>,
path: &Arc<Path>, path: &Arc<Path>,
expected: Vec<(u32, BreakpointKind)>, expected: Vec<(u32, Breakpoint)>,
) { ) {
if expected.len() == 0usize { if expected.len() == 0usize {
assert!(!breakpoints.contains_key(path)); assert!(!breakpoints.contains_key(path));
@ -17401,7 +17401,15 @@ fn assert_breakpoint(
.get(path) .get(path)
.unwrap() .unwrap()
.into_iter() .into_iter()
.map(|breakpoint| (breakpoint.position, breakpoint.kind.clone())) .map(|breakpoint| {
(
breakpoint.position,
Breakpoint {
kind: breakpoint.kind.clone(),
state: breakpoint.state,
},
)
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
breakpoint.sort_by_key(|(cached_position, _)| *cached_position); breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
@ -17429,12 +17437,18 @@ fn add_log_breakpoint_at_cursor(
let kind = BreakpointKind::Log(Arc::from(log_message)); let kind = BreakpointKind::Log(Arc::from(log_message));
(breakpoint_position, Breakpoint { kind }) (
breakpoint_position,
Breakpoint {
kind,
state: BreakpointState::Enabled,
},
)
}); });
editor.edit_breakpoint_at_anchor( editor.edit_breakpoint_at_anchor(
anchor, anchor,
bp.kind, bp,
BreakpointEditAction::EditLogMessage(log_message.into()), BreakpointEditAction::EditLogMessage(log_message.into()),
cx, cx,
); );
@ -17522,7 +17536,10 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
assert_breakpoint( assert_breakpoint(
&breakpoints, &breakpoints,
&abs_path, &abs_path,
vec![(0, BreakpointKind::Standard), (3, BreakpointKind::Standard)], vec![
(0, Breakpoint::new_standard()),
(3, Breakpoint::new_standard()),
],
); );
editor.update_in(cx, |editor, window, cx| { editor.update_in(cx, |editor, window, cx| {
@ -17541,7 +17558,11 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
}); });
assert_eq!(1, breakpoints.len()); assert_eq!(1, breakpoints.len());
assert_breakpoint(&breakpoints, &abs_path, vec![(3, BreakpointKind::Standard)]); assert_breakpoint(
&breakpoints,
&abs_path,
vec![(3, Breakpoint::new_standard())],
);
editor.update_in(cx, |editor, window, cx| { editor.update_in(cx, |editor, window, cx| {
editor.move_to_end(&MoveToEnd, window, cx); editor.move_to_end(&MoveToEnd, window, cx);
@ -17628,7 +17649,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
assert_breakpoint( assert_breakpoint(
&breakpoints, &breakpoints,
&abs_path, &abs_path,
vec![(0, BreakpointKind::Log("hello world".into()))], vec![(0, Breakpoint::new_log("hello world"))],
); );
// Removing a log message from a log breakpoint should remove it // Removing a log message from a log breakpoint should remove it
@ -17669,7 +17690,10 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
assert_breakpoint( assert_breakpoint(
&breakpoints, &breakpoints,
&abs_path, &abs_path,
vec![(0, BreakpointKind::Standard), (3, BreakpointKind::Standard)], vec![
(0, Breakpoint::new_standard()),
(3, Breakpoint::new_standard()),
],
); );
editor.update_in(cx, |editor, window, cx| { editor.update_in(cx, |editor, window, cx| {
@ -17690,8 +17714,8 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
&breakpoints, &breakpoints,
&abs_path, &abs_path,
vec![ vec![
(0, BreakpointKind::Standard), (0, Breakpoint::new_standard()),
(3, BreakpointKind::Log("hello world".into())), (3, Breakpoint::new_log("hello world")),
], ],
); );
@ -17713,8 +17737,167 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
&breakpoints, &breakpoints,
&abs_path, &abs_path,
vec![ vec![
(0, BreakpointKind::Standard), (0, Breakpoint::new_standard()),
(3, BreakpointKind::Log("hello Earth !!".into())), (3, Breakpoint::new_log("hello Earth !!")),
],
);
}
/// This also tests that Editor::breakpoint_at_cursor_head is working properly
/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
/// or when breakpoints were placed out of order. This tests for a regression too
#[gpui::test]
async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/a"),
json!({
"main.rs": sample_text,
}),
)
.await;
let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/a"),
json!({
"main.rs": sample_text,
}),
)
.await;
let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let worktree_id = workspace
.update(cx, |workspace, _window, cx| {
workspace.project().update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
})
})
.unwrap();
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "main.rs"), cx)
})
.await
.unwrap();
let (editor, cx) = cx.add_window_view(|window, cx| {
Editor::new(
EditorMode::Full,
MultiBuffer::build_from_buffer(buffer, cx),
Some(project.clone()),
window,
cx,
)
});
let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
let abs_path = project.read_with(cx, |project, cx| {
project
.absolute_path(&project_path, cx)
.map(|path_buf| Arc::from(path_buf.to_owned()))
.unwrap()
});
// assert we can add breakpoint on the first line
editor.update_in(cx, |editor, window, cx| {
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
editor.move_to_end(&MoveToEnd, window, cx);
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
editor.move_up(&MoveUp, window, cx);
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
});
let breakpoints = editor.update(cx, |editor, cx| {
editor
.breakpoint_store()
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.clone()
});
assert_eq!(1, breakpoints.len());
assert_breakpoint(
&breakpoints,
&abs_path,
vec![
(0, Breakpoint::new_standard()),
(2, Breakpoint::new_standard()),
(3, Breakpoint::new_standard()),
],
);
editor.update_in(cx, |editor, window, cx| {
editor.move_to_beginning(&MoveToBeginning, window, cx);
editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
editor.move_to_end(&MoveToEnd, window, cx);
editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
});
let breakpoints = editor.update(cx, |editor, cx| {
editor
.breakpoint_store()
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.clone()
});
let disable_breakpoint = {
let mut bp = Breakpoint::new_standard();
bp.state = BreakpointState::Disabled;
bp
};
assert_eq!(1, breakpoints.len());
assert_breakpoint(
&breakpoints,
&abs_path,
vec![
(0, disable_breakpoint.clone()),
(2, Breakpoint::new_standard()),
(3, disable_breakpoint.clone()),
],
);
editor.update_in(cx, |editor, window, cx| {
editor.move_to_beginning(&MoveToBeginning, window, cx);
editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
editor.move_to_end(&MoveToEnd, window, cx);
editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
editor.move_up(&MoveUp, window, cx);
editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
});
let breakpoints = editor.update(cx, |editor, cx| {
editor
.breakpoint_store()
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.clone()
});
assert_eq!(1, breakpoints.len());
assert_breakpoint(
&breakpoints,
&abs_path,
vec![
(0, Breakpoint::new_standard()),
(2, disable_breakpoint),
(3, Breakpoint::new_standard()),
], ],
); );
} }

View file

@ -58,7 +58,7 @@ use multi_buffer::{
MultiBufferRow, RowInfo, MultiBufferRow, RowInfo,
}; };
use project::{ use project::{
debugger::breakpoint_store::{Breakpoint, BreakpointKind}, debugger::breakpoint_store::Breakpoint,
project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings}, project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings},
}; };
use settings::Settings; use settings::Settings;
@ -525,6 +525,8 @@ impl EditorElement {
if cx.has_flag::<Debugger>() { if cx.has_flag::<Debugger>() {
register_action(editor, window, Editor::toggle_breakpoint); register_action(editor, window, Editor::toggle_breakpoint);
register_action(editor, window, Editor::edit_log_breakpoint); register_action(editor, window, Editor::edit_log_breakpoint);
register_action(editor, window, Editor::enable_breakpoint);
register_action(editor, window, Editor::disable_breakpoint);
} }
} }
@ -1950,8 +1952,6 @@ impl EditorElement {
breakpoints breakpoints
.into_iter() .into_iter()
.filter_map(|(display_row, (text_anchor, bp))| { .filter_map(|(display_row, (text_anchor, bp))| {
let row = MultiBufferRow { 0: display_row.0 };
if row_infos if row_infos
.get((display_row.0.saturating_sub(range.start.0)) as usize) .get((display_row.0.saturating_sub(range.start.0)) as usize)
.is_some_and(|row_info| row_info.expand_info.is_some()) .is_some_and(|row_info| row_info.expand_info.is_some())
@ -1963,11 +1963,13 @@ impl EditorElement {
return None; return None;
} }
let row =
MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(&snapshot).row);
if snapshot.is_line_folded(row) { if snapshot.is_line_folded(row) {
return None; return None;
} }
let button = editor.render_breakpoint(text_anchor, display_row, &bp.kind, cx); let button = editor.render_breakpoint(text_anchor, display_row, &bp, cx);
let button = prepaint_gutter_button( let button = prepaint_gutter_button(
button, button,
@ -2065,6 +2067,7 @@ impl EditorElement {
{ {
return None; return None;
} }
let button = editor.render_run_indicator( let button = editor.render_run_indicator(
&self.style, &self.style,
Some(display_row) == active_task_indicator_row, Some(display_row) == active_task_indicator_row,
@ -6827,9 +6830,7 @@ impl Element for EditorElement {
gutter_breakpoint_point, gutter_breakpoint_point,
Bias::Left, Bias::Left,
); );
let breakpoint = Breakpoint { let breakpoint = Breakpoint::new_standard();
kind: BreakpointKind::Standard,
};
(position, breakpoint) (position, breakpoint)
}); });

View file

@ -256,6 +256,21 @@ impl BreakpointStore {
breakpoint_set.breakpoints.push(breakpoint.clone()); breakpoint_set.breakpoints.push(breakpoint.clone());
} }
} }
BreakpointEditAction::InvertState => {
if let Some((_, bp)) = breakpoint_set
.breakpoints
.iter_mut()
.find(|value| breakpoint == **value)
{
if bp.is_enabled() {
bp.state = BreakpointState::Disabled;
} else {
bp.state = BreakpointState::Enabled;
}
} else {
log::error!("Attempted to invert a breakpoint's state that doesn't exist ");
}
}
BreakpointEditAction::EditLogMessage(log_message) => { BreakpointEditAction::EditLogMessage(log_message) => {
if !log_message.is_empty() { if !log_message.is_empty() {
breakpoint.1.kind = BreakpointKind::Log(log_message.clone()); breakpoint.1.kind = BreakpointKind::Log(log_message.clone());
@ -351,7 +366,7 @@ impl BreakpointStore {
&'a self, &'a self,
buffer: &'a Entity<Buffer>, buffer: &'a Entity<Buffer>,
range: Option<Range<text::Anchor>>, range: Option<Range<text::Anchor>>,
buffer_snapshot: BufferSnapshot, buffer_snapshot: &'a BufferSnapshot,
cx: &App, cx: &App,
) -> impl Iterator<Item = &'a (text::Anchor, Breakpoint)> + 'a { ) -> impl Iterator<Item = &'a (text::Anchor, Breakpoint)> + 'a {
let abs_path = Self::abs_path_from_buffer(buffer, cx); let abs_path = Self::abs_path_from_buffer(buffer, cx);
@ -361,11 +376,10 @@ impl BreakpointStore {
.flat_map(move |file_breakpoints| { .flat_map(move |file_breakpoints| {
file_breakpoints.breakpoints.iter().filter({ file_breakpoints.breakpoints.iter().filter({
let range = range.clone(); let range = range.clone();
let buffer_snapshot = buffer_snapshot.clone();
move |(position, _)| { move |(position, _)| {
if let Some(range) = &range { if let Some(range) = &range {
position.cmp(&range.start, &buffer_snapshot).is_ge() position.cmp(&range.start, buffer_snapshot).is_ge()
&& position.cmp(&range.end, &buffer_snapshot).is_le() && position.cmp(&range.end, buffer_snapshot).is_le()
} else { } else {
true true
} }
@ -417,6 +431,7 @@ impl BreakpointStore {
position, position,
path: path.clone(), path: path.clone(),
kind: breakpoint.kind.clone(), kind: breakpoint.kind.clone(),
state: breakpoint.state,
} }
}) })
.collect() .collect()
@ -439,6 +454,7 @@ impl BreakpointStore {
position, position,
path: path.clone(), path: path.clone(),
kind: breakpoint.kind.clone(), kind: breakpoint.kind.clone(),
state: breakpoint.state,
} }
}) })
.collect(), .collect(),
@ -487,9 +503,13 @@ impl BreakpointStore {
for bp in bps { for bp in bps {
let position = snapshot.anchor_before(PointUtf16::new(bp.position, 0)); let position = snapshot.anchor_before(PointUtf16::new(bp.position, 0));
breakpoints_for_file breakpoints_for_file.breakpoints.push((
.breakpoints position,
.push((position, Breakpoint { kind: bp.kind })) Breakpoint {
kind: bp.kind,
state: bp.state,
},
))
} }
new_breakpoints.insert(path, breakpoints_for_file); new_breakpoints.insert(path, breakpoints_for_file);
} }
@ -530,6 +550,7 @@ type LogMessage = Arc<str>;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum BreakpointEditAction { pub enum BreakpointEditAction {
Toggle, Toggle,
InvertState,
EditLogMessage(LogMessage), EditLogMessage(LogMessage),
} }
@ -569,16 +590,60 @@ impl Hash for BreakpointKind {
} }
} }
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
pub enum BreakpointState {
Enabled,
Disabled,
}
impl BreakpointState {
#[inline]
pub fn is_enabled(&self) -> bool {
matches!(self, BreakpointState::Enabled)
}
#[inline]
pub fn is_disabled(&self) -> bool {
matches!(self, BreakpointState::Disabled)
}
#[inline]
pub fn to_int(&self) -> i32 {
match self {
BreakpointState::Enabled => 0,
BreakpointState::Disabled => 1,
}
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Breakpoint { pub struct Breakpoint {
pub kind: BreakpointKind, pub kind: BreakpointKind,
pub state: BreakpointState,
} }
impl Breakpoint { impl Breakpoint {
pub fn new_standard() -> Self {
Self {
kind: BreakpointKind::Standard,
state: BreakpointState::Enabled,
}
}
pub fn new_log(log_message: &str) -> Self {
Self {
kind: BreakpointKind::Log(log_message.to_owned().into()),
state: BreakpointState::Enabled,
}
}
fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> { fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
Some(client::proto::Breakpoint { Some(client::proto::Breakpoint {
position: Some(serialize_text_anchor(position)), position: Some(serialize_text_anchor(position)),
state: match self.state {
BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
},
kind: match self.kind { kind: match self.kind {
BreakpointKind::Standard => proto::BreakpointKind::Standard.into(), BreakpointKind::Standard => proto::BreakpointKind::Standard.into(),
BreakpointKind::Log(_) => proto::BreakpointKind::Log.into(), BreakpointKind::Log(_) => proto::BreakpointKind::Log.into(),
@ -599,8 +664,22 @@ impl Breakpoint {
} }
None | Some(proto::BreakpointKind::Standard) => BreakpointKind::Standard, None | Some(proto::BreakpointKind::Standard) => BreakpointKind::Standard,
}, },
state: match proto::BreakpointState::from_i32(breakpoint.state) {
Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
},
}) })
} }
#[inline]
pub fn is_enabled(&self) -> bool {
self.state.is_enabled()
}
#[inline]
pub fn is_disabled(&self) -> bool {
self.state.is_disabled()
}
} }
#[derive(Clone, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Debug, Hash, PartialEq, Eq)]
@ -608,6 +687,7 @@ pub struct SerializedBreakpoint {
pub position: u32, pub position: u32,
pub path: Arc<Path>, pub path: Arc<Path>,
pub kind: BreakpointKind, pub kind: BreakpointKind,
pub state: BreakpointState,
} }
impl From<SerializedBreakpoint> for dap::SourceBreakpoint { impl From<SerializedBreakpoint> for dap::SourceBreakpoint {

View file

@ -358,6 +358,7 @@ impl LocalMode {
.breakpoint_store .breakpoint_store
.read_with(cx, |store, cx| store.breakpoints_from_path(&abs_path, cx)) .read_with(cx, |store, cx| store.breakpoints_from_path(&abs_path, cx))
.into_iter() .into_iter()
.filter(|bp| bp.state.is_enabled())
.map(Into::into) .map(Into::into)
.collect(); .collect();
@ -388,7 +389,11 @@ impl LocalMode {
let breakpoints = if ignore_breakpoints { let breakpoints = if ignore_breakpoints {
vec![] vec![]
} else { } else {
breakpoints.into_iter().map(Into::into).collect() breakpoints
.into_iter()
.filter(|bp| bp.state.is_enabled())
.map(Into::into)
.collect()
}; };
breakpoint_tasks.push(self.request( breakpoint_tasks.push(self.request(

View file

@ -2640,9 +2640,15 @@ enum BreakpointKind {
Log = 1; Log = 1;
} }
enum BreakpointState {
Enabled = 0;
Disabled = 1;
}
message Breakpoint { message Breakpoint {
Anchor position = 1; Anchor position = 1;
BreakpointState state = 2;
BreakpointKind kind = 3; BreakpointKind kind = 3;
optional string message = 4; optional string message = 4;
} }

View file

@ -11,7 +11,7 @@ use crate::connection::Connection;
pub struct Statement<'a> { pub struct Statement<'a> {
/// vector of pointers to the raw SQLite statement objects. /// vector of pointers to the raw SQLite statement objects.
/// it holds the actual prepared statements that will be executed. /// it holds the actual prepared statements that will be executed.
raw_statements: Vec<*mut sqlite3_stmt>, pub raw_statements: Vec<*mut sqlite3_stmt>,
/// Index of the current statement being executed from the `raw_statements` vector. /// Index of the current statement being executed from the `raw_statements` vector.
current_statement: usize, current_statement: usize,
/// A reference to the database connection. /// A reference to the database connection.

View file

@ -13,7 +13,7 @@ use client::DevServerProjectId;
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
use gpui::{point, size, Axis, Bounds, WindowBounds, WindowId}; use gpui::{point, size, Axis, Bounds, WindowBounds, WindowId};
use itertools::Itertools; use itertools::Itertools;
use project::debugger::breakpoint_store::{BreakpointKind, SerializedBreakpoint}; use project::debugger::breakpoint_store::{BreakpointKind, BreakpointState, SerializedBreakpoint};
use language::{LanguageName, Toolchain}; use language::{LanguageName, Toolchain};
use project::WorktreeId; use project::WorktreeId;
@ -148,9 +148,43 @@ impl Column for SerializedWindowBounds {
pub struct Breakpoint { pub struct Breakpoint {
pub position: u32, pub position: u32,
pub kind: BreakpointKind, pub kind: BreakpointKind,
pub state: BreakpointState,
} }
/// Wrapper for DB type of a breakpoint /// Wrapper for DB type of a breakpoint
struct BreakpointStateWrapper<'a>(Cow<'a, BreakpointState>);
impl From<BreakpointState> for BreakpointStateWrapper<'static> {
fn from(kind: BreakpointState) -> Self {
BreakpointStateWrapper(Cow::Owned(kind))
}
}
impl StaticColumnCount for BreakpointStateWrapper<'_> {
fn column_count() -> usize {
1
}
}
impl Bind for BreakpointStateWrapper<'_> {
fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
statement.bind(&self.0.to_int(), start_index)
}
}
impl Column for BreakpointStateWrapper<'_> {
fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
let state = statement.column_int(start_index)?;
match state {
0 => Ok((BreakpointState::Enabled.into(), start_index + 1)),
1 => Ok((BreakpointState::Disabled.into(), start_index + 1)),
_ => Err(anyhow::anyhow!("Invalid BreakpointState discriminant")),
}
}
}
/// Wrapper for DB type of a breakpoint
#[derive(Debug)]
struct BreakpointKindWrapper<'a>(Cow<'a, BreakpointKind>); struct BreakpointKindWrapper<'a>(Cow<'a, BreakpointKind>);
impl From<BreakpointKind> for BreakpointKindWrapper<'static> { impl From<BreakpointKind> for BreakpointKindWrapper<'static> {
@ -200,7 +234,7 @@ struct Breakpoints(Vec<Breakpoint>);
impl sqlez::bindable::StaticColumnCount for Breakpoint { impl sqlez::bindable::StaticColumnCount for Breakpoint {
fn column_count() -> usize { fn column_count() -> usize {
1 + BreakpointKindWrapper::column_count() 1 + BreakpointKindWrapper::column_count() + BreakpointStateWrapper::column_count()
} }
} }
@ -211,9 +245,13 @@ impl sqlez::bindable::Bind for Breakpoint {
start_index: i32, start_index: i32,
) -> anyhow::Result<i32> { ) -> anyhow::Result<i32> {
let next_index = statement.bind(&self.position, start_index)?; let next_index = statement.bind(&self.position, start_index)?;
statement.bind( let next_index = statement.bind(
&BreakpointKindWrapper(Cow::Borrowed(&self.kind)), &BreakpointKindWrapper(Cow::Borrowed(&self.kind)),
next_index, next_index,
)?;
statement.bind(
&BreakpointStateWrapper(Cow::Borrowed(&self.state)),
next_index,
) )
} }
} }
@ -225,11 +263,13 @@ impl Column for Breakpoint {
.with_context(|| format!("Failed to read BreakPoint at index {start_index}"))? .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))?
as u32; as u32;
let (kind, next_index) = BreakpointKindWrapper::column(statement, start_index + 1)?; let (kind, next_index) = BreakpointKindWrapper::column(statement, start_index + 1)?;
let (state, next_index) = BreakpointStateWrapper::column(statement, next_index)?;
Ok(( Ok((
Breakpoint { Breakpoint {
position, position,
kind: kind.0.into_owned(), kind: kind.0.into_owned(),
state: state.0.into_owned(),
}, },
next_index, next_index,
)) ))
@ -245,16 +285,9 @@ impl Column for Breakpoints {
match statement.column_type(index) { match statement.column_type(index) {
Ok(SqlType::Null) => break, Ok(SqlType::Null) => break,
_ => { _ => {
let position = statement let (breakpoint, next_index) = Breakpoint::column(statement, index)?;
.column_int(index)
.with_context(|| format!("Failed to read BreakPoint at index {index}"))?
as u32;
let (kind, next_index) = BreakpointKindWrapper::column(statement, index + 1)?;
breakpoints.push(Breakpoint { breakpoints.push(breakpoint);
position,
kind: kind.0.into_owned(),
});
index = next_index; index = next_index;
} }
} }
@ -535,6 +568,9 @@ define_connection! {
CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array); CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array);
ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT; ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT;
), ),
sql!(
ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL
)
]; ];
} }
@ -690,7 +726,7 @@ impl WorkspaceDb {
) -> BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>> { ) -> BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>> {
let breakpoints: Result<Vec<(PathBuf, Breakpoint)>> = self let breakpoints: Result<Vec<(PathBuf, Breakpoint)>> = self
.select_bound(sql! { .select_bound(sql! {
SELECT path, breakpoint_location, kind SELECT path, breakpoint_location, kind, log_message, state
FROM breakpoints FROM breakpoints
WHERE workspace_id = ? WHERE workspace_id = ?
}) })
@ -712,6 +748,7 @@ impl WorkspaceDb {
position: breakpoint.position, position: breakpoint.position,
path, path,
kind: breakpoint.kind, kind: breakpoint.kind,
state: breakpoint.state,
}); });
} }
@ -739,15 +776,17 @@ impl WorkspaceDb {
.context("Clearing old breakpoints")?; .context("Clearing old breakpoints")?;
for bp in breakpoints { for bp in breakpoints {
let kind = BreakpointKindWrapper::from(bp.kind); let kind = BreakpointKindWrapper::from(bp.kind);
let state = BreakpointStateWrapper::from(bp.state);
match conn.exec_bound(sql!( match conn.exec_bound(sql!(
INSERT INTO breakpoints (workspace_id, path, breakpoint_location, kind, log_message) INSERT INTO breakpoints (workspace_id, path, breakpoint_location, kind, log_message, state)
VALUES (?1, ?2, ?3, ?4, ?5);))? VALUES (?1, ?2, ?3, ?4, ?5, ?6);))?
(( ((
workspace.id, workspace.id,
path.as_ref(), path.as_ref(),
bp.position, bp.position,
kind, kind,
state,
)) { )) {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
@ -1415,11 +1454,19 @@ mod tests {
let breakpoint = Breakpoint { let breakpoint = Breakpoint {
position: 123, position: 123,
kind: BreakpointKind::Standard, kind: BreakpointKind::Standard,
state: BreakpointState::Enabled,
}; };
let log_breakpoint = Breakpoint { let log_breakpoint = Breakpoint {
position: 456, position: 456,
kind: BreakpointKind::Log("Test log message".into()), kind: BreakpointKind::Log("Test log message".into()),
state: BreakpointState::Enabled,
};
let disable_breakpoint = Breakpoint {
position: 578,
kind: BreakpointKind::Standard,
state: BreakpointState::Disabled,
}; };
let workspace = SerializedWorkspace { let workspace = SerializedWorkspace {
@ -1439,11 +1486,19 @@ mod tests {
position: breakpoint.position, position: breakpoint.position,
path: Arc::from(path), path: Arc::from(path),
kind: breakpoint.kind.clone(), kind: breakpoint.kind.clone(),
state: breakpoint.state,
}, },
SerializedBreakpoint { SerializedBreakpoint {
position: log_breakpoint.position, position: log_breakpoint.position,
path: Arc::from(path), path: Arc::from(path),
kind: log_breakpoint.kind.clone(), kind: log_breakpoint.kind.clone(),
state: log_breakpoint.state,
},
SerializedBreakpoint {
position: disable_breakpoint.position,
path: Arc::from(path),
kind: disable_breakpoint.kind.clone(),
state: disable_breakpoint.state,
}, },
], ],
); );
@ -1458,13 +1513,22 @@ mod tests {
let loaded = db.workspace_for_roots(&["/tmp"]).unwrap(); let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap(); let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
assert_eq!(loaded_breakpoints.len(), 2); assert_eq!(loaded_breakpoints.len(), 3);
assert_eq!(loaded_breakpoints[0].position, breakpoint.position); assert_eq!(loaded_breakpoints[0].position, breakpoint.position);
assert_eq!(loaded_breakpoints[0].kind, breakpoint.kind); assert_eq!(loaded_breakpoints[0].kind, breakpoint.kind);
assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
assert_eq!(loaded_breakpoints[1].position, log_breakpoint.position); assert_eq!(loaded_breakpoints[1].position, log_breakpoint.position);
assert_eq!(loaded_breakpoints[1].kind, log_breakpoint.kind); assert_eq!(loaded_breakpoints[1].kind, log_breakpoint.kind);
assert_eq!(loaded_breakpoints[0].path, Arc::from(path)); assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
assert_eq!(loaded_breakpoints[1].path, Arc::from(path)); assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
assert_eq!(loaded_breakpoints[2].position, disable_breakpoint.position);
assert_eq!(loaded_breakpoints[2].kind, disable_breakpoint.kind);
assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
} }
#[gpui::test] #[gpui::test]