debugger: A support for data breakpoint's on variables (#34391)

Anthony Eid , Piotr Osiewicz , Mikayla Maki , and Mikayla Maki created

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>

Change summary

crates/debugger_ui/src/debugger_ui.rs                     |   2 
crates/debugger_ui/src/session/running/breakpoint_list.rs | 177 +++++++
crates/debugger_ui/src/session/running/memory_view.rs     |  79 +++
crates/debugger_ui/src/session/running/variable_list.rs   | 189 +++++++-
crates/project/src/debugger/dap_command.rs                | 148 ++++++-
crates/project/src/debugger/session.rs                    |  82 +++
6 files changed, 589 insertions(+), 88 deletions(-)

Detailed changes

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -83,6 +83,8 @@ actions!(
         Rerun,
         /// Toggles expansion of the selected item in the debugger UI.
         ToggleExpandItem,
+        /// Set a data breakpoint on the selected variable or memory region.
+        ToggleDataBreakpoint,
     ]
 );
 

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

@@ -24,10 +24,10 @@ use project::{
 };
 use ui::{
     ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div,
-    Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, Indicator,
-    InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement,
-    Render, RenderOnce, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
-    Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
+    Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, InteractiveElement,
+    IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
+    Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Toggleable,
+    Tooltip, Window, div, h_flex, px, v_flex,
 };
 use util::ResultExt;
 use workspace::Workspace;
@@ -46,6 +46,7 @@ actions!(
 pub(crate) enum SelectedBreakpointKind {
     Source,
     Exception,
+    Data,
 }
 pub(crate) struct BreakpointList {
     workspace: WeakEntity<Workspace>,
@@ -188,6 +189,9 @@ impl BreakpointList {
                 BreakpointEntryKind::ExceptionBreakpoint(bp) => {
                     (SelectedBreakpointKind::Exception, bp.is_enabled)
                 }
+                BreakpointEntryKind::DataBreakpoint(bp) => {
+                    (SelectedBreakpointKind::Data, bp.0.is_enabled)
+                }
             })
         })
     }
@@ -391,7 +395,8 @@ impl BreakpointList {
                 let row = line_breakpoint.breakpoint.row;
                 self.go_to_line_breakpoint(path, row, window, cx);
             }
-            BreakpointEntryKind::ExceptionBreakpoint(_) => {}
+            BreakpointEntryKind::DataBreakpoint(_)
+            | BreakpointEntryKind::ExceptionBreakpoint(_) => {}
         }
     }
 
@@ -421,6 +426,10 @@ impl BreakpointList {
                 let id = exception_breakpoint.id.clone();
                 self.toggle_exception_breakpoint(&id, cx);
             }
+            BreakpointEntryKind::DataBreakpoint(data_breakpoint) => {
+                let id = data_breakpoint.0.dap.data_id.clone();
+                self.toggle_data_breakpoint(&id, cx);
+            }
         }
         cx.notify();
     }
@@ -441,7 +450,7 @@ impl BreakpointList {
                 let row = line_breakpoint.breakpoint.row;
                 self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
             }
-            BreakpointEntryKind::ExceptionBreakpoint(_) => {}
+            _ => {}
         }
         cx.notify();
     }
@@ -490,6 +499,14 @@ impl BreakpointList {
         cx.notify();
     }
 
+    fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
+        if let Some(session) = &self.session {
+            session.update(cx, |this, cx| {
+                this.toggle_data_breakpoint(&id, cx);
+            });
+        }
+    }
+
     fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
         if let Some(session) = &self.session {
             session.update(cx, |this, cx| {
@@ -642,6 +659,7 @@ impl BreakpointList {
             SelectedBreakpointKind::Exception => {
                 "Exception Breakpoints cannot be removed from the breakpoint list"
             }
+            SelectedBreakpointKind::Data => "Remove data breakpoint from a breakpoint list",
         });
         let toggle_label = selection_kind.map(|(_, is_enabled)| {
             if is_enabled {
@@ -783,8 +801,20 @@ impl Render for BreakpointList {
                     weak: weak.clone(),
                 })
         });
-        self.breakpoints
-            .extend(breakpoints.chain(exception_breakpoints));
+        let data_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
+            session
+                .read(cx)
+                .data_breakpoints()
+                .map(|state| BreakpointEntry {
+                    kind: BreakpointEntryKind::DataBreakpoint(DataBreakpoint(state.clone())),
+                    weak: weak.clone(),
+                })
+        });
+        self.breakpoints.extend(
+            breakpoints
+                .chain(data_breakpoints)
+                .chain(exception_breakpoints),
+        );
         v_flex()
             .id("breakpoint-list")
             .key_context("BreakpointList")
