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

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>
This commit is contained in:
Anthony Eid 2025-03-31 15:12:23 -04:00 committed by GitHub
parent dc64ec9cc8
commit d517a212dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 368 additions and 50 deletions

View file

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

View file

@ -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,
);

View file

@ -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(

View file

@ -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,
}

View file

@ -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 {

View file

@ -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]