diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 948638719159a0416320d7460a8eef5aa10f1f45..6ac1df7259171c480060726639d873cdf15d9a90 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -414,6 +414,8 @@ actions!( Tab, Backtab, ToggleBreakpoint, + DisableBreakpoint, + EnableBreakpoint, EditLogBreakpoint, ToggleAutoSignatureHelp, ToggleGitBlameInline, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ac7110c66ccaec0525807bf6fc6dd6e5f68183da..53a5713873057263976d1753c8fb7cfb6714740d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -116,7 +116,9 @@ use linked_editing_ranges::refresh_linked_ranges; use mouse_context_menu::MouseContextMenu; use persistence::DB; use project::{ - debugger::breakpoint_store::{BreakpointEditAction, BreakpointStore, BreakpointStoreEvent}, + debugger::breakpoint_store::{ + BreakpointEditAction, BreakpointState, BreakpointStore, BreakpointStoreEvent, + }, ProjectPath, }; @@ -6019,13 +6021,7 @@ impl Editor { cx: &mut Context, ) -> Option { let color = Color::Muted; - 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() { Some( @@ -6062,7 +6058,6 @@ impl Editor { editor.set_breakpoint_context_menu( row, position, - bp_kind.clone(), event.down.position, window, cx, @@ -6115,7 +6110,7 @@ impl Editor { for breakpoint in breakpoint_store .read(cx) - .breakpoints(&buffer, None, buffer_snapshot.clone(), cx) + .breakpoints(&buffer, None, &buffer_snapshot, cx) { let point = buffer_snapshot.summary_for_anchor::(&breakpoint.0); 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) ..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 { - continue; - }; - - let Some(buffer) = - project.read_with(cx, |this, cx| this.buffer_for_id(info.buffer_id, cx)) - 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; }; - - if buffer.read(cx).file().is_none() { - continue; - } let breakpoints = breakpoint_store.read(cx).breakpoints( &buffer, - Some(info.range.context.start..info.range.context.end), - info.buffer.clone(), + Some( + buffer_snapshot.anchor_before(range.start) + ..buffer_snapshot.anchor_after(range.end), + ), + buffer_snapshot, 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::(&info.range.context.start); - for (anchor, breakpoint) in breakpoints { - let as_row = info.buffer.summary_for_anchor::(&anchor).row; - let delta = as_row - buffer_start.row; - - let position = excerpt_head + DisplayPoint::new(DisplayRow(delta), 0); + let multi_buffer_anchor = + 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 anchor = snapshot.display_point_to_anchor(position, Bias::Left); - - breakpoint_display_points.insert(position.row(), (anchor, breakpoint.clone())); + breakpoint_display_points + .insert(position.row(), (multi_buffer_anchor, breakpoint.clone())); } } @@ -6192,30 +6171,80 @@ impl Editor { fn breakpoint_context_menu( &self, anchor: Anchor, - kind: Arc, window: &mut Window, cx: &mut Context, ) -> Entity { let weak_editor = cx.weak_entity(); 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::(&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" } 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| { menu.on_blur_subscription(Subscription::new(|| {})) .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 breakpoint = breakpoint.clone(); + move |_window, cx| { + weak_editor + .update(cx, |this, cx| { + this.edit_breakpoint_at_anchor( + anchor, + 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, - BreakpointKind::Standard, + breakpoint.as_ref().clone(), BreakpointEditAction::Toggle, cx, ); @@ -6223,10 +6252,10 @@ impl Editor { .log_err(); } }) - .entry(second_entry_msg, None, move |window, cx| { + .entry(log_breakpoint_msg, None, move |window, cx| { weak_editor .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(); }) @@ -6237,44 +6266,51 @@ impl Editor { &self, position: Anchor, row: DisplayRow, - kind: &BreakpointKind, + breakpoint: &Breakpoint, cx: &mut Context, ) -> IconButton { - let color = if self - .gutter_breakpoint_indicator - .is_some_and(|gutter_bp| gutter_bp.row() == row) - { - Color::Hint - } else { - Color::Debugger + let (color, icon) = { + let color = if self + .gutter_breakpoint_indicator + .is_some_and(|point| point.row() == row) + { + Color::Hint + } else if breakpoint.is_disabled() { + Color::Custom(Color::Debugger.color(cx).opacity(0.5)) + } else { + Color::Debugger + }; + let icon = match &breakpoint.kind { + BreakpointKind::Standard => ui::IconName::DebugBreakpoint, + BreakpointKind::Log(_) => ui::IconName::DebugLogBreakpoint, + }; + (color, icon) }; - let icon = match &kind { - BreakpointKind::Standard => ui::IconName::DebugBreakpoint, - BreakpointKind::Log(_) => ui::IconName::DebugLogBreakpoint, - }; - let arc_kind = Arc::new(kind.clone()); - let arc_kind2 = arc_kind.clone(); + let breakpoint = Arc::from(breakpoint.clone()); IconButton::new(("breakpoint_indicator", row.0 as usize), icon) .icon_size(IconSize::XSmall) .size(ui::ButtonSize::None) .icon_color(color) .style(ButtonStyle::Transparent) - .on_click(cx.listener(move |editor, _e, window, cx| { - window.focus(&editor.focus_handle(cx)); - editor.edit_breakpoint_at_anchor( - position, - arc_kind.as_ref().clone(), - BreakpointEditAction::Toggle, - cx, - ); + .on_click(cx.listener({ + let breakpoint = breakpoint.clone(); + + move |editor, _e, window, cx| { + window.focus(&editor.focus_handle(cx)); + editor.edit_breakpoint_at_anchor( + position, + breakpoint.as_ref().clone(), + BreakpointEditAction::Toggle, + cx, + ); + } })) .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { editor.set_breakpoint_context_menu( row, Some(position), - arc_kind2.clone(), event.down.position, window, cx, @@ -6422,13 +6458,7 @@ impl Editor { cx: &mut Context, ) -> IconButton { let color = Color::Muted; - 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) .shape(ui::IconButtonShape::Square) @@ -6446,14 +6476,7 @@ impl Editor { ); })) .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { - editor.set_breakpoint_context_menu( - row, - position, - bp_kind.clone(), - event.down.position, - window, - cx, - ); + editor.set_breakpoint_context_menu(row, position, event.down.position, window, cx); })) } @@ -8430,9 +8453,8 @@ impl Editor { fn set_breakpoint_context_menu( &mut self, - row: DisplayRow, + display_row: DisplayRow, position: Option, - kind: Arc, clicked_point: gpui::Point, window: &mut Window, cx: &mut Context, @@ -8444,10 +8466,9 @@ impl Editor { .buffer .read(cx) .snapshot(cx) - .anchor_before(Point::new(row.0, 0u32)); + .anchor_before(Point::new(display_row.0, 0u32)); - let context_menu = - self.breakpoint_context_menu(position.unwrap_or(source), kind, window, cx); + let context_menu = self.breakpoint_context_menu(position.unwrap_or(source), window, cx); self.mouse_context_menu = MouseContextMenu::pinned_to_editor( self, @@ -8462,13 +8483,14 @@ impl Editor { fn add_edit_breakpoint_block( &mut self, anchor: Anchor, - kind: &BreakpointKind, + breakpoint: &Breakpoint, window: &mut Window, cx: &mut Context, ) { let weak_editor = cx.weak_entity(); - let bp_prompt = - cx.new(|cx| BreakpointPromptEditor::new(weak_editor, anchor, kind.clone(), window, cx)); + let bp_prompt = cx.new(|cx| { + BreakpointPromptEditor::new(weak_editor, anchor, breakpoint.clone(), window, cx) + }); let height = bp_prompt.update(cx, |this, cx| { this.prompt @@ -8495,36 +8517,45 @@ impl Editor { }); } - pub(crate) fn breakpoint_at_cursor_head( + fn breakpoint_at_cursor_head( &self, window: &mut Window, cx: &mut Context, ) -> Option<(Anchor, Breakpoint)> { 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, + ) -> Option<(Anchor, Breakpoint)> { let snapshot = self.snapshot(window, cx); - // We Set the column position to zero so this function interacts correctly - // 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 breakpoint_position = snapshot.buffer_snapshot.anchor_before(Point::new(row, 0)); - let project = self.project.clone(); + let project = self.project.clone()?; - let buffer_id = cursor_position.text_anchor.buffer_id?; - let enclosing_excerpt = snapshot - .buffer_snapshot - .excerpt_ids_for_range(cursor_position..cursor_position) - .next()?; - let buffer = project?.read_with(cx, |project, cx| project.buffer_for_id(buffer_id, cx))?; + let buffer_id = breakpoint_position.buffer_id.or_else(|| { + snapshot + .buffer_snapshot + .buffer_id_for_excerpt(breakpoint_position.excerpt_id) + })?; + + 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 row = buffer_snapshot - .summary_for_anchor::(&cursor_position.text_anchor) + .summary_for_anchor::(&breakpoint_position.text_anchor) .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 .breakpoint_store .as_ref()? @@ -8532,12 +8563,12 @@ impl Editor { breakpoint_store .breakpoints( &buffer, - Some(cursor_position.text_anchor..(text::Anchor::MAX)), - buffer_snapshot.clone(), + Some(breakpoint_position.text_anchor..anchor_end.text_anchor), + &buffer_snapshot, cx, ) .next() - .and_then(move |(anchor, bp)| { + .and_then(|(anchor, bp)| { let breakpoint_row = buffer_snapshot .summary_for_anchor::(anchor) .row; @@ -8576,11 +8607,48 @@ impl Editor { breakpoint_position, Breakpoint { 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, + ) { + 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, + ) { + 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( @@ -8592,7 +8660,7 @@ impl Editor { let edit_action = BreakpointEditAction::Toggle; 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 { let cursor_position: Point = self.selections.newest(cx).head(); @@ -8604,7 +8672,7 @@ impl Editor { self.edit_breakpoint_at_anchor( breakpoint_position, - BreakpointKind::Standard, + Breakpoint::new_standard(), edit_action, cx, ); @@ -8614,7 +8682,7 @@ impl Editor { pub fn edit_breakpoint_at_anchor( &mut self, breakpoint_position: Anchor, - kind: BreakpointKind, + breakpoint: Breakpoint, edit_action: BreakpointEditAction, cx: &mut Context, ) { @@ -8643,7 +8711,7 @@ impl Editor { breakpoint_store.update(cx, |breakpoint_store, cx| { breakpoint_store.toggle_breakpoint( buffer, - (breakpoint_position.text_anchor, Breakpoint { kind }), + (breakpoint_position.text_anchor, breakpoint), edit_action, cx, ); @@ -19605,7 +19673,7 @@ struct BreakpointPromptEditor { pub(crate) prompt: Entity, editor: WeakEntity, breakpoint_anchor: Anchor, - kind: BreakpointKind, + breakpoint: Breakpoint, block_ids: HashSet, gutter_dimensions: Arc>, _subscriptions: Vec, @@ -19617,13 +19685,15 @@ impl BreakpointPromptEditor { fn new( editor: WeakEntity, breakpoint_anchor: Anchor, - kind: BreakpointKind, + breakpoint: Breakpoint, window: &mut Window, cx: &mut Context, ) -> Self { let buffer = cx.new(|cx| { Buffer::local( - kind.log_message() + breakpoint + .kind + .log_message() .map(|msg| msg.to_string()) .unwrap_or_default(), cx, @@ -19655,7 +19725,7 @@ impl BreakpointPromptEditor { prompt, editor, breakpoint_anchor, - kind, + breakpoint, gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())), block_ids: Default::default(), _subscriptions: vec![], @@ -19682,7 +19752,7 @@ impl BreakpointPromptEditor { editor.update(cx, |editor, cx| { editor.edit_breakpoint_at_anchor( self.breakpoint_anchor, - self.kind.clone(), + self.breakpoint.clone(), BreakpointEditAction::EditLogMessage(log_message.into()), cx, ); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 5a81ecca0ca17626e89c612c4e85f09d045fdb75..3c21fd44939b754b9616c8da8bc01ea016501f8c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -32,7 +32,7 @@ use multi_buffer::{IndentGuide, PathKey}; use parking_lot::Mutex; use pretty_assertions::{assert_eq, assert_ne}; use project::{ - debugger::breakpoint_store::{BreakpointKind, SerializedBreakpoint}, + debugger::breakpoint_store::{BreakpointKind, BreakpointState, SerializedBreakpoint}, project_settings::{LspSettings, ProjectSettings}, FakeFs, }; @@ -17392,7 +17392,7 @@ async fn assert_highlighted_edits( fn assert_breakpoint( breakpoints: &BTreeMap, Vec>, path: &Arc, - expected: Vec<(u32, BreakpointKind)>, + expected: Vec<(u32, Breakpoint)>, ) { if expected.len() == 0usize { assert!(!breakpoints.contains_key(path)); @@ -17401,7 +17401,15 @@ fn assert_breakpoint( .get(path) .unwrap() .into_iter() - .map(|breakpoint| (breakpoint.position, breakpoint.kind.clone())) + .map(|breakpoint| { + ( + breakpoint.position, + Breakpoint { + kind: breakpoint.kind.clone(), + state: breakpoint.state, + }, + ) + }) .collect::>(); 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)); - (breakpoint_position, Breakpoint { kind }) + ( + breakpoint_position, + Breakpoint { + kind, + state: BreakpointState::Enabled, + }, + ) }); editor.edit_breakpoint_at_anchor( anchor, - bp.kind, + bp, BreakpointEditAction::EditLogMessage(log_message.into()), cx, ); @@ -17522,7 +17536,10 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { assert_breakpoint( &breakpoints, &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| { @@ -17541,7 +17558,11 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { }); 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.move_to_end(&MoveToEnd, window, cx); @@ -17628,7 +17649,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { assert_breakpoint( &breakpoints, &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 @@ -17669,7 +17690,10 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { assert_breakpoint( &breakpoints, &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| { @@ -17690,8 +17714,8 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { &breakpoints, &abs_path, vec![ - (0, BreakpointKind::Standard), - (3, BreakpointKind::Log("hello world".into())), + (0, Breakpoint::new_standard()), + (3, Breakpoint::new_log("hello world")), ], ); @@ -17713,8 +17737,167 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { &breakpoints, &abs_path, vec![ - (0, BreakpointKind::Standard), - (3, BreakpointKind::Log("hello Earth !!".into())), + (0, Breakpoint::new_standard()), + (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()), ], ); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 8ccfa0d76aac5c4e8cd696a7bb8f6fb7a2cbe1d0..d053f5fca491fd4bfdd5819017baa0fb89653761 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -58,7 +58,7 @@ use multi_buffer::{ MultiBufferRow, RowInfo, }; use project::{ - debugger::breakpoint_store::{Breakpoint, BreakpointKind}, + debugger::breakpoint_store::Breakpoint, project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings}, }; use settings::Settings; @@ -525,6 +525,8 @@ impl EditorElement { if cx.has_flag::() { register_action(editor, window, Editor::toggle_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 .into_iter() .filter_map(|(display_row, (text_anchor, bp))| { - let row = MultiBufferRow { 0: display_row.0 }; - if row_infos .get((display_row.0.saturating_sub(range.start.0)) as usize) .is_some_and(|row_info| row_info.expand_info.is_some()) @@ -1963,11 +1963,13 @@ impl EditorElement { return None; } + let row = + MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(&snapshot).row); if snapshot.is_line_folded(row) { 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( button, @@ -2065,6 +2067,7 @@ impl EditorElement { { return None; } + let button = editor.render_run_indicator( &self.style, Some(display_row) == active_task_indicator_row, @@ -6827,9 +6830,7 @@ impl Element for EditorElement { gutter_breakpoint_point, Bias::Left, ); - let breakpoint = Breakpoint { - kind: BreakpointKind::Standard, - }; + let breakpoint = Breakpoint::new_standard(); (position, breakpoint) }); diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 365c52525a6b8c9e5dcad1f2f07ffe5792ef15dc..ca30b850a8025620dcf466570e1b67d8a64afeaa 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -256,6 +256,21 @@ impl BreakpointStore { 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) => { if !log_message.is_empty() { breakpoint.1.kind = BreakpointKind::Log(log_message.clone()); @@ -351,7 +366,7 @@ impl BreakpointStore { &'a self, buffer: &'a Entity, range: Option>, - buffer_snapshot: BufferSnapshot, + buffer_snapshot: &'a BufferSnapshot, cx: &App, ) -> impl Iterator + 'a { let abs_path = Self::abs_path_from_buffer(buffer, cx); @@ -361,11 +376,10 @@ impl BreakpointStore { .flat_map(move |file_breakpoints| { file_breakpoints.breakpoints.iter().filter({ let range = range.clone(); - let buffer_snapshot = buffer_snapshot.clone(); move |(position, _)| { if let Some(range) = &range { - position.cmp(&range.start, &buffer_snapshot).is_ge() - && position.cmp(&range.end, &buffer_snapshot).is_le() + position.cmp(&range.start, buffer_snapshot).is_ge() + && position.cmp(&range.end, buffer_snapshot).is_le() } else { true } @@ -417,6 +431,7 @@ impl BreakpointStore { position, path: path.clone(), kind: breakpoint.kind.clone(), + state: breakpoint.state, } }) .collect() @@ -439,6 +454,7 @@ impl BreakpointStore { position, path: path.clone(), kind: breakpoint.kind.clone(), + state: breakpoint.state, } }) .collect(), @@ -487,9 +503,13 @@ impl BreakpointStore { for bp in bps { let position = snapshot.anchor_before(PointUtf16::new(bp.position, 0)); - breakpoints_for_file - .breakpoints - .push((position, Breakpoint { kind: bp.kind })) + breakpoints_for_file.breakpoints.push(( + position, + Breakpoint { + kind: bp.kind, + state: bp.state, + }, + )) } new_breakpoints.insert(path, breakpoints_for_file); } @@ -530,6 +550,7 @@ type LogMessage = Arc; #[derive(Clone, Debug)] pub enum BreakpointEditAction { Toggle, + InvertState, 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)] pub struct Breakpoint { pub kind: BreakpointKind, + pub state: BreakpointState, } 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 { Some(client::proto::Breakpoint { 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 { BreakpointKind::Standard => proto::BreakpointKind::Standard.into(), BreakpointKind::Log(_) => proto::BreakpointKind::Log.into(), @@ -599,8 +664,22 @@ impl Breakpoint { } 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)] @@ -608,6 +687,7 @@ pub struct SerializedBreakpoint { pub position: u32, pub path: Arc, pub kind: BreakpointKind, + pub state: BreakpointState, } impl From for dap::SourceBreakpoint { diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 5579bb51a07a2b992b31a04ecd77f5117a0bb2ed..e8abab984918bd2371e2761a1ae2c41379fd5710 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -358,6 +358,7 @@ impl LocalMode { .breakpoint_store .read_with(cx, |store, cx| store.breakpoints_from_path(&abs_path, cx)) .into_iter() + .filter(|bp| bp.state.is_enabled()) .map(Into::into) .collect(); @@ -388,7 +389,11 @@ impl LocalMode { let breakpoints = if ignore_breakpoints { vec![] } 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( diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index f9314a97b5d2d585f6db9d9bf39342d9ab28be6a..276a6378687694464ed6b1be4e5b39bbef33e558 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -2640,9 +2640,15 @@ enum BreakpointKind { Log = 1; } +enum BreakpointState { + Enabled = 0; + Disabled = 1; +} + message Breakpoint { Anchor position = 1; + BreakpointState state = 2; BreakpointKind kind = 3; optional string message = 4; } diff --git a/crates/sqlez/src/statement.rs b/crates/sqlez/src/statement.rs index af703e13e6bf96a029c64a4e416c2b4facfb4ecf..e0509719d5e7b3e51ca94a79187a3592a83cb96f 100644 --- a/crates/sqlez/src/statement.rs +++ b/crates/sqlez/src/statement.rs @@ -11,7 +11,7 @@ use crate::connection::Connection; pub struct Statement<'a> { /// vector of pointers to the raw SQLite statement objects. /// 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. current_statement: usize, /// A reference to the database connection. diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 45681d75b6da7e621d171bae164dbf5c52f84052..b0c9010b6375aea3bdbc113be8b8c4d886e1d83a 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -13,7 +13,7 @@ use client::DevServerProjectId; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{point, size, Axis, Bounds, WindowBounds, WindowId}; use itertools::Itertools; -use project::debugger::breakpoint_store::{BreakpointKind, SerializedBreakpoint}; +use project::debugger::breakpoint_store::{BreakpointKind, BreakpointState, SerializedBreakpoint}; use language::{LanguageName, Toolchain}; use project::WorktreeId; @@ -148,9 +148,43 @@ impl Column for SerializedWindowBounds { pub struct Breakpoint { pub position: u32, pub kind: BreakpointKind, + pub state: BreakpointState, } /// Wrapper for DB type of a breakpoint +struct BreakpointStateWrapper<'a>(Cow<'a, BreakpointState>); + +impl From 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 { + 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>); impl From for BreakpointKindWrapper<'static> { @@ -200,7 +234,7 @@ struct Breakpoints(Vec); impl sqlez::bindable::StaticColumnCount for Breakpoint { 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, ) -> anyhow::Result { let next_index = statement.bind(&self.position, start_index)?; - statement.bind( + let next_index = statement.bind( &BreakpointKindWrapper(Cow::Borrowed(&self.kind)), 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}"))? as u32; let (kind, next_index) = BreakpointKindWrapper::column(statement, start_index + 1)?; + let (state, next_index) = BreakpointStateWrapper::column(statement, next_index)?; Ok(( Breakpoint { position, kind: kind.0.into_owned(), + state: state.0.into_owned(), }, next_index, )) @@ -245,16 +285,9 @@ impl Column for Breakpoints { match statement.column_type(index) { Ok(SqlType::Null) => break, _ => { - let position = statement - .column_int(index) - .with_context(|| format!("Failed to read BreakPoint at index {index}"))? - as u32; - let (kind, next_index) = BreakpointKindWrapper::column(statement, index + 1)?; + let (breakpoint, next_index) = Breakpoint::column(statement, index)?; - breakpoints.push(Breakpoint { - position, - kind: kind.0.into_owned(), - }); + breakpoints.push(breakpoint); index = next_index; } } @@ -535,6 +568,9 @@ define_connection! { CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array); 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, Vec> { let breakpoints: Result> = self .select_bound(sql! { - SELECT path, breakpoint_location, kind + SELECT path, breakpoint_location, kind, log_message, state FROM breakpoints WHERE workspace_id = ? }) @@ -712,6 +748,7 @@ impl WorkspaceDb { position: breakpoint.position, path, kind: breakpoint.kind, + state: breakpoint.state, }); } @@ -739,15 +776,17 @@ impl WorkspaceDb { .context("Clearing old breakpoints")?; for bp in breakpoints { let kind = BreakpointKindWrapper::from(bp.kind); + let state = BreakpointStateWrapper::from(bp.state); match conn.exec_bound(sql!( - INSERT INTO breakpoints (workspace_id, path, breakpoint_location, kind, log_message) - VALUES (?1, ?2, ?3, ?4, ?5);))? + INSERT INTO breakpoints (workspace_id, path, breakpoint_location, kind, log_message, state) + VALUES (?1, ?2, ?3, ?4, ?5, ?6);))? (( workspace.id, path.as_ref(), bp.position, kind, + state, )) { Ok(_) => {} Err(err) => { @@ -1415,11 +1454,19 @@ mod tests { let breakpoint = Breakpoint { position: 123, kind: BreakpointKind::Standard, + state: BreakpointState::Enabled, }; let log_breakpoint = Breakpoint { position: 456, 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 { @@ -1439,11 +1486,19 @@ mod tests { position: breakpoint.position, path: Arc::from(path), kind: breakpoint.kind.clone(), + state: breakpoint.state, }, SerializedBreakpoint { position: log_breakpoint.position, path: Arc::from(path), 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_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].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].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[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]