Debugger: Add conditional and hit conditional breakpoint functionality (#27760)

Anthony Eid and Remco Smits created

This PR adds conditional and hit condition breakpoint functionality 

cc @osiewicz 

Co-authored-by: Remco Smits: <djsmits12@gmail.com>

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>

Change summary

crates/editor/src/code_context_menus.rs         |  12 +
crates/editor/src/editor.rs                     | 151 +++++++++++++++---
crates/editor/src/editor_tests.rs               |  10 
crates/project/src/debugger/breakpoint_store.rs | 103 ++++++++++++
crates/proto/proto/zed.proto                    |   2 
crates/workspace/src/persistence.rs             | 140 ++++++++++++++++-
6 files changed, 368 insertions(+), 50 deletions(-)

Detailed changes

crates/editor/src/code_context_menus.rs 🔗

@@ -1,3 +1,4 @@
+use feature_flags::{Debugger, FeatureFlagAppExt as _};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     AnyElement, BackgroundExecutor, Entity, Focusable, FontWeight, ListSizingBehavior,
@@ -992,6 +993,17 @@ impl CodeActionsMenu {
                     .iter()
                     .skip(range.start)
                     .take(range.end - range.start)
+                    .filter(|action| {
+                        if action
+                            .as_task()
+                            .map(|task| matches!(task.task_type(), task::TaskType::Debug(_)))
+                            .unwrap_or(false)
+                        {
+                            cx.has_flag::<Debugger>()
+                        } else {
+                            true
+                        }
+                    })
                     .enumerate()
                     .map(|(ix, action)| {
                         let item_ix = range.start + ix;

crates/editor/src/editor.rs 🔗

@@ -4798,6 +4798,8 @@ impl Editor {
                                 Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx)
                             });
 
+                    let debugger_flag = cx.has_flag::<Debugger>();
+
                     Some(cx.spawn_in(window, async move |editor, cx| {
                         let task_context = match task_context {
                             Some(task_context) => task_context.await,
@@ -4813,12 +4815,22 @@ impl Editor {
                                     )),
                                 })
                             });
-                        let spawn_straight_away = resolved_tasks
+                        let spawn_straight_away = resolved_tasks.as_ref().map_or(false, |tasks| {
+                            tasks
+                                .templates
+                                .iter()
+                                .filter(|task| {
+                                    if matches!(task.1.task_type(), task::TaskType::Debug(_)) {
+                                        debugger_flag
+                                    } else {
+                                        true
+                                    }
+                                })
+                                .count()
+                                == 1
+                        }) && code_actions
                             .as_ref()
-                            .map_or(false, |tasks| tasks.templates.len() == 1)
-                            && code_actions
-                                .as_ref()
-                                .map_or(true, |actions| actions.is_empty());
+                            .map_or(true, |actions| actions.is_empty());
                         if let Ok(task) = editor.update_in(cx, |editor, window, cx| {
                             *editor.context_menu.borrow_mut() =
                                 Some(CodeContextMenu::CodeActions(CodeActionsMenu {
@@ -6292,6 +6304,22 @@ impl Editor {
             "Set Log Breakpoint"
         };
 
+        let condition_breakpoint_msg =
+            if breakpoint.as_ref().is_some_and(|bp| bp.condition.is_some()) {
+                "Edit Condition Breakpoint"
+            } else {
+                "Set Condition Breakpoint"
+            };
+
+        let hit_condition_breakpoint_msg = if breakpoint
+            .as_ref()
+            .is_some_and(|bp| bp.hit_condition.is_some())
+        {
+            "Edit Hit Condition Breakpoint"
+        } else {
+            "Set Hit Condition Breakpoint"
+        };
+
         let set_breakpoint_msg = if breakpoint.as_ref().is_some() {
             "Unset Breakpoint"
         } else {
@@ -6303,12 +6331,7 @@ impl Editor {
             BreakpointState::Disabled => Some("Enable"),
         });
 
-        let breakpoint = breakpoint.unwrap_or_else(|| {
-            Arc::new(Breakpoint {
-                state: BreakpointState::Enabled,
-                message: None,
-            })
-        });
+        let breakpoint = breakpoint.unwrap_or_else(|| Arc::new(Breakpoint::new_standard()));
 
         ui::ContextMenu::build(window, cx, |menu, _, _cx| {
             menu.on_blur_subscription(Subscription::new(|| {}))
@@ -6347,10 +6370,50 @@ impl Editor {
                             .log_err();
                     }
                 })
-                .entry(log_breakpoint_msg, None, move |window, cx| {
+                .entry(log_breakpoint_msg, None, {
+                    let breakpoint = breakpoint.clone();
+                    let weak_editor = weak_editor.clone();
+                    move |window, cx| {
+                        weak_editor
+                            .update(cx, |this, cx| {
+                                this.add_edit_breakpoint_block(
+                                    anchor,
+                                    breakpoint.as_ref(),
+                                    BreakpointPromptEditAction::Log,
+                                    window,
+                                    cx,
+                                );
+                            })
+                            .log_err();
+                    }
+                })
+                .entry(condition_breakpoint_msg, None, {
+                    let breakpoint = breakpoint.clone();
+                    let weak_editor = weak_editor.clone();
+                    move |window, cx| {
+                        weak_editor
+                            .update(cx, |this, cx| {
+                                this.add_edit_breakpoint_block(
+                                    anchor,
+                                    breakpoint.as_ref(),
+                                    BreakpointPromptEditAction::Condition,
+                                    window,
+                                    cx,
+                                );
+                            })
+                            .log_err();
+                    }
+                })
+                .entry(hit_condition_breakpoint_msg, None, move |window, cx| {
                     weak_editor
                         .update(cx, |this, cx| {
-                            this.add_edit_breakpoint_block(anchor, breakpoint.as_ref(), window, cx);
+                            this.add_edit_breakpoint_block(
+                                anchor,
+                                breakpoint.as_ref(),
+                                BreakpointPromptEditAction::HitCondition,
+                                window,
+                                cx,
+                            );
                         })
                         .log_err();
                 })
@@ -8597,12 +8660,20 @@ impl Editor {
         &mut self,
         anchor: Anchor,
         breakpoint: &Breakpoint,
+        edit_action: BreakpointPromptEditAction,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         let weak_editor = cx.weak_entity();
         let bp_prompt = cx.new(|cx| {
-            BreakpointPromptEditor::new(weak_editor, anchor, breakpoint.clone(), window, cx)
+            BreakpointPromptEditor::new(
+                weak_editor,
+                anchor,
+                breakpoint.clone(),
+                edit_action,
+                window,
+                cx,
+            )
         });
 
         let height = bp_prompt.update(cx, |this, cx| {
@@ -8721,11 +8792,13 @@ impl Editor {
                     Breakpoint {
                         message: None,
                         state: BreakpointState::Enabled,
+                        condition: None,
+                        hit_condition: None,
                     },
                 )
             });
 
-        self.add_edit_breakpoint_block(anchor, &bp, window, cx);
+        self.add_edit_breakpoint_block(anchor, &bp, BreakpointPromptEditAction::Log, window, cx);
     }
 
     pub fn enable_breakpoint(
@@ -19861,11 +19934,18 @@ impl Global for KillRing {}
 
 const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
 
+enum BreakpointPromptEditAction {
+    Log,
+    Condition,
+    HitCondition,
+}
+
 struct BreakpointPromptEditor {
     pub(crate) prompt: Entity<Editor>,
     editor: WeakEntity<Editor>,
     breakpoint_anchor: Anchor,
     breakpoint: Breakpoint,
+    edit_action: BreakpointPromptEditAction,
     block_ids: HashSet<CustomBlockId>,
     gutter_dimensions: Arc<Mutex<GutterDimensions>>,
     _subscriptions: Vec<Subscription>,
@@ -19878,19 +19958,19 @@ impl BreakpointPromptEditor {
         editor: WeakEntity<Editor>,
         breakpoint_anchor: Anchor,
         breakpoint: Breakpoint,
+        edit_action: BreakpointPromptEditAction,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
-        let buffer = cx.new(|cx| {
-            Buffer::local(
-                breakpoint
-                    .message
-                    .as_ref()
-                    .map(|msg| msg.to_string())
-                    .unwrap_or_default(),
-                cx,
-            )
-        });
+        let base_text = match edit_action {
+            BreakpointPromptEditAction::Log => breakpoint.message.as_ref(),
+            BreakpointPromptEditAction::Condition => breakpoint.condition.as_ref(),
+            BreakpointPromptEditAction::HitCondition => breakpoint.hit_condition.as_ref(),
+        }
+        .map(|msg| msg.to_string())
+        .unwrap_or_default();
+
+        let buffer = cx.new(|cx| Buffer::local(base_text, cx));
         let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 
         let prompt = cx.new(|cx| {
@@ -19906,7 +19986,11 @@ impl BreakpointPromptEditor {
             prompt.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
             prompt.set_show_cursor_when_unfocused(false, cx);
             prompt.set_placeholder_text(
-                "Message to log when breakpoint is hit. Expressions within {} are interpolated.",
+                match edit_action {
+                    BreakpointPromptEditAction::Log => "Message to log when a breakpoint is hit. Expressions within {} are interpolated.",
+                    BreakpointPromptEditAction::Condition => "Condition when a breakpoint is hit. Expressions within {} are interpolated.",
+                    BreakpointPromptEditAction::HitCondition => "How many breakpoint hits to ignore",
+                },
                 cx,
             );
 
@@ -19918,6 +20002,7 @@ impl BreakpointPromptEditor {
             editor,
             breakpoint_anchor,
             breakpoint,
+            edit_action,
             gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())),
             block_ids: Default::default(),
             _subscriptions: vec![],
@@ -19930,7 +20015,7 @@ impl BreakpointPromptEditor {
 
     fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
         if let Some(editor) = self.editor.upgrade() {
-            let log_message = self
+            let message = self
                 .prompt
                 .read(cx)
                 .buffer
@@ -19945,7 +20030,17 @@ impl BreakpointPromptEditor {
                 editor.edit_breakpoint_at_anchor(
                     self.breakpoint_anchor,
                     self.breakpoint.clone(),
-                    BreakpointEditAction::EditLogMessage(log_message.into()),
+                    match self.edit_action {
+                        BreakpointPromptEditAction::Log => {
+                            BreakpointEditAction::EditLogMessage(message.into())
+                        }
+                        BreakpointPromptEditAction::Condition => {
+                            BreakpointEditAction::EditCondition(message.into())
+                        }
+                        BreakpointPromptEditAction::HitCondition => {
+                            BreakpointEditAction::EditHitCondition(message.into())
+                        }
+                    },
                     cx,
                 );
 

crates/editor/src/editor_tests.rs 🔗

@@ -17387,6 +17387,8 @@ fn assert_breakpoint(
                     Breakpoint {
                         message: breakpoint.message.clone(),
                         state: breakpoint.state,
+                        condition: breakpoint.condition.clone(),
+                        hit_condition: breakpoint.hit_condition.clone(),
                     },
                 )
             })
@@ -17415,13 +17417,7 @@ fn add_log_breakpoint_at_cursor(
                 .buffer_snapshot
                 .anchor_before(Point::new(cursor_position.row, 0));
 
-            (
-                breakpoint_position,
-                Breakpoint {
-                    message: Some(Arc::from(log_message)),
-                    state: BreakpointState::Enabled,
-                },
-            )
+            (breakpoint_position, Breakpoint::new_log(&log_message))
         });
 
     editor.edit_breakpoint_at_anchor(

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

@@ -294,6 +294,60 @@ impl BreakpointStore {
                     })
                 }
             }
+            BreakpointEditAction::EditHitCondition(hit_condition) => {
+                if !hit_condition.is_empty() {
+                    let found_bp =
+                        breakpoint_set
+                            .breakpoints
+                            .iter_mut()
+                            .find_map(|(other_pos, other_bp)| {
+                                if breakpoint.0 == *other_pos {
+                                    Some(other_bp)
+                                } else {
+                                    None
+                                }
+                            });
+
+                    if let Some(found_bp) = found_bp {
+                        found_bp.hit_condition = Some(hit_condition.clone());
+                    } else {
+                        breakpoint.1.hit_condition = Some(hit_condition.clone());
+                        // We did not remove any breakpoint, hence let's toggle one.
+                        breakpoint_set.breakpoints.push(breakpoint.clone());
+                    }
+                } else if breakpoint.1.hit_condition.is_some() {
+                    breakpoint_set.breakpoints.retain(|(other_pos, other)| {
+                        &breakpoint.0 != other_pos && other.hit_condition.is_none()
+                    })
+                }
+            }
+            BreakpointEditAction::EditCondition(condition) => {
+                if !condition.is_empty() {
+                    let found_bp =
+                        breakpoint_set
+                            .breakpoints
+                            .iter_mut()
+                            .find_map(|(other_pos, other_bp)| {
+                                if breakpoint.0 == *other_pos {
+                                    Some(other_bp)
+                                } else {
+                                    None
+                                }
+                            });
+
+                    if let Some(found_bp) = found_bp {
+                        found_bp.condition = Some(condition.clone());
+                    } else {
+                        breakpoint.1.condition = Some(condition.clone());
+                        // We did not remove any breakpoint, hence let's toggle one.
+                        breakpoint_set.breakpoints.push(breakpoint.clone());
+                    }
+                } else if breakpoint.1.condition.is_some() {
+                    breakpoint_set.breakpoints.retain(|(other_pos, other)| {
+                        &breakpoint.0 != other_pos && other.condition.is_none()
+                    })
+                }
+            }
         }
 
         if breakpoint_set.breakpoints.is_empty() {
@@ -424,6 +478,8 @@ impl BreakpointStore {
                             path: path.clone(),
                             state: breakpoint.state,
                             message: breakpoint.message.clone(),
+                            condition: breakpoint.condition.clone(),
+                            hit_condition: breakpoint.hit_condition.clone(),
                         }
                     })
                     .collect()
@@ -447,6 +503,8 @@ impl BreakpointStore {
                                 path: path.clone(),
                                 message: breakpoint.message.clone(),
                                 state: breakpoint.state,
+                                hit_condition: breakpoint.hit_condition.clone(),
+                                condition: breakpoint.condition.clone(),
                             }
                         })
                         .collect(),
@@ -500,6 +558,8 @@ impl BreakpointStore {
                             Breakpoint {
                                 message: bp.message,
                                 state: bp.state,
+                                condition: bp.condition,
+                                hit_condition: bp.hit_condition,
                             },
                         ))
                     }
@@ -537,13 +597,15 @@ pub enum BreakpointStoreEvent {
 
 impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
 
-type LogMessage = Arc<str>;
+type BreakpointMessage = Arc<str>;
 
 #[derive(Clone, Debug)]
 pub enum BreakpointEditAction {
     Toggle,
     InvertState,
-    EditLogMessage(LogMessage),
+    EditLogMessage(BreakpointMessage),
+    EditCondition(BreakpointMessage),
+    EditHitCondition(BreakpointMessage),
 }
 
 #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
@@ -574,7 +636,10 @@ impl BreakpointState {
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 pub struct Breakpoint {
-    pub message: Option<Arc<str>>,
+    pub message: Option<BreakpointMessage>,
+    /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
+    pub hit_condition: Option<BreakpointMessage>,
+    pub condition: Option<BreakpointMessage>,
     pub state: BreakpointState,
 }
 
@@ -582,6 +647,17 @@ impl Breakpoint {
     pub fn new_standard() -> Self {
         Self {
             state: BreakpointState::Enabled,
+            hit_condition: None,
+            condition: None,
+            message: None,
+        }
+    }
+
+    pub fn new_condition(hit_condition: &str) -> Self {
+        Self {
+            state: BreakpointState::Enabled,
+            condition: None,
+            hit_condition: Some(hit_condition.into()),
             message: None,
         }
     }
@@ -589,6 +665,8 @@ impl Breakpoint {
     pub fn new_log(log_message: &str) -> Self {
         Self {
             state: BreakpointState::Enabled,
+            hit_condition: None,
+            condition: None,
             message: Some(log_message.into()),
         }
     }
@@ -601,6 +679,11 @@ impl Breakpoint {
                 BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
             },
             message: self.message.as_ref().map(|s| String::from(s.as_ref())),
+            condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
+            hit_condition: self
+                .hit_condition
+                .as_ref()
+                .map(|s| String::from(s.as_ref())),
         })
     }
 
@@ -610,7 +693,9 @@ impl Breakpoint {
                 Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
                 None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
             },
-            message: breakpoint.message.map(|message| message.into()),
+            message: breakpoint.message.map(Into::into),
+            condition: breakpoint.condition.map(Into::into),
+            hit_condition: breakpoint.hit_condition.map(Into::into),
         })
     }
 
@@ -631,6 +716,8 @@ pub struct SourceBreakpoint {
     pub row: u32,
     pub path: Arc<Path>,
     pub message: Option<Arc<str>>,
+    pub condition: Option<Arc<str>>,
+    pub hit_condition: Option<Arc<str>>,
     pub state: BreakpointState,
 }
 
@@ -639,8 +726,12 @@ impl From<SourceBreakpoint> for dap::SourceBreakpoint {
         Self {
             line: bp.row as u64 + 1,
             column: None,
-            condition: None,
-            hit_condition: None,
+            condition: bp
+                .condition
+                .map(|condition| String::from(condition.as_ref())),
+            hit_condition: bp
+                .hit_condition
+                .map(|hit_condition| String::from(hit_condition.as_ref())),
             log_message: bp.message.map(|message| String::from(message.as_ref())),
             mode: None,
         }

crates/proto/proto/zed.proto 🔗

@@ -2673,6 +2673,8 @@ message Breakpoint {
     BreakpointState state = 2;
     reserved 3;
     optional string message = 4;
+    optional string condition = 5;
+    optional string hit_condition = 6;
 }
 
 message BreakpointsForFile {

crates/workspace/src/persistence.rs 🔗

@@ -148,6 +148,8 @@ impl Column for SerializedWindowBounds {
 pub struct Breakpoint {
     pub position: u32,
     pub message: Option<Arc<str>>,
+    pub condition: Option<Arc<str>>,
+    pub hit_condition: Option<Arc<str>>,
     pub state: BreakpointState,
 }
 
@@ -190,7 +192,8 @@ struct Breakpoints(Vec<Breakpoint>);
 
 impl sqlez::bindable::StaticColumnCount for Breakpoint {
     fn column_count() -> usize {
-        2 + BreakpointStateWrapper::column_count()
+        // Position, log message, condition message, and hit condition message
+        4 + BreakpointStateWrapper::column_count()
     }
 }
 
@@ -202,6 +205,8 @@ impl sqlez::bindable::Bind for Breakpoint {
     ) -> anyhow::Result<i32> {
         let next_index = statement.bind(&self.position, start_index)?;
         let next_index = statement.bind(&self.message, next_index)?;
+        let next_index = statement.bind(&self.condition, next_index)?;
+        let next_index = statement.bind(&self.hit_condition, next_index)?;
         statement.bind(
             &BreakpointStateWrapper(Cow::Borrowed(&self.state)),
             next_index,
@@ -216,12 +221,16 @@ impl Column for Breakpoint {
             .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))?
             as u32;
         let (message, next_index) = Option::<String>::column(statement, start_index + 1)?;
+        let (condition, next_index) = Option::<String>::column(statement, next_index)?;
+        let (hit_condition, next_index) = Option::<String>::column(statement, next_index)?;
         let (state, next_index) = BreakpointStateWrapper::column(statement, next_index)?;
 
         Ok((
             Breakpoint {
                 position,
                 message: message.map(Arc::from),
+                condition: condition.map(Arc::from),
+                hit_condition: hit_condition.map(Arc::from),
                 state: state.0.into_owned(),
             },
             next_index,
@@ -527,7 +536,11 @@ define_connection! {
     sql!(
         ALTER TABLE breakpoints DROP COLUMN kind
     ),
-    sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL)
+    sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL),
+    sql!(
+        ALTER TABLE breakpoints ADD COLUMN condition TEXT;
+        ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
+    ),
     ];
 }
 
@@ -680,7 +693,7 @@ impl WorkspaceDb {
     fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
         let breakpoints: Result<Vec<(PathBuf, Breakpoint)>> = self
             .select_bound(sql! {
-                SELECT path, breakpoint_location, log_message, state
+                SELECT path, breakpoint_location, log_message, condition, hit_condition, state
                 FROM breakpoints
                 WHERE workspace_id = ?
             })
@@ -700,10 +713,20 @@ impl WorkspaceDb {
                         row: breakpoint.position,
                         path,
                         message: breakpoint.message,
+                        condition: breakpoint.condition,
+                        hit_condition: breakpoint.hit_condition,
                         state: breakpoint.state,
                     });
                 }
 
+                for (path, bps) in map.iter() {
+                    log::debug!(
+                        "Got {} breakpoints from path: {}",
+                        bps.len(),
+                        path.to_string_lossy()
+                    );
+                }
+
                 map
             }
             Err(msg) => {
@@ -727,20 +750,23 @@ impl WorkspaceDb {
                     conn.exec_bound(sql!(DELETE FROM breakpoints WHERE workspace_id = ?1 AND path = ?2))?((workspace.id, path.as_ref()))
                     .context("Clearing old breakpoints")?;
                     for bp in breakpoints {
-                        let message = bp.message;
                         let state = BreakpointStateWrapper::from(bp.state);
                         match conn.exec_bound(sql!(
-                            INSERT INTO breakpoints (workspace_id, path, breakpoint_location,  log_message, state)
-                            VALUES (?1, ?2, ?3, ?4, ?5);))?
+                            INSERT INTO breakpoints (workspace_id, path, breakpoint_location,  log_message, condition, hit_condition, state)
+                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);))?
 
                         ((
                             workspace.id,
                             path.as_ref(),
                             bp.row,
-                            message,
+                            bp.message,
+                            bp.condition,
+                            bp.hit_condition,
                             state,
                         )) {
-                            Ok(_) => {}
+                            Ok(_) => {
+                                log::debug!("Stored breakpoint at row: {} in path: {}", bp.row, path.to_string_lossy())
+                            }
                             Err(err) => {
                                 log::error!("{err}");
                                 continue;
@@ -1409,18 +1435,40 @@ mod tests {
             position: 123,
             message: None,
             state: BreakpointState::Enabled,
+            condition: None,
+            hit_condition: None,
         };
 
         let log_breakpoint = Breakpoint {
             position: 456,
             message: Some("Test log message".into()),
             state: BreakpointState::Enabled,
+            condition: None,
+            hit_condition: None,
         };
 
         let disable_breakpoint = Breakpoint {
             position: 578,
             message: None,
             state: BreakpointState::Disabled,
+            condition: None,
+            hit_condition: None,
+        };
+
+        let condition_breakpoint = Breakpoint {
+            position: 789,
+            message: None,
+            state: BreakpointState::Enabled,
+            condition: Some("x > 5".into()),
+            hit_condition: None,
+        };
+
+        let hit_condition_breakpoint = Breakpoint {
+            position: 999,
+            message: None,
+            state: BreakpointState::Enabled,
+            condition: None,
+            hit_condition: Some(">= 3".into()),
         };
 
         let workspace = SerializedWorkspace {
@@ -1441,18 +1489,40 @@ mod tests {
                             path: Arc::from(path),
                             message: breakpoint.message.clone(),
                             state: breakpoint.state,
+                            condition: breakpoint.condition.clone(),
+                            hit_condition: breakpoint.hit_condition.clone(),
                         },
                         SourceBreakpoint {
                             row: log_breakpoint.position,
                             path: Arc::from(path),
                             message: log_breakpoint.message.clone(),
                             state: log_breakpoint.state,
+                            condition: log_breakpoint.condition.clone(),
+                            hit_condition: log_breakpoint.hit_condition.clone(),
                         },
                         SourceBreakpoint {
                             row: disable_breakpoint.position,
                             path: Arc::from(path),
                             message: disable_breakpoint.message.clone(),
                             state: disable_breakpoint.state,
+                            condition: disable_breakpoint.condition.clone(),
+                            hit_condition: disable_breakpoint.hit_condition.clone(),
+                        },
+                        SourceBreakpoint {
+                            row: condition_breakpoint.position,
+                            path: Arc::from(path),
+                            message: condition_breakpoint.message.clone(),
+                            state: condition_breakpoint.state,
+                            condition: condition_breakpoint.condition.clone(),
+                            hit_condition: condition_breakpoint.hit_condition.clone(),
+                        },
+                        SourceBreakpoint {
+                            row: hit_condition_breakpoint.position,
+                            path: Arc::from(path),
+                            message: hit_condition_breakpoint.message.clone(),
+                            state: hit_condition_breakpoint.state,
+                            condition: hit_condition_breakpoint.condition.clone(),
+                            hit_condition: hit_condition_breakpoint.hit_condition.clone(),
                         },
                     ],
                 );
@@ -1467,22 +1537,74 @@ 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(), 3);
+        assert_eq!(loaded_breakpoints.len(), 5);
 
+        // normal breakpoint
         assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
         assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
+        assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
+        assert_eq!(
+            loaded_breakpoints[0].hit_condition,
+            breakpoint.hit_condition
+        );
         assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
         assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
 
+        // enabled breakpoint
         assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
         assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
+        assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
+        assert_eq!(
+            loaded_breakpoints[1].hit_condition,
+            log_breakpoint.hit_condition
+        );
         assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
         assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
 
+        // disable breakpoint
         assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
         assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
+        assert_eq!(
+            loaded_breakpoints[2].condition,
+            disable_breakpoint.condition
+        );
+        assert_eq!(
+            loaded_breakpoints[2].hit_condition,
+            disable_breakpoint.hit_condition
+        );
         assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
         assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
+
+        // condition breakpoint
+        assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
+        assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
+        assert_eq!(
+            loaded_breakpoints[3].condition,
+            condition_breakpoint.condition
+        );
+        assert_eq!(
+            loaded_breakpoints[3].hit_condition,
+            condition_breakpoint.hit_condition
+        );
+        assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
+        assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
+
+        // hit condition breakpoint
+        assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
+        assert_eq!(
+            loaded_breakpoints[4].message,
+            hit_condition_breakpoint.message
+        );
+        assert_eq!(
+            loaded_breakpoints[4].condition,
+            hit_condition_breakpoint.condition
+        );
+        assert_eq!(
+            loaded_breakpoints[4].hit_condition,
+            hit_condition_breakpoint.hit_condition
+        );
+        assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
+        assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
     }
 
     #[gpui::test]