@@ -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::<DebuggerHistoryFeatureFlag>(), |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::<DebuggerHistoryFeatureFlag>(),
+ |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<RunningState>,
+ 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<RunningState>,
+ ) -> 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<usize>, running_state: Entity<RunningState>, 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(
@@ -1436,15 +1436,23 @@ impl Session {
.push_back(std::mem::take(&mut self.active_snapshot));
}
- pub fn history(&self) -> &VecDeque<SessionSnapshot> {
+ pub fn historic_snapshots(&self) -> &VecDeque<SessionSnapshot> {
&self.snapshots
}
- pub fn go_back_to_history(&mut self, ix: Option<usize>, cx: &mut Context<'_, Session>) {
+ pub fn select_historic_snapshot(&mut self, ix: Option<usize>, cx: &mut Context<Session>) {
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<usize> {
+ pub fn active_snapshot_index(&self) -> Option<usize> {
self.selected_snapshot_index
}
@@ -2272,7 +2280,7 @@ impl Session {
}
pub fn continue_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
- 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>,
) {
- 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>,
) {
- 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>,
) {
- 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>,
) {
- 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;
@@ -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<IconButton> for SplitButtonKind {
+ fn from(icon_button: IconButton) -> Self {
+ Self::IconButton(icon_button)
+ }
+}
+
+impl From<ButtonLike> 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<SplitButtonKind>, 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()