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

### 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
This commit is contained in:
Anthony Eid 2025-05-05 17:27:20 -04:00 committed by GitHub
parent 6e28400e17
commit 1aa92d9928
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 590 additions and 302 deletions

View file

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

View file

@ -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,66 +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);
},
))
.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<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 {

View file

@ -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);
}
}
}
}
}

View file

@ -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,
)

View file

@ -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 {}

View file

@ -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;

View file

@ -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")
}
}
}
}

View file

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