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()