diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs index be89deea0d..e1176633e5 100644 --- a/crates/dap/src/debugger_settings.rs +++ b/crates/dap/src/debugger_settings.rs @@ -4,6 +4,14 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum DebugPanelDockPosition { + Left, + Bottom, + Right, +} + #[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)] #[serde(default)] pub struct DebuggerSettings { @@ -31,6 +39,10 @@ pub struct DebuggerSettings { /// /// Default: true pub format_dap_log_messages: bool, + /// The dock position of the debug panel + /// + /// Default: Bottom + pub dock: DebugPanelDockPosition, } impl Default for DebuggerSettings { @@ -42,6 +54,7 @@ impl Default for DebuggerSettings { timeout: 2000, log_dap_communications: true, format_dap_log_messages: true, + dock: DebugPanelDockPosition::Bottom, } } } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 3790dd126f..846c374b27 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -9,6 +9,7 @@ use crate::{new_session_modal::NewSessionModal, session::DebugSession}; use anyhow::Result; use command_palette_hooks::CommandPaletteFilter; use dap::adapters::DebugAdapterName; +use dap::debugger_settings::DebugPanelDockPosition; use dap::{ ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent, client::SessionId, debugger_settings::DebuggerSettings, @@ -21,11 +22,13 @@ use gpui::{ }; use language::Buffer; +use project::Fs; use project::debugger::session::{Session, SessionStateEvent}; use project::{Project, debugger::session::ThreadStatus}; use rpc::proto::{self}; use settings::Settings; use std::any::TypeId; +use std::sync::Arc; use task::{DebugScenario, TaskContext}; use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*}; use workspace::SplitDirection; @@ -62,6 +65,7 @@ pub struct DebugPanel { workspace: WeakEntity, focus_handle: FocusHandle, context_menu: Option<(Entity, Point, Subscription)>, + fs: Arc, } impl DebugPanel { @@ -82,6 +86,7 @@ impl DebugPanel { project, workspace: workspace.weak_handle(), context_menu: None, + fs: workspace.app_state().fs.clone(), }; debug_panel @@ -284,7 +289,7 @@ impl DebugPanel { }) .ok(); - let serialized_layout = persistence::get_serialized_pane_layout(adapter_name).await; + let serialized_layout = persistence::get_serialized_layout(adapter_name).await; let (debug_session, workspace) = this.update_in(cx, |this, window, cx| { this.sessions.retain(|session| { @@ -303,6 +308,7 @@ impl DebugPanel { session, cx.weak_entity(), serialized_layout, + this.position(window, cx).axis(), window, cx, ); @@ -599,66 +605,143 @@ impl DebugPanel { fn top_controls_strip(&self, window: &mut Window, cx: &mut Context) -> Option
{ let active_session = self.active_session.clone(); let focus_handle = self.focus_handle.clone(); + let is_side = self.position(window, cx).axis() == gpui::Axis::Horizontal; + let div = if is_side { v_flex() } else { h_flex() }; + let weak_panel = cx.weak_entity(); + + let new_session_button = || { + IconButton::new("debug-new-session", IconName::Plus) + .icon_size(IconSize::Small) + .on_click({ + let workspace = self.workspace.clone(); + let weak_panel = weak_panel.clone(); + let past_debug_definition = self.past_debug_definition.clone(); + move |_, window, cx| { + let weak_panel = weak_panel.clone(); + let past_debug_definition = past_debug_definition.clone(); + + let _ = workspace.update(cx, |this, cx| { + let workspace = cx.weak_entity(); + this.toggle_modal(window, cx, |window, cx| { + NewSessionModal::new( + past_debug_definition, + weak_panel, + workspace, + None, + window, + cx, + ) + }); + }); + } + }) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "New Debug Session", + &CreateDebuggingSession, + &focus_handle, + window, + cx, + ) + } + }) + }; Some( - h_flex() - .border_b_1() + div.border_b_1() .border_color(cx.theme().colors().border) .p_1() .justify_between() .w_full() + .when(is_side, |this| this.gap_1()) .child( - h_flex().gap_2().w_full().when_some( - active_session - .as_ref() - .map(|session| session.read(cx).running_state()), - |this, running_session| { - let thread_status = running_session - .read(cx) - .thread_status(cx) - .unwrap_or(project::debugger::session::ThreadStatus::Exited); - let capabilities = running_session.read(cx).capabilities(cx); - this.map(|this| { - if thread_status == ThreadStatus::Running { - this.child( - IconButton::new("debug-pause", IconName::DebugPause) + h_flex() + .child( + h_flex().gap_2().w_full().when_some( + active_session + .as_ref() + .map(|session| session.read(cx).running_state()), + |this, running_session| { + let thread_status = + running_session.read(cx).thread_status(cx).unwrap_or( + project::debugger::session::ThreadStatus::Exited, + ); + let capabilities = running_session.read(cx).capabilities(cx); + this.map(|this| { + if thread_status == ThreadStatus::Running { + this.child( + IconButton::new( + "debug-pause", + IconName::DebugPause, + ) + .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .on_click(window.listener_for( + &running_session, + |this, _, _window, cx| { + this.pause_thread(cx); + }, + )) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Pause program", + &Pause, + &focus_handle, + window, + cx, + ) + } + }), + ) + } else { + this.child( + IconButton::new( + "debug-continue", + IconName::DebugContinue, + ) + .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .on_click(window.listener_for( + &running_session, + |this, _, _window, cx| this.continue_thread(cx), + )) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Continue program", + &Continue, + &focus_handle, + window, + cx, + ) + } + }), + ) + } + }) + .child( + IconButton::new("debug-step-over", IconName::ArrowRight) .icon_size(IconSize::XSmall) .shape(ui::IconButtonShape::Square) .on_click(window.listener_for( &running_session, |this, _, _window, cx| { - this.pause_thread(cx); + this.step_over(cx); }, )) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Pause program", - &Pause, - &focus_handle, - window, - cx, - ) - } - }), - ) - } else { - this.child( - IconButton::new("debug-continue", IconName::DebugContinue) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) - .on_click(window.listener_for( - &running_session, - |this, _, _window, cx| this.continue_thread(cx), - )) .disabled(thread_status != ThreadStatus::Stopped) .tooltip({ let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( - "Continue program", - &Continue, + "Step over", + &StepOver, &focus_handle, window, cx, @@ -666,240 +749,197 @@ impl DebugPanel { } }), ) - } - }) - .child( - IconButton::new("debug-step-over", IconName::ArrowRight) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) - .on_click(window.listener_for( - &running_session, - |this, _, _window, cx| { - this.step_over(cx); - }, - )) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Step over", - &StepOver, - &focus_handle, - window, - cx, - ) - } - }), - ) - .child( - IconButton::new("debug-step-out", IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) - .on_click(window.listener_for( - &running_session, - |this, _, _window, cx| { - this.step_out(cx); - }, - )) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Step out", - &StepOut, - &focus_handle, - window, - cx, - ) - } - }), - ) - .child( - IconButton::new("debug-step-into", IconName::ArrowDownRight) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) - .on_click(window.listener_for( - &running_session, - |this, _, _window, cx| { - this.step_in(cx); - }, - )) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Step in", - &StepInto, - &focus_handle, - window, - cx, - ) - } - }), - ) - .child(Divider::vertical()) - .child( - IconButton::new( - "debug-enable-breakpoint", - IconName::DebugDisabledBreakpoint, - ) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) - .disabled(thread_status != ThreadStatus::Stopped), - ) - .child( - IconButton::new("debug-disable-breakpoint", IconName::CircleOff) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) - .disabled(thread_status != ThreadStatus::Stopped), - ) - .child( - IconButton::new("debug-disable-all-breakpoints", IconName::BugOff) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) - .disabled( - thread_status == ThreadStatus::Exited - || thread_status == ThreadStatus::Ended, + .child( + IconButton::new("debug-step-out", IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .on_click(window.listener_for( + &running_session, + |this, _, _window, cx| { + this.step_out(cx); + }, + )) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Step out", + &StepOut, + &focus_handle, + window, + cx, + ) + } + }), ) - .on_click(window.listener_for( - &running_session, - |this, _, _window, cx| { - this.toggle_ignore_breakpoints(cx); - }, - )) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Disable all breakpoints", - &ToggleIgnoreBreakpoints, - &focus_handle, - window, - cx, - ) - } - }), - ) - .child(Divider::vertical()) - .child( - IconButton::new("debug-restart", IconName::DebugRestart) - .icon_size(IconSize::XSmall) - .on_click(window.listener_for( - &running_session, - |this, _, _window, cx| { - this.restart_session(cx); - }, - )) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Restart", - &Restart, - &focus_handle, - window, - cx, - ) - } - }), - ) - .child( - IconButton::new("debug-stop", IconName::Power) - .icon_size(IconSize::XSmall) - .on_click(window.listener_for( - &running_session, - |this, _, _window, cx| { - this.stop_thread(cx); - }, - )) - .disabled( - thread_status != ThreadStatus::Stopped - && thread_status != ThreadStatus::Running, + .child( + IconButton::new( + "debug-step-into", + IconName::ArrowDownRight, + ) + .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .on_click(window.listener_for( + &running_session, + |this, _, _window, cx| { + this.step_in(cx); + }, + )) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Step in", + &StepInto, + &focus_handle, + window, + cx, + ) + } + }), ) - .tooltip({ - let focus_handle = focus_handle.clone(); - let label = if capabilities - .supports_terminate_threads_request - .unwrap_or_default() - { - "Terminate Thread" - } else { - "Terminate All Threads" - }; - move |window, cx| { - Tooltip::for_action_in( - label, - &Stop, - &focus_handle, - window, - cx, + .child(Divider::vertical()) + .child( + IconButton::new( + "debug-enable-breakpoint", + IconName::DebugDisabledBreakpoint, + ) + .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .disabled(thread_status != ThreadStatus::Stopped), + ) + .child( + IconButton::new( + "debug-disable-breakpoint", + IconName::CircleOff, + ) + .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .disabled(thread_status != ThreadStatus::Stopped), + ) + .child( + IconButton::new( + "debug-disable-all-breakpoints", + IconName::BugOff, + ) + .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .disabled( + thread_status == ThreadStatus::Exited + || thread_status == ThreadStatus::Ended, + ) + .on_click(window.listener_for( + &running_session, + |this, _, _window, cx| { + this.toggle_ignore_breakpoints(cx); + }, + )) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Disable all breakpoints", + &ToggleIgnoreBreakpoints, + &focus_handle, + window, + cx, + ) + } + }), + ) + .child(Divider::vertical()) + .child( + IconButton::new("debug-restart", IconName::DebugRestart) + .icon_size(IconSize::XSmall) + .on_click(window.listener_for( + &running_session, + |this, _, _window, cx| { + this.restart_session(cx); + }, + )) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Restart", + &Restart, + &focus_handle, + window, + cx, + ) + } + }), + ) + .child( + IconButton::new("debug-stop", IconName::Power) + .icon_size(IconSize::XSmall) + .on_click(window.listener_for( + &running_session, + |this, _, _window, cx| { + this.stop_thread(cx); + }, + )) + .disabled( + thread_status != ThreadStatus::Stopped + && thread_status != ThreadStatus::Running, ) - } - }), - ) - }, - ), + .tooltip({ + let focus_handle = focus_handle.clone(); + let label = if capabilities + .supports_terminate_threads_request + .unwrap_or_default() + { + "Terminate Thread" + } else { + "Terminate All Threads" + }; + move |window, cx| { + Tooltip::for_action_in( + label, + &Stop, + &focus_handle, + window, + cx, + ) + } + }), + ) + }, + ), + ) + .justify_around() + .when(is_side, |this| this.child(new_session_button())), ) .child( h_flex() .gap_2() - .when_some( - active_session - .as_ref() - .map(|session| session.read(cx).running_state()) - .cloned(), - |this, session| { - this.child( - session.update(cx, |this, cx| this.thread_dropdown(window, cx)), - ) - .child(Divider::vertical()) - }, - ) - .when_some(active_session.as_ref(), |this, session| { - let context_menu = self.sessions_drop_down_menu(session, window, cx); - this.child(context_menu).child(Divider::vertical()) - }) + .when(is_side, |this| this.justify_between()) .child( - IconButton::new("debug-new-session", IconName::Plus) - .icon_size(IconSize::Small) - .on_click({ - let workspace = self.workspace.clone(); - let weak_panel = cx.weak_entity(); - let past_debug_definition = self.past_debug_definition.clone(); - move |_, window, cx| { - let weak_panel = weak_panel.clone(); - let past_debug_definition = past_debug_definition.clone(); - - let _ = workspace.update(cx, |this, cx| { - let workspace = cx.weak_entity(); - this.toggle_modal(window, cx, |window, cx| { - NewSessionModal::new( - past_debug_definition, - weak_panel, - workspace, - None, - window, - cx, - ) - }); - }); - } + h_flex().when_some( + active_session + .as_ref() + .map(|session| session.read(cx).running_state()) + .cloned(), + |this, session| { + this.child( + session.update(cx, |this, cx| { + this.thread_dropdown(window, cx) + }), + ) + .when(!is_side, |this| this.gap_2().child(Divider::vertical())) + }, + ), + ) + .child( + h_flex() + .when_some(active_session.as_ref(), |this, session| { + let context_menu = + self.sessions_drop_down_menu(session, window, cx); + this.child(context_menu).gap_2().child(Divider::vertical()) }) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "New Debug Session", - &CreateDebuggingSession, - &focus_handle, - window, - cx, - ) - } - }), + .when(!is_side, |this| this.child(new_session_button())), ), ), ) @@ -967,20 +1007,45 @@ impl Panel for DebugPanel { "DebugPanel" } - fn position(&self, _window: &Window, _cx: &App) -> DockPosition { - DockPosition::Bottom + fn position(&self, _window: &Window, cx: &App) -> DockPosition { + match DebuggerSettings::get_global(cx).dock { + DebugPanelDockPosition::Left => DockPosition::Left, + DebugPanelDockPosition::Bottom => DockPosition::Bottom, + DebugPanelDockPosition::Right => DockPosition::Right, + } } - fn position_is_valid(&self, position: DockPosition) -> bool { - position == DockPosition::Bottom + fn position_is_valid(&self, _: DockPosition) -> bool { + true } fn set_position( &mut self, - _position: DockPosition, - _window: &mut Window, - _cx: &mut Context, + position: DockPosition, + window: &mut Window, + cx: &mut Context, ) { + if position.axis() != self.position(window, cx).axis() { + self.sessions.iter().for_each(|session_item| { + session_item.update(cx, |item, cx| { + item.running_state() + .update(cx, |state, _| state.invert_axies()) + }) + }); + } + + settings::update_settings_file::( + self.fs.clone(), + cx, + move |settings, _| { + let dock = match position { + DockPosition::Left => DebugPanelDockPosition::Left, + DockPosition::Bottom => DebugPanelDockPosition::Bottom, + DockPosition::Right => DebugPanelDockPosition::Right, + }; + settings.dock = dock; + }, + ); } fn size(&self, _window: &Window, _: &App) -> Pixels { diff --git a/crates/debugger_ui/src/persistence.rs b/crates/debugger_ui/src/persistence.rs index ce3a2c0811..bbbb8323fe 100644 --- a/crates/debugger_ui/src/persistence.rs +++ b/crates/debugger_ui/src/persistence.rs @@ -69,19 +69,22 @@ impl From for SharedString { } #[derive(Debug, Serialize, Deserialize)] -pub(crate) struct SerializedAxis(pub Axis); +pub(crate) struct SerializedLayout { + pub(crate) panes: SerializedPaneLayout, + pub(crate) dock_axis: Axis, +} -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub(crate) enum SerializedPaneLayout { Pane(SerializedPane), Group { - axis: SerializedAxis, + axis: Axis, flexes: Option>, children: Vec, }, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub(crate) struct SerializedPane { pub children: Vec, pub active_item: Option, @@ -91,7 +94,7 @@ const DEBUGGER_PANEL_PREFIX: &str = "debugger_panel_"; pub(crate) async fn serialize_pane_layout( adapter_name: DebugAdapterName, - pane_group: SerializedPaneLayout, + pane_group: SerializedLayout, ) -> anyhow::Result<()> { if let Ok(serialized_pane_group) = serde_json::to_string(&pane_group) { KEY_VALUE_STORE @@ -107,10 +110,18 @@ pub(crate) async fn serialize_pane_layout( } } -pub(crate) fn build_serialized_pane_layout( +pub(crate) fn build_serialized_layout( pane_group: &Member, - cx: &mut App, -) -> SerializedPaneLayout { + dock_axis: Axis, + cx: &App, +) -> SerializedLayout { + SerializedLayout { + dock_axis, + panes: build_serialized_pane_layout(pane_group, cx), + } +} + +pub(crate) fn build_serialized_pane_layout(pane_group: &Member, cx: &App) -> SerializedPaneLayout { match pane_group { Member::Axis(PaneAxis { axis, @@ -118,7 +129,7 @@ pub(crate) fn build_serialized_pane_layout( flexes, bounding_boxes: _, }) => SerializedPaneLayout::Group { - axis: SerializedAxis(*axis), + axis: *axis, children: members .iter() .map(|member| build_serialized_pane_layout(member, cx)) @@ -129,7 +140,7 @@ pub(crate) fn build_serialized_pane_layout( } } -fn serialize_pane(pane: &Entity, cx: &mut App) -> SerializedPane { +fn serialize_pane(pane: &Entity, cx: &App) -> SerializedPane { let pane = pane.read(cx); let children = pane .items() @@ -150,20 +161,21 @@ fn serialize_pane(pane: &Entity, cx: &mut App) -> SerializedPane { } } -pub(crate) async fn get_serialized_pane_layout( +pub(crate) async fn get_serialized_layout( adapter_name: impl AsRef, -) -> Option { +) -> Option { let key = format!("{DEBUGGER_PANEL_PREFIX}-{}", adapter_name.as_ref()); KEY_VALUE_STORE .read_kvp(&key) .log_err() .flatten() - .and_then(|value| serde_json::from_str::(&value).ok()) + .and_then(|value| serde_json::from_str::(&value).ok()) } pub(crate) fn deserialize_pane_layout( serialized: SerializedPaneLayout, + should_invert: bool, workspace: &WeakEntity, project: &Entity, stack_frame_list: &Entity, @@ -187,6 +199,7 @@ pub(crate) fn deserialize_pane_layout( for child in children { if let Some(new_member) = deserialize_pane_layout( child, + should_invert, workspace, project, stack_frame_list, @@ -213,7 +226,7 @@ pub(crate) fn deserialize_pane_layout( } Some(Member::Axis(PaneAxis::load( - axis.0, + if should_invert { axis.invert() } else { axis }, members, flexes.clone(), ))) @@ -307,3 +320,28 @@ pub(crate) fn deserialize_pane_layout( } } } + +#[cfg(test)] +impl SerializedPaneLayout { + pub(crate) fn in_order(&self) -> Vec { + let mut panes = vec![]; + + Self::inner_in_order(&self, &mut panes); + panes + } + + fn inner_in_order(&self, panes: &mut Vec) { + match self { + SerializedPaneLayout::Pane(_) => panes.push((*self).clone()), + SerializedPaneLayout::Group { + axis: _, + flexes: _, + children, + } => { + for child in children { + child.inner_in_order(panes); + } + } + } + } +} diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index aa06ea1961..b70b8cdedf 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -3,7 +3,9 @@ pub mod running; use std::sync::OnceLock; use dap::client::SessionId; -use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity}; +use gpui::{ + App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, +}; use project::Project; use project::debugger::session::Session; use project::worktree_store::WorktreeStore; @@ -15,8 +17,7 @@ use workspace::{ item::{self, Item}, }; -use crate::debugger_panel::DebugPanel; -use crate::persistence::SerializedPaneLayout; +use crate::{debugger_panel::DebugPanel, persistence::SerializedLayout}; pub struct DebugSession { remote_id: Option, @@ -40,7 +41,8 @@ impl DebugSession { workspace: WeakEntity, session: Entity, _debug_panel: WeakEntity, - serialized_pane_layout: Option, + serialized_layout: Option, + dock_axis: Axis, window: &mut Window, cx: &mut App, ) -> Entity { @@ -49,7 +51,8 @@ impl DebugSession { session.clone(), project.clone(), workspace.clone(), - serialized_pane_layout, + serialized_layout, + dock_axis, window, cx, ) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 3736a673ab..caf227cf9c 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -7,7 +7,7 @@ pub mod variable_list; use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration}; -use crate::persistence::{self, DebuggerPaneItem, SerializedPaneLayout}; +use crate::persistence::{self, DebuggerPaneItem, SerializedLayout}; use super::DebugPanelItemEvent; use anyhow::{Result, anyhow}; @@ -22,7 +22,7 @@ use dap::{ }; use futures::{SinkExt, channel::mpsc}; use gpui::{ - Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable, + Action as _, AnyView, AppContext, Axis, Entity, EntityId, EventEmitter, FocusHandle, Focusable, NoAction, Pixels, Point, Subscription, Task, WeakEntity, }; use language::Buffer; @@ -73,6 +73,7 @@ pub struct RunningState { panes: PaneGroup, active_pane: Option>, pane_close_subscriptions: HashMap, + dock_axis: Axis, _schedule_serialize: Option>, } @@ -510,7 +511,8 @@ impl RunningState { session: Entity, project: Entity, workspace: WeakEntity, - serialized_pane_layout: Option, + serialized_pane_layout: Option, + dock_axis: Axis, window: &mut Window, cx: &mut Context, ) -> Self { @@ -589,7 +591,8 @@ impl RunningState { let mut pane_close_subscriptions = HashMap::default(); let panes = if let Some(root) = serialized_pane_layout.and_then(|serialized_layout| { persistence::deserialize_pane_layout( - serialized_layout, + serialized_layout.panes, + dock_axis != serialized_layout.dock_axis, &workspace, &project, &stack_frame_list, @@ -617,6 +620,7 @@ impl RunningState { &loaded_source_list, &console, &breakpoint_list, + dock_axis, &mut pane_close_subscriptions, window, cx, @@ -643,6 +647,7 @@ impl RunningState { loaded_sources_list: loaded_source_list, pane_close_subscriptions, debug_terminal, + dock_axis, _schedule_serialize: None, } } @@ -1056,12 +1061,16 @@ impl RunningState { .timer(Duration::from_millis(100)) .await; - let Some((adapter_name, pane_group)) = this - .update(cx, |this, cx| { + let Some((adapter_name, pane_layout)) = this + .read_with(cx, |this, cx| { let adapter_name = this.session.read(cx).adapter(); ( adapter_name, - persistence::build_serialized_pane_layout(&this.panes.root, cx), + persistence::build_serialized_layout( + &this.panes.root, + this.dock_axis, + cx, + ), ) }) .ok() @@ -1069,7 +1078,7 @@ impl RunningState { return; }; - persistence::serialize_pane_layout(adapter_name, pane_group) + persistence::serialize_pane_layout(adapter_name, pane_layout) .await .log_err(); @@ -1195,6 +1204,11 @@ impl RunningState { &self.variable_list } + #[cfg(test)] + pub(crate) fn serialized_layout(&self, cx: &App) -> SerializedLayout { + persistence::build_serialized_layout(&self.panes.root, self.dock_axis, cx) + } + pub fn capabilities(&self, cx: &App) -> Capabilities { self.session().read(cx).capabilities().clone() } @@ -1408,6 +1422,7 @@ impl RunningState { loaded_source_list: &Entity, console: &Entity, breakpoints: &Entity, + dock_axis: Axis, subscriptions: &mut HashMap, window: &mut Window, cx: &mut Context<'_, RunningState>, @@ -1528,7 +1543,7 @@ impl RunningState { ); let group_root = workspace::PaneAxis::new( - gpui::Axis::Horizontal, + dock_axis.invert(), [leftmost_pane, center_pane, rightmost_pane] .into_iter() .map(workspace::Member::Pane) @@ -1537,6 +1552,11 @@ impl RunningState { Member::Axis(group_root) } + + pub(crate) fn invert_axies(&mut self) { + self.dock_axis = self.dock_axis.invert(); + self.panes.invert_axies(); + } } impl EventEmitter for RunningState {} diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index 59a6b7dd85..c99ccc12ea 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -23,6 +23,8 @@ mod debugger_panel; #[cfg(test)] mod module_list; #[cfg(test)] +mod persistence; +#[cfg(test)] mod stack_frame_list; #[cfg(test)] mod variable_list; diff --git a/crates/debugger_ui/src/tests/persistence.rs b/crates/debugger_ui/src/tests/persistence.rs new file mode 100644 index 0000000000..f5fb4f0ab2 --- /dev/null +++ b/crates/debugger_ui/src/tests/persistence.rs @@ -0,0 +1,131 @@ +use std::iter::zip; + +use crate::{ + debugger_panel::DebugPanel, + persistence::SerializedPaneLayout, + tests::{init_test, init_test_workspace, start_debug_session}, +}; +use dap::{StoppedEvent, StoppedEventReason, messages::Events}; +use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; +use project::{FakeFs, Project}; +use serde_json::json; +use util::path; +use workspace::{Panel, dock::DockPosition}; + +#[gpui::test] +async fn test_invert_axis_on_panel_position_change( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + fs.insert_tree( + path!("/project"), + json!({ + "main.rs": "fn main() {\n println!(\"Hello, world!\");\n}", + }), + ) + .await; + + let project = Project::test(fs, [path!("/project").as_ref()], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + // Start a debug session + let session = start_debug_session(&workspace, cx, |_| {}).unwrap(); + let client = session.update(cx, |session, _| session.adapter_client().unwrap()); + + // Setup thread response + client.on_request::(move |_, _| { + Ok(dap::ThreadsResponse { threads: vec![] }) + }); + + cx.run_until_parked(); + + client + .fake_event(Events::Stopped(StoppedEvent { + reason: StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + let (debug_panel, dock_position) = workspace + .update(cx, |workspace, window, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let dock_position = debug_panel.read(cx).position(window, cx); + (debug_panel, dock_position) + }) + .unwrap(); + + assert_eq!( + dock_position, + DockPosition::Bottom, + "Default dock position should be bottom for debug panel" + ); + + let pre_serialized_layout = debug_panel + .read_with(cx, |panel, cx| { + panel + .active_session() + .unwrap() + .read(cx) + .running_state() + .read(cx) + .serialized_layout(cx) + }) + .panes; + + let post_serialized_layout = debug_panel + .update_in(cx, |panel, window, cx| { + panel.set_position(DockPosition::Right, window, cx); + + panel + .active_session() + .unwrap() + .read(cx) + .running_state() + .read(cx) + .serialized_layout(cx) + }) + .panes; + + let pre_panes = pre_serialized_layout.in_order(); + let post_panes = post_serialized_layout.in_order(); + + assert_eq!(pre_panes.len(), post_panes.len()); + + for (pre, post) in zip(pre_panes, post_panes) { + match (pre, post) { + ( + SerializedPaneLayout::Group { + axis: pre_axis, + flexes: pre_flexes, + children: _, + }, + SerializedPaneLayout::Group { + axis: post_axis, + flexes: post_flexes, + children: _, + }, + ) => { + assert_ne!(pre_axis, post_axis); + assert_eq!(pre_flexes, post_flexes); + } + (SerializedPaneLayout::Pane(pre_pane), SerializedPaneLayout::Pane(post_pane)) => { + assert_eq!(pre_pane.children, post_pane.children); + assert_eq!(pre_pane.active_item, post_pane.active_item); + } + _ => { + panic!("Variants don't match") + } + } + } +} diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 9e0faf1e3c..e9c1e75c60 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -176,6 +176,10 @@ impl PaneGroup { }; self.pane_at_pixel_position(target) } + + pub fn invert_axies(&mut self) { + self.root.invert_pane_axies(); + } } #[derive(Debug, Clone)] @@ -441,6 +445,18 @@ impl Member { Member::Pane(pane) => panes.push(pane), } } + + fn invert_pane_axies(&mut self) { + match self { + Self::Axis(axis) => { + axis.axis = axis.axis.invert(); + for member in axis.members.iter_mut() { + member.invert_pane_axies(); + } + } + Self::Pane(_) => {} + } + } } #[derive(Debug, Clone)]