diff --git a/Cargo.lock b/Cargo.lock index efd9b5b824..6f36e2ced5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4248,6 +4248,7 @@ dependencies = [ "util", "workspace", "workspace-hack", + "zed_actions", "zlog", ] diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 23971bc458..0817330c8b 100644 --- a/assets/keymaps/default-linux.json +++ b/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": { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index b8ea238f68..0bd8753233 100644 --- a/assets/keymaps/default-macos.json +++ b/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, diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index e306b7d76c..dfd317480a 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/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"] } diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index b1d8e81094..1091c992ef 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/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, breakpoint_store: Entity, worktree_store: Entity, - list_state: ListState, scrollbar_state: ScrollbarState, breakpoints: Vec, session: Entity, hide_scrollbar_task: Option>, show_scrollbar: bool, focus_handle: FocusHandle, + scroll_handle: UniformListScrollHandle, + selected_ix: Option, } impl Focusable for BreakpointList { @@ -56,38 +59,205 @@ 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 = 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, + row: u32, + action: BreakpointEditAction, + cx: &mut Context, + ) { + 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, + row: u32, + window: &mut Window, + cx: &mut Context, + ) { + 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 + .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, cx: &mut Context) { + 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) { + 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, + ) { + 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, + ) { + 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) { + 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) { + 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, + ) { + 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, + ) { + 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) { const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| { @@ -103,6 +273,30 @@ impl BreakpointList { })) } + fn render_list(&mut self, _window: &mut Window, cx: &mut Context) -> 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) -> Option> { 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, - ) -> impl ui::IntoElement { - let old_len = self.breakpoints.len(); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> 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, - line: SharedString, + line: u32, breakpoint: SourceBreakpoint, } impl LineBreakpoint { - fn render(self, weak: WeakEntity) -> 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, + ) -> 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 - .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) -> ListItem { + fn render( + &mut self, + ix: usize, + focus_handle: FocusHandle, + list: WeakEntity, + ) -> 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, } -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()) } } } diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 4619562ed7..aafe458688 100644 --- a/crates/zed_actions/src/lib.rs +++ b/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]);