Allow enabling/disabling breakpoints (#27280)

Anthony Eid , Piotr , Conrad , and Mikayla Maki created

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>

Change summary

crates/editor/src/actions.rs                    |   2 
crates/editor/src/editor.rs                     | 320 +++++++++++-------
crates/editor/src/editor_tests.rs               | 209 +++++++++++
crates/editor/src/element.rs                    |  15 
crates/project/src/debugger/breakpoint_store.rs |  96 +++++
crates/project/src/debugger/session.rs          |   7 
crates/proto/proto/zed.proto                    |   6 
crates/sqlez/src/statement.rs                   |   2 
crates/workspace/src/persistence.rs             |  98 ++++-
9 files changed, 583 insertions(+), 172 deletions(-)

Detailed changes

crates/editor/src/actions.rs 🔗

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

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<Self>,
     ) -> Option<IconButton> {
         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::<Point>(&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::<Point>(&info.range.context.start);
-
             for (anchor, breakpoint) in breakpoints {
-                let as_row = info.buffer.summary_for_anchor::<Point>(&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<BreakpointKind>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Entity<ui::ContextMenu> {
         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::<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"
         } 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<Self>,
     ) -> 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<Self>,
     ) -> 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<Anchor>,
-        kind: Arc<BreakpointKind>,
         clicked_point: gpui::Point<Pixels>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -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<Self>,
     ) {
         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<Self>,
     ) -> 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<Self>,
+    ) -> 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::<text::PointUtf16>(&cursor_position.text_anchor)
+            .summary_for_anchor::<text::PointUtf16>(&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::<text::PointUtf16>(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<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(
@@ -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<Self>,
     ) {
@@ -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>,
     editor: WeakEntity<Editor>,
     breakpoint_anchor: Anchor,
-    kind: BreakpointKind,
+    breakpoint: Breakpoint,
     block_ids: HashSet<CustomBlockId>,
     gutter_dimensions: Arc<Mutex<GutterDimensions>>,
     _subscriptions: Vec<Subscription>,
@@ -19617,13 +19685,15 @@ impl BreakpointPromptEditor {
     fn new(
         editor: WeakEntity<Editor>,
         breakpoint_anchor: Anchor,
-        kind: BreakpointKind,
+        breakpoint: Breakpoint,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> 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,
                 );

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<Arc<Path>, Vec<SerializedBreakpoint>>,
     path: &Arc<Path>,
-    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::<Vec<_>>();
 
         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()),
         ],
     );
 }

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::<Debugger>() {
             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)
                                 });

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<Buffer>,
         range: Option<Range<text::Anchor>>,
-        buffer_snapshot: BufferSnapshot,
+        buffer_snapshot: &'a BufferSnapshot,
         cx: &App,
     ) -> impl Iterator<Item = &'a (text::Anchor, Breakpoint)> + '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<str>;
 #[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<client::proto::Breakpoint> {
         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<Path>,
     pub kind: BreakpointKind,
+    pub state: BreakpointState,
 }
 
 impl From<SerializedBreakpoint> for dap::SourceBreakpoint {

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(

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;
 }

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.

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<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>);
 
 impl From<BreakpointKind> for BreakpointKindWrapper<'static> {
@@ -200,7 +234,7 @@ struct Breakpoints(Vec<Breakpoint>);
 
 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<i32> {
         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<Arc<Path>, Vec<SerializedBreakpoint>> {
         let breakpoints: Result<Vec<(PathBuf, Breakpoint)>> = 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]