Detailed changes
@@ -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,
]
);
@@ -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 {
@@ -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()
@@ -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(
@@ -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;
@@ -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,