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

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>
This commit is contained in:
Anthony Eid 2025-07-14 13:45:46 -04:00 committed by GitHub
parent 8b6b039b63
commit fd5650d4ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 588 additions and 87 deletions

View file

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

View file

@ -24,10 +24,10 @@ use project::{
}; };
use ui::{ use ui::{
ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div, ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div,
Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, Indicator, Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, InteractiveElement,
InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
Render, RenderOnce, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Toggleable,
Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex, Tooltip, Window, div, h_flex, px, v_flex,
}; };
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
@ -46,6 +46,7 @@ actions!(
pub(crate) enum SelectedBreakpointKind { pub(crate) enum SelectedBreakpointKind {
Source, Source,
Exception, Exception,
Data,
} }
pub(crate) struct BreakpointList { pub(crate) struct BreakpointList {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
@ -188,6 +189,9 @@ impl BreakpointList {
BreakpointEntryKind::ExceptionBreakpoint(bp) => { BreakpointEntryKind::ExceptionBreakpoint(bp) => {
(SelectedBreakpointKind::Exception, bp.is_enabled) (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; let row = line_breakpoint.breakpoint.row;
self.go_to_line_breakpoint(path, row, window, cx); 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(); let id = exception_breakpoint.id.clone();
self.toggle_exception_breakpoint(&id, cx); 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(); cx.notify();
} }
@ -441,7 +450,7 @@ impl BreakpointList {
let row = line_breakpoint.breakpoint.row; let row = line_breakpoint.breakpoint.row;
self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx); self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
} }
BreakpointEntryKind::ExceptionBreakpoint(_) => {} _ => {}
} }
cx.notify(); cx.notify();
} }
@ -490,6 +499,14 @@ impl BreakpointList {
cx.notify(); 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>) { fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
if let Some(session) = &self.session { if let Some(session) = &self.session {
session.update(cx, |this, cx| { session.update(cx, |this, cx| {
@ -642,6 +659,7 @@ impl BreakpointList {
SelectedBreakpointKind::Exception => { SelectedBreakpointKind::Exception => {
"Exception Breakpoints cannot be removed from the breakpoint list" "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)| { let toggle_label = selection_kind.map(|(_, is_enabled)| {
if is_enabled { if is_enabled {
@ -783,8 +801,20 @@ impl Render for BreakpointList {
weak: weak.clone(), weak: weak.clone(),
}) })
}); });
self.breakpoints let data_breakpoints = self.session.as_ref().into_iter().flat_map(|session| {
.extend(breakpoints.chain(exception_breakpoints)); 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() v_flex()
.id("breakpoint-list") .id("breakpoint-list")
.key_context("BreakpointList") .key_context("BreakpointList")
@ -905,7 +935,11 @@ impl LineBreakpoint {
.ok(); .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 |_, _, _| {}); .on_mouse_down(MouseButton::Left, move |_, _, _| {});
ListItem::new(SharedString::from(format!( ListItem::new(SharedString::from(format!(
@ -996,6 +1030,103 @@ struct ExceptionBreakpoint {
data: ExceptionBreakpointsFilter, data: ExceptionBreakpointsFilter,
is_enabled: bool, 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 { impl ExceptionBreakpoint {
fn render( fn render(
@ -1062,7 +1193,11 @@ impl ExceptionBreakpoint {
} }
}) })
.cursor_pointer() .cursor_pointer()
.child(Indicator::icon(Icon::new(IconName::Flame)).color(color)), .child(
Icon::new(IconName::Flame)
.color(color)
.size(IconSize::Small),
),
) )
.child( .child(
h_flex() h_flex()
@ -1105,6 +1240,7 @@ impl ExceptionBreakpoint {
enum BreakpointEntryKind { enum BreakpointEntryKind {
LineBreakpoint(LineBreakpoint), LineBreakpoint(LineBreakpoint),
ExceptionBreakpoint(ExceptionBreakpoint), ExceptionBreakpoint(ExceptionBreakpoint),
DataBreakpoint(DataBreakpoint),
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -1140,6 +1276,14 @@ impl BreakpointEntry {
focus_handle, focus_handle,
self.weak.clone(), 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 exception_breakpoint.id
) )
.into(), .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) => { BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
line_breakpoint.breakpoint.condition.is_some() line_breakpoint.breakpoint.condition.is_some()
} }
// We don't support conditions on exception breakpoints // We don't support conditions on exception/data breakpoints
BreakpointEntryKind::ExceptionBreakpoint(_) => false, _ => 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. // TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
Self::empty() 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)] #[derive(IntoElement)]
struct BreakpointOptionsStrip { struct BreakpointOptionsStrip {

View file

@ -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 editor::{Editor, EditorElement, EditorStyle};
use gpui::{ use gpui::{
@ -8,7 +14,7 @@ use gpui::{
deferred, point, size, uniform_list, deferred, point, size, uniform_list,
}; };
use notifications::status_toast::{StatusToast, ToastIcon}; 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 settings::Settings;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
@ -20,7 +26,7 @@ use ui::{
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
use crate::session::running::stack_frame_list::StackFrameList; use crate::{ToggleDataBreakpoint, session::running::stack_frame_list::StackFrameList};
actions!(debugger, [GoToSelectedAddress]); 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>) { fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
if let Some(SelectedMemoryRange::DragComplete(drag)) = &self.view_state.selection { if let Some(SelectedMemoryRange::DragComplete(drag)) = &self.view_state.selection {
// Go into memory writing mode. // Go into memory writing mode.
@ -599,18 +647,30 @@ impl MemoryView {
let session = self.session.clone(); let session = self.session.clone();
let context_menu = ContextMenu::build(window, cx, |menu, _, cx| { 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 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| { session.update(cx, |this, cx| {
this.read_memory(range.clone(), cx) this.read_memory(range.clone(), cx)
.any(|cell| cell.0.is_none()) .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", "Go To Selected Address",
GoToSelectedAddress.boxed_clone(), 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); cx.focus_view(&context_menu, window);
@ -834,6 +894,7 @@ impl Render for MemoryView {
.on_action(cx.listener(Self::go_to_address)) .on_action(cx.listener(Self::go_to_address))
.p_1() .p_1()
.on_action(cx.listener(Self::confirm)) .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_down))
.on_action(cx.listener(Self::page_up)) .on_action(cx.listener(Self::page_up))
.size_full() .size_full()

View file

@ -13,7 +13,10 @@ use gpui::{
uniform_list, uniform_list,
}; };
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious}; 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 std::{collections::HashMap, ops::Range, sync::Arc};
use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*}; use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*};
use util::{debug_panic, maybe}; use util::{debug_panic, maybe};
@ -220,6 +223,7 @@ impl VariableList {
SessionEvent::Variables | SessionEvent::Watchers => { SessionEvent::Variables | SessionEvent::Watchers => {
this.build_entries(cx); this.build_entries(cx);
} }
_ => {} _ => {}
}), }),
cx.on_focus_out(&focus_handle, window, |this, _, _, cx| { cx.on_focus_out(&focus_handle, window, |this, _, _, cx| {
@ -625,22 +629,69 @@ impl VariableList {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let supports_set_variable = self let (supports_set_variable, supports_data_breakpoints, supports_go_to_memory) =
.session self.session.read_with(cx, |session, _| {
.read(cx) (
session
.capabilities() .capabilities()
.supports_set_variable .supports_set_variable
.unwrap_or_default(); .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, _, _| { let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
menu.when(entry.as_variable().is_some(), |menu| { menu.when_some(entry.as_variable(), |menu, _| {
menu.action("Copy Name", CopyVariableName.boxed_clone()) menu.action("Copy Name", CopyVariableName.boxed_clone())
.action("Copy Value", CopyVariableValue.boxed_clone()) .action("Copy Value", CopyVariableValue.boxed_clone())
.when(supports_set_variable, |menu| { .when(supports_set_variable, |menu| {
menu.action("Edit Value", EditVariable.boxed_clone()) 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()) .action("Watch Variable", AddWatch.boxed_clone())
.action("Go To Memory", GoToMemory.boxed_clone()) .when(can_toggle_data_breakpoint, |menu| {
menu.action(
"Toggle Data Breakpoint",
crate::ToggleDataBreakpoint.boxed_clone(),
)
})
}) })
.when(entry.as_watcher().is_some(), |menu| { .when(entry.as_watcher().is_some(), |menu| {
menu.action("Copy Name", CopyVariableName.boxed_clone()) menu.action("Copy Name", CopyVariableName.boxed_clone())
@ -650,9 +701,10 @@ impl VariableList {
}) })
.action("Remove Watch", RemoveWatch.boxed_clone()) .action("Remove Watch", RemoveWatch.boxed_clone())
}) })
.context(self.focus_handle.clone()) .context(focus_handle.clone())
}); });
_ = this.update(cx, |this, cx| {
cx.focus_view(&context_menu, window); cx.focus_view(&context_menu, window);
let subscription = cx.subscribe_in( let subscription = cx.subscribe_in(
&context_menu, &context_menu,
@ -668,7 +720,65 @@ impl VariableList {
}, },
); );
self.open_context_menu = Some((context_menu, position, subscription)); this.open_context_menu = Some((context_menu, position, subscription));
});
})
})
.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)
});
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();
});
})
.detach();
} }
fn copy_variable_name( fn copy_variable_name(
@ -1415,6 +1525,7 @@ impl Render for VariableList {
.on_action(cx.listener(Self::edit_variable)) .on_action(cx.listener(Self::edit_variable))
.on_action(cx.listener(Self::add_watcher)) .on_action(cx.listener(Self::add_watcher))
.on_action(cx.listener(Self::remove_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)) .on_action(cx.listener(Self::jump_to_variable_memory))
.child( .child(
uniform_list( uniform_list(

View file

@ -11,6 +11,7 @@ use dap::{
proto_conversions::ProtoConversion, proto_conversions::ProtoConversion,
requests::{Continue, Next}, requests::{Continue, Next},
}; };
use rpc::proto; use rpc::proto;
use serde_json::Value; use serde_json::Value;
use util::ResultExt; 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 struct VariablesCommand {
pub variables_reference: u64, pub variables_reference: u64,
pub filter: Option<VariablesArgumentsFilter>, pub filter: Option<VariablesArgumentsFilter>,
@ -1667,6 +1668,130 @@ impl LocalDapCommand for SetBreakpoints {
Ok(message.breakpoints) 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)] #[derive(Clone, Debug, Hash, PartialEq)]
pub(super) enum SetExceptionBreakpoints { pub(super) enum SetExceptionBreakpoints {
Plain { 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) struct ReadMemory {
pub(crate) memory_reference: String, pub(crate) memory_reference: String,
pub(crate) offset: Option<u64>, 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 { impl LocalDapCommand for dap::WriteMemoryArguments {
type Response = dap::WriteMemoryResponse; type Response = dap::WriteMemoryResponse;
type DapRequest = dap::requests::WriteMemory; type DapRequest = dap::requests::WriteMemory;

View file

@ -1,17 +1,17 @@
use crate::debugger::breakpoint_store::BreakpointSessionState; 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 crate::debugger::memory::{self, Memory, MemoryIterator, MemoryPageBuilder, PageAddress};
use super::breakpoint_store::{ use super::breakpoint_store::{
BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint, BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint,
}; };
use super::dap_command::{ use super::dap_command::{
self, Attach, ConfigurationDone, ContinueCommand, DisconnectCommand, EvaluateCommand, self, Attach, ConfigurationDone, ContinueCommand, DataBreakpointInfoCommand, DisconnectCommand,
Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, LocationsCommand, ModulesCommand, EvaluateCommand, Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, LocationsCommand,
NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, ScopesCommand, ModulesCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand,
SetExceptionBreakpoints, SetVariableValueCommand, StackTraceCommand, StepBackCommand, ScopesCommand, SetDataBreakpointsCommand, SetExceptionBreakpoints, SetVariableValueCommand,
StepCommand, StepInCommand, StepOutCommand, TerminateCommand, TerminateThreadsCommand, StackTraceCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand,
ThreadsCommand, VariablesCommand, TerminateCommand, TerminateThreadsCommand, ThreadsCommand, VariablesCommand,
}; };
use super::dap_store::DapStore; use super::dap_store::DapStore;
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
@ -138,6 +138,13 @@ pub struct Watcher {
pub presentation_hint: Option<VariablePresentationHint>, 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 { pub enum SessionState {
Building(Option<Task<Result<()>>>), Building(Option<Task<Result<()>>>),
Running(RunningMode), Running(RunningMode),
@ -686,6 +693,7 @@ pub struct Session {
pub(crate) breakpoint_store: Entity<BreakpointStore>, pub(crate) breakpoint_store: Entity<BreakpointStore>,
ignore_breakpoints: bool, ignore_breakpoints: bool,
exception_breakpoints: BTreeMap<String, (ExceptionBreakpointsFilter, IsEnabled)>, exception_breakpoints: BTreeMap<String, (ExceptionBreakpointsFilter, IsEnabled)>,
data_breakpoints: BTreeMap<String, DataBreakpointState>,
background_tasks: Vec<Task<()>>, background_tasks: Vec<Task<()>>,
restart_task: Option<Task<()>>, restart_task: Option<Task<()>>,
task_context: TaskContext, task_context: TaskContext,
@ -780,6 +788,7 @@ pub enum SessionEvent {
request: RunInTerminalRequestArguments, request: RunInTerminalRequestArguments,
sender: mpsc::Sender<Result<u32>>, sender: mpsc::Sender<Result<u32>>,
}, },
DataBreakpointInfo,
ConsoleOutput, ConsoleOutput,
} }
@ -856,6 +865,7 @@ impl Session {
is_session_terminated: false, is_session_terminated: false,
ignore_breakpoints: false, ignore_breakpoints: false,
breakpoint_store, breakpoint_store,
data_breakpoints: Default::default(),
exception_breakpoints: Default::default(), exception_breakpoints: Default::default(),
label, label,
adapter, adapter,
@ -1670,6 +1680,7 @@ impl Session {
self.invalidate_command_type::<ModulesCommand>(); self.invalidate_command_type::<ModulesCommand>();
self.invalidate_command_type::<LoadedSourcesCommand>(); self.invalidate_command_type::<LoadedSourcesCommand>();
self.invalidate_command_type::<ThreadsCommand>(); self.invalidate_command_type::<ThreadsCommand>();
self.invalidate_command_type::<DataBreakpointInfoCommand>();
self.invalidate_command_type::<ReadMemory>(); self.invalidate_command_type::<ReadMemory>();
let executor = self.as_running().map(|running| running.executor.clone()); let executor = self.as_running().map(|running| running.executor.clone());
if let Some(executor) = executor { 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( pub fn exception_breakpoints(
&self, &self,
) -> impl Iterator<Item = &(ExceptionBreakpointsFilter, IsEnabled)> { ) -> 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 { pub fn breakpoints_enabled(&self) -> bool {
self.ignore_breakpoints self.ignore_breakpoints
} }
@ -2500,6 +2554,20 @@ impl Session {
.unwrap_or_default() .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( pub fn set_variable_value(
&mut self, &mut self,
stack_frame_id: u64, stack_frame_id: u64,