debugger: Add keyboard navigation for breakpoint list (#31221)

Cole Miller created

Release Notes:

- Debugger Beta: made it possible to navigate the breakpoint list using
menu keybindings.

Change summary

Cargo.lock                                                |   1 
assets/keymaps/default-linux.json                         |   7 
assets/keymaps/default-macos.json                         |   7 
crates/debugger_ui/Cargo.toml                             |   1 
crates/debugger_ui/src/session/running/breakpoint_list.rs | 538 ++++++--
crates/zed_actions/src/lib.rs                             |   2 
6 files changed, 385 insertions(+), 171 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4248,6 +4248,7 @@ dependencies = [
  "util",
  "workspace",
  "workspace-hack",
+ "zed_actions",
  "zlog",
 ]
 

assets/keymaps/default-linux.json 🔗

@@ -872,6 +872,13 @@
       "ctrl-i": "debugger::ToggleSessionPicker"
     }
   },
+  {
+    "context": "BreakpointList",
+    "bindings": {
+      "space": "debugger::ToggleEnableBreakpoint",
+      "backspace": "debugger::UnsetBreakpoint"
+    }
+  },
   {
     "context": "CollabPanel && not_editing",
     "bindings": {

assets/keymaps/default-macos.json 🔗

@@ -932,6 +932,13 @@
       "cmd-i": "debugger::ToggleSessionPicker"
     }
   },
