Allow enabling/disabling breakpoints (#27280)

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>
This commit is contained in:
Anthony Eid 2025-03-26 02:06:08 -04:00 committed by GitHub
parent df583d73b9
commit d70ac64fe4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 583 additions and 172 deletions

View file

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