diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index 2056232e9b..c932f1b600 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/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, ] ); diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 78c87db2e6..6ac4b1c878 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/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, @@ -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) { + 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) { 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, + ix: usize, + is_selected: bool, + focus_handle: FocusHandle, + list: WeakEntity, + ) -> 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 { diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index e9dcb0839d..9d94644954 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/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, + ) { + 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) { 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::() 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() diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index c7df449ee6..b158314b50 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/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, ) { - let supports_set_variable = self - .session - .read(cx) - .capabilities() - .supports_set_variable - .unwrap_or_default(); + 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 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 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, + ) { + 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( diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 1a3587024e..1cb611680c 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/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, @@ -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, + }, + Expression { + expression: String, + frame_id: Option, + }, + Address { + address: String, + bytes: Option, + }, +} + +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, + pub mode: Option, +} + +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) -> ::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: ::Response, + ) -> Result { + Ok(message) + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub(crate) struct SetDataBreakpointsCommand { + pub breakpoints: Vec, +} + +impl LocalDapCommand for SetDataBreakpointsCommand { + type Response = Vec; + type DapRequest = dap::requests::SetDataBreakpoints; + + fn is_supported(capabilities: &Capabilities) -> bool { + capabilities.supports_data_breakpoints.unwrap_or(false) + } + + fn to_dap(&self) -> ::Arguments { + dap::SetDataBreakpointsArguments { + breakpoints: self.breakpoints.clone(), + } + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + 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, @@ -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) -> ::Arguments { - self.clone() - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - impl LocalDapCommand for dap::WriteMemoryArguments { type Response = dap::WriteMemoryResponse; type DapRequest = dap::requests::WriteMemory; diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 53c13e13c3..cf157ce4f9 100644 --- a/crates/project/src/debugger/session.rs +++ b/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, } +#[derive(Debug, Clone, PartialEq)] +pub struct DataBreakpointState { + pub dap: dap::DataBreakpoint, + pub is_enabled: bool, + pub context: Arc, +} + pub enum SessionState { Building(Option>>), Running(RunningMode), @@ -686,6 +693,7 @@ pub struct Session { pub(crate) breakpoint_store: Entity, ignore_breakpoints: bool, exception_breakpoints: BTreeMap, + data_breakpoints: BTreeMap, background_tasks: Vec>, restart_task: Option>, task_context: TaskContext, @@ -780,6 +788,7 @@ pub enum SessionEvent { request: RunInTerminalRequestArguments, sender: mpsc::Sender>, }, + 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::(); self.invalidate_command_type::(); self.invalidate_command_type::(); + self.invalidate_command_type::(); self.invalidate_command_type::(); 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 { + self.data_breakpoints.values() + } + pub fn exception_breakpoints( &self, ) -> impl Iterator { @@ -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) { + 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, + data_id: String, + dap: dap::DataBreakpoint, + cx: &mut Context, + ) { + 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, + mode: Option, + cx: &mut Context, + ) -> Task> { + 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,