debugger: Fix breakpoint color only updating when mouse moves (#46427)

The other day I was testing the debugging feature and I noticed a
**very** small visual bug. When you click on the breakpoint circle to
set a breakpoint, the breakpoint color remains in the wrong state until
the mouse moves.

After debugging for a while, I noticed that the issue was that the
PhantomVariableIndicator variable attribute
`collides_with_existing_breakpoint` was not being updated until the
mouse moved. With the following change, the attribute is updated when
the user clicks the breakpoint, toggleling tha attribute.

### Prior behaviour:

https://github.com/user-attachments/assets/3dfa8e25-e970-49a3-8e87-0ecadabb3a3c

As you can see in the video, prior to this change, the breakpoint would
only change to the correct color when the mouse moved.
 
### New behaviour

https://github.com/user-attachments/assets/356e84dc-42e5-4440-afab-0fbc4b3a1f0a

With this new change, the breakpoint updates right after the user
clicks, no need to move the mouse.

### Disclaimers

- I'm new to this codebase, so any feedback is extremely welcomed! If
you know of a better place where this could be handled, let me know and
I'll explore further.


https://github.com/user-attachments/assets/12d4b9df-e5d7-4955-b58d-dd63563c5c1b

Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <anthony@zed.dev>
This commit is contained in:
Iago BermúdezandAnthony Eid authored and GitHub committed 2026-02-05 09:59:03 +00:00
1 parent 87dff2a1db
commit 1fe80040b1
2 files changed
+130

No files matched your search

+28
View File
@@ -8984,6 +8984,7 @@ impl Editor {
};
window.focus(&editor.focus_handle(cx), cx);
editor.update_breakpoint_collision_on_toggle(row, &edit_action);
editor.edit_breakpoint_at_anchor(
position,
breakpoint.as_ref().clone(),
@@ -11847,7 +11848,19 @@ impl Editor {
return;
}
let snapshot = self.snapshot(window, cx);
for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) {
if self.gutter_breakpoint_indicator.0.is_some() {
let display_row = anchor
.to_point(snapshot.buffer_snapshot())
.to_display_point(&snapshot.display_snapshot)
.row();
self.update_breakpoint_collision_on_toggle(
display_row,
&BreakpointEditAction::Toggle,
);
}
if let Some(breakpoint) = breakpoint {
self.edit_breakpoint_at_anchor(
anchor,
@@ -11866,6 +11879,21 @@ impl Editor {
}
}
fn update_breakpoint_collision_on_toggle(
&mut self,
display_row: DisplayRow,
edit_action: &BreakpointEditAction,
) {
if let Some(ref mut breakpoint_indicator) = self.gutter_breakpoint_indicator.0 {
if breakpoint_indicator.display_row == display_row
&& matches!(edit_action, BreakpointEditAction::Toggle)
{
breakpoint_indicator.collides_with_existing_breakpoint =
!breakpoint_indicator.collides_with_existing_breakpoint;
}
}
}
pub fn edit_breakpoint_at_anchor(
&mut self,
breakpoint_position: Anchor,
+102
View File
@@ -24836,6 +24836,108 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn test_breakpoint_phantom_indicator_collision_on_toggle(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 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, rel_path("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,
)
});
// Simulate hovering over row 0 with no existing breakpoint.
editor.update(cx, |editor, _cx| {
editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
display_row: DisplayRow(0),
is_active: true,
collides_with_existing_breakpoint: false,
});
});
// Toggle breakpoint on the same row (row 0) — collision should flip to true.
editor.update_in(cx, |editor, window, cx| {
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
});
editor.update(cx, |editor, _cx| {
let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
assert!(
indicator.collides_with_existing_breakpoint,
"Adding a breakpoint on the hovered row should set collision to true"
);
});
// Toggle again on the same row — breakpoint is removed, collision should flip back to false.
editor.update_in(cx, |editor, window, cx| {
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
});
editor.update(cx, |editor, _cx| {
let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
assert!(
!indicator.collides_with_existing_breakpoint,
"Removing a breakpoint on the hovered row should set collision to false"
);
});
// Now move cursor to row 2 while phantom indicator stays on row 0.
editor.update_in(cx, |editor, window, cx| {
editor.move_down(&MoveDown, window, cx);
editor.move_down(&MoveDown, window, cx);
});
// Ensure phantom indicator is still on row 0, not colliding.
editor.update(cx, |editor, _cx| {
editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
display_row: DisplayRow(0),
is_active: true,
collides_with_existing_breakpoint: false,
});
});
// Toggle breakpoint on row 2 (cursor row) — phantom on row 0 should NOT be affected.
editor.update_in(cx, |editor, window, cx| {
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
});
editor.update(cx, |editor, _cx| {
let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
assert!(
!indicator.collides_with_existing_breakpoint,
"Toggling a breakpoint on a different row should not affect the phantom indicator"
);
});
}
#[gpui::test]
async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
init_test(cx, |_| {});