+  {
+    "context": "BreakpointList",
+    "bindings": {
+      "space": "debugger::ToggleEnableBreakpoint",
+      "backspace": "debugger::UnsetBreakpoint"
+    }
+  },
   {
     "context": "CollabPanel && not_editing",
     "use_key_equivalents": true,

crates/debugger_ui/Cargo.toml 🔗

@@ -63,6 +63,7 @@ workspace.workspace = true
 workspace-hack.workspace = true
 debugger_tools = { workspace = true, optional = true }
 unindent = { workspace = true, optional = true }
+zed_actions.workspace = true
 
 [dev-dependencies]
 dap = { workspace = true, features = ["test-support"] }

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

@@ -1,13 +1,14 @@
 use std::{
     path::{Path, PathBuf},
+    sync::Arc,
     time::Duration,
 };
 
 use dap::ExceptionBreakpointsFilter;
 use editor::Editor;
 use gpui::{
-    AppContext, Entity, FocusHandle, Focusable, ListState, MouseButton, Stateful, Task, WeakEntity,
-    list,
+    AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful, Task,
+    UniformListScrollHandle, WeakEntity, uniform_list,
 };
 use language::Point;
 use project::{
@@ -19,25 +20,27 @@ use project::{
     worktree_store::WorktreeStore,
 };
 use ui::{
-    App, Clickable, Color, Context, Div, Icon, IconButton, IconName, Indicator, InteractiveElement,
-    IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
-    Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Tooltip, Window,
-    div, h_flex, px, v_flex,
+    App, ButtonCommon, Clickable, Color, Context, Div, FluentBuilder as _, Icon, IconButton,
+    IconName, Indicator, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem,
+    ParentElement, Render, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
+    Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
 };
-use util::{ResultExt, maybe};
+use util::ResultExt;
 use workspace::Workspace;
+use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
 
 pub(crate) struct BreakpointList {
     workspace: WeakEntity<Workspace>,
     breakpoint_store: Entity<BreakpointStore>,
     worktree_store: Entity<WorktreeStore>,
-    list_state: ListState,
     scrollbar_state: ScrollbarState,
     breakpoints: Vec<BreakpointEntry>,
     session: Entity<Session>,
     hide_scrollbar_task: Option<Task<()>>,
     show_scrollbar: bool,
     focus_handle: FocusHandle,
+    scroll_handle: UniformListScrollHandle,
+    selected_ix: Option<usize>,
 }
 
 impl Focusable for BreakpointList {
@@ -56,36 +59,203 @@ impl BreakpointList {
         let project = project.read(cx);
         let breakpoint_store = project.breakpoint_store();
         let worktree_store = project.worktree_store();
+        let focus_handle = cx.focus_handle();
+        let scroll_handle = UniformListScrollHandle::new();
+        let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
 
-        cx.new(|cx| {
-            let weak: gpui::WeakEntity<Self> = cx.weak_entity();
-            let list_state = ListState::new(
-                0,
-                gpui::ListAlignment::Top,
-                px(1000.),
-                move |ix, window, cx| {
-                    let Ok(Some(breakpoint)) =
-                        weak.update(cx, |this, _| this.breakpoints.get(ix).cloned())
-                    else {
-                        return div().into_any_element();
-                    };
-
-                    breakpoint.render(window, cx).into_any_element()
-                },
-            );
+        cx.new(|_| {
             Self {
                 breakpoint_store,
                 worktree_store,
-                scrollbar_state: ScrollbarState::new(list_state.clone()),
-                list_state,
+                scrollbar_state,
+                // list_state,
                 breakpoints: Default::default(),
                 hide_scrollbar_task: None,
                 show_scrollbar: false,
                 workspace,
                 session,
-                focus_handle: cx.focus_handle(),
+                focus_handle,
+                scroll_handle,
+                selected_ix: None,
+            }
+        })
+    }
+
+    fn edit_line_breakpoint(
+        &mut self,
+        path: Arc<Path>,
+        row: u32,
+        action: BreakpointEditAction,
+        cx: &mut Context<Self>,
+    ) {
+        self.breakpoint_store.update(cx, |breakpoint_store, cx| {
+            if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
+                breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
+            } else {
+                log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
+            }
+        })
+    }
+
+    fn go_to_line_breakpoint(
+        &mut self,
+        path: Arc<Path>,
+        row: u32,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let task = self
+            .worktree_store
+            .update(cx, |this, cx| this.find_or_create_worktree(path, false, cx));
+        cx.spawn_in(window, async move |this, cx| {
+            let (worktree, relative_path) = task.await?;
+            let worktree_id = worktree.update(cx, |this, _| this.id())?;
+            let item = this
+                .update_in(cx, |this, window, cx| {
+                    this.workspace.update(cx, |this, cx| {
+                        this.open_path((worktree_id, relative_path), None, true, window, cx)
+                    })
+                })??
+                .await?;
+            if let Some(editor) = item.downcast::<Editor>() {
+                editor
+                    .update_in(cx, |this, window, cx| {
+                        this.go_to_singleton_buffer_point(Point { row, column: 0 }, window, cx);
+                    })
+                    .ok();
             }
+            anyhow::Ok(())
         })
+        .detach();
+    }
+
+    fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
+        self.selected_ix = ix;
+        if let Some(ix) = ix {
+            self.scroll_handle
+                .scroll_to_item(ix, ScrollStrategy::Center);
+        }
+        cx.notify();
+    }
+
+    fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
+        let ix = match self.selected_ix {
+            _ if self.breakpoints.len() == 0 => None,
+            None => Some(0),
+            Some(ix) => {
+                if ix == self.breakpoints.len() - 1 {
+                    Some(0)
+                } else {
+                    Some(ix + 1)
+                }
+            }
+        };
+        self.select_ix(ix, cx);
+    }
+
+    fn select_previous(
+        &mut self,
+        _: &menu::SelectPrevious,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let ix = match self.selected_ix {
+            _ if self.breakpoints.len() == 0 => None,
+            None => Some(self.breakpoints.len() - 1),
+            Some(ix) => {
+                if ix == 0 {
+                    Some(self.breakpoints.len() - 1)
+                } else {
+                    Some(ix - 1)
+                }
+            }
+        };
+        self.select_ix(ix, cx);
+    }
+
+    fn select_first(
+        &mut self,
+        _: &menu::SelectFirst,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let ix = if self.breakpoints.len() > 0 {
+            Some(0)
+        } else {
+            None
+        };
+        self.select_ix(ix, cx);
+    }
+
+    fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
+        let ix = if self.breakpoints.len() > 0 {
+            Some(self.breakpoints.len() - 1)
+        } else {
+            None
+        };
+        self.select_ix(ix, cx);
+    }
+
+    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
+        let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
+            return;
+        };
+
+        match &mut entry.kind {
+            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+                let path = line_breakpoint.breakpoint.path.clone();
+                let row = line_breakpoint.breakpoint.row;
+                self.go_to_line_breakpoint(path, row, window, cx);
+            }
+            BreakpointEntryKind::ExceptionBreakpoint(_) => {}
+        }
+    }
+
+    fn toggle_enable_breakpoint(
+        &mut self,
+        _: &ToggleEnableBreakpoint,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
+            return;
+        };
+
+        match &mut entry.kind {
+            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+                let path = line_breakpoint.breakpoint.path.clone();
+                let row = line_breakpoint.breakpoint.row;
+                self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
+            }
+            BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
+                let id = exception_breakpoint.id.clone();
+                self.session.update(cx, |session, cx| {
+                    session.toggle_exception_breakpoint(&id, cx);
+                });
+            }
+        }
+        cx.notify();
+    }
+
+    fn unset_breakpoint(
+        &mut self,
+        _: &UnsetBreakpoint,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
+            return;
+        };
+
+        match &mut entry.kind {
+            BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+                let path = line_breakpoint.breakpoint.path.clone();
+                let row = line_breakpoint.breakpoint.row;
+                self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
+            }
+            BreakpointEntryKind::ExceptionBreakpoint(_) => {}
+        }
+        cx.notify();
     }
 
     fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -103,6 +273,30 @@ impl BreakpointList {
         }))
     }
 