@@ -905,7 +935,11 @@ impl LineBreakpoint {
                     .ok();
                 }
             })
-            .child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
+            .child(
+                Icon::new(icon_name)
+                    .color(Color::Debugger)
+                    .size(IconSize::XSmall),
+            )
             .on_mouse_down(MouseButton::Left, move |_, _, _| {});
 
         ListItem::new(SharedString::from(format!(
@@ -996,6 +1030,103 @@ struct ExceptionBreakpoint {
     data: ExceptionBreakpointsFilter,
     is_enabled: bool,
 }
+#[derive(Clone, Debug)]
+struct DataBreakpoint(project::debugger::session::DataBreakpointState);
+
+impl DataBreakpoint {
+    fn render(
+        &self,
+        props: SupportedBreakpointProperties,
+        strip_mode: Option<ActiveBreakpointStripMode>,
+        ix: usize,
+        is_selected: bool,
+        focus_handle: FocusHandle,
+        list: WeakEntity<BreakpointList>,
+    ) -> ListItem {
+        let color = if self.0.is_enabled {
+            Color::Debugger
+        } else {
+            Color::Muted
+        };
+        let is_enabled = self.0.is_enabled;
+        let id = self.0.dap.data_id.clone();
+        ListItem::new(SharedString::from(format!(
+            "data-breakpoint-ui-item-{}",
+            self.0.dap.data_id
+        )))
+        .rounded()
+        .start_slot(
+            div()
+                .id(SharedString::from(format!(
+                    "data-breakpoint-ui-item-{}-click-handler",
+                    self.0.dap.data_id
+                )))
+                .tooltip({
+                    let focus_handle = focus_handle.clone();
+                    move |window, cx| {
+                        Tooltip::for_action_in(
+                            if is_enabled {
+                                "Disable Data Breakpoint"
+                            } else {
+                                "Enable Data Breakpoint"
+                            },
+                            &ToggleEnableBreakpoint,
+                            &focus_handle,
+                            window,
+                            cx,
+                        )
+                    }
+                })
+                .on_click({
+                    let list = list.clone();
+                    move |_, _, cx| {
+                        list.update(cx, |this, cx| {
+                            this.toggle_data_breakpoint(&id, cx);
+                        })
+                        .ok();
+                    }
+                })
+                .cursor_pointer()
+                .child(
+                    Icon::new(IconName::Binary)
+                        .color(color)
+                        .size(IconSize::Small),
+                ),
+        )
+        .child(
+            h_flex()
+                .w_full()
+                .mr_4()
+                .py_0p5()
+                .justify_between()
+                .child(
+                    v_flex()
+                        .py_1()
+                        .gap_1()
+                        .min_h(px(26.))
+                        .justify_center()
+                        .id(("data-breakpoint-label", ix))
+                        .child(
+                            Label::new(self.0.context.human_readable_label())
+                                .size(LabelSize::Small)
+                                .line_height_style(ui::LineHeightStyle::UiLabel),
+                        ),
+                )
+                .child(BreakpointOptionsStrip {
+                    props,
+                    breakpoint: BreakpointEntry {
+                        kind: BreakpointEntryKind::DataBreakpoint(self.clone()),
+                        weak: list,
+                    },
+                    is_selected,
+                    focus_handle,
+                    strip_mode,
+                    index: ix,
+                }),
+        )
+        .toggle_state(is_selected)
+    }
+}
 
 impl ExceptionBreakpoint {
     fn render(
@@ -1062,7 +1193,11 @@ impl ExceptionBreakpoint {
                     }
                 })
                 .cursor_pointer()
-                .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
+                .child(
+                    Icon::new(IconName::Flame)
+                        .color(color)
+                        .size(IconSize::Small),
+                ),
         )
         .child(
             h_flex()
@@ -1105,6 +1240,7 @@ impl ExceptionBreakpoint {
 enum BreakpointEntryKind {
     LineBreakpoint(LineBreakpoint),
     ExceptionBreakpoint(ExceptionBreakpoint),
+    DataBreakpoint(DataBreakpoint),
 }
 
 #[derive(Clone, Debug)]
@@ -1140,6 +1276,14 @@ impl BreakpointEntry {
                     focus_handle,
                     self.weak.clone(),
                 ),
+            BreakpointEntryKind::DataBreakpoint(data_breakpoint) => data_breakpoint.render(
+                props.for_data_breakpoints(),
+                strip_mode,
+                ix,
+                is_selected,
+                focus_handle,
+                self.weak.clone(),
+            ),
         }
     }
 
@@ -1155,6 +1299,11 @@ impl BreakpointEntry {
                 exception_breakpoint.id
             )
             .into(),
+            BreakpointEntryKind::DataBreakpoint(data_breakpoint) => format!(
+                "data-breakpoint-control-strip--{}",
+                data_breakpoint.0.dap.data_id
+            )
+            .into(),
         }
     }
 
