diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index ef40720fc9..c69f428148 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -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() }); diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 8b56351796..5775de540e 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -148,7 +148,7 @@ impl Render for BreakpointList { cx: &mut ui::Context, ) -> 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)| { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 696992b6b9..0ad5a04daa 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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, window: &mut Window, cx: &mut Context, - ) -> HashMap { + ) -> HashMap)> { 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, cx: &mut Context, ) -> 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)>, cx: &mut Context, ) -> 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::(anchor) + .summary_for_anchor::(&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, ); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b00597cc8c..ee0776c97b 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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() }); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index cca91c2df0..ccd75ae988 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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)], snapshot: &EditorSnapshot, - breakpoints: HashMap, + breakpoints: HashMap)>, 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)], snapshot: &EditorSnapshot, - breakpoints: &mut HashMap, + breakpoints: &mut HashMap)>, window: &mut Window, cx: &mut App, ) -> Vec { @@ -7437,8 +7437,10 @@ impl Element for EditorElement { editor.active_breakpoints(start_row..end_row, window, cx) }); if cx.has_flag::() { - 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) }); } }) diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index cf1f8271f0..7f15ea68e5 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -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, + } + + 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, // 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, _subscription: Arc, } @@ -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, + ) { + 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, cx: &App) -> Option> { 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, - mut breakpoint: (text::Anchor, Breakpoint), + mut breakpoint: BreakpointWithPosition, edit_action: BreakpointEditAction, cx: &mut Context, ) { @@ -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>, buffer_snapshot: &'a BufferSnapshot, cx: &App, - ) -> impl Iterator + 'a { + ) -> impl Iterator)> + '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, (text::Anchor, Breakpoint))> { + ) -> Option<(Entity, BreakpointWithPosition)> { self.breakpoints.get(path).and_then(|breakpoints| { let snapshot = breakpoints.buffer.read(cx).text_snapshot(); breakpoints .breakpoints .iter() - .find(|(anchor, _)| anchor.summary::(&snapshot).row == row) - .map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.clone())) + .find(|bp| bp.position().summary::(&snapshot).row == row) + .map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.bp.clone())) }) } - pub fn breakpoints_from_path(&self, path: &Arc, cx: &App) -> Vec { + pub fn breakpoints_from_path(&self, path: &Arc) -> Vec { + 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, + cx: &App, + ) -> Vec { 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::(position).row; + .map(|bp| { + let position = snapshot.summary_for_anchor::(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, Vec> { + pub fn all_breakpoints(&self) -> BTreeMap, Vec> { + 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, Vec> { 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::(position).row; + .map(|breakpoint| { + let position = snapshot + .summary_for_anchor::(&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, /// 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, + pub hit_condition: Option>, pub condition: Option, pub state: BreakpointState, } @@ -788,7 +950,12 @@ impl Breakpoint { } } - fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option { + fn to_proto( + &self, + _path: &Path, + position: &text::Anchor, + session_states: &HashMap, + ) -> Option { 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(), }) } diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 3c2eeabe4e..d866e3ad95 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -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, 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::>(); 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, 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 => { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4dc6be97f8..16bcd3704c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -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}, diff --git a/crates/proto/build.rs b/crates/proto/build.rs index b16aad1b69..2997e302b6 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -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(); diff --git a/crates/proto/proto/debugger.proto b/crates/proto/proto/debugger.proto index ad98053c05..60434383eb 100644 --- a/crates/proto/proto/debugger.proto +++ b/crates/proto/proto/debugger.proto @@ -16,6 +16,12 @@ message Breakpoint { optional string message = 4; optional string condition = 5; optional string hit_condition = 6; + map 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; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 069e750593..4457e65418 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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);