debugger: Surface validity of breakpoints (#30380)

We now show on the breakpoint itself whether it can ever be hit.

![image](https://github.com/user-attachments/assets/148d7712-53c9-4a0a-9fc0-4ff80dec5fb1)

Release Notes:

- N/A

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Michael Sloan <michael@zed.dev>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: peppidesu <bakker.pepijn@gmail.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Ben Kunkle <ben.kunkle@gmail.com>
Co-authored-by: Jens Krause <47693+sectore@users.noreply.github.com>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
Co-authored-by: Max Nordlund <max.nordlund@gmail.com>
Co-authored-by: Finn Evers <dev@bahn.sh>
Co-authored-by: tidely <43219534+tidely@users.noreply.github.com>
Co-authored-by: Sergei Kartsev <kartsevsb@gmail.com>
Co-authored-by: Shardul Vaidya <31039336+5herlocked@users.noreply.github.com>
Co-authored-by: Chris Kelly <amateurhuman@gmail.com>
Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Umesh Yadav <23421535+imumesh18@users.noreply.github.com>
Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: william341 <wwokwilliam@gmail.com>
Co-authored-by: Liam <33645555+lj3954@users.noreply.github.com>
Co-authored-by: AidanV <aidanvanduyne@gmail.com>
Co-authored-by: imumesh18 <umesh4257@gmail.com>
Co-authored-by: d1y <chenhonzhou@gmail.com>
Co-authored-by: AidanV <84053180+AidanV@users.noreply.github.com>
Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
Co-authored-by: 张小白 <364772080@qq.com>
Co-authored-by: THELOSTSOUL <1095533751@qq.com>
Co-authored-by: Ron Harel <55725807+ronharel02@users.noreply.github.com>
Co-authored-by: Tristan Hume <tristan@anthropic.com>
Co-authored-by: Stanislav Alekseev <43210583+WeetHet@users.noreply.github.com>
Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Oleksiy Syvokon <oleksiy@zed.dev>
Co-authored-by: Thomas David Baker <bakert@gmail.com>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Rob McBroom <github@skurfer.com>
Co-authored-by: CharlesChen0823 <yongchen0823@gmail.com>
This commit is contained in:
Piotr Osiewicz
2025-05-20 15:56:15 +00:00
committed by GitHub
co-authored by Anthony Cole Miller Michael Sloan Marshall Bowers Ben Kunkle Danilo Leal Agus Zubiaga Ben Brandt Agus Zubiaga Danilo Leal Richard Feldman Max Brunsfeld Smit Barmase peppidesu Kirill Bulatov Ben Kunkle Jens Krause Bennet Bo Fenner Max Nordlund Finn Evers tidely Sergei Kartsev Shardul Vaidya Chris Kelly Peter Tripp Umesh Yadav Julia Ryan Cole Miller Conrad Irwin william341 Liam AidanV imumesh18 d1y AidanV Anthony Eid 张小白 THELOSTSOUL Ron Harel Tristan Hume Stanislav Alekseev Joseph T. Lyons Remco Smits Anthony Eid Oleksiy Syvokon Thomas David Baker Nate Butler Mikayla Maki Rob McBroom CharlesChen0823
parent 36ae564b61
commit 17cf04558b
11 changed files with 439 additions and 237 deletions
+8 -8
View File
@@ -2517,7 +2517,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2526,7 +2526,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -2550,7 +2550,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2559,7 +2559,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -2583,7 +2583,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2592,7 +2592,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -2616,7 +2616,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2625,7 +2625,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -148,7 +148,7 @@ impl Render for BreakpointList {
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
let old_len = self.breakpoints.len();
let breakpoints = self.breakpoint_store.read(cx).all_breakpoints(cx);
let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
self.breakpoints.clear();
let weak = cx.weak_entity();
let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
+35 -23
View File
@@ -122,10 +122,11 @@ use markdown::Markdown;
use mouse_context_menu::MouseContextMenu;
use persistence::DB;
use project::{
ProjectPath,
BreakpointWithPosition, ProjectPath,
debugger::{
breakpoint_store::{
BreakpointEditAction, BreakpointState, BreakpointStore, BreakpointStoreEvent,
BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
BreakpointStoreEvent,
},
session::{Session, SessionEvent},
},
@@ -198,7 +199,7 @@ use theme::{
};
use ui::{
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
IconSize, Key, Tooltip, h_flex, prelude::*,
IconSize, Indicator, Key, Tooltip, h_flex, prelude::*,
};
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
use workspace::{
@@ -6997,7 +6998,7 @@ impl Editor {
range: Range<DisplayRow>,
window: &mut Window,
cx: &mut Context<Self>,
) -> HashMap<DisplayRow, (Anchor, Breakpoint)> {
) -> HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)> {
let mut breakpoint_display_points = HashMap::default();
let Some(breakpoint_store) = self.breakpoint_store.clone() else {
@@ -7031,15 +7032,17 @@ impl Editor {
buffer_snapshot,
cx,
);
for (anchor, breakpoint) in breakpoints {
for (breakpoint, state) in breakpoints {
let multi_buffer_anchor =
Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), *anchor);
Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), breakpoint.position);
let position = multi_buffer_anchor
.to_point(&multi_buffer_snapshot)
.to_display_point(&snapshot);
breakpoint_display_points
.insert(position.row(), (multi_buffer_anchor, breakpoint.clone()));
breakpoint_display_points.insert(
position.row(),
(multi_buffer_anchor, breakpoint.bp.clone(), state),
);
}
}
@@ -7214,8 +7217,10 @@ impl Editor {
position: Anchor,
row: DisplayRow,
breakpoint: &Breakpoint,
state: Option<BreakpointSessionState>,
cx: &mut Context<Self>,
) -> IconButton {
let is_rejected = state.is_some_and(|s| !s.verified);
// Is it a breakpoint that shows up when hovering over gutter?
let (is_phantom, collides_with_existing) = self.gutter_breakpoint_indicator.0.map_or(
(false, false),
@@ -7241,6 +7246,8 @@ impl Editor {
let color = if is_phantom {
Color::Hint
} else if is_rejected {
Color::Disabled
} else {
Color::Debugger
};
@@ -7268,9 +7275,18 @@ impl Editor {
}
let primary_text = SharedString::from(primary_text);
let focus_handle = self.focus_handle.clone();
let meta = if is_rejected {
"No executable code is associated with this line."
} else {
"Right-click for more options."
};
IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
.icon_size(IconSize::XSmall)
.size(ui::ButtonSize::None)
.when(is_rejected, |this| {
this.indicator(Indicator::icon(Icon::new(IconName::Warning)).color(Color::Warning))
})
.icon_color(color)
.style(ButtonStyle::Transparent)
.on_click(cx.listener({
@@ -7302,14 +7318,7 @@ impl Editor {
);
}))
.tooltip(move |window, cx| {
Tooltip::with_meta_in(
primary_text.clone(),
None,
"Right-click for more options",
&focus_handle,
window,
cx,
)
Tooltip::with_meta_in(primary_text.clone(), None, meta, &focus_handle, window, cx)
})
}
@@ -7449,11 +7458,11 @@ impl Editor {
_style: &EditorStyle,
is_active: bool,
row: DisplayRow,
breakpoint: Option<(Anchor, Breakpoint)>,
breakpoint: Option<(Anchor, Breakpoint, Option<BreakpointSessionState>)>,
cx: &mut Context<Self>,
) -> IconButton {
let color = Color::Muted;
let position = breakpoint.as_ref().map(|(anchor, _)| *anchor);
let position = breakpoint.as_ref().map(|(anchor, _, _)| *anchor);
IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play)
.shape(ui::IconButtonShape::Square)
@@ -9633,16 +9642,16 @@ impl Editor {
cx,
)
.next()
.and_then(|(anchor, bp)| {
.and_then(|(bp, _)| {
let breakpoint_row = buffer_snapshot
.summary_for_anchor::<text::PointUtf16>(anchor)
.summary_for_anchor::<text::PointUtf16>(&bp.position)
.row;
if breakpoint_row == row {
snapshot
.buffer_snapshot
.anchor_in_excerpt(enclosing_excerpt, *anchor)
.map(|anchor| (anchor, bp.clone()))
.anchor_in_excerpt(enclosing_excerpt, bp.position)
.map(|position| (position, bp.bp.clone()))
} else {
None
}
@@ -9805,7 +9814,10 @@ impl Editor {
breakpoint_store.update(cx, |breakpoint_store, cx| {
breakpoint_store.toggle_breakpoint(
buffer,
(breakpoint_position.text_anchor, breakpoint),
BreakpointWithPosition {
position: breakpoint_position.text_anchor,
bp: breakpoint,
},
edit_action,
cx,
);
+11 -11
View File
@@ -18716,7 +18716,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18741,7 +18741,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18763,7 +18763,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18830,7 +18830,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18851,7 +18851,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18871,7 +18871,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18894,7 +18894,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18917,7 +18917,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -19010,7 +19010,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -19042,7 +19042,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -19078,7 +19078,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
+10 -8
View File
@@ -62,7 +62,7 @@ use multi_buffer::{
use project::{
ProjectPath,
debugger::breakpoint_store::Breakpoint,
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings},
};
use settings::Settings;
@@ -2317,7 +2317,7 @@ impl EditorElement {
gutter_hitbox: &Hitbox,
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
snapshot: &EditorSnapshot,
breakpoints: HashMap<DisplayRow, (Anchor, Breakpoint)>,
breakpoints: HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
row_infos: &[RowInfo],
window: &mut Window,
cx: &mut App,
@@ -2325,7 +2325,7 @@ impl EditorElement {
self.editor.update(cx, |editor, cx| {
breakpoints
.into_iter()
.filter_map(|(display_row, (text_anchor, bp))| {
.filter_map(|(display_row, (text_anchor, bp, state))| {
if row_infos
.get((display_row.0.saturating_sub(range.start.0)) as usize)
.is_some_and(|row_info| {
@@ -2348,7 +2348,7 @@ impl EditorElement {
return None;
}
let button = editor.render_breakpoint(text_anchor, display_row, &bp, cx);
let button = editor.render_breakpoint(text_anchor, display_row, &bp, state, cx);
let button = prepaint_gutter_button(
button,
@@ -2378,7 +2378,7 @@ impl EditorElement {
gutter_hitbox: &Hitbox,
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
snapshot: &EditorSnapshot,
breakpoints: &mut HashMap<DisplayRow, (Anchor, Breakpoint)>,
breakpoints: &mut HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
window: &mut Window,
cx: &mut App,
) -> Vec<AnyElement> {
@@ -7437,8 +7437,10 @@ impl Element for EditorElement {
editor.active_breakpoints(start_row..end_row, window, cx)
});
if cx.has_flag::<DebuggerFeatureFlag>() {
for display_row in breakpoint_rows.keys() {
active_rows.entry(*display_row).or_default().breakpoint = true;
for (display_row, (_, bp, state)) in &breakpoint_rows {
if bp.is_enabled() && state.is_none_or(|s| s.verified) {
active_rows.entry(*display_row).or_default().breakpoint = true;
}
}
}
@@ -7478,7 +7480,7 @@ impl Element for EditorElement {
let breakpoint = Breakpoint::new_standard();
phantom_breakpoint.collides_with_existing_breakpoint =
false;
(position, breakpoint)
(position, breakpoint, None)
});
}
})
+276 -97
View File
@@ -2,8 +2,9 @@
//!
//! Breakpoints are separate from a session because they're not associated with any particular debug session. They can also be set up without a session running.
use anyhow::{Result, anyhow};
use breakpoints_in_file::BreakpointsInFile;
use collections::BTreeMap;
pub use breakpoints_in_file::{BreakpointSessionState, BreakpointWithPosition};
use breakpoints_in_file::{BreakpointsInFile, StatefulBreakpoint};
use collections::{BTreeMap, HashMap};
use dap::{StackFrameId, client::SessionId};
use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Subscription, Task};
use itertools::Itertools;
@@ -14,21 +15,54 @@ use rpc::{
};
use std::{hash::Hash, ops::Range, path::Path, sync::Arc, u32};
use text::{Point, PointUtf16};
use util::maybe;
use crate::{Project, ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore};
use super::session::ThreadId;
mod breakpoints_in_file {
use collections::HashMap;
use language::{BufferEvent, DiskState};
use super::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BreakpointWithPosition {
pub position: text::Anchor,
pub bp: Breakpoint,
}
/// A breakpoint with per-session data about it's state (as seen by the Debug Adapter).
#[derive(Clone, Debug)]
pub struct StatefulBreakpoint {
pub bp: BreakpointWithPosition,
pub session_state: HashMap<SessionId, BreakpointSessionState>,
}
impl StatefulBreakpoint {
pub(super) fn new(bp: BreakpointWithPosition) -> Self {
Self {
bp,
session_state: Default::default(),
}
}
pub(super) fn position(&self) -> &text::Anchor {
&self.bp.position
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct BreakpointSessionState {
/// Session-specific identifier for the breakpoint, as assigned by Debug Adapter.
pub id: u64,
pub verified: bool,
}
#[derive(Clone)]
pub(super) struct BreakpointsInFile {
pub(super) buffer: Entity<Buffer>,
// TODO: This is.. less than ideal, as it's O(n) and does not return entries in order. We'll have to change TreeMap to support passing in the context for comparisons
pub(super) breakpoints: Vec<(text::Anchor, Breakpoint)>,
pub(super) breakpoints: Vec<StatefulBreakpoint>,
_subscription: Arc<Subscription>,
}
@@ -199,9 +233,26 @@ impl BreakpointStore {
.breakpoints
.into_iter()
.filter_map(|breakpoint| {
let anchor = language::proto::deserialize_anchor(breakpoint.position.clone()?)?;
let position =
language::proto::deserialize_anchor(breakpoint.position.clone()?)?;
let session_state = breakpoint
.session_state
.iter()
.map(|(session_id, state)| {
let state = BreakpointSessionState {
id: state.id,
verified: state.verified,
};
(SessionId::from_proto(*session_id), state)
})
.collect();
let breakpoint = Breakpoint::from_proto(breakpoint)?;
Some((anchor, breakpoint))
let bp = BreakpointWithPosition {
position,
bp: breakpoint,
};
Some(StatefulBreakpoint { bp, session_state })
})
.collect();
@@ -231,7 +282,7 @@ impl BreakpointStore {
.payload
.breakpoint
.ok_or_else(|| anyhow!("Breakpoint not present in RPC payload"))?;
let anchor = language::proto::deserialize_anchor(
let position = language::proto::deserialize_anchor(
breakpoint
.position
.clone()
@@ -244,7 +295,10 @@ impl BreakpointStore {
breakpoints.update(&mut cx, |this, cx| {
this.toggle_breakpoint(
buffer,
(anchor, breakpoint),
BreakpointWithPosition {
position,
bp: breakpoint,
},
BreakpointEditAction::Toggle,
cx,
);
@@ -261,13 +315,76 @@ impl BreakpointStore {
breakpoints: breakpoint_set
.breakpoints
.iter()
.filter_map(|(anchor, bp)| bp.to_proto(&path, anchor))
.filter_map(|breakpoint| {
breakpoint.bp.bp.to_proto(
&path,
&breakpoint.position(),
&breakpoint.session_state,
)
})
.collect(),
});
}
}
}
pub(crate) fn update_session_breakpoint(
&mut self,
session_id: SessionId,
_: dap::BreakpointEventReason,
breakpoint: dap::Breakpoint,
) {
maybe!({
let event_id = breakpoint.id?;
let state = self
.breakpoints
.values_mut()
.find_map(|breakpoints_in_file| {
breakpoints_in_file
.breakpoints
.iter_mut()
.find_map(|state| {
let state = state.session_state.get_mut(&session_id)?;
if state.id == event_id {
Some(state)
} else {
None
}
})
})?;
state.verified = breakpoint.verified;
Some(())
});
}
pub(super) fn mark_breakpoints_verified(
&mut self,
session_id: SessionId,
abs_path: &Path,
it: impl Iterator<Item = (BreakpointWithPosition, BreakpointSessionState)>,
) {
maybe!({
let breakpoints = self.breakpoints.get_mut(abs_path)?;
for (breakpoint, state) in it {
if let Some(to_update) = breakpoints
.breakpoints
.iter_mut()
.find(|bp| *bp.position() == breakpoint.position)
{
to_update
.session_state
.entry(session_id)
.insert_entry(state);
}
}
Some(())
});
}
pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
worktree::File::from_dyn(buffer.read(cx).file())
.and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok())
@@ -277,7 +394,7 @@ impl BreakpointStore {
pub fn toggle_breakpoint(
&mut self,
buffer: Entity<Buffer>,
mut breakpoint: (text::Anchor, Breakpoint),
mut breakpoint: BreakpointWithPosition,
edit_action: BreakpointEditAction,
cx: &mut Context<Self>,
) {
@@ -295,54 +412,57 @@ impl BreakpointStore {
let len_before = breakpoint_set.breakpoints.len();
breakpoint_set
.breakpoints
.retain(|value| &breakpoint != value);
.retain(|value| breakpoint != value.bp);
if len_before == breakpoint_set.breakpoints.len() {
// We did not remove any breakpoint, hence let's toggle one.
breakpoint_set.breakpoints.push(breakpoint.clone());
breakpoint_set
.breakpoints
.push(StatefulBreakpoint::new(breakpoint.clone()));
}
}
BreakpointEditAction::InvertState => {
if let Some((_, bp)) = breakpoint_set
if let Some(bp) = breakpoint_set
.breakpoints
.iter_mut()
.find(|value| breakpoint == **value)
.find(|value| breakpoint == value.bp)
{
let bp = &mut bp.bp.bp;
if bp.is_enabled() {
bp.state = BreakpointState::Disabled;
} else {
bp.state = BreakpointState::Enabled;
}
} else {
breakpoint.1.state = BreakpointState::Disabled;
breakpoint_set.breakpoints.push(breakpoint.clone());
breakpoint.bp.state = BreakpointState::Disabled;
breakpoint_set
.breakpoints
.push(StatefulBreakpoint::new(breakpoint.clone()));
}
}
BreakpointEditAction::EditLogMessage(log_message) => {
if !log_message.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
}
});
let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|bp| {
if breakpoint.position == *bp.position() {
Some(&mut bp.bp.bp)
} else {
None
}
});
if let Some(found_bp) = found_bp {
found_bp.message = Some(log_message.clone());
} else {
breakpoint.1.message = Some(log_message.clone());
breakpoint.bp.message = Some(log_message.clone());
// We did not remove any breakpoint, hence let's toggle one.
breakpoint_set.breakpoints.push(breakpoint.clone());
breakpoint_set
.breakpoints
.push(StatefulBreakpoint::new(breakpoint.clone()));
}
} else if breakpoint.1.message.is_some() {
} else if breakpoint.bp.message.is_some() {
if let Some(position) = breakpoint_set
.breakpoints
.iter()
.find_position(|(pos, bp)| &breakpoint.0 == pos && bp == &breakpoint.1)
.find_position(|other| breakpoint == other.bp)
.map(|res| res.0)
{
breakpoint_set.breakpoints.remove(position);
@@ -353,30 +473,28 @@ 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
}
});
let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| {
if breakpoint.position == *other.position() {
Some(&mut other.bp.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());
breakpoint.bp.hit_condition = Some(hit_condition.clone());
// We did not remove any breakpoint, hence let's toggle one.
breakpoint_set.breakpoints.push(breakpoint.clone());
breakpoint_set
.breakpoints
.push(StatefulBreakpoint::new(breakpoint.clone()))
}
} else if breakpoint.1.hit_condition.is_some() {
} else if breakpoint.bp.hit_condition.is_some() {
if let Some(position) = breakpoint_set
.breakpoints
.iter()
.find_position(|(pos, bp)| &breakpoint.0 == pos && bp == &breakpoint.1)
.find_position(|bp| breakpoint == bp.bp)
.map(|res| res.0)
{
breakpoint_set.breakpoints.remove(position);
@@ -387,30 +505,28 @@ impl BreakpointStore {
}
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
}
});
let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| {
if breakpoint.position == *other.position() {
Some(&mut other.bp.bp)
} else {
None
}
});
if let Some(found_bp) = found_bp {
found_bp.condition = Some(condition.clone());
} else {
breakpoint.1.condition = Some(condition.clone());
breakpoint.bp.condition = Some(condition.clone());
// We did not remove any breakpoint, hence let's toggle one.
breakpoint_set.breakpoints.push(breakpoint.clone());
breakpoint_set
.breakpoints
.push(StatefulBreakpoint::new(breakpoint.clone()));
}
} else if breakpoint.1.condition.is_some() {
} else if breakpoint.bp.condition.is_some() {
if let Some(position) = breakpoint_set
.breakpoints
.iter()
.find_position(|(pos, bp)| &breakpoint.0 == pos && bp == &breakpoint.1)
.find_position(|bp| breakpoint == bp.bp)
.map(|res| res.0)
{
breakpoint_set.breakpoints.remove(position);
@@ -425,7 +541,11 @@ impl BreakpointStore {
self.breakpoints.remove(&abs_path);
}
if let BreakpointStoreMode::Remote(remote) = &self.mode {
if let Some(breakpoint) = breakpoint.1.to_proto(&abs_path, &breakpoint.0) {
if let Some(breakpoint) =
breakpoint
.bp
.to_proto(&abs_path, &breakpoint.position, &HashMap::default())
{
cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint {
project_id: remote._upstream_project_id,
path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
@@ -441,7 +561,11 @@ impl BreakpointStore {
breakpoint_set
.breakpoints
.iter()
.filter_map(|(anchor, bp)| bp.to_proto(&abs_path, anchor))
.filter_map(|bp| {
bp.bp
.bp
.to_proto(&abs_path, bp.position(), &bp.session_state)
})
.collect()
})
.unwrap_or_default();
@@ -485,21 +609,31 @@ impl BreakpointStore {
range: Option<Range<text::Anchor>>,
buffer_snapshot: &'a BufferSnapshot,
cx: &App,
) -> impl Iterator<Item = &'a (text::Anchor, Breakpoint)> + 'a {
) -> impl Iterator<Item = (&'a BreakpointWithPosition, Option<BreakpointSessionState>)> + 'a
{
let abs_path = Self::abs_path_from_buffer(buffer, cx);
let active_session_id = self
.active_stack_frame
.as_ref()
.map(|frame| frame.session_id);
abs_path
.and_then(|path| self.breakpoints.get(&path))
.into_iter()
.flat_map(move |file_breakpoints| {
file_breakpoints.breakpoints.iter().filter({
file_breakpoints.breakpoints.iter().filter_map({
let range = range.clone();
move |(position, _)| {
move |bp| {
if let Some(range) = &range {
position.cmp(&range.start, buffer_snapshot).is_ge()
&& position.cmp(&range.end, buffer_snapshot).is_le()
} else {
true
if bp.position().cmp(&range.start, buffer_snapshot).is_lt()
|| bp.position().cmp(&range.end, buffer_snapshot).is_gt()
{
return None;
}
}
let session_state = active_session_id
.and_then(|id| bp.session_state.get(&id))
.copied();
Some((&bp.bp, session_state))
}
})
})
@@ -549,34 +683,46 @@ impl BreakpointStore {
path: &Path,
row: u32,
cx: &App,
) -> Option<(Entity<Buffer>, (text::Anchor, Breakpoint))> {
) -> Option<(Entity<Buffer>, BreakpointWithPosition)> {
self.breakpoints.get(path).and_then(|breakpoints| {
let snapshot = breakpoints.buffer.read(cx).text_snapshot();
breakpoints
.breakpoints
.iter()
.find(|(anchor, _)| anchor.summary::<Point>(&snapshot).row == row)
.map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.clone()))
.find(|bp| bp.position().summary::<Point>(&snapshot).row == row)
.map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.bp.clone()))
})
}
pub fn breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SourceBreakpoint> {
pub fn breakpoints_from_path(&self, path: &Arc<Path>) -> Vec<BreakpointWithPosition> {
self.breakpoints
.get(path)
.map(|bp| bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect())
.unwrap_or_default()
}
pub fn source_breakpoints_from_path(
&self,
path: &Arc<Path>,
cx: &App,
) -> Vec<SourceBreakpoint> {
self.breakpoints
.get(path)
.map(|bp| {
let snapshot = bp.buffer.read(cx).snapshot();
bp.breakpoints
.iter()
.map(|(position, breakpoint)| {
let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
.map(|bp| {
let position = snapshot.summary_for_anchor::<PointUtf16>(bp.position()).row;
let bp = &bp.bp;
SourceBreakpoint {
row: position,
path: path.clone(),
state: breakpoint.state,
message: breakpoint.message.clone(),
condition: breakpoint.condition.clone(),
hit_condition: breakpoint.hit_condition.clone(),
state: bp.bp.state,
message: bp.bp.message.clone(),
condition: bp.bp.condition.clone(),
hit_condition: bp.bp.hit_condition.clone(),
}
})
.collect()
@@ -584,7 +730,18 @@ impl BreakpointStore {
.unwrap_or_default()
}
pub fn all_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
pub fn all_breakpoints(&self) -> BTreeMap<Arc<Path>, Vec<BreakpointWithPosition>> {
self.breakpoints
.iter()
.map(|(path, bp)| {
(
path.clone(),
bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect(),
)
})
.collect()
}
pub fn all_source_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
self.breakpoints
.iter()
.map(|(path, bp)| {
@@ -593,15 +750,18 @@ impl BreakpointStore {
path.clone(),
bp.breakpoints
.iter()
.map(|(position, breakpoint)| {
let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
.map(|breakpoint| {
let position = snapshot
.summary_for_anchor::<PointUtf16>(&breakpoint.position())
.row;
let breakpoint = &breakpoint.bp;
SourceBreakpoint {
row: position,
path: path.clone(),
message: breakpoint.message.clone(),
state: breakpoint.state,
hit_condition: breakpoint.hit_condition.clone(),
condition: breakpoint.condition.clone(),
message: breakpoint.bp.message.clone(),
state: breakpoint.bp.state,
hit_condition: breakpoint.bp.hit_condition.clone(),
condition: breakpoint.bp.condition.clone(),
}
})
.collect(),
@@ -656,15 +816,17 @@ impl BreakpointStore {
continue;
}
let position = snapshot.anchor_after(point);
breakpoints_for_file.breakpoints.push((
position,
Breakpoint {
message: bp.message,
state: bp.state,
condition: bp.condition,
hit_condition: bp.hit_condition,
},
))
breakpoints_for_file
.breakpoints
.push(StatefulBreakpoint::new(BreakpointWithPosition {
position,
bp: Breakpoint {
message: bp.message,
state: bp.state,
condition: bp.condition,
hit_condition: bp.hit_condition,
},
}))
}
new_breakpoints.insert(path, breakpoints_for_file);
}
@@ -755,7 +917,7 @@ impl BreakpointState {
pub struct Breakpoint {
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 hit_condition: Option<Arc<str>>,
pub condition: Option<BreakpointMessage>,
pub state: BreakpointState,
}
@@ -788,7 +950,12 @@ impl Breakpoint {
}
}
fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
fn to_proto(
&self,
_path: &Path,
position: &text::Anchor,
session_states: &HashMap<SessionId, BreakpointSessionState>,
) -> Option<client::proto::Breakpoint> {
Some(client::proto::Breakpoint {
position: Some(serialize_text_anchor(position)),
state: match self.state {
@@ -801,6 +968,18 @@ impl Breakpoint {
.hit_condition
.as_ref()
.map(|s| String::from(s.as_ref())),
session_state: session_states
.iter()
.map(|(session_id, state)| {
(
session_id.to_proto(),
proto::BreakpointSessionState {
id: state.id,
verified: state.verified,
},
)
})
.collect(),
})
}
+87 -18
View File
@@ -1,3 +1,5 @@
use crate::debugger::breakpoint_store::BreakpointSessionState;
use super::breakpoint_store::{
BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint,
};
@@ -218,25 +220,55 @@ impl LocalMode {
breakpoint_store: &Entity<BreakpointStore>,
cx: &mut App,
) -> Task<()> {
let breakpoints = breakpoint_store
.read_with(cx, |store, cx| store.breakpoints_from_path(&abs_path, cx))
let breakpoints =
breakpoint_store
.read_with(cx, |store, cx| {
store.source_breakpoints_from_path(&abs_path, cx)
})
.into_iter()
.filter(|bp| bp.state.is_enabled())
.chain(self.tmp_breakpoint.iter().filter_map(|breakpoint| {
breakpoint.path.eq(&abs_path).then(|| breakpoint.clone())
}))
.map(Into::into)
.collect();
let raw_breakpoints = breakpoint_store
.read(cx)
.breakpoints_from_path(&abs_path)
.into_iter()
.filter(|bp| bp.state.is_enabled())
.chain(self.tmp_breakpoint.clone())
.map(Into::into)
.collect();
.filter(|bp| bp.bp.state.is_enabled())
.collect::<Vec<_>>();
let task = self.request(dap_command::SetBreakpoints {
source: client_source(&abs_path),
source_modified: Some(matches!(reason, BreakpointUpdatedReason::FileSaved)),
breakpoints,
});
cx.background_spawn(async move {
match task.await {
Ok(_) => {}
Err(err) => log::warn!("Set breakpoints request failed for path: {}", err),
let session_id = self.client.id();
let breakpoint_store = breakpoint_store.downgrade();
cx.spawn(async move |cx| match cx.background_spawn(task).await {
Ok(breakpoints) => {
let breakpoints =
breakpoints
.into_iter()
.zip(raw_breakpoints)
.filter_map(|(dap_bp, zed_bp)| {
Some((
zed_bp,
BreakpointSessionState {
id: dap_bp.id?,
verified: dap_bp.verified,
},
))
});
breakpoint_store
.update(cx, |this, _| {
this.mark_breakpoints_verified(session_id, &abs_path, breakpoints);
})
.ok();
}
Err(err) => log::warn!("Set breakpoints request failed for path: {}", err),
})
}
@@ -271,8 +303,11 @@ impl LocalMode {
cx: &App,
) -> Task<HashMap<Arc<Path>, anyhow::Error>> {
let mut breakpoint_tasks = Vec::new();
let breakpoints = breakpoint_store.read_with(cx, |store, cx| store.all_breakpoints(cx));
let breakpoints =
breakpoint_store.read_with(cx, |store, cx| store.all_source_breakpoints(cx));
let mut raw_breakpoints = breakpoint_store.read_with(cx, |this, _| this.all_breakpoints());
debug_assert_eq!(raw_breakpoints.len(), breakpoints.len());
let session_id = self.client.id();
for (path, breakpoints) in breakpoints {
let breakpoints = if ignore_breakpoints {
vec![]
@@ -284,14 +319,46 @@ impl LocalMode {
.collect()
};
breakpoint_tasks.push(
self.request(dap_command::SetBreakpoints {
let raw_breakpoints = raw_breakpoints
.remove(&path)
.unwrap_or_default()
.into_iter()
.filter(|bp| bp.bp.state.is_enabled());
let error_path = path.clone();
let send_request = self
.request(dap_command::SetBreakpoints {
source: client_source(&path),
source_modified: Some(false),
breakpoints,
})
.map(|result| result.map_err(|e| (path, e))),
);
.map(|result| result.map_err(move |e| (error_path, e)));
let task = cx.spawn({
let breakpoint_store = breakpoint_store.downgrade();
async move |cx| {
let breakpoints = cx.background_spawn(send_request).await?;
let breakpoints = breakpoints.into_iter().zip(raw_breakpoints).filter_map(
|(dap_bp, zed_bp)| {
Some((
zed_bp,
BreakpointSessionState {
id: dap_bp.id?,
verified: dap_bp.verified,
},
))
},
);
breakpoint_store
.update(cx, |this, _| {
this.mark_breakpoints_verified(session_id, &path, breakpoints);
})
.ok();
Ok(())
}
});
breakpoint_tasks.push(task);
}
cx.background_spawn(async move {
@@ -1204,7 +1271,9 @@ impl Session {
self.output_token.0 += 1;
cx.notify();
}
Events::Breakpoint(_) => {}
Events::Breakpoint(event) => self.breakpoint_store.update(cx, |store, _| {
store.update_session_breakpoint(self.session_id(), event.reason, event.breakpoint);
}),
Events::Module(event) => {
match event.reason {
dap::ModuleEventReason::New => {
+1
View File
@@ -47,6 +47,7 @@ use dap::{DapRegistry, client::DebugAdapterClient};
use collections::{BTreeSet, HashMap, HashSet};
use debounced_delay::DebouncedDelay;
pub use debugger::breakpoint_store::BreakpointWithPosition;
use debugger::{
breakpoint_store::{ActiveStackFrame, BreakpointStore},
dap_store::{DapStore, DapStoreEvent},
-1
View File
@@ -3,7 +3,6 @@ fn main() {
build
.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")
.type_attribute("ProjectPath", "#[derive(Hash, Eq)]")
.type_attribute("Breakpoint", "#[derive(Hash, Eq)]")
.type_attribute("Anchor", "#[derive(Hash, Eq)]")
.compile_protos(&["proto/zed.proto"], &["proto"])
.unwrap();
+6 -69
View File
@@ -16,6 +16,12 @@ message Breakpoint {
optional string message = 4;
optional string condition = 5;
optional string hit_condition = 6;
map<uint64, BreakpointSessionState> session_state = 7;
}
message BreakpointSessionState {
uint64 id = 1;
bool verified = 2;
}
message BreakpointsForFile {
@@ -30,63 +36,6 @@ message ToggleBreakpoint {
Breakpoint breakpoint = 3;
}
enum DebuggerThreadItem {
Console = 0;
LoadedSource = 1;
Modules = 2;
Variables = 3;
}
message DebuggerSetVariableState {
string name = 1;
DapScope scope = 2;
string value = 3;
uint64 stack_frame_id = 4;
optional string evaluate_name = 5;
uint64 parent_variables_reference = 6;
}
message VariableListOpenEntry {
oneof entry {
DebuggerOpenEntryScope scope = 1;
DebuggerOpenEntryVariable variable = 2;
}
}
message DebuggerOpenEntryScope {
string name = 1;
}
message DebuggerOpenEntryVariable {
string scope_name = 1;
string name = 2;
uint64 depth = 3;
}
message VariableListEntrySetState {
uint64 depth = 1;
DebuggerSetVariableState state = 2;
}
message VariableListEntryVariable {
uint64 depth = 1;
DapScope scope = 2;
DapVariable variable = 3;
bool has_children = 4;
uint64 container_reference = 5;
}
message DebuggerScopeVariableIndex {
repeated uint64 fetched_ids = 1;
repeated DebuggerVariableContainer variables = 2;
}
message DebuggerVariableContainer {
uint64 container_reference = 1;
DapVariable variable = 2;
uint64 depth = 3;
}
enum DapThreadStatus {
Running = 0;
Stopped = 1;
@@ -94,18 +43,6 @@ enum DapThreadStatus {
Ended = 3;
}
message VariableListScopes {
uint64 stack_frame_id = 1;
repeated DapScope scopes = 2;
}
message VariableListVariables {
uint64 stack_frame_id = 1;
uint64 scope_id = 2;
DebuggerScopeVariableIndex variables = 3;
}
enum VariablesArgumentsFilter {
Indexed = 0;
Named = 1;
+4 -1
View File
@@ -4999,7 +4999,10 @@ impl Workspace {
if let Some(location) = self.serialize_workspace_location(cx) {
let breakpoints = self.project.update(cx, |project, cx| {
project.breakpoint_store().read(cx).all_breakpoints(cx)
project
.breakpoint_store()
.read(cx)
.all_source_breakpoints(cx)
});
let center_group = build_serialized_pane_group(&self.center.root, window, cx);