@@ -1172,8 +1321,8 @@ impl BreakpointEntry {
             BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
                 line_breakpoint.breakpoint.condition.is_some()
             }
-            // We don't support conditions on exception breakpoints
-            BreakpointEntryKind::ExceptionBreakpoint(_) => false,
+            // We don't support conditions on exception/data breakpoints
+            _ => false,
         }
     }
 
@@ -1225,6 +1374,10 @@ impl SupportedBreakpointProperties {
         // TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
         Self::empty()
     }
+    fn for_data_breakpoints(self) -> Self {
+        // TODO: we don't yet support conditions for data breakpoints at the data layer, hence all props are disabled here.
+        Self::empty()
+    }
 }
 #[derive(IntoElement)]
 struct BreakpointOptionsStrip {

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

@@ -1,4 +1,10 @@
-use std::{fmt::Write, ops::RangeInclusive, sync::LazyLock, time::Duration};
+use std::{
+    cell::LazyCell,
+    fmt::Write,
+    ops::RangeInclusive,
+    sync::{Arc, LazyLock},
+    time::Duration,
+};
 
 use editor::{Editor, EditorElement, EditorStyle};
 use gpui::{
@@ -8,7 +14,7 @@ use gpui::{
     deferred, point, size, uniform_list,
 };
 use notifications::status_toast::{StatusToast, ToastIcon};
-use project::debugger::{MemoryCell, session::Session};
+use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session::Session};
 use settings::Settings;
 use theme::ThemeSettings;
 use ui::{
@@ -20,7 +26,7 @@ use ui::{
 use util::ResultExt;
 use workspace::Workspace;
 
-use crate::session::running::stack_frame_list::StackFrameList;
+use crate::{ToggleDataBreakpoint, session::running::stack_frame_list::StackFrameList};
 
 actions!(debugger, [GoToSelectedAddress]);
 
@@ -446,6 +452,48 @@ impl MemoryView {
         }
     }
 
+    fn toggle_data_breakpoint(
+        &mut self,
+        _: &crate::ToggleDataBreakpoint,
+        _: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(SelectedMemoryRange::DragComplete(selection)) = self.view_state.selection.clone()
+        else {
+            return;
+        };
+        let range = selection.memory_range();
+        let context = Arc::new(DataBreakpointContext::Address {
+            address: range.start().to_string(),
+            bytes: Some(*range.end() - *range.start()),
+        });
+
+        self.session.update(cx, |this, cx| {
+            let data_breakpoint_info = this.data_breakpoint_info(context.clone(), None, cx);
+            cx.spawn(async move |this, cx| {
+                if let Some(info) = data_breakpoint_info.await {
+                    let Some(data_id) = info.data_id.clone() else {
+                        return;
+                    };
+                    _ = this.update(cx, |this, cx| {
+                        this.create_data_breakpoint(
+                            context,
+                            data_id.clone(),
+                            dap::DataBreakpoint {
+                                data_id,
+                                access_type: None,
+                                condition: None,
+                                hit_condition: None,
+                            },
+                            cx,
+                        );
+                    });
+                }
+            })
+            .detach();
+        })
+    }
+
     fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
         if let Some(SelectedMemoryRange::DragComplete(drag)) = &self.view_state.selection {
             // Go into memory writing mode.
@@ -599,18 +647,30 @@ impl MemoryView {
         let session = self.session.clone();
         let context_menu = ContextMenu::build(window, cx, |menu, _, cx| {
             let range_too_large = range.end() - range.start() > std::mem::size_of::<u64>() as u64;
-            let memory_unreadable = |cx| {
+            let caps = session.read(cx).capabilities();
+            let supports_data_breakpoints = caps.supports_data_breakpoints.unwrap_or_default()
+                && caps.supports_data_breakpoint_bytes.unwrap_or_default();
+            let memory_unreadable = LazyCell::new(|| {
                 session.update(cx, |this, cx| {
                     this.read_memory(range.clone(), cx)
                         .any(|cell| cell.0.is_none())
                 })
-            };
-            menu.action_disabled_when(
-                range_too_large || memory_unreadable(cx),
+            });
+
+            let mut menu = menu.action_disabled_when(
+                range_too_large || *memory_unreadable,
                 "Go To Selected Address",
                 GoToSelectedAddress.boxed_clone(),
-            )
-            .context(self.focus_handle.clone())
+            );
+
+            if supports_data_breakpoints {
+                menu = menu.action_disabled_when(
+                    *memory_unreadable,
+                    "Set Data Breakpoint",
+                    ToggleDataBreakpoint.boxed_clone(),
+                );
+            }
+            menu.context(self.focus_handle.clone())
         });
 
         cx.focus_view(&context_menu, window);
@@ -834,6 +894,7 @@ impl Render for MemoryView {
             .on_action(cx.listener(Self::go_to_address))
             .p_1()
             .on_action(cx.listener(Self::confirm))
+            .on_action(cx.listener(Self::toggle_data_breakpoint))
             .on_action(cx.listener(Self::page_down))
             .on_action(cx.listener(Self::page_up))
             .size_full()

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

@@ -13,7 +13,10 @@ use gpui::{
     uniform_list,
 };
 use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
-use project::debugger::session::{Session, SessionEvent, Watcher};
+use project::debugger::{
+    dap_command::DataBreakpointContext,
+    session::{Session, SessionEvent, Watcher},
+};
 use std::{collections::HashMap, ops::Range, sync::Arc};
 use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*};
 use util::{debug_panic, maybe};
@@ -220,6 +223,7 @@ impl VariableList {
                 SessionEvent::Variables | SessionEvent::Watchers => {
                     this.build_entries(cx);
                 }
+
                 _ => {}
             }),
             cx.on_focus_out(&focus_handle, window, |this, _, _, cx| {
@@ -625,50 +629,156 @@ impl VariableList {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let supports_set_variable = self
-            .session
-            .read(cx)
-            .capabilities()
-            .supports_set_variable
-            .unwrap_or_default();
-
-        let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
-            menu.when(entry.as_variable().is_some(), |menu| {
-                menu.action("Copy Name", CopyVariableName.boxed_clone())
-                    .action("Copy Value", CopyVariableValue.boxed_clone())
-                    .when(supports_set_variable, |menu| {
-                        menu.action("Edit Value", EditVariable.boxed_clone())
+        let (supports_set_variable, supports_data_breakpoints, supports_go_to_memory) =
+            self.session.read_with(cx, |session, _| {
+                (
+                    session
+                        .capabilities()
+                        .supports_set_variable
+                        .unwrap_or_default(),
+                    session
+                        .capabilities()
+                        .supports_data_breakpoints
+                        .unwrap_or_default(),
+                    session
+                        .capabilities()
+                        .supports_read_memory_request
+                        .unwrap_or_default(),
+                )
+            });
+        let can_toggle_data_breakpoint = entry
+            .as_variable()
+            .filter(|_| supports_data_breakpoints)
+            .and_then(|variable| {
+                let variables_reference = self
+                    .entry_states
+                    .get(&entry.path)
+                    .map(|state| state.parent_reference)?;
+                Some(self.session.update(cx, |session, cx| {
+                    session.data_breakpoint_info(
+                        Arc::new(DataBreakpointContext::Variable {
+                            variables_reference,
+                            name: variable.name.clone(),
+                            bytes: None,
+                        }),
+                        None,
+                        cx,
+                    )
+                }))
+            });
+
+        let focus_handle = self.focus_handle.clone();
+        cx.spawn_in(window, async move |this, cx| {
+            let can_toggle_data_breakpoint = if let Some(task) = can_toggle_data_breakpoint {
+                task.await.is_some()
+            } else {
+                true
+            };
+            cx.update(|window, cx| {
+                let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
+                    menu.when_some(entry.as_variable(), |menu, _| {
+                        menu.action("Copy Name", CopyVariableName.boxed_clone())
+                            .action("Copy Value", CopyVariableValue.boxed_clone())
+                            .when(supports_set_variable, |menu| {
+                                menu.action("Edit Value", EditVariable.boxed_clone())
+                            })
+                            .when(supports_go_to_memory, |menu| {
+                                menu.action("Go To Memory", GoToMemory.boxed_clone())
+                            })
+                            .action("Watch Variable", AddWatch.boxed_clone())
+                            .when(can_toggle_data_breakpoint, |menu| {
+                                menu.action(
+                                    "Toggle Data Breakpoint",
+                                    crate::ToggleDataBreakpoint.boxed_clone(),
+                                )
+                            })
                     })
-                    .action("Watch Variable", AddWatch.boxed_clone())
-                    .action("Go To Memory", GoToMemory.boxed_clone())
-            })
-            .when(entry.as_watcher().is_some(), |menu| {
-                menu.action("Copy Name", CopyVariableName.boxed_clone())
-                    .action("Copy Value", CopyVariableValue.boxed_clone())
-                    .when(supports_set_variable, |menu| {
-                        menu.action("Edit Value", EditVariable.boxed_clone())
+                    .when(entry.as_watcher().is_some(), |menu| {
+                        menu.action("Copy Name", CopyVariableName.boxed_clone())
+                            .action("Copy Value", CopyVariableValue.boxed_clone())
+                            .when(supports_set_variable, |menu| {
+                                menu.action("Edit Value", EditVariable.boxed_clone())
+                            })
+                            .action("Remove Watch", RemoveWatch.boxed_clone())
                     })
-                    .action("Remove Watch", RemoveWatch.boxed_clone())
+                    .context(focus_handle.clone())
+                });
+
+                _ = this.update(cx, |this, cx| {
+                    cx.focus_view(&context_menu, window);
+                    let subscription = cx.subscribe_in(
+                        &context_menu,
+                        window,
+                        |this, _, _: &DismissEvent, window, cx| {
+                            if this.open_context_menu.as_ref().is_some_and(|context_menu| {
+                                context_menu.0.focus_handle(cx).contains_focused(window, cx)
+                            }) {
+                                cx.focus_self(window);
+                            }
+                            this.open_context_menu.take();
+                            cx.notify();
+                        },
+                    );
+
+                    this.open_context_menu = Some((context_menu, position, subscription));
+                });
             })
-            .context(self.focus_handle.clone())
+        })
+        .detach();
+    }
+
+    fn toggle_data_breakpoint(
+        &mut self,
+        _: &crate::ToggleDataBreakpoint,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(entry) = self
+            .selection
+            .as_ref()
+            .and_then(|selection| self.entries.iter().find(|entry| &entry.path == selection))
+        else {
+            return;
+        };
+
+        let Some((name, var_ref)) = entry.as_variable().map(|var| &var.name).zip(
+            self.entry_states
+                .get(&entry.path)
+                .map(|state| state.parent_reference),
+        ) else {
+            return;
+        };
+
+        let context = Arc::new(DataBreakpointContext::Variable {
+            variables_reference: var_ref,
+            name: name.clone(),
+            bytes: None,
+        });
+        let data_breakpoint = self.session.update(cx, |session, cx| {
+            session.data_breakpoint_info(context.clone(), None, cx)
         });
 
-        cx.focus_view(&context_menu, window);
-        let subscription = cx.subscribe_in(
-            &context_menu,
-            window,
-            |this, _, _: &DismissEvent, window, cx| {
-                if this.open_context_menu.as_ref().is_some_and(|context_menu| {
-                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
-                }) {
-                    cx.focus_self(window);
-                }
-                this.open_context_menu.take();
+        let session = self.session.downgrade();
+        cx.spawn(async move |_, cx| {
+            let Some(data_id) = data_breakpoint.await.and_then(|info| info.data_id) else {
+                return;
+            };
+            _ = session.update(cx, |session, cx| {
+                session.create_data_breakpoint(
+                    context,
+                    data_id.clone(),
+                    dap::DataBreakpoint {
+                        data_id,
+                        access_type: None,
+                        condition: None,
+                        hit_condition: None,
+                    },
+                    cx,
+                );
                 cx.notify();
-            },
-        );
-
-        self.open_context_menu = Some((context_menu, position, subscription));
+            });
+        })
+        .detach();
     }
 
     fn copy_variable_name(
@@ -1415,6 +1525,7 @@ impl Render for VariableList {
             .on_action(cx.listener(Self::edit_variable))
             .on_action(cx.listener(Self::add_watcher))
             .on_action(cx.listener(Self::remove_watcher))
+            .on_action(cx.listener(Self::toggle_data_breakpoint))
             .on_action(cx.listener(Self::jump_to_variable_memory))
             .child(
                 uniform_list(

crates/project/src/debugger/dap_command.rs 🔗

@@ -11,6 +11,7 @@ use dap::{
     proto_conversions::ProtoConversion,
     requests::{Continue, Next},
 };
+
 use rpc::proto;
 use serde_json::Value;
 use util::ResultExt;
@@ -813,7 +814,7 @@ impl DapCommand for RestartCommand {
     }
 }
 
-#[derive(Debug, Hash, PartialEq, Eq)]
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
 pub struct VariablesCommand {
     pub variables_reference: u64,
     pub filter: Option<VariablesArgumentsFilter>,
@@ -1667,6 +1668,130 @@ impl LocalDapCommand for SetBreakpoints {
         Ok(message.breakpoints)
     }
 }
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub enum DataBreakpointContext {
+    Variable {
+        variables_reference: u64,
+        name: String,
+        bytes: Option<u64>,
+    },
+    Expression {
+        expression: String,
+        frame_id: Option<u64>,
+    },
+    Address {
+        address: String,
+        bytes: Option<u64>,
+    },
+}
+
+impl DataBreakpointContext {
+    pub fn human_readable_label(&self) -> String {
+        match self {
+            DataBreakpointContext::Variable { name, .. } => format!("Variable: {}", name),
+            DataBreakpointContext::Expression { expression, .. } => {
+                format!("Expression: {}", expression)
+            }
+            DataBreakpointContext::Address { address, bytes } => {
+                let mut label = format!("Address: {}", address);
+                if let Some(bytes) = bytes {
+                    label.push_str(&format!(
+                        " ({} byte{})",
+                        bytes,
+                        if *bytes == 1 { "" } else { "s" }
+                    ));
+                }
+                label
+            }
+        }
+    }
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub(crate) struct DataBreakpointInfoCommand {
+    pub context: Arc<DataBreakpointContext>,
+    pub mode: Option<String>,
+}
+
+impl LocalDapCommand for DataBreakpointInfoCommand {
+    type Response = dap::DataBreakpointInfoResponse;
+    type DapRequest = dap::requests::DataBreakpointInfo;
+    const CACHEABLE: bool = true;
+
+    // todo(debugger): We should expand this trait in the future to take a &self
+    // Depending on this command is_supported could be differentb
+    fn is_supported(capabilities: &Capabilities) -> bool {
+        capabilities.supports_data_breakpoints.unwrap_or(false)
+    }
+
+    fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments {
+        let (variables_reference, name, frame_id, as_address, bytes) = match &*self.context {
+            DataBreakpointContext::Variable {
+                variables_reference,
+                name,
+                bytes,
+            } => (
+                Some(*variables_reference),
+                name.clone(),
+                None,
+                Some(false),
+                *bytes,
+            ),
+            DataBreakpointContext::Expression {
+                expression,
+                frame_id,
+            } => (None, expression.clone(), *frame_id, Some(false), None),
+            DataBreakpointContext::Address { address, bytes } => {
+                (None, address.clone(), None, Some(true), *bytes)
+            }
+        };
+
+        dap::DataBreakpointInfoArguments {
+            variables_reference,
+            name,
+            frame_id,
+            bytes,
+            as_address,
+            mode: self.mode.clone(),
+        }
+    }
+
+    fn response_from_dap(
+        &self,
+        message: <Self::DapRequest as dap::requests::Request>::Response,
+    ) -> Result<Self::Response> {
+        Ok(message)
+    }
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub(crate) struct SetDataBreakpointsCommand {
+    pub breakpoints: Vec<dap::DataBreakpoint>,
+}
+
+impl LocalDapCommand for SetDataBreakpointsCommand {
+    type Response = Vec<dap::Breakpoint>;
+    type DapRequest = dap::requests::SetDataBreakpoints;
+
+    fn is_supported(capabilities: &Capabilities) -> bool {
+        capabilities.supports_data_breakpoints.unwrap_or(false)
+    }
+
+    fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments {
+        dap::SetDataBreakpointsArguments {
+            breakpoints: self.breakpoints.clone(),
+        }
+    }
+
+    fn response_from_dap(
+        &self,
+        message: <Self::DapRequest as dap::requests::Request>::Response,
+    ) -> Result<Self::Response> {
+        Ok(message.breakpoints)
+    }
+}
+
 #[derive(Clone, Debug, Hash, PartialEq)]
 pub(super) enum SetExceptionBreakpoints {
     Plain {
@@ -1776,7 +1901,7 @@ impl DapCommand for LocationsCommand {
     }
 }
 
-#[derive(Debug, Hash, PartialEq, Eq)]
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
 pub(crate) struct ReadMemory {
     pub(crate) memory_reference: String,
     pub(crate) offset: Option<u64>,
@@ -1829,25 +1954,6 @@ impl LocalDapCommand for ReadMemory {
     }
 }
 
-impl LocalDapCommand for dap::DataBreakpointInfoArguments {
-    type Response = dap::DataBreakpointInfoResponse;
-    type DapRequest = dap::requests::DataBreakpointInfo;
-    const CACHEABLE: bool = true;
-    fn is_supported(capabilities: &Capabilities) -> bool {
-        capabilities.supports_data_breakpoints.unwrap_or_default()
-    }
-    fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments {
-        self.clone()
-    }
-
-    fn response_from_dap(
-        &self,
-        message: <Self::DapRequest as dap::requests::Request>::Response,
-    ) -> Result<Self::Response> {
-        Ok(message)
-    }
-}
-
 impl LocalDapCommand for dap::WriteMemoryArguments {
     type Response = dap::WriteMemoryResponse;
     type DapRequest = dap::requests::WriteMemory;

crates/project/src/debugger/session.rs 🔗

@@ -1,17 +1,17 @@
 use crate::debugger::breakpoint_store::BreakpointSessionState;
-use crate::debugger::dap_command::ReadMemory;
+use crate::debugger::dap_command::{DataBreakpointContext, ReadMemory};
 use crate::debugger::memory::{self, Memory, MemoryIterator, MemoryPageBuilder, PageAddress};
 
 use super::breakpoint_store::{
     BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint,
 };
 use super::dap_command::{
-    self, Attach, ConfigurationDone, ContinueCommand, DisconnectCommand, EvaluateCommand,
-    Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, LocationsCommand, ModulesCommand,
-    NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, ScopesCommand,
-    SetExceptionBreakpoints, SetVariableValueCommand, StackTraceCommand, StepBackCommand,
-    StepCommand, StepInCommand, StepOutCommand, TerminateCommand, TerminateThreadsCommand,
-    ThreadsCommand, VariablesCommand,
+    self, Attach, ConfigurationDone, ContinueCommand, DataBreakpointInfoCommand, DisconnectCommand,
+    EvaluateCommand, Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, LocationsCommand,
+    ModulesCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand,
+    ScopesCommand, SetDataBreakpointsCommand, SetExceptionBreakpoints, SetVariableValueCommand,
+    StackTraceCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand,
+    TerminateCommand, TerminateThreadsCommand, ThreadsCommand, VariablesCommand,
 };
 use super::dap_store::DapStore;
 use anyhow::{Context as _, Result, anyhow};
@@ -138,6 +138,13 @@ pub struct Watcher {
     pub presentation_hint: Option<VariablePresentationHint>,
 }
 
+#[derive(Debug, Clone, PartialEq)]
+pub struct DataBreakpointState {
+    pub dap: dap::DataBreakpoint,
+    pub is_enabled: bool,
+    pub context: Arc<DataBreakpointContext>,
+}
+
 pub enum SessionState {
     Building(Option<Task<Result<()>>>),
     Running(RunningMode),
@@ -686,6 +693,7 @@ pub struct Session {
     pub(crate) breakpoint_store: Entity<BreakpointStore>,
     ignore_breakpoints: bool,
     exception_breakpoints: BTreeMap<String, (ExceptionBreakpointsFilter, IsEnabled)>,
+    data_breakpoints: BTreeMap<String, DataBreakpointState>,
     background_tasks: Vec<Task<()>>,
     restart_task: Option<Task<()>>,
     task_context: TaskContext,
@@ -780,6 +788,7 @@ pub enum SessionEvent {
         request: RunInTerminalRequestArguments,
         sender: mpsc::Sender<Result<u32>>,
     },
+    DataBreakpointInfo,
     ConsoleOutput,
 }
 
@@ -856,6 +865,7 @@ impl Session {
                 is_session_terminated: false,
                 ignore_breakpoints: false,
                 breakpoint_store,
+                data_breakpoints: Default::default(),
                 exception_breakpoints: Default::default(),
                 label,
                 adapter,
@@ -1670,6 +1680,7 @@ impl Session {
         self.invalidate_command_type::<ModulesCommand>();
         self.invalidate_command_type::<LoadedSourcesCommand>();
         self.invalidate_command_type::<ThreadsCommand>();
+        self.invalidate_command_type::<DataBreakpointInfoCommand>();
         self.invalidate_command_type::<ReadMemory>();
         let executor = self.as_running().map(|running| running.executor.clone());
         if let Some(executor) = executor {
@@ -1906,6 +1917,10 @@ impl Session {
         }
     }
 
+    pub fn data_breakpoints(&self) -> impl Iterator<Item = &DataBreakpointState> {
+        self.data_breakpoints.values()
+    }
+
     pub fn exception_breakpoints(
         &self,
     ) -> impl Iterator<Item = &(ExceptionBreakpointsFilter, IsEnabled)> {
@@ -1939,6 +1954,45 @@ impl Session {
         }
     }
 
+    pub fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context<'_, Session>) {
+        if let Some(state) = self.data_breakpoints.get_mut(id) {
+            state.is_enabled = !state.is_enabled;
+            self.send_exception_breakpoints(cx);
+        }
+    }
+
+    fn send_data_breakpoints(&mut self, cx: &mut Context<Self>) {
+        if let Some(mode) = self.as_running() {
+            let breakpoints = self
+                .data_breakpoints
+                .values()
+                .filter_map(|state| state.is_enabled.then(|| state.dap.clone()))
+                .collect();
+            let command = SetDataBreakpointsCommand { breakpoints };
+            mode.request(command).detach_and_log_err(cx);
+        }
+    }
+
+    pub fn create_data_breakpoint(
+        &mut self,
+        context: Arc<DataBreakpointContext>,
+        data_id: String,
+        dap: dap::DataBreakpoint,
+        cx: &mut Context<Self>,
+    ) {
+        if self.data_breakpoints.remove(&data_id).is_none() {
+            self.data_breakpoints.insert(
+                data_id,
+                DataBreakpointState {
+                    dap,
+                    is_enabled: true,
+                    context,
+                },
+            );
+        }
+        self.send_data_breakpoints(cx);
+    }
+
     pub fn breakpoints_enabled(&self) -> bool {
         self.ignore_breakpoints
     }
@@ -2500,6 +2554,20 @@ impl Session {
             .unwrap_or_default()
     }
 
+    pub fn data_breakpoint_info(
+        &mut self,
+        context: Arc<DataBreakpointContext>,
+        mode: Option<String>,
+        cx: &mut Context<Self>,
+    ) -> Task<Option<dap::DataBreakpointInfoResponse>> {
+        let command = DataBreakpointInfoCommand {
+            context: context.clone(),
+            mode,
+        };
+
+        self.request(command, |_, response, _| response.ok(), cx)
+    }
+
     pub fn set_variable_value(
         &mut self,
         stack_frame_id: u64,