+    fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let selected_ix = self.selected_ix;
+        let focus_handle = self.focus_handle.clone();
+        uniform_list(
+            cx.entity(),
+            "breakpoint-list",
+            self.breakpoints.len(),
+            move |this, range, window, cx| {
+                range
+                    .clone()
+                    .zip(&mut this.breakpoints[range])
+                    .map(|(ix, breakpoint)| {
+                        breakpoint
+                            .render(ix, focus_handle.clone(), window, cx)
+                            .toggle_state(Some(ix) == selected_ix)
+                            .into_any_element()
+                    })
+                    .collect()
+            },
+        )
+        .track_scroll(self.scroll_handle.clone())
+        .flex_grow()
+    }
+
     fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
         if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) {
             return None;
@@ -142,12 +336,8 @@ impl BreakpointList {
     }
 }
 impl Render for BreakpointList {
-    fn render(
-        &mut self,
-        _window: &mut ui::Window,
-        cx: &mut ui::Context<Self>,
-    ) -> impl ui::IntoElement {
-        let old_len = self.breakpoints.len();
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
+        // let old_len = self.breakpoints.len();
         let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
         self.breakpoints.clear();
         let weak = cx.weak_entity();
@@ -183,7 +373,7 @@ impl Render for BreakpointList {
                     .map(ToOwned::to_owned)
                     .map(SharedString::from)?;
                 let weak = weak.clone();
-                let line = format!("Line {}", breakpoint.row + 1).into();
+                let line = breakpoint.row + 1;
                 Some(BreakpointEntry {
                     kind: BreakpointEntryKind::LineBreakpoint(LineBreakpoint {
                         name,
@@ -209,11 +399,9 @@ impl Render for BreakpointList {
                 });
         self.breakpoints
             .extend(breakpoints.chain(exception_breakpoints));
-        if self.breakpoints.len() != old_len {
-            self.list_state.reset(self.breakpoints.len());
-        }
         v_flex()
             .id("breakpoint-list")
+            .key_context("BreakpointList")
             .track_focus(&self.focus_handle)
             .on_hover(cx.listener(|this, hovered, window, cx| {
                 if *hovered {
@@ -224,9 +412,16 @@ impl Render for BreakpointList {
                     this.hide_scrollbar(window, cx);
                 }
             }))
+            .on_action(cx.listener(Self::select_next))
+            .on_action(cx.listener(Self::select_previous))
+            .on_action(cx.listener(Self::select_first))
+            .on_action(cx.listener(Self::select_last))
+            .on_action(cx.listener(Self::confirm))
+            .on_action(cx.listener(Self::toggle_enable_breakpoint))
+            .on_action(cx.listener(Self::unset_breakpoint))
             .size_full()
             .m_0p5()
-            .child(list(self.list_state.clone()).flex_grow())
+            .child(self.render_list(window, cx))
             .children(self.render_vertical_scrollbar(cx))
     }
 }
@@ -234,55 +429,58 @@ impl Render for BreakpointList {
 struct LineBreakpoint {
     name: SharedString,
     dir: Option<SharedString>,
-    line: SharedString,
+    line: u32,
     breakpoint: SourceBreakpoint,
 }
 
 impl LineBreakpoint {
-    fn render(self, weak: WeakEntity<BreakpointList>) -> ListItem {
-        let LineBreakpoint {
-            name,
-            dir,
-            line,
-            breakpoint,
-        } = self;
-        let icon_name = if breakpoint.state.is_enabled() {
+    fn render(
+        &mut self,
+        ix: usize,
+        focus_handle: FocusHandle,
+        weak: WeakEntity<BreakpointList>,
+    ) -> ListItem {
+        let icon_name = if self.breakpoint.state.is_enabled() {
             IconName::DebugBreakpoint
         } else {
             IconName::DebugDisabledBreakpoint
         };
-        let path = breakpoint.path;
-        let row = breakpoint.row;
+        let path = self.breakpoint.path.clone();
+        let row = self.breakpoint.row;
+        let is_enabled = self.breakpoint.state.is_enabled();
         let indicator = div()
             .id(SharedString::from(format!(
                 "breakpoint-ui-toggle-{:?}/{}:{}",
-                dir, name, line
+                self.dir, self.name, self.line
             )))
             .cursor_pointer()
-            .tooltip(Tooltip::text(if breakpoint.state.is_enabled() {
-                "Disable Breakpoint"
-            } else {
-                "Enable Breakpoint"
-            }))
+            .tooltip({
+                let focus_handle = focus_handle.clone();
+                move |window, cx| {
+                    Tooltip::for_action_in(
+                        if is_enabled {
+                            "Disable Breakpoint"
+                        } else {
+                            "Enable Breakpoint"
+                        },
+                        &ToggleEnableBreakpoint,
+                        &focus_handle,
+                        window,
+                        cx,
+                    )
+                }
+            })
             .on_click({
                 let weak = weak.clone();
                 let path = path.clone();
                 move |_, _, cx| {
-                    weak.update(cx, |this, cx| {
-                        this.breakpoint_store.update(cx, |this, cx| {
-                            if let Some((buffer, breakpoint)) =
-                                this.breakpoint_at_row(&path, row, cx)
-                            {
-                                this.toggle_breakpoint(
-                                    buffer,
-                                    breakpoint,
-                                    BreakpointEditAction::InvertState,
-                                    cx,
-                                );
-                            } else {
-                                log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
-                            }
-                        })
+                    weak.update(cx, |breakpoint_list, cx| {
+                        breakpoint_list.edit_line_breakpoint(
+                            path.clone(),
+                            row,
+                            BreakpointEditAction::InvertState,
+                            cx,
+                        );
                     })
                     .ok();
                 }
@@ -291,8 +489,17 @@ impl LineBreakpoint {
             .on_mouse_down(MouseButton::Left, move |_, _, _| {});
         ListItem::new(SharedString::from(format!(
             "breakpoint-ui-item-{:?}/{}:{}",
-            dir, name, line
+            self.dir, self.name, self.line
         )))
+        .on_click({
+            let weak = weak.clone();
+            move |_, _, cx| {
+                weak.update(cx, |breakpoint_list, cx| {
+                    breakpoint_list.select_ix(Some(ix), cx);
+                })
+                .ok();
+            }
+        })
         .start_slot(indicator)
         .rounded()
         .on_secondary_mouse_down(|_, _, cx| {
@@ -302,7 +509,7 @@ impl LineBreakpoint {
             IconButton::new(
                 SharedString::from(format!(
                     "breakpoint-ui-on-click-go-to-line-remove-{:?}/{}:{}",
-                    dir, name, line
+                    self.dir, self.name, self.line
                 )),
                 IconName::Close,
             )
@@ -310,103 +517,60 @@ impl LineBreakpoint {
                 let weak = weak.clone();
                 let path = path.clone();
                 move |_, _, cx| {
-                    weak.update(cx, |this, cx| {
-                        this.breakpoint_store.update(cx, |this, cx| {
-                            if let Some((buffer, breakpoint)) =
-                                this.breakpoint_at_row(&path, row, cx)
-                            {
-                                this.toggle_breakpoint(
-                                    buffer,
-                                    breakpoint,
-                                    BreakpointEditAction::Toggle,
-                                    cx,
-                                );
-                            } else {
-                                log::error!("Couldn't find breakpoint at row event though it exists: row {row}")
-                            }
-                        })
+                    weak.update(cx, |breakpoint_list, cx| {
+                        breakpoint_list.edit_line_breakpoint(
+                            path.clone(),
+                            row,
+                            BreakpointEditAction::Toggle,
+                            cx,
+                        );
                     })
                     .ok();
                 }
             })
-            .icon_size(ui::IconSize::XSmall),
+            .tooltip(move |window, cx| {
+                Tooltip::for_action_in(
+                    "Unset Breakpoint",
+                    &UnsetBreakpoint,
+                    &focus_handle,
+                    window,
+                    cx,
+                )
+            })
+            .icon_size(ui::IconSize::Indicator),
         )
         .child(
             v_flex()
+                .py_1()
+                .gap_1()
+                .min_h(px(22.))
+                .justify_center()
                 .id(SharedString::from(format!(
                     "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
-                    dir, name, line
+                    self.dir, self.name, self.line
                 )))
                 .on_click(move |_, window, cx| {
-                    let path = path.clone();
-                    let weak = weak.clone();
-                    let row = breakpoint.row;
-                    maybe!({
-                        let task = weak
-                            .update(cx, |this, cx| {
-                                this.worktree_store.update(cx, |this, cx| {
-                                    this.find_or_create_worktree(path, false, cx)
-                                })
-                            })
-                            .ok()?;
-                        window
-                            .spawn(cx, async move |cx| {
-                                let (worktree, relative_path) = task.await?;
-                                let worktree_id = worktree.update(cx, |this, _| this.id())?;
-                                let item = weak
-                                    .update_in(cx, |this, window, cx| {
-                                        this.workspace.update(cx, |this, cx| {
-                                            this.open_path(
-                                                (worktree_id, relative_path),
-                                                None,
-                                                true,
-                                                window,
-                                                cx,
-                                            )
-                                        })
-                                    })??
-                                    .await?;
-                                if let Some(editor) = item.downcast::<Editor>() {
-                                    editor
-                                        .update_in(cx, |this, window, cx| {
-                                            this.go_to_singleton_buffer_point(
-                                                Point { row, column: 0 },
-                                                window,
-                                                cx,
-                                            );
-                                        })
-                                        .ok();
-                                }
-                                anyhow::Ok(())
-                            })
-                            .detach();
-
-                        Some(())
-                    });
+                    weak.update(cx, |breakpoint_list, cx| {
+                        breakpoint_list.select_ix(Some(ix), cx);
+                        breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
+                    })
+                    .ok();
                 })
                 .cursor_pointer()
-                .py_1()
-                .items_center()
                 .child(
                     h_flex()
                         .gap_1()
                         .child(
-                            Label::new(name)
+                            Label::new(format!("{}:{}", self.name, self.line))
                                 .size(LabelSize::Small)
                                 .line_height_style(ui::LineHeightStyle::UiLabel),
                         )
-                        .children(dir.map(|dir| {
+                        .children(self.dir.clone().map(|dir| {
                             Label::new(dir)
                                 .color(Color::Muted)
                                 .size(LabelSize::Small)
                                 .line_height_style(ui::LineHeightStyle::UiLabel)
                         })),
-                )
-                .child(
-                    Label::new(line)
-                        .size(LabelSize::XSmall)
-                        .color(Color::Muted)
-                        .line_height_style(ui::LineHeightStyle::UiLabel),
                 ),
         )
     }
@@ -419,17 +583,31 @@ struct ExceptionBreakpoint {
 }
 
 impl ExceptionBreakpoint {
-    fn render(self, list: WeakEntity<BreakpointList>) -> ListItem {
+    fn render(
+        &mut self,
+        ix: usize,
+        focus_handle: FocusHandle,
+        list: WeakEntity<BreakpointList>,
+    ) -> ListItem {
         let color = if self.is_enabled {
             Color::Debugger
         } else {
             Color::Muted
         };
         let id = SharedString::from(&self.id);
+        let is_enabled = self.is_enabled;
+
         ListItem::new(SharedString::from(format!(
             "exception-breakpoint-ui-item-{}",
             self.id
         )))
+        .on_click({
+            let list = list.clone();
+            move |_, _, cx| {
+                list.update(cx, |list, cx| list.select_ix(Some(ix), cx))
+                    .ok();
+            }
+        })
         .rounded()
         .on_secondary_mouse_down(|_, _, cx| {
             cx.stop_propagation();
@@ -440,38 +618,49 @@ impl ExceptionBreakpoint {
                     "exception-breakpoint-ui-item-{}-click-handler",
                     self.id
                 )))
-                .tooltip(Tooltip::text(if self.is_enabled {
-                    "Disable Exception Breakpoint"
-                } else {
-                    "Enable Exception Breakpoint"
-                }))
-                .on_click(move |_, _, cx| {
-                    list.update(cx, |this, cx| {
-                        this.session.update(cx, |this, cx| {
-                            this.toggle_exception_breakpoint(&id, cx);
-                        });
-                        cx.notify();
-                    })
-                    .ok();
+                .tooltip(move |window, cx| {
+                    Tooltip::for_action_in(
+                        if is_enabled {
+                            "Disable Exception Breakpoint"
+                        } else {
+                            "Enable Exception Breakpoint"
+                        },
+                        &ToggleEnableBreakpoint,
+                        &focus_handle,
+                        window,
+                        cx,
+                    )
+                })
+                .on_click({
+                    let list = list.clone();
+                    move |_, _, cx| {
+                        list.update(cx, |this, cx| {
+                            this.session.update(cx, |this, cx| {
+                                this.toggle_exception_breakpoint(&id, cx);
+                            });
+                            cx.notify();
+                        })
+                        .ok();
+                    }
                 })
                 .cursor_pointer()
                 .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
         )
         .child(
-            div()
+            v_flex()
                 .py_1()
                 .gap_1()
+                .min_h(px(22.))
+                .justify_center()
+                .id(("exception-breakpoint-label", ix))
                 .child(
-                    Label::new(self.data.label)
+                    Label::new(self.data.label.clone())
                         .size(LabelSize::Small)
                         .line_height_style(ui::LineHeightStyle::UiLabel),
                 )
-                .children(self.data.description.map(|description| {
-                    Label::new(description)
-                        .size(LabelSize::XSmall)
-                        .line_height_style(ui::LineHeightStyle::UiLabel)
-                        .color(Color::Muted)
-                })),
+                .when_some(self.data.description.clone(), |el, description| {
+                    el.tooltip(Tooltip::text(description))
+                }),
         )
     }
 }
@@ -486,14 +675,21 @@ struct BreakpointEntry {
     kind: BreakpointEntryKind,
     weak: WeakEntity<BreakpointList>,
 }
-impl RenderOnce for BreakpointEntry {
-    fn render(self, _: &mut ui::Window, _: &mut App) -> impl ui::IntoElement {
-        match self.kind {
+
+impl BreakpointEntry {
+    fn render(
+        &mut self,
+        ix: usize,
+        focus_handle: FocusHandle,
+        _: &mut Window,
+        _: &mut App,
+    ) -> ListItem {
+        match &mut self.kind {
             BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
-                line_breakpoint.render(self.weak)
+                line_breakpoint.render(ix, focus_handle, self.weak.clone())
             }
             BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
-                exception_breakpoint.render(self.weak)
+                exception_breakpoint.render(ix, focus_handle, self.weak.clone())
             }
         }
     }

crates/zed_actions/src/lib.rs 🔗

@@ -339,3 +339,5 @@ pub mod outline {
 
 actions!(zed_predict_onboarding, [OpenZedPredictOnboarding]);
 actions!(git_onboarding, [OpenGitIntegrationOnboarding]);
+
+actions!(debugger, [ToggleEnableBreakpoint, UnsetBreakpoint]);