debugger: Add data breakpoint access type support (#34639)

Anthony Eid created

Release Notes:

- Support specifying a data breakpoint's access type - Read, Write, Read
& Write

Change summary

Cargo.lock                                              |  1 
crates/debugger_ui/Cargo.toml                           | 11 +
crates/debugger_ui/src/debugger_ui.rs                   | 20 +++
crates/debugger_ui/src/session/running/memory_view.rs   |  2 
crates/debugger_ui/src/session/running/variable_list.rs | 61 +++++++++-
5 files changed, 76 insertions(+), 19 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4427,6 +4427,7 @@ dependencies = [
  "pretty_assertions",
  "project",
  "rpc",
+ "schemars",
  "serde",
  "serde_json",
  "serde_json_lenient",

crates/debugger_ui/Cargo.toml 🔗

@@ -35,6 +35,7 @@ command_palette_hooks.workspace = true
 dap.workspace = true
 dap_adapters = { workspace = true, optional = true }
 db.workspace = true
+debugger_tools.workspace = true
 editor.workspace = true
 file_icons.workspace = true
 futures.workspace = true
@@ -54,6 +55,7 @@ picker.workspace = true
 pretty_assertions.workspace = true
 project.workspace = true
 rpc.workspace = true
+schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 serde_json_lenient.workspace = true
@@ -66,14 +68,13 @@ telemetry.workspace = true
 terminal_view.workspace = true
 text.workspace = true
 theme.workspace = true
-tree-sitter.workspace = true
 tree-sitter-json.workspace = true
+tree-sitter.workspace = true
 ui.workspace = true
+unindent = { workspace = true, optional = true }
 util.workspace = true
-workspace.workspace = true
 workspace-hack.workspace = true
-debugger_tools.workspace = true
-unindent = { workspace = true, optional = true }
+workspace.workspace = true
 zed_actions.workspace = true
 
 [dev-dependencies]
@@ -83,8 +84,8 @@ debugger_tools = { workspace = true, features = ["test-support"] }
 editor = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
 project = { workspace = true, features = ["test-support"] }
+tree-sitter-go.workspace = true
 unindent.workspace = true
 util = { workspace = true, features = ["test-support"] }
 workspace = { workspace = true, features = ["test-support"] }
 zlog.workspace = true
-tree-sitter-go.workspace = true

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -3,10 +3,12 @@ use std::any::TypeId;
 use dap::debugger_settings::DebuggerSettings;
 use debugger_panel::DebugPanel;
 use editor::Editor;
-use gpui::{App, DispatchPhase, EntityInputHandler, actions};
+use gpui::{Action, App, DispatchPhase, EntityInputHandler, actions};
 use new_process_modal::{NewProcessModal, NewProcessMode};
 use onboarding_modal::DebuggerOnboardingModal;
 use project::debugger::{self, breakpoint_store::SourceBreakpoint, session::ThreadStatus};
+use schemars::JsonSchema;
+use serde::Deserialize;
 use session::DebugSession;
 use settings::Settings;
 use stack_trace_view::StackTraceView;
@@ -83,11 +85,23 @@ 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,
     ]
 );
 
+/// Extends selection down by a specified number of lines.
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
+#[action(namespace = debugger)]
+#[serde(deny_unknown_fields)]
+/// Set a data breakpoint on the selected variable or memory region.
+pub struct ToggleDataBreakpoint {
+    /// The type of data breakpoint
+    /// Read & Write
+    /// Read
+    /// Write
+    #[serde(default)]
+    pub access_type: Option<dap::DataBreakpointAccessType>,
+}
+
 actions!(
     dev,
     [

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

@@ -688,7 +688,7 @@ impl MemoryView {
                 menu = menu.action_disabled_when(
                     *memory_unreadable,
                     "Set Data Breakpoint",
-                    ToggleDataBreakpoint.boxed_clone(),
+                    ToggleDataBreakpoint { access_type: None }.boxed_clone(),
                 );
             }
             menu.context(self.focus_handle.clone())

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

@@ -670,9 +670,9 @@ impl VariableList {
         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()
+                task.await
             } else {
-                true
+                None
             };
             cx.update(|window, cx| {
                 let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
@@ -686,11 +686,35 @@ impl VariableList {
                                 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(),
-                                )
+                            .when_some(can_toggle_data_breakpoint, |mut menu, data_info| {
+                                menu = menu.separator();
+                                if let Some(access_types) = data_info.access_types {
+                                    for access in access_types {
+                                        menu = menu.action(
+                                            format!(
+                                                "Toggle {} Data Breakpoint",
+                                                match access {
+                                                    dap::DataBreakpointAccessType::Read => "Read",
+                                                    dap::DataBreakpointAccessType::Write => "Write",
+                                                    dap::DataBreakpointAccessType::ReadWrite =>
+                                                        "Read/Write",
+                                                }
+                                            ),
+                                            crate::ToggleDataBreakpoint {
+                                                access_type: Some(access),
+                                            }
+                                            .boxed_clone(),
+                                        );
+                                    }
+
+                                    menu
+                                } else {
+                                    menu.action(
+                                        "Toggle Data Breakpoint",
+                                        crate::ToggleDataBreakpoint { access_type: None }
+                                            .boxed_clone(),
+                                    )
+                                }
                             })
                     })
                     .when(entry.as_watcher().is_some(), |menu| {
@@ -729,7 +753,7 @@ impl VariableList {
 
     fn toggle_data_breakpoint(
         &mut self,
-        _: &crate::ToggleDataBreakpoint,
+        data_info: &crate::ToggleDataBreakpoint,
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -759,17 +783,34 @@ impl VariableList {
         });
 
         let session = self.session.downgrade();
+        let access_type = data_info.access_type;
         cx.spawn(async move |_, cx| {
-            let Some(data_id) = data_breakpoint.await.and_then(|info| info.data_id) else {
+            let Some((data_id, access_types)) = data_breakpoint
+                .await
+                .and_then(|info| Some((info.data_id?, info.access_types)))
+            else {
                 return;
             };
+
+            // Because user's can manually add this action to the keymap
+            // we check if access type is supported
+            let access_type = match access_types {
+                None => None,
+                Some(access_types) => {
+                    if access_type.is_some_and(|access_type| access_types.contains(&access_type)) {
+                        access_type
+                    } else {
+                        None
+                    }
+                }
+            };
             _ = session.update(cx, |session, cx| {
                 session.create_data_breakpoint(
                     context,
                     data_id.clone(),
                     dap::DataBreakpoint {
                         data_id,
-                        access_type: None,
+                        access_type,
                         condition: None,
                         hit_condition: None,
                     },