Debugger: Basic breakpoint improvements (#27687)

Anthony Eid created

This PR does three things

- Right clicking within the gutter outside of the gutter fold area
bounds opens a breakpoint context menu
- Disabled breakpoints are now outline with the debugger accent color
instead of being fully colored at half opacity
- Clicking a breakpoint acts differently now
- Clicking a breakpoint while holding the platform modifier key will
disable/enable it
- Clicking a breakpoint hint while holding the platform modifier key
will set a disabled breakpoint
- Clicking a disabled breakpoint will enable it instead of deleting it

Release Notes:

- N/A

Change summary

assets/icons/debug_disabled_breakpoint.svg      |  1 
assets/icons/debug_disabled_log_breakpoint.svg  |  1 
crates/editor/src/editor.rs                     | 24 ++++++++++++------
crates/editor/src/editor_tests.rs               |  4 +++
crates/editor/src/element.rs                    | 18 ++++++++++++++
crates/icons/src/icons.rs                       |  2 +
crates/project/src/debugger/breakpoint_store.rs |  3 +
7 files changed, 44 insertions(+), 9 deletions(-)

Detailed changes

assets/icons/debug_disabled_breakpoint.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle"><circle cx="12" cy="12" r="10"/></svg>

assets/icons/debug_disabled_log_breakpoint.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>

crates/editor/src/editor.rs 🔗

@@ -6279,20 +6279,22 @@ impl Editor {
         cx: &mut Context<Self>,
     ) -> IconButton {
         let (color, icon) = {
+            let icon = match (&breakpoint.kind, breakpoint.is_disabled()) {
+                (BreakpointKind::Standard, false) => ui::IconName::DebugBreakpoint,
+                (BreakpointKind::Log(_), false) => ui::IconName::DebugLogBreakpoint,
+                (BreakpointKind::Standard, true) => ui::IconName::DebugDisabledBreakpoint,
+                (BreakpointKind::Log(_), true) => ui::IconName::DebugDisabledLogBreakpoint,
+            };
+
             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)
         };
 
@@ -6306,12 +6308,18 @@ impl Editor {
             .on_click(cx.listener({
                 let breakpoint = breakpoint.clone();
 
-                move |editor, _e, window, cx| {
+                move |editor, event: &ClickEvent, window, cx| {
+                    let edit_action = if event.modifiers().platform || breakpoint.is_disabled() {
+                        BreakpointEditAction::InvertState
+                    } else {
+                        BreakpointEditAction::Toggle
+                    };
+
                     window.focus(&editor.focus_handle(cx));
                     editor.edit_breakpoint_at_anchor(
                         position,
                         breakpoint.as_ref().clone(),
-                        BreakpointEditAction::Toggle,
+                        edit_action,
                         cx,
                     );
                 }

crates/editor/src/editor_tests.rs 🔗

@@ -17842,6 +17842,10 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
         editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
         editor.move_to_end(&MoveToEnd, window, cx);
         editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
+        // Disabling a breakpoint that doesn't exist should do nothing
+        editor.move_up(&MoveUp, window, cx);
+        editor.move_up(&MoveUp, window, cx);
+        editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
     });
 
     let breakpoints = editor.update(cx, |editor, cx| {

crates/editor/src/element.rs 🔗

@@ -713,9 +713,27 @@ impl EditorElement {
         window: &mut Window,
         cx: &mut Context<Editor>,
     ) {
+        if position_map.gutter_hitbox.is_hovered(window) {
+            let gutter_right_padding = editor.gutter_dimensions.right_padding;
+            let hitbox = &position_map.gutter_hitbox;
+
+            if event.position.x <= hitbox.bounds.right() - gutter_right_padding {
+                let point_for_position = position_map.point_for_position(event.position);
+                editor.set_breakpoint_context_menu(
+                    point_for_position.previous_valid.row(),
+                    None,
+                    event.position,
+                    window,
+                    cx,
+                );
+            }
+            return;
+        }
+
         if !position_map.text_hitbox.is_hovered(window) {
             return;
         }
+
         let point_for_position = position_map.point_for_position(event.position);
         mouse_context_menu::deploy_context_menu(
             editor,

crates/icons/src/icons.rs 🔗

@@ -70,6 +70,8 @@ pub enum IconName {
     CursorIBeam,
     Dash,
     DebugBreakpoint,
+    DebugDisabledBreakpoint,
+    DebugDisabledLogBreakpoint,
     DebugIgnoreBreakpoints,
     DebugPause,
     DebugContinue,

crates/project/src/debugger/breakpoint_store.rs 🔗

@@ -268,7 +268,8 @@ impl BreakpointStore {
                         bp.state = BreakpointState::Enabled;
                     }
                 } else {
-                    log::error!("Attempted to invert a breakpoint's state that doesn't exist ");
+                    breakpoint.1.state = BreakpointState::Disabled;
+                    breakpoint_set.breakpoints.push(breakpoint.clone());
                 }
             }
             BreakpointEditAction::EditLogMessage(log_message) => {