From ef76f07b1ec8e4bdf996666b5522c08add4b2288 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 6 Dec 2025 22:08:33 +0100 Subject: [PATCH] debugger: Make historic snapshot button a dropdown menu (#44307) This allows users to select any snapshot in the debugger history feature and go back to the active session snapshot. We also change variable names to use hsitoric snapshot instead of history and move the snapshot icon to the back of the debugger top control strip. https://github.com/user-attachments/assets/805de8d0-30c1-4719-8af7-2d47e1df1da4 Release Notes: - N/A Co-authored-by: Anthony Eid --- crates/debugger_ui/src/debugger_panel.rs | 212 ++++++++++++------ .../src/session/running/stack_frame_list.rs | 1 - crates/project/src/debugger/session.rs | 24 +- .../ui/src/components/button/split_button.rs | 32 ++- 4 files changed, 190 insertions(+), 79 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index fe81ac641196dbbc5ceecaede0785ca72336c261..bdb308aafd0d2899f17bef732ac38239c4df6dda 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -17,9 +17,9 @@ use dap::{client::SessionId, debugger_settings::DebuggerSettings}; use editor::{Editor, MultiBufferOffset, ToPoint}; use feature_flags::{FeatureFlag, FeatureFlagAppExt as _}; use gpui::{ - Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId, - EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, - WeakEntity, anchored, deferred, + Action, App, AsyncWindowContext, ClipboardItem, Context, Corner, DismissEvent, Entity, + EntityId, EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, + Subscription, Task, WeakEntity, anchored, deferred, }; use itertools::Itertools as _; @@ -32,7 +32,9 @@ use settings::Settings; use std::sync::{Arc, LazyLock}; use task::{DebugScenario, TaskContext}; use tree_sitter::{Query, StreamingIterator as _}; -use ui::{ContextMenu, Divider, PopoverMenuHandle, Tab, Tooltip, prelude::*}; +use ui::{ + ContextMenu, Divider, PopoverMenu, PopoverMenuHandle, SplitButton, Tab, Tooltip, prelude::*, +}; use util::rel_path::RelPath; use util::{ResultExt, debug_panic, maybe}; use workspace::SplitDirection; @@ -669,6 +671,12 @@ impl DebugPanel { ) }; + let thread_status = active_session + .as_ref() + .map(|session| session.read(cx).running_state()) + .and_then(|state| state.read(cx).thread_status(cx)) + .unwrap_or(project::debugger::session::ThreadStatus::Exited); + Some( div.w_full() .py_1() @@ -686,10 +694,6 @@ impl DebugPanel { .as_ref() .map(|session| session.read(cx).running_state()), |this, running_state| { - let thread_status = - running_state.read(cx).thread_status(cx).unwrap_or( - project::debugger::session::ThreadStatus::Exited, - ); let capabilities = running_state.read(cx).capabilities(cx); let supports_detach = running_state.read(cx).session().read(cx).is_attached(); @@ -812,34 +816,6 @@ impl DebugPanel { } }), ) - .when(cx.has_flag::(), |this| { - this.child( - IconButton::new( - "debug-back-in-history", - IconName::HistoryRerun, - ) - .icon_size(IconSize::Small) - .on_click( - window.listener_for( - running_state, - |this, _, _window, cx| { - this.session().update(cx, |session, cx| { - let ix = session - .active_history() - .unwrap_or_else(|| { - session.history().len() - }); - - session.go_back_to_history( - Some(ix.saturating_sub(1)), - cx, - ); - }) - }, - ), - ), - ) - }) .child(Divider::vertical()) .child( IconButton::new("debug-restart", IconName::RotateCcw) @@ -906,36 +882,53 @@ impl DebugPanel { } }), ) + .when(supports_detach, |div| { + div.child( + IconButton::new( + "debug-disconnect", + IconName::DebugDetach, + ) + .disabled( + thread_status != ThreadStatus::Stopped + && thread_status != ThreadStatus::Running, + ) + .icon_size(IconSize::Small) + .on_click(window.listener_for( + running_state, + |this, _, _, cx| { + this.detach_client(cx); + }, + )) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |_window, cx| { + Tooltip::for_action_in( + "Detach", + &Detach, + &focus_handle, + cx, + ) + } + }), + ) + }) .when( - supports_detach, - |div| { - div.child( - IconButton::new( - "debug-disconnect", - IconName::DebugDetach, - ) - .disabled( - thread_status != ThreadStatus::Stopped - && thread_status != ThreadStatus::Running, + cx.has_flag::(), + |this| { + this.child(Divider::vertical()).child( + SplitButton::new( + self.render_history_button( + &running_state, + thread_status, + window, + ), + self.render_history_toggle_button( + thread_status, + &running_state, + ) + .into_any_element(), ) - .icon_size(IconSize::Small) - .on_click(window.listener_for( - running_state, - |this, _, _, cx| { - this.detach_client(cx); - }, - )) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |_window, cx| { - Tooltip::for_action_in( - "Detach", - &Detach, - &focus_handle, - cx, - ) - } - }), + .style(ui::SplitButtonStyle::Outlined), ) }, ) @@ -1352,6 +1345,97 @@ impl DebugPanel { }); } } + + fn render_history_button( + &self, + running_state: &Entity, + thread_status: ThreadStatus, + window: &mut Window, + ) -> IconButton { + IconButton::new("debug-back-in-history", IconName::HistoryRerun) + .icon_size(IconSize::Small) + .on_click(window.listener_for(running_state, |this, _, _window, cx| { + this.session().update(cx, |session, cx| { + let ix = session + .active_snapshot_index() + .unwrap_or_else(|| session.historic_snapshots().len()); + + session.select_historic_snapshot(Some(ix.saturating_sub(1)), cx); + }) + })) + .disabled( + thread_status == ThreadStatus::Running || thread_status == ThreadStatus::Stepping, + ) + } + + fn render_history_toggle_button( + &self, + thread_status: ThreadStatus, + running_state: &Entity, + ) -> impl IntoElement { + PopoverMenu::new("debug-back-in-history-menu") + .trigger( + ui::ButtonLike::new_rounded_right("debug-back-in-history-menu-trigger") + .layer(ui::ElevationIndex::ModalSurface) + .size(ui::ButtonSize::None) + .child( + div() + .px_1() + .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), + ) + .disabled( + thread_status == ThreadStatus::Running + || thread_status == ThreadStatus::Stepping, + ), + ) + .menu({ + let running_state = running_state.clone(); + move |window, cx| { + let handler = + |ix: Option, running_state: Entity, cx: &mut App| { + running_state.update(cx, |state, cx| { + state.session().update(cx, |session, cx| { + session.select_historic_snapshot(ix, cx); + }) + }) + }; + + let running_state = running_state.clone(); + Some(ContextMenu::build( + window, + cx, + move |mut context_menu, _window, cx| { + let history = running_state + .read(cx) + .session() + .read(cx) + .historic_snapshots(); + + context_menu = context_menu.entry("Current State", None, { + let running_state = running_state.clone(); + move |_window, cx| { + handler(None, running_state.clone(), cx); + } + }); + context_menu = context_menu.separator(); + + for (ix, _) in history.iter().enumerate().rev() { + context_menu = + context_menu.entry(format!("history-{}", ix + 1), None, { + let running_state = running_state.clone(); + move |_window, cx| { + handler(Some(ix), running_state.clone(), cx); + } + }); + } + + context_menu + }, + )) + } + }) + .anchor(Corner::TopRight) + } } async fn register_session_inner( diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index 5ecdc0f74be97c01ace933fd3513535040599bac..a715e2248d14e253a9762c1bcf9f50c1db09d64c 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -227,7 +227,6 @@ impl StackFrameList { } this.update_in(cx, |this, window, cx| { this.build_entries(select_first, window, cx); - cx.notify(); }) .ok(); }) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 9d4d307f990bfc5f00190f74ce3f1f957e71bacc..65e903e178f6bb010c34315c1c5d5a7bf9cbe44e 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1436,15 +1436,23 @@ impl Session { .push_back(std::mem::take(&mut self.active_snapshot)); } - pub fn history(&self) -> &VecDeque { + pub fn historic_snapshots(&self) -> &VecDeque { &self.snapshots } - pub fn go_back_to_history(&mut self, ix: Option, cx: &mut Context<'_, Session>) { + pub fn select_historic_snapshot(&mut self, ix: Option, cx: &mut Context) { if self.selected_snapshot_index == ix { return; } + if self + .selected_snapshot_index + .is_some_and(|ix| self.snapshots.len() <= ix) + { + debug_panic!("Attempted to select a debug session with an out of bounds index"); + return; + } + self.selected_snapshot_index = ix; if ix.is_some() { @@ -1454,7 +1462,7 @@ impl Session { cx.notify(); } - pub fn active_history(&self) -> Option { + pub fn active_snapshot_index(&self) -> Option { self.selected_snapshot_index } @@ -2272,7 +2280,7 @@ impl Session { } pub fn continue_thread(&mut self, thread_id: ThreadId, cx: &mut Context) { - self.go_back_to_history(None, cx); + self.select_historic_snapshot(None, cx); let supports_single_thread_execution_requests = self.capabilities.supports_single_thread_execution_requests; @@ -2309,7 +2317,7 @@ impl Session { granularity: SteppingGranularity, cx: &mut Context, ) { - self.go_back_to_history(None, cx); + self.select_historic_snapshot(None, cx); let supports_single_thread_execution_requests = self.capabilities.supports_single_thread_execution_requests; @@ -2341,7 +2349,7 @@ impl Session { granularity: SteppingGranularity, cx: &mut Context, ) { - self.go_back_to_history(None, cx); + self.select_historic_snapshot(None, cx); let supports_single_thread_execution_requests = self.capabilities.supports_single_thread_execution_requests; @@ -2373,7 +2381,7 @@ impl Session { granularity: SteppingGranularity, cx: &mut Context, ) { - self.go_back_to_history(None, cx); + self.select_historic_snapshot(None, cx); let supports_single_thread_execution_requests = self.capabilities.supports_single_thread_execution_requests; @@ -2405,7 +2413,7 @@ impl Session { granularity: SteppingGranularity, cx: &mut Context, ) { - self.go_back_to_history(None, cx); + self.select_historic_snapshot(None, cx); let supports_single_thread_execution_requests = self.capabilities.supports_single_thread_execution_requests; diff --git a/crates/ui/src/components/button/split_button.rs b/crates/ui/src/components/button/split_button.rs index 14b9fd153cd5ad662467c75ff81700587667cee3..48f06ff3789e69b6d19cde2322932f4bd6e89f97 100644 --- a/crates/ui/src/components/button/split_button.rs +++ b/crates/ui/src/components/button/split_button.rs @@ -4,7 +4,7 @@ use gpui::{ }; use theme::ActiveTheme; -use crate::{ElevationIndex, h_flex}; +use crate::{ElevationIndex, IconButton, h_flex}; use super::ButtonLike; @@ -15,6 +15,23 @@ pub enum SplitButtonStyle { Transparent, } +pub enum SplitButtonKind { + ButtonLike(ButtonLike), + IconButton(IconButton), +} + +impl From for SplitButtonKind { + fn from(icon_button: IconButton) -> Self { + Self::IconButton(icon_button) + } +} + +impl From for SplitButtonKind { + fn from(button_like: ButtonLike) -> Self { + Self::ButtonLike(button_like) + } +} + /// /// A button with two parts: a primary action on the left and a secondary action on the right. /// /// The left side is a [`ButtonLike`] with the main action, while the right side can contain @@ -23,15 +40,15 @@ pub enum SplitButtonStyle { /// The two sections are visually separated by a divider, but presented as a unified control. #[derive(IntoElement)] pub struct SplitButton { - pub left: ButtonLike, - pub right: AnyElement, + left: SplitButtonKind, + right: AnyElement, style: SplitButtonStyle, } impl SplitButton { - pub fn new(left: ButtonLike, right: AnyElement) -> Self { + pub fn new(left: impl Into, right: AnyElement) -> Self { Self { - left, + left: left.into(), right, style: SplitButtonStyle::Filled, } @@ -56,7 +73,10 @@ impl RenderOnce for SplitButton { this.border_1() .border_color(cx.theme().colors().border.opacity(0.8)) }) - .child(div().flex_grow().child(self.left)) + .child(div().flex_grow().child(match self.left { + SplitButtonKind::ButtonLike(button) => button.into_any_element(), + SplitButtonKind::IconButton(icon) => icon.into_any_element(), + })) .child( div() .h_full()