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 serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources}; 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)] #[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)]
#[serde(default)] #[serde(default)]
pub struct DebuggerSettings { pub struct DebuggerSettings {
@ -31,6 +39,10 @@ pub struct DebuggerSettings {
/// ///
/// Default: true /// Default: true
pub format_dap_log_messages: bool, pub format_dap_log_messages: bool,
/// The dock position of the debug panel
///
/// Default: Bottom
pub dock: DebugPanelDockPosition,
} }
impl Default for DebuggerSettings { impl Default for DebuggerSettings {
@ -42,6 +54,7 @@ impl Default for DebuggerSettings {
timeout: 2000, timeout: 2000,
log_dap_communications: true, log_dap_communications: true,
format_dap_log_messages: 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 anyhow::Result;
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;
use dap::adapters::DebugAdapterName; use dap::adapters::DebugAdapterName;
use dap::debugger_settings::DebugPanelDockPosition;
use dap::{ use dap::{
ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent, ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
client::SessionId, debugger_settings::DebuggerSettings, client::SessionId, debugger_settings::DebuggerSettings,
@ -21,11 +22,13 @@ use gpui::{
}; };
use language::Buffer; use language::Buffer;
use project::Fs;
use project::debugger::session::{Session, SessionStateEvent}; use project::debugger::session::{Session, SessionStateEvent};
use project::{Project, debugger::session::ThreadStatus}; use project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self}; use rpc::proto::{self};
use settings::Settings; use settings::Settings;
use std::any::TypeId; use std::any::TypeId;
use std::sync::Arc;
use task::{DebugScenario, TaskContext}; use task::{DebugScenario, TaskContext};
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*}; use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
use workspace::SplitDirection; use workspace::SplitDirection;
@ -62,6 +65,7 @@ pub struct DebugPanel {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
fs: Arc<dyn Fs>,
} }
impl DebugPanel { impl DebugPanel {
@ -82,6 +86,7 @@ impl DebugPanel {
project, project,
workspace: workspace.weak_handle(), workspace: workspace.weak_handle(),
context_menu: None, context_menu: None,
fs: workspace.app_state().fs.clone(),
}; };
debug_panel debug_panel
@ -284,7 +289,7 @@ impl DebugPanel {
}) })
.ok(); .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| { let (debug_session, workspace) = this.update_in(cx, |this, window, cx| {
this.sessions.retain(|session| { this.sessions.retain(|session| {
@ -303,6 +308,7 @@ impl DebugPanel {
session, session,
cx.weak_entity(), cx.weak_entity(),
serialized_layout, serialized_layout,
this.position(window, cx).axis(),
window, window,
cx, cx,
); );
@ -599,66 +605,143 @@ impl DebugPanel {
fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> { fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
let active_session = self.active_session.clone(); let active_session = self.active_session.clone();
let focus_handle = self.focus_handle.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( Some(
h_flex() div.border_b_1()
.border_b_1()
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.p_1() .p_1()
.justify_between() .justify_between()
.w_full() .w_full()
.when(is_side, |this| this.gap_1())
.child( .child(
h_flex().gap_2().w_full().when_some( h_flex()
active_session .child(
.as_ref() h_flex().gap_2().w_full().when_some(
.map(|session| session.read(cx).running_state()), active_session
|this, running_session| { .as_ref()
let thread_status = running_session .map(|session| session.read(cx).running_state()),
.read(cx) |this, running_session| {
.thread_status(cx) let thread_status =
.unwrap_or(project::debugger::session::ThreadStatus::Exited); running_session.read(cx).thread_status(cx).unwrap_or(
let capabilities = running_session.read(cx).capabilities(cx); project::debugger::session::ThreadStatus::Exited,
this.map(|this| { );
if thread_status == ThreadStatus::Running { let capabilities = running_session.read(cx).capabilities(cx);
this.child( this.map(|this| {
IconButton::new("debug-pause", IconName::DebugPause) 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) .icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square) .shape(ui::IconButtonShape::Square)
.on_click(window.listener_for( .on_click(window.listener_for(
&running_session, &running_session,
|this, _, _window, cx| { |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) .disabled(thread_status != ThreadStatus::Stopped)
.tooltip({ .tooltip({
let focus_handle = focus_handle.clone(); let focus_handle = focus_handle.clone();
move |window, cx| { move |window, cx| {
Tooltip::for_action_in( Tooltip::for_action_in(
"Continue program", "Step over",
&Continue, &StepOver,
&focus_handle, &focus_handle,
window, window,
cx, cx,
@ -666,240 +749,197 @@ impl DebugPanel {
} }
}), }),
) )
} .child(
}) IconButton::new("debug-step-out", IconName::ArrowUpRight)
.child( .icon_size(IconSize::XSmall)
IconButton::new("debug-step-over", IconName::ArrowRight) .shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall) .on_click(window.listener_for(
.shape(ui::IconButtonShape::Square) &running_session,
.on_click(window.listener_for( |this, _, _window, cx| {
&running_session, this.step_out(cx);
|this, _, _window, cx| { },
this.step_over(cx); ))
}, .disabled(thread_status != ThreadStatus::Stopped)
)) .tooltip({
.disabled(thread_status != ThreadStatus::Stopped) let focus_handle = focus_handle.clone();
.tooltip({ move |window, cx| {
let focus_handle = focus_handle.clone(); Tooltip::for_action_in(
move |window, cx| { "Step out",
Tooltip::for_action_in( &StepOut,
"Step over", &focus_handle,
&StepOver, window,
&focus_handle, cx,
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,
) )
.on_click(window.listener_for( .child(
&running_session, IconButton::new(
|this, _, _window, cx| { "debug-step-into",
this.toggle_ignore_breakpoints(cx); IconName::ArrowDownRight,
}, )
)) .icon_size(IconSize::XSmall)
.tooltip({ .shape(ui::IconButtonShape::Square)
let focus_handle = focus_handle.clone(); .on_click(window.listener_for(
move |window, cx| { &running_session,
Tooltip::for_action_in( |this, _, _window, cx| {
"Disable all breakpoints", this.step_in(cx);
&ToggleIgnoreBreakpoints, },
&focus_handle, ))
window, .disabled(thread_status != ThreadStatus::Stopped)
cx, .tooltip({
) let focus_handle = focus_handle.clone();
} move |window, cx| {
}), Tooltip::for_action_in(
) "Step in",
.child(Divider::vertical()) &StepInto,
.child( &focus_handle,
IconButton::new("debug-restart", IconName::DebugRestart) window,
.icon_size(IconSize::XSmall) cx,
.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({ .child(Divider::vertical())
let focus_handle = focus_handle.clone(); .child(
let label = if capabilities IconButton::new(
.supports_terminate_threads_request "debug-enable-breakpoint",
.unwrap_or_default() IconName::DebugDisabledBreakpoint,
{ )
"Terminate Thread" .icon_size(IconSize::XSmall)
} else { .shape(ui::IconButtonShape::Square)
"Terminate All Threads" .disabled(thread_status != ThreadStatus::Stopped),
}; )
move |window, cx| { .child(
Tooltip::for_action_in( IconButton::new(
label, "debug-disable-breakpoint",
&Stop, IconName::CircleOff,
&focus_handle, )
window, .icon_size(IconSize::XSmall)
cx, .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( .child(
h_flex() h_flex()
.gap_2() .gap_2()
.when_some( .when(is_side, |this| this.justify_between())
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())
})
.child( .child(
IconButton::new("debug-new-session", IconName::Plus) h_flex().when_some(
.icon_size(IconSize::Small) active_session
.on_click({ .as_ref()
let workspace = self.workspace.clone(); .map(|session| session.read(cx).running_state())
let weak_panel = cx.weak_entity(); .cloned(),
let past_debug_definition = self.past_debug_definition.clone(); |this, session| {
move |_, window, cx| { this.child(
let weak_panel = weak_panel.clone(); session.update(cx, |this, cx| {
let past_debug_definition = past_debug_definition.clone(); this.thread_dropdown(window, cx)
}),
let _ = workspace.update(cx, |this, cx| { )
let workspace = cx.weak_entity(); .when(!is_side, |this| this.gap_2().child(Divider::vertical()))
this.toggle_modal(window, cx, |window, cx| { },
NewSessionModal::new( ),
past_debug_definition, )
weak_panel, .child(
workspace, h_flex()
None, .when_some(active_session.as_ref(), |this, session| {
window, let context_menu =
cx, self.sessions_drop_down_menu(session, window, cx);
) this.child(context_menu).gap_2().child(Divider::vertical())
});
});
}
}) })
.tooltip({ .when(!is_side, |this| this.child(new_session_button())),
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"New Debug Session",
&CreateDebuggingSession,
&focus_handle,
window,
cx,
)
}
}),
), ),
), ),
) )
@ -967,20 +1007,45 @@ impl Panel for DebugPanel {
"DebugPanel" "DebugPanel"
} }
fn position(&self, _window: &Window, _cx: &App) -> DockPosition { fn position(&self, _window: &Window, cx: &App) -> DockPosition {
DockPosition::Bottom 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 { fn position_is_valid(&self, _: DockPosition) -> bool {
position == DockPosition::Bottom true
} }
fn set_position( fn set_position(
&mut self, &mut self,
_position: DockPosition, position: DockPosition,
_window: &mut Window, window: &mut Window,
_cx: &mut Context<Self>, 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 { fn size(&self, _window: &Window, _: &App) -> Pixels {

View file

@ -69,19 +69,22 @@ impl From<DebuggerPaneItem> for SharedString {
} }
#[derive(Debug, Serialize, Deserialize)] #[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 { pub(crate) enum SerializedPaneLayout {
Pane(SerializedPane), Pane(SerializedPane),
Group { Group {
axis: SerializedAxis, axis: Axis,
flexes: Option<Vec<f32>>, flexes: Option<Vec<f32>>,
children: Vec<SerializedPaneLayout>, children: Vec<SerializedPaneLayout>,
}, },
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub(crate) struct SerializedPane { pub(crate) struct SerializedPane {
pub children: Vec<DebuggerPaneItem>, pub children: Vec<DebuggerPaneItem>,
pub active_item: Option<DebuggerPaneItem>, pub active_item: Option<DebuggerPaneItem>,
@ -91,7 +94,7 @@ const DEBUGGER_PANEL_PREFIX: &str = "debugger_panel_";
pub(crate) async fn serialize_pane_layout( pub(crate) async fn serialize_pane_layout(
adapter_name: DebugAdapterName, adapter_name: DebugAdapterName,
pane_group: SerializedPaneLayout, pane_group: SerializedLayout,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if let Ok(serialized_pane_group) = serde_json::to_string(&pane_group) { if let Ok(serialized_pane_group) = serde_json::to_string(&pane_group) {
KEY_VALUE_STORE 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, pane_group: &Member,
cx: &mut App, dock_axis: Axis,
) -> SerializedPaneLayout { 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 { match pane_group {
Member::Axis(PaneAxis { Member::Axis(PaneAxis {
axis, axis,
@ -118,7 +129,7 @@ pub(crate) fn build_serialized_pane_layout(
flexes, flexes,
bounding_boxes: _, bounding_boxes: _,
}) => SerializedPaneLayout::Group { }) => SerializedPaneLayout::Group {
axis: SerializedAxis(*axis), axis: *axis,
children: members children: members
.iter() .iter()
.map(|member| build_serialized_pane_layout(member, cx)) .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 pane = pane.read(cx);
let children = pane let children = pane
.items() .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>, adapter_name: impl AsRef<str>,
) -> Option<SerializedPaneLayout> { ) -> Option<SerializedLayout> {
let key = format!("{DEBUGGER_PANEL_PREFIX}-{}", adapter_name.as_ref()); let key = format!("{DEBUGGER_PANEL_PREFIX}-{}", adapter_name.as_ref());
KEY_VALUE_STORE KEY_VALUE_STORE
.read_kvp(&key) .read_kvp(&key)
.log_err() .log_err()
.flatten() .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( pub(crate) fn deserialize_pane_layout(
serialized: SerializedPaneLayout, serialized: SerializedPaneLayout,
should_invert: bool,
workspace: &WeakEntity<Workspace>, workspace: &WeakEntity<Workspace>,
project: &Entity<Project>, project: &Entity<Project>,
stack_frame_list: &Entity<StackFrameList>, stack_frame_list: &Entity<StackFrameList>,
@ -187,6 +199,7 @@ pub(crate) fn deserialize_pane_layout(
for child in children { for child in children {
if let Some(new_member) = deserialize_pane_layout( if let Some(new_member) = deserialize_pane_layout(
child, child,
should_invert,
workspace, workspace,
project, project,
stack_frame_list, stack_frame_list,
@ -213,7 +226,7 @@ pub(crate) fn deserialize_pane_layout(
} }
Some(Member::Axis(PaneAxis::load( Some(Member::Axis(PaneAxis::load(
axis.0, if should_invert { axis.invert() } else { axis },
members, members,
flexes.clone(), 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 std::sync::OnceLock;
use dap::client::SessionId; 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::Project;
use project::debugger::session::Session; use project::debugger::session::Session;
use project::worktree_store::WorktreeStore; use project::worktree_store::WorktreeStore;
@ -15,8 +17,7 @@ use workspace::{
item::{self, Item}, item::{self, Item},
}; };
use crate::debugger_panel::DebugPanel; use crate::{debugger_panel::DebugPanel, persistence::SerializedLayout};
use crate::persistence::SerializedPaneLayout;
pub struct DebugSession { pub struct DebugSession {
remote_id: Option<workspace::ViewId>, remote_id: Option<workspace::ViewId>,
@ -40,7 +41,8 @@ impl DebugSession {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
session: Entity<Session>, session: Entity<Session>,
_debug_panel: WeakEntity<DebugPanel>, _debug_panel: WeakEntity<DebugPanel>,
serialized_pane_layout: Option<SerializedPaneLayout>, serialized_layout: Option<SerializedLayout>,
dock_axis: Axis,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Entity<Self> { ) -> Entity<Self> {
@ -49,7 +51,8 @@ impl DebugSession {
session.clone(), session.clone(),
project.clone(), project.clone(),
workspace.clone(), workspace.clone(),
serialized_pane_layout, serialized_layout,
dock_axis,
window, window,
cx, cx,
) )

View file

@ -7,7 +7,7 @@ pub mod variable_list;
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration}; 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 super::DebugPanelItemEvent;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
@ -22,7 +22,7 @@ use dap::{
}; };
use futures::{SinkExt, channel::mpsc}; use futures::{SinkExt, channel::mpsc};
use gpui::{ 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, NoAction, Pixels, Point, Subscription, Task, WeakEntity,
}; };
use language::Buffer; use language::Buffer;
@ -73,6 +73,7 @@ pub struct RunningState {
panes: PaneGroup, panes: PaneGroup,
active_pane: Option<Entity<Pane>>, active_pane: Option<Entity<Pane>>,
pane_close_subscriptions: HashMap<EntityId, Subscription>, pane_close_subscriptions: HashMap<EntityId, Subscription>,
dock_axis: Axis,
_schedule_serialize: Option<Task<()>>, _schedule_serialize: Option<Task<()>>,
} }
@ -510,7 +511,8 @@ impl RunningState {
session: Entity<Session>, session: Entity<Session>,
project: Entity<Project>, project: Entity<Project>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
serialized_pane_layout: Option<SerializedPaneLayout>, serialized_pane_layout: Option<SerializedLayout>,
dock_axis: Axis,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
@ -589,7 +591,8 @@ impl RunningState {
let mut pane_close_subscriptions = HashMap::default(); let mut pane_close_subscriptions = HashMap::default();
let panes = if let Some(root) = serialized_pane_layout.and_then(|serialized_layout| { let panes = if let Some(root) = serialized_pane_layout.and_then(|serialized_layout| {
persistence::deserialize_pane_layout( persistence::deserialize_pane_layout(
serialized_layout, serialized_layout.panes,
dock_axis != serialized_layout.dock_axis,
&workspace, &workspace,
&project, &project,
&stack_frame_list, &stack_frame_list,
@ -617,6 +620,7 @@ impl RunningState {
&loaded_source_list, &loaded_source_list,
&console, &console,
&breakpoint_list, &breakpoint_list,
dock_axis,
&mut pane_close_subscriptions, &mut pane_close_subscriptions,
window, window,
cx, cx,
@ -643,6 +647,7 @@ impl RunningState {
loaded_sources_list: loaded_source_list, loaded_sources_list: loaded_source_list,
pane_close_subscriptions, pane_close_subscriptions,
debug_terminal, debug_terminal,
dock_axis,
_schedule_serialize: None, _schedule_serialize: None,
} }
} }
@ -1056,12 +1061,16 @@ impl RunningState {
.timer(Duration::from_millis(100)) .timer(Duration::from_millis(100))
.await; .await;
let Some((adapter_name, pane_group)) = this let Some((adapter_name, pane_layout)) = this
.update(cx, |this, cx| { .read_with(cx, |this, cx| {
let adapter_name = this.session.read(cx).adapter(); let adapter_name = this.session.read(cx).adapter();
( (
adapter_name, adapter_name,
persistence::build_serialized_pane_layout(&this.panes.root, cx), persistence::build_serialized_layout(
&this.panes.root,
this.dock_axis,
cx,
),
) )
}) })
.ok() .ok()
@ -1069,7 +1078,7 @@ impl RunningState {
return; return;
}; };
persistence::serialize_pane_layout(adapter_name, pane_group) persistence::serialize_pane_layout(adapter_name, pane_layout)
.await .await
.log_err(); .log_err();
@ -1195,6 +1204,11 @@ impl RunningState {
&self.variable_list &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 { pub fn capabilities(&self, cx: &App) -> Capabilities {
self.session().read(cx).capabilities().clone() self.session().read(cx).capabilities().clone()
} }
@ -1408,6 +1422,7 @@ impl RunningState {
loaded_source_list: &Entity<LoadedSourceList>, loaded_source_list: &Entity<LoadedSourceList>,
console: &Entity<Console>, console: &Entity<Console>,
breakpoints: &Entity<BreakpointList>, breakpoints: &Entity<BreakpointList>,
dock_axis: Axis,
subscriptions: &mut HashMap<EntityId, Subscription>, subscriptions: &mut HashMap<EntityId, Subscription>,
window: &mut Window, window: &mut Window,
cx: &mut Context<'_, RunningState>, cx: &mut Context<'_, RunningState>,
@ -1528,7 +1543,7 @@ impl RunningState {
); );
let group_root = workspace::PaneAxis::new( let group_root = workspace::PaneAxis::new(
gpui::Axis::Horizontal, dock_axis.invert(),
[leftmost_pane, center_pane, rightmost_pane] [leftmost_pane, center_pane, rightmost_pane]
.into_iter() .into_iter()
.map(workspace::Member::Pane) .map(workspace::Member::Pane)
@ -1537,6 +1552,11 @@ impl RunningState {
Member::Axis(group_root) 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 {} impl EventEmitter<DebugPanelItemEvent> for RunningState {}

View file

@ -23,6 +23,8 @@ mod debugger_panel;
#[cfg(test)] #[cfg(test)]
mod module_list; mod module_list;
#[cfg(test)] #[cfg(test)]
mod persistence;
#[cfg(test)]
mod stack_frame_list; mod stack_frame_list;
#[cfg(test)] #[cfg(test)]
mod variable_list; 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) self.pane_at_pixel_position(target)
} }
pub fn invert_axies(&mut self) {
self.root.invert_pane_axies();
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -441,6 +445,18 @@ impl Member {
Member::Pane(pane) => panes.push(pane), 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)] #[derive(Debug, Clone)]