debugger: Enable setting debug panel dock position to the side (#29914)

Anthony Eid created

### Preview
<img width="301" alt="Screenshot 2025-05-05 at 11 08 43 PM"
src="https://github.com/user-attachments/assets/aa445117-1c1c-4d90-a3bb-049f8417eca4"
/>


Setups the ground work to write debug panel persistence tests and allows
users to change the dock position of the debug panel.


Release Notes:

- N/A

Change summary

crates/dap/src/debugger_settings.rs         |  13 
crates/debugger_ui/src/debugger_panel.rs    | 579 ++++++++++++----------
crates/debugger_ui/src/persistence.rs       |  66 ++
crates/debugger_ui/src/session.rs           |  13 
crates/debugger_ui/src/session/running.rs   |  38 +
crates/debugger_ui/src/tests.rs             |   2 
crates/debugger_ui/src/tests/persistence.rs | 131 +++++
crates/workspace/src/pane_group.rs          |  16 
8 files changed, 573 insertions(+), 285 deletions(-)

Detailed changes

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,
         }
     }
 }

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<Workspace>,
     focus_handle: FocusHandle,
     context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
+    fs: Arc<dyn Fs>,
 }
 
 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,43 +605,143 @@ impl DebugPanel {
     fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
         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);
                                                 },
                                             ))
+                                            .disabled(thread_status != ThreadStatus::Stopped)
                                             .tooltip({
                                                 let focus_handle = focus_handle.clone();
                                                 move |window, cx| {
                                                     Tooltip::for_action_in(
-                                                        "Pause program",
-                                                        &Pause,
+                                                        "Step over",
+                                                        &StepOver,
                                                         &focus_handle,
                                                         window,
                                                         cx,
@@ -643,22 +749,23 @@ impl DebugPanel {
                                                 }
                                             }),
                                     )
-                                } else {
-                                    this.child(
-                                        IconButton::new("debug-continue", IconName::DebugContinue)
+                                    .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.continue_thread(cx),
+                                                |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(
-                                                        "Continue program",
-                                                        &Continue,
+                                                        "Step out",
+                                                        &StepOut,
                                                         &focus_handle,
                                                         window,
                                                         cx,
@@ -666,240 +773,173 @@ 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-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,
+                                                )
+                                            }
+                                        }),
                                     )
-                                    .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(Divider::vertical())
+                                    .child(
+                                        IconButton::new(
+                                            "debug-enable-breakpoint",
+                                            IconName::DebugDisabledBreakpoint,
+                                        )
+                                        .icon_size(IconSize::XSmall)
+                                        .shape(ui::IconButtonShape::Square)
+                                        .disabled(thread_status != ThreadStatus::Stopped),
                                     )
-                                    .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(
+                                        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(is_side, |this| this.justify_between())
+                        .child(
+                            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()))
+                                },
+                            ),
                         )
-                        .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())
-                        })
                         .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(), |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<Self>,
+        position: DockPosition,
+        window: &mut Window,
+        cx: &mut Context<Self>,
     ) {
+        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::<DebuggerSettings>(
+            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 {

crates/debugger_ui/src/persistence.rs 🔗

@@ -69,19 +69,22 @@ impl From<DebuggerPaneItem> 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<Vec<f32>>,
         children: Vec<SerializedPaneLayout>,
     },
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, Clone)]
 pub(crate) struct SerializedPane {
     pub children: Vec<DebuggerPaneItem>,
     pub active_item: Option<DebuggerPaneItem>,
@@ -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<Pane>, cx: &mut App) -> SerializedPane {
+fn serialize_pane(pane: &Entity<Pane>, cx: &App) -> SerializedPane {
     let pane = pane.read(cx);
     let children = pane
         .items()
@@ -150,20 +161,21 @@ fn serialize_pane(pane: &Entity<Pane>, cx: &mut App) -> SerializedPane {
     }
 }
 
-pub(crate) async fn get_serialized_pane_layout(
+pub(crate) async fn get_serialized_layout(
     adapter_name: impl AsRef<str>,
-) -> Option<SerializedPaneLayout> {
+) -> Option<SerializedLayout> {
     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::<SerializedPaneLayout>(&value).ok())
+        .and_then(|value| serde_json::from_str::<SerializedLayout>(&value).ok())
 }
 
 pub(crate) fn deserialize_pane_layout(
     serialized: SerializedPaneLayout,
+    should_invert: bool,
     workspace: &WeakEntity<Workspace>,
     project: &Entity<Project>,
     stack_frame_list: &Entity<StackFrameList>,
@@ -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<SerializedPaneLayout> {
+        let mut panes = vec![];
+
+        Self::inner_in_order(&self, &mut panes);
+        panes
+    }
+
+    fn inner_in_order(&self, panes: &mut Vec<SerializedPaneLayout>) {
+        match self {
+            SerializedPaneLayout::Pane(_) => panes.push((*self).clone()),
+            SerializedPaneLayout::Group {
+                axis: _,
+                flexes: _,
+                children,
+            } => {
+                for child in children {
+                    child.inner_in_order(panes);
+                }
+            }
+        }
+    }
+}

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<workspace::ViewId>,
@@ -40,7 +41,8 @@ impl DebugSession {
         workspace: WeakEntity<Workspace>,
         session: Entity<Session>,
         _debug_panel: WeakEntity<DebugPanel>,
-        serialized_pane_layout: Option<SerializedPaneLayout>,
+        serialized_layout: Option<SerializedLayout>,
+        dock_axis: Axis,
         window: &mut Window,
         cx: &mut App,
     ) -> Entity<Self> {
@@ -49,7 +51,8 @@ impl DebugSession {
                 session.clone(),
                 project.clone(),
                 workspace.clone(),
-                serialized_pane_layout,
+                serialized_layout,
+                dock_axis,
                 window,
                 cx,
             )

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<Entity<Pane>>,
     pane_close_subscriptions: HashMap<EntityId, Subscription>,
+    dock_axis: Axis,
     _schedule_serialize: Option<Task<()>>,
 }
 
@@ -510,7 +511,8 @@ impl RunningState {
         session: Entity<Session>,
         project: Entity<Project>,
         workspace: WeakEntity<Workspace>,
-        serialized_pane_layout: Option<SerializedPaneLayout>,
+        serialized_pane_layout: Option<SerializedLayout>,
+        dock_axis: Axis,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> 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<LoadedSourceList>,
         console: &Entity<Console>,
         breakpoints: &Entity<BreakpointList>,
+        dock_axis: Axis,
         subscriptions: &mut HashMap<EntityId, Subscription>,
         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<DebugPanelItemEvent> for RunningState {}

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;

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::<dap::requests::Threads, _>(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::<DebugPanel>(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")
+            }
+        }
+    }
+}

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