debugger: Surface validity of breakpoints (#30380)

Piotr Osiewicz , 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 , and CharlesChen0823 created

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>

Change summary

crates/collab/src/tests/editor_tests.rs                   |  16 
crates/debugger_ui/src/session/running/breakpoint_list.rs |   2 
crates/editor/src/editor.rs                               |  58 
crates/editor/src/editor_tests.rs                         |  22 
crates/editor/src/element.rs                              |  18 
crates/project/src/debugger/breakpoint_store.rs           | 373 ++++++--
crates/project/src/debugger/session.rs                    | 105 ++
crates/project/src/project.rs                             |   1 
crates/proto/build.rs                                     |   1 
crates/proto/proto/debugger.proto                         |  75 -
crates/workspace/src/workspace.rs                         |   5 
11 files changed, 439 insertions(+), 237 deletions(-)

Detailed changes

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

crates/debugger_ui/src/session/running/breakpoint_list.rs 🔗

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

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

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

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<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)
                                     });
                             }
                         })

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

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

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

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();

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

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