debugger: Remember pane layout from previous debugger session (#28692)

Anthony Eid , Piotr Osiewicz , and Cole Miller created

This PR makes a debugger's pane layout persistent across session's that
use the same debug adapter.

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Cole Miller <m@cole-miller.net>

Change summary

Cargo.lock                                                |   1 
crates/db/src/kvp.rs                                      |   1 
crates/debugger_ui/Cargo.toml                             |   1 
crates/debugger_ui/src/debugger_panel.rs                  |  68 +
crates/debugger_ui/src/debugger_ui.rs                     |   1 
crates/debugger_ui/src/persistence.rs                     | 259 ++++++
crates/debugger_ui/src/session.rs                         |   3 
crates/debugger_ui/src/session/running.rs                 | 339 +++++---
crates/debugger_ui/src/session/running/breakpoint_list.rs |   2 
crates/debugger_ui/src/tests.rs                           |   1 
crates/debugger_ui/src/tests/debugger_panel.rs            |   6 
crates/debugger_ui/src/tests/variable_list.rs             |   2 
crates/project/src/debugger/session.rs                    |  12 
13 files changed, 548 insertions(+), 148 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4177,6 +4177,7 @@ dependencies = [
  "collections",
  "command_palette_hooks",
  "dap",
+ "db",
  "editor",
  "env_logger 0.11.8",
  "feature_flags",

crates/db/src/kvp.rs 🔗

@@ -1,6 +1,7 @@
 use sqlez_macros::sql;
 
 use crate::{define_connection, query};
+pub static DEBUGGER_PANEL_PREFIX: &str = "debugger_panel_";
 
 define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
     &[sql!(

crates/debugger_ui/Cargo.toml 🔗

@@ -28,6 +28,7 @@ client.workspace = true
 collections.workspace = true
 command_palette_hooks.workspace = true
 dap.workspace = true
+db.workspace = true
 editor.workspace = true
 feature_flags.workspace = true
 futures.workspace = true

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     ClearAllBreakpoints, Continue, CreateDebuggingSession, Disconnect, Pause, Restart, StepBack,
-    StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
+    StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, persistence,
 };
 use crate::{new_session_modal::NewSessionModal, session::DebugSession};
 use anyhow::{Result, anyhow};
@@ -293,35 +293,49 @@ impl DebugPanel {
                     );
                 };
 
-                let Some(project) = self.project.upgrade() else {
-                    return log::error!("Debug Panel out lived it's weak reference to Project");
-                };
+                let adapter_name = session.read(cx).adapter_name();
 
-                if self
-                    .sessions
-                    .iter()
-                    .any(|item| item.read(cx).session_id(cx) == *session_id)
-                {
-                    // We already have an item for this session.
-                    return;
-                }
-                let session_item = DebugSession::running(
-                    project,
-                    self.workspace.clone(),
-                    session,
-                    cx.weak_entity(),
-                    window,
-                    cx,
-                );
+                let session_id = *session_id;
+                cx.spawn_in(window, async move |this, cx| {
+                    let serialized_layout =
+                        persistence::get_serialized_pane_layout(adapter_name).await;
 
-                if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
-                    // We might want to make this an event subscription and only notify when a new thread is selected
-                    // This is used to filter the command menu correctly
-                    cx.observe(&running, |_, _, cx| cx.notify()).detach();
-                }
+                    this.update_in(cx, |this, window, cx| {
+                        let Some(project) = this.project.upgrade() else {
+                            return log::error!(
+                                "Debug Panel out lived it's weak reference to Project"
+                            );
+                        };
 
-                self.sessions.push(session_item.clone());
-                self.activate_session(session_item, window, cx);
+                        if this
+                            .sessions
+                            .iter()
+                            .any(|item| item.read(cx).session_id(cx) == session_id)
+                        {
+                            // We already have an item for this session.
+                            return;
+                        }
+                        let session_item = DebugSession::running(
+                            project,
+                            this.workspace.clone(),
+                            session,
+                            cx.weak_entity(),
+                            serialized_layout,
+                            window,
+                            cx,
+                        );
+
+                        if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
+                            // We might want to make this an event subscription and only notify when a new thread is selected
+                            // This is used to filter the command menu correctly
+                            cx.observe(&running, |_, _, cx| cx.notify()).detach();
+                        }
+
+                        this.sessions.push(session_item.clone());
+                        this.activate_session(session_item, window, cx);
+                    })
+                })
+                .detach();
             }
             dap_store::DapStoreEvent::RunInTerminal {
                 title,

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -13,6 +13,7 @@ use workspace::{ShutdownDebugAdapters, Workspace};
 pub mod attach_modal;
 pub mod debugger_panel;
 mod new_session_modal;
+mod persistence;
 pub(crate) mod session;
 
 #[cfg(test)]

crates/debugger_ui/src/persistence.rs 🔗

@@ -0,0 +1,259 @@
+use collections::HashMap;
+use db::kvp::KEY_VALUE_STORE;
+use gpui::{Axis, Context, Entity, EntityId, Focusable, Subscription, WeakEntity, Window};
+use project::Project;
+use serde::{Deserialize, Serialize};
+use ui::{App, SharedString};
+use util::ResultExt;
+use workspace::{Member, Pane, PaneAxis, Workspace};
+
+use crate::session::running::{
+    self, RunningState, SubView, breakpoint_list::BreakpointList, console::Console,
+    module_list::ModuleList, stack_frame_list::StackFrameList, variable_list::VariableList,
+};
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub(crate) enum DebuggerPaneItem {
+    Console,
+    Variables,
+    BreakpointList,
+    Frames,
+    Modules,
+}
+
+impl DebuggerPaneItem {
+    pub(crate) fn to_shared_string(self) -> SharedString {
+        match self {
+            DebuggerPaneItem::Console => SharedString::new_static("Console"),
+            DebuggerPaneItem::Variables => SharedString::new_static("Variables"),
+            DebuggerPaneItem::BreakpointList => SharedString::new_static("Breakpoints"),
+            DebuggerPaneItem::Frames => SharedString::new_static("Frames"),
+            DebuggerPaneItem::Modules => SharedString::new_static("Modules"),
+        }
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub(crate) struct SerializedAxis(pub Axis);
+
+#[derive(Debug, Serialize, Deserialize)]
+pub(crate) enum SerializedPaneLayout {
+    Pane(SerializedPane),
+    Group {
+        axis: SerializedAxis,
+        flexes: Option<Vec<f32>>,
+        children: Vec<SerializedPaneLayout>,
+    },
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub(crate) struct SerializedPane {
+    pub children: Vec<DebuggerPaneItem>,
+    pub active_item: Option<DebuggerPaneItem>,
+}
+
+pub(crate) async fn serialize_pane_layout(
+    adapter_name: SharedString,
+    pane_group: SerializedPaneLayout,
+) -> anyhow::Result<()> {
+    if let Ok(serialized_pane_group) = serde_json::to_string(&pane_group) {
+        KEY_VALUE_STORE
+            .write_kvp(
+                format!("{}-{adapter_name}", db::kvp::DEBUGGER_PANEL_PREFIX),
+                serialized_pane_group,
+            )
+            .await
+    } else {
+        Err(anyhow::anyhow!(
+            "Failed to serialize pane group with serde_json as a string"
+        ))
+    }
+}
+
+pub(crate) fn build_serialized_pane_layout(
+    pane_group: &Member,
+    cx: &mut App,
+) -> SerializedPaneLayout {
+    match pane_group {
+        Member::Axis(PaneAxis {
+            axis,
+            members,
+            flexes,
+            bounding_boxes: _,
+        }) => SerializedPaneLayout::Group {
+            axis: SerializedAxis(*axis),
+            children: members
+                .iter()
+                .map(|member| build_serialized_pane_layout(member, cx))
+                .collect::<Vec<_>>(),
+            flexes: Some(flexes.lock().clone()),
+        },
+        Member::Pane(pane_handle) => SerializedPaneLayout::Pane(serialize_pane(pane_handle, cx)),
+    }
+}
+
+fn serialize_pane(pane: &Entity<Pane>, cx: &mut App) -> SerializedPane {
+    let pane = pane.read(cx);
+    let children = pane
+        .items()
+        .filter_map(|item| {
+            item.act_as::<SubView>(cx)
+                .map(|view| view.read(cx).view_kind())
+        })
+        .collect::<Vec<_>>();
+
+    let active_item = pane
+        .active_item()
+        .and_then(|item| item.act_as::<SubView>(cx))
+        .map(|view| view.read(cx).view_kind());
+
+    SerializedPane {
+        children,
+        active_item,
+    }
+}
+
+pub(crate) async fn get_serialized_pane_layout(
+    adapter_name: impl AsRef<str>,
+) -> Option<SerializedPaneLayout> {
+    let key = format!(
+        "{}-{}",
+        db::kvp::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())
+}
+
+pub(crate) fn deserialize_pane_layout(
+    serialized: SerializedPaneLayout,
+    workspace: &WeakEntity<Workspace>,
+    project: &Entity<Project>,
+    stack_frame_list: &Entity<StackFrameList>,
+    variable_list: &Entity<VariableList>,
+    module_list: &Entity<ModuleList>,
+    console: &Entity<Console>,
+    breakpoint_list: &Entity<BreakpointList>,
+    subscriptions: &mut HashMap<EntityId, Subscription>,
+    window: &mut Window,
+    cx: &mut Context<RunningState>,
+) -> Option<Member> {
+    match serialized {
+        SerializedPaneLayout::Group {
+            axis,
+            flexes,
+            children,
+        } => {
+            let mut members = Vec::new();
+            for child in children {
+                if let Some(new_member) = deserialize_pane_layout(
+                    child,
+                    workspace,
+                    project,
+                    stack_frame_list,
+                    variable_list,
+                    module_list,
+                    console,
+                    breakpoint_list,
+                    subscriptions,
+                    window,
+                    cx,
+                ) {
+                    members.push(new_member);
+                }
+            }
+
+            if members.is_empty() {
+                return None;
+            }
+
+            if members.len() == 1 {
+                return Some(members.remove(0));
+            }
+
+            Some(Member::Axis(PaneAxis::load(
+                axis.0,
+                members,
+                flexes.clone(),
+            )))
+        }
+        SerializedPaneLayout::Pane(serialized_pane) => {
+            let pane = running::new_debugger_pane(workspace.clone(), project.clone(), window, cx);
+            subscriptions.insert(
+                pane.entity_id(),
+                cx.subscribe_in(&pane, window, RunningState::handle_pane_event),
+            );
+
+            let sub_views: Vec<_> = serialized_pane
+                .children
+                .iter()
+                .map(|child| match child {
+                    DebuggerPaneItem::Frames => Box::new(SubView::new(
+                        pane.focus_handle(cx),
+                        stack_frame_list.clone().into(),
+                        DebuggerPaneItem::Frames,
+                        None,
+                        cx,
+                    )),
+                    DebuggerPaneItem::Variables => Box::new(SubView::new(
+                        variable_list.focus_handle(cx),
+                        variable_list.clone().into(),
+                        DebuggerPaneItem::Variables,
+                        None,
+                        cx,
+                    )),
+                    DebuggerPaneItem::BreakpointList => Box::new(SubView::new(
+                        breakpoint_list.focus_handle(cx),
+                        breakpoint_list.clone().into(),
+                        DebuggerPaneItem::BreakpointList,
+                        None,
+                        cx,
+                    )),
+                    DebuggerPaneItem::Modules => Box::new(SubView::new(
+                        pane.focus_handle(cx),
+                        module_list.clone().into(),
+                        DebuggerPaneItem::Modules,
+                        None,
+                        cx,
+                    )),
+
+                    DebuggerPaneItem::Console => Box::new(SubView::new(
+                        pane.focus_handle(cx),
+                        console.clone().into(),
+                        DebuggerPaneItem::Console,
+                        Some(Box::new({
+                            let console = console.clone().downgrade();
+                            move |cx| {
+                                console
+                                    .read_with(cx, |console, cx| console.show_indicator(cx))
+                                    .unwrap_or_default()
+                            }
+                        })),
+                        cx,
+                    )),
+                })
+                .collect();
+
+            pane.update(cx, |pane, cx| {
+                let mut active_idx = 0;
+                for (idx, sub_view) in sub_views.into_iter().enumerate() {
+                    if serialized_pane
+                        .active_item
+                        .is_some_and(|active| active == sub_view.read(cx).view_kind())
+                    {
+                        active_idx = idx;
+                    }
+                    pane.add_item(sub_view, false, false, None, window, cx);
+                }
+
+                pane.activate_item(active_idx, false, false, window, cx);
+            });
+
+            Some(Member::Pane(pane.clone()))
+        }
+    }
+}

crates/debugger_ui/src/session.rs 🔗

@@ -16,6 +16,7 @@ use workspace::{
 };
 
 use crate::debugger_panel::DebugPanel;
+use crate::persistence::SerializedPaneLayout;
 
 pub(crate) enum DebugSessionState {
     Running(Entity<running::RunningState>),
@@ -52,6 +53,7 @@ impl DebugSession {
         workspace: WeakEntity<Workspace>,
         session: Entity<Session>,
         _debug_panel: WeakEntity<DebugPanel>,
+        serialized_pane_layout: Option<SerializedPaneLayout>,
         window: &mut Window,
         cx: &mut App,
     ) -> Entity<Self> {
@@ -60,6 +62,7 @@ impl DebugSession {
                 session.clone(),
                 project.clone(),
                 workspace.clone(),
+                serialized_pane_layout,
                 window,
                 cx,
             )

crates/debugger_ui/src/session/running.rs 🔗

@@ -1,11 +1,13 @@
-mod breakpoint_list;
-mod console;
-mod loaded_source_list;
-mod module_list;
+pub(crate) mod breakpoint_list;
+pub(crate) mod console;
+pub(crate) mod loaded_source_list;
+pub(crate) mod module_list;
 pub mod stack_frame_list;
 pub mod variable_list;
 
-use std::{any::Any, ops::ControlFlow, sync::Arc};
+use std::{any::Any, ops::ControlFlow, sync::Arc, time::Duration};
+
+use crate::persistence::{self, DebuggerPaneItem, SerializedPaneLayout};
 
 use super::DebugPanelItemEvent;
 use breakpoint_list::BreakpointList;
@@ -14,7 +16,7 @@ use console::Console;
 use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings};
 use gpui::{
     Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
-    NoAction, Subscription, WeakEntity,
+    NoAction, Subscription, Task, WeakEntity,
 };
 use loaded_source_list::LoadedSourceList;
 use module_list::ModuleList;
@@ -33,8 +35,8 @@ use ui::{
 use util::ResultExt;
 use variable_list::VariableList;
 use workspace::{
-    ActivePaneDecorator, DraggedTab, Item, Pane, PaneGroup, Workspace, item::TabContentParams,
-    move_item, pane::Event,
+    ActivePaneDecorator, DraggedTab, Item, Member, Pane, PaneGroup, Workspace,
+    item::TabContentParams, move_item, pane::Event,
 };
 
 pub struct RunningState {
@@ -51,6 +53,7 @@ pub struct RunningState {
     _console: Entity<Console>,
     panes: PaneGroup,
     pane_close_subscriptions: HashMap<EntityId, Subscription>,
+    _schedule_serialize: Option<Task<()>>,
 }
 
 impl Render for RunningState {
@@ -84,28 +87,32 @@ impl Render for RunningState {
     }
 }
 
-struct SubView {
+pub(crate) struct SubView {
     inner: AnyView,
     pane_focus_handle: FocusHandle,
-    tab_name: SharedString,
+    kind: DebuggerPaneItem,
     show_indicator: Box<dyn Fn(&App) -> bool>,
 }
 
 impl SubView {
-    fn new(
+    pub(crate) fn new(
         pane_focus_handle: FocusHandle,
         view: AnyView,
-        tab_name: SharedString,
+        kind: DebuggerPaneItem,
         show_indicator: Option<Box<dyn Fn(&App) -> bool>>,
         cx: &mut App,
     ) -> Entity<Self> {
         cx.new(|_| Self {
-            tab_name,
+            kind,
             inner: view,
             pane_focus_handle,
             show_indicator: show_indicator.unwrap_or(Box::new(|_| false)),
         })
     }
+
+    pub(crate) fn view_kind(&self) -> DebuggerPaneItem {
+        self.kind
+    }
 }
 impl Focusable for SubView {
     fn focus_handle(&self, _: &App) -> FocusHandle {
@@ -116,13 +123,19 @@ impl EventEmitter<()> for SubView {}
 impl Item for SubView {
     type Event = ();
 
+    /// This is used to serialize debugger pane layouts
+    /// A SharedString gets converted to a enum and back during serialization/deserialization.
+    fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
+        Some(self.kind.to_shared_string())
+    }
+
     fn tab_content(
         &self,
         params: workspace::item::TabContentParams,
         _: &Window,
         cx: &App,
     ) -> AnyElement {
-        let label = Label::new(self.tab_name.clone())
+        let label = Label::new(self.kind.to_shared_string())
             .size(ui::LabelSize::Small)
             .color(params.text_color())
             .line_height_style(ui::LineHeightStyle::UiLabel);
@@ -146,7 +159,7 @@ impl Render for SubView {
     }
 }
 
-fn new_debugger_pane(
+pub(crate) fn new_debugger_pane(
     workspace: WeakEntity<Workspace>,
     project: Entity<Project>,
     window: &mut Window,
@@ -185,7 +198,7 @@ fn new_debugger_pane(
                             new_debugger_pane(workspace.clone(), project.clone(), window, cx);
                         let _previous_subscription = running.pane_close_subscriptions.insert(
                             new_pane.entity_id(),
-                            cx.subscribe(&new_pane, RunningState::handle_pane_event),
+                            cx.subscribe_in(&new_pane, window, RunningState::handle_pane_event),
                         );
                         debug_assert!(_previous_subscription.is_none());
                         running
@@ -354,6 +367,7 @@ impl RunningState {
         session: Entity<Session>,
         project: Entity<Project>,
         workspace: WeakEntity<Workspace>,
+        serialized_pane_layout: Option<SerializedPaneLayout>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -382,6 +396,8 @@ impl RunningState {
             )
         });
 
+        let breakpoints = BreakpointList::new(session.clone(), workspace.clone(), &project, cx);
+
         let _subscriptions = vec![
             cx.observe(&module_list, |_, _, cx| cx.notify()),
             cx.subscribe_in(&session, window, |this, _, event, window, cx| {
@@ -407,112 +423,40 @@ impl RunningState {
             }),
         ];
 
-        let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
-        leftmost_pane.update(cx, |this, cx| {
-            this.add_item(
-                Box::new(SubView::new(
-                    this.focus_handle(cx),
-                    stack_frame_list.clone().into(),
-                    SharedString::new_static("Frames"),
-                    None,
-                    cx,
-                )),
-                true,
-                false,
-                None,
+        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,
+                &workspace,
+                &project,
+                &stack_frame_list,
+                &variable_list,
+                &module_list,
+                &console,
+                &breakpoints,
+                &mut pane_close_subscriptions,
                 window,
                 cx,
-            );
-            let breakpoints = BreakpointList::new(session.clone(), workspace.clone(), &project, cx);
-            this.add_item(
-                Box::new(SubView::new(
-                    breakpoints.focus_handle(cx),
-                    breakpoints.into(),
-                    SharedString::new_static("Breakpoints"),
-                    None,
-                    cx,
-                )),
-                true,
-                false,
-                None,
-                window,
-                cx,
-            );
-            this.activate_item(0, false, false, window, cx);
-        });
-        let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
-        center_pane.update(cx, |this, cx| {
-            this.add_item(
-                Box::new(SubView::new(
-                    variable_list.focus_handle(cx),
-                    variable_list.clone().into(),
-                    SharedString::new_static("Variables"),
-                    None,
-                    cx,
-                )),
-                true,
-                false,
-                None,
-                window,
-                cx,
-            );
-            this.add_item(
-                Box::new(SubView::new(
-                    this.focus_handle(cx),
-                    module_list.clone().into(),
-                    SharedString::new_static("Modules"),
-                    None,
-                    cx,
-                )),
-                false,
-                false,
-                None,
-                window,
-                cx,
-            );
-            this.activate_item(0, false, false, window, cx);
-        });
-        let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
-        rightmost_pane.update(cx, |this, cx| {
-            let weak_console = console.downgrade();
-            this.add_item(
-                Box::new(SubView::new(
-                    this.focus_handle(cx),
-                    console.clone().into(),
-                    SharedString::new_static("Console"),
-                    Some(Box::new(move |cx| {
-                        weak_console
-                            .read_with(cx, |console, cx| console.show_indicator(cx))
-                            .unwrap_or_default()
-                    })),
-                    cx,
-                )),
-                true,
-                false,
-                None,
+            )
+        }) {
+            workspace::PaneGroup::with_root(root)
+        } else {
+            pane_close_subscriptions.clear();
+            let root = Self::default_pane_layout(
+                project,
+                &workspace,
+                &stack_frame_list,
+                &variable_list,
+                &module_list,
+                &console,
+                breakpoints,
+                &mut pane_close_subscriptions,
                 window,
                 cx,
             );
-        });
-        let pane_close_subscriptions = HashMap::from_iter(
-            [&leftmost_pane, &center_pane, &rightmost_pane]
-                .into_iter()
-                .map(|entity| {
-                    (
-                        entity.entity_id(),
-                        cx.subscribe(entity, Self::handle_pane_event),
-                    )
-                }),
-        );
-        let group_root = workspace::PaneAxis::new(
-            gpui::Axis::Horizontal,
-            [leftmost_pane, center_pane, rightmost_pane]
-                .into_iter()
-                .map(workspace::Member::Pane)
-                .collect(),
-        );
 
-        let panes = PaneGroup::with_root(workspace::Member::Axis(group_root));
+            workspace::PaneGroup::with_root(root)
+        };
 
         Self {
             session,
@@ -528,21 +472,57 @@ impl RunningState {
             _module_list: module_list,
             _console: console,
             pane_close_subscriptions,
+            _schedule_serialize: None,
+        }
+    }
+
+    fn serialize_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        if self._schedule_serialize.is_none() {
+            self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| {
+                cx.background_executor()
+                    .timer(Duration::from_millis(100))
+                    .await;
+
+                let Some((adapter_name, pane_group)) = this
+                    .update(cx, |this, cx| {
+                        let adapter_name = this.session.read(cx).adapter_name();
+                        (
+                            adapter_name,
+                            persistence::build_serialized_pane_layout(&this.panes.root, cx),
+                        )
+                    })
+                    .ok()
+                else {
+                    return;
+                };
+
+                persistence::serialize_pane_layout(adapter_name, pane_group)
+                    .await
+                    .log_err();
+
+                this.update(cx, |this, _| {
+                    this._schedule_serialize.take();
+                })
+                .ok();
+            }));
         }
     }
 
-    fn handle_pane_event(
+    pub(crate) fn handle_pane_event(
         this: &mut RunningState,
-        source_pane: Entity<Pane>,
+        source_pane: &Entity<Pane>,
         event: &Event,
+        window: &mut Window,
         cx: &mut Context<RunningState>,
     ) {
+        this.serialize_layout(window, cx);
         if let Event::Remove { .. } = event {
             let _did_find_pane = this.panes.remove(&source_pane).is_ok();
             debug_assert!(_did_find_pane);
             cx.notify();
         }
     }
+
     pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
         if self.thread_id.is_some() {
             self.stack_frame_list
@@ -586,7 +566,7 @@ impl RunningState {
             .find_map(|pane| {
                 pane.read(cx)
                     .items_of_type::<SubView>()
-                    .position(|view| view.read(cx).tab_name == *"Modules")
+                    .position(|view| view.read(cx).view_kind().to_shared_string() == *"Modules")
                     .map(|view| (view, pane))
             })
             .unwrap();
@@ -802,6 +782,127 @@ impl RunningState {
             }),
         )
     }
+
+    fn default_pane_layout(
+        project: Entity<Project>,
+        workspace: &WeakEntity<Workspace>,
+        stack_frame_list: &Entity<StackFrameList>,
+        variable_list: &Entity<VariableList>,
+        module_list: &Entity<ModuleList>,
+        console: &Entity<Console>,
+        breakpoints: Entity<BreakpointList>,
+        subscriptions: &mut HashMap<EntityId, Subscription>,
+        window: &mut Window,
+        cx: &mut Context<'_, RunningState>,
+    ) -> Member {
+        let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
+        leftmost_pane.update(cx, |this, cx| {
+            this.add_item(
+                Box::new(SubView::new(
+                    this.focus_handle(cx),
+                    stack_frame_list.clone().into(),
+                    DebuggerPaneItem::Frames,
+                    None,
+                    cx,
+                )),
+                true,
+                false,
+                None,
+                window,
+                cx,
+            );
+            this.add_item(
+                Box::new(SubView::new(
+                    breakpoints.focus_handle(cx),
+                    breakpoints.into(),
+                    DebuggerPaneItem::BreakpointList,
+                    None,
+                    cx,
+                )),
+                true,
+                false,
+                None,
+                window,
+                cx,
+            );
+            this.activate_item(0, false, false, window, cx);
+        });
+        let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
+        center_pane.update(cx, |this, cx| {
+            this.add_item(
+                Box::new(SubView::new(
+                    variable_list.focus_handle(cx),
+                    variable_list.clone().into(),
+                    DebuggerPaneItem::Variables,
+                    None,
+                    cx,
+                )),
+                true,
+                false,
+                None,
+                window,
+                cx,
+            );
+            this.add_item(
+                Box::new(SubView::new(
+                    this.focus_handle(cx),
+                    module_list.clone().into(),
+                    DebuggerPaneItem::Modules,
+                    None,
+                    cx,
+                )),
+                false,
+                false,
+                None,
+                window,
+                cx,
+            );
+            this.activate_item(0, false, false, window, cx);
+        });
+        let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
+        rightmost_pane.update(cx, |this, cx| {
+            let weak_console = console.downgrade();
+            this.add_item(
+                Box::new(SubView::new(
+                    this.focus_handle(cx),
+                    console.clone().into(),
+                    DebuggerPaneItem::Console,
+                    Some(Box::new(move |cx| {
+                        weak_console
+                            .read_with(cx, |console, cx| console.show_indicator(cx))
+                            .unwrap_or_default()
+                    })),
+                    cx,
+                )),
+                true,
+                false,
+                None,
+                window,
+                cx,
+            );
+        });
+
+        subscriptions.extend(
+            [&leftmost_pane, &center_pane, &rightmost_pane]
+                .into_iter()
+                .map(|entity| {
+                    (
+                        entity.entity_id(),
+                        cx.subscribe_in(entity, window, Self::handle_pane_event),
+                    )
+                }),
+        );
+
+        let group_root = workspace::PaneAxis::new(
+            gpui::Axis::Horizontal,
+            [leftmost_pane, center_pane, rightmost_pane]
+                .into_iter()
+                .map(workspace::Member::Pane)
+                .collect(),
+        );
+
+        Member::Axis(group_root)
+    }
 }
 
 impl EventEmitter<DebugPanelItemEvent> for RunningState {}

crates/debugger_ui/src/session/running/breakpoint_list.rs 🔗

@@ -27,7 +27,7 @@ use ui::{
 use util::{ResultExt, maybe};
 use workspace::Workspace;
 
-pub(super) struct BreakpointList {
+pub(crate) struct BreakpointList {
     workspace: WeakEntity<Workspace>,
     breakpoint_store: Entity<BreakpointStore>,
     worktree_store: Entity<WorktreeStore>,

crates/debugger_ui/src/tests.rs 🔗

@@ -68,6 +68,7 @@ pub async fn init_test_workspace(
     workspace_handle
 }
 
+#[track_caller]
 pub fn active_debug_session_panel(
     workspace: WindowHandle<Workspace>,
     cx: &mut TestAppContext,

crates/debugger_ui/src/tests/debugger_panel.rs 🔗

@@ -81,6 +81,8 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
         })
         .await;
 
+    cx.run_until_parked();
+
     // assert we have a debug panel item before the session has stopped
     workspace
         .update(cx, |workspace, _window, cx| {
@@ -229,6 +231,8 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
         })
         .await;
 
+    cx.run_until_parked();
+
     // assert we have a debug panel item before the session has stopped
     workspace
         .update(cx, |workspace, _window, cx| {
@@ -1052,6 +1056,8 @@ async fn test_debug_panel_item_thread_status_reset_on_failure(
         }))
         .await;
 
+    cx.run_until_parked();
+
     let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| {
         item.mode()
             .as_running()

crates/debugger_ui/src/tests/variable_list.rs 🔗

@@ -1538,6 +1538,8 @@ async fn test_variable_list_only_sends_requests_when_rendering(
         })
         .await;
 
+    cx.run_until_parked();
+
     let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| {
         let state = item
             .mode()

crates/project/src/debugger/session.rs 🔗

@@ -30,7 +30,8 @@ use dap::{
 use futures::channel::oneshot;
 use futures::{FutureExt, future::Shared};
 use gpui::{
-    App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Task, WeakEntity,
+    App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, SharedString,
+    Task, WeakEntity,
 };
 use rpc::AnyProtoClient;
 use serde_json::{Value, json};
@@ -125,6 +126,7 @@ type UpstreamProjectId = u64;
 struct RemoteConnection {
     _client: AnyProtoClient,
     _upstream_project_id: UpstreamProjectId,
+    _adapter_name: SharedString,
 }
 
 impl RemoteConnection {
@@ -996,6 +998,7 @@ impl Session {
     ) -> Self {
         Self {
             mode: Mode::Remote(RemoteConnection {
+                _adapter_name: SharedString::new(""), // todo(debugger) we need to pipe in the right values to deserialize the debugger pane layout
                 _client: client,
                 _upstream_project_id: upstream_project_id,
             }),
@@ -1044,6 +1047,13 @@ impl Session {
         &self.capabilities
     }
 
+    pub fn adapter_name(&self) -> SharedString {
+        match &self.mode {
+            Mode::Local(local_mode) => local_mode.adapter.name().into(),
+            Mode::Remote(remote_mode) => remote_mode._adapter_name.clone(),
+        }
+    }
+
     pub fn configuration(&self) -> Option<DebugAdapterConfig> {
         if let Mode::Local(local_mode) = &self.mode {
             Some(local_mode.config.clone())