Debugger UI: Dynamic session contents (#28033)

Piotr Osiewicz , Anthony Eid , and Anthony created

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Anthony <anthony@zed.dev>

Change summary

crates/debugger_ui/src/debugger_panel.rs                | 206 +---
crates/debugger_ui/src/debugger_ui.rs                   |  14 
crates/debugger_ui/src/session.rs                       |  19 
crates/debugger_ui/src/session/running.rs               | 459 +++++++---
crates/debugger_ui/src/session/running/variable_list.rs |  44 
crates/debugger_ui/src/tests.rs                         |   2 
crates/debugger_ui/src/tests/console.rs                 |  11 
crates/debugger_ui/src/tests/debugger_panel.rs          |  46 
crates/debugger_ui/src/tests/module_list.rs             |  12 
crates/debugger_ui/src/tests/stack_frame_list.rs        |   2 
crates/debugger_ui/src/tests/variable_list.rs           |  29 
crates/terminal_view/src/terminal_panel.rs              |  13 
crates/workspace/src/pane_group.rs                      | 325 ++++---
crates/workspace/src/workspace.rs                       |  14 
14 files changed, 661 insertions(+), 535 deletions(-)

Detailed changes

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -30,9 +30,8 @@ use task::DebugTaskDefinition;
 use terminal_view::terminal_panel::TerminalPanel;
 use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
 use workspace::{
-    Pane, Workspace,
+    Workspace,
     dock::{DockPosition, Panel, PanelEvent},
-    pane,
 };
 
 pub enum DebugPanelEvent {
@@ -55,11 +54,13 @@ pub enum DebugPanelEvent {
 actions!(debug_panel, [ToggleFocus]);
 pub struct DebugPanel {
     size: Pixels,
-    pane: Entity<Pane>,
+    sessions: Vec<Entity<DebugSession>>,
+    active_session: Option<Entity<DebugSession>>,
     /// This represents the last debug definition that was created in the new session modal
     pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
     project: WeakEntity<Project>,
     workspace: WeakEntity<Workspace>,
+    focus_handle: FocusHandle,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -72,36 +73,17 @@ impl DebugPanel {
         cx.new(|cx| {
             let project = workspace.project().clone();
             let dap_store = project.read(cx).dap_store();
-            let pane = cx.new(|cx| {
-                let mut pane = Pane::new(
-                    workspace.weak_handle(),
-                    project.clone(),
-                    Default::default(),
-                    None,
-                    gpui::NoAction.boxed_clone(),
-                    window,
-                    cx,
-                );
-                pane.set_can_split(None);
-                pane.set_can_navigate(true, cx);
-                pane.display_nav_history_buttons(None);
-                pane.set_should_display_tab_bar(|_window, _cx| false);
-                pane.set_close_pane_if_empty(true, cx);
-
-                pane
-            });
 
-            let _subscriptions = vec![
-                cx.observe(&pane, |_, _, cx| cx.notify()),
-                cx.subscribe_in(&pane, window, Self::handle_pane_event),
-                cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event),
-            ];
+            let _subscriptions =
+                vec![cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event)];
 
             let debug_panel = Self {
-                pane,
                 size: px(300.),
+                sessions: vec![],
+                active_session: None,
                 _subscriptions,
                 past_debug_definition: None,
+                focus_handle: cx.focus_handle(),
                 project: project.downgrade(),
                 workspace: workspace.weak_handle(),
             };
@@ -130,7 +112,7 @@ impl DebugPanel {
                 cx.observe(&debug_panel, |_, debug_panel, cx| {
                     let (has_active_session, supports_restart, support_step_back) = debug_panel
                         .update(cx, |this, cx| {
-                            this.active_session(cx)
+                            this.active_session()
                                 .map(|item| {
                                     let running = item.read(cx).mode().as_running().cloned();
 
@@ -192,11 +174,8 @@ impl DebugPanel {
         })
     }
 
-    pub fn active_session(&self, cx: &App) -> Option<Entity<DebugSession>> {
-        self.pane
-            .read(cx)
-            .active_item()
-            .and_then(|panel| panel.downcast::<DebugSession>())
+    pub fn active_session(&self) -> Option<Entity<DebugSession>> {
+        self.active_session.clone()
     }
 
     pub fn debug_panel_items_by_client(
@@ -204,10 +183,8 @@ impl DebugPanel {
         client_id: &SessionId,
         cx: &Context<Self>,
     ) -> Vec<Entity<DebugSession>> {
-        self.pane
-            .read(cx)
-            .items()
-            .filter_map(|item| item.downcast::<DebugSession>())
+        self.sessions
+            .iter()
             .filter(|item| item.read(cx).session_id(cx) == Some(*client_id))
             .map(|item| item.clone())
             .collect()
@@ -218,15 +195,14 @@ impl DebugPanel {
         client_id: SessionId,
         cx: &mut Context<Self>,
     ) -> Option<Entity<DebugSession>> {
-        self.pane
-            .read(cx)
-            .items()
-            .filter_map(|item| item.downcast::<DebugSession>())
+        self.sessions
+            .iter()
             .find(|item| {
                 let item = item.read(cx);
 
                 item.session_id(cx) == Some(client_id)
             })
+            .cloned()
     }
 
     fn handle_dap_store_event(
@@ -248,10 +224,11 @@ impl DebugPanel {
                     return log::error!("Debug Panel out lived it's weak reference to Project");
                 };
 
-                if self.pane.read_with(cx, |pane, cx| {
-                    pane.items_of_type::<DebugSession>()
-                        .any(|item| item.read(cx).session_id(cx) == Some(*session_id))
-                }) {
+                if self
+                    .sessions
+                    .iter()
+                    .any(|item| item.read(cx).session_id(cx) == Some(*session_id))
+                {
                     // We already have an item for this session.
                     return;
                 }
@@ -264,11 +241,8 @@ impl DebugPanel {
                     cx,
                 );
 
-                self.pane.update(cx, |pane, cx| {
-                    pane.add_item(Box::new(session_item), true, true, None, window, cx);
-                    window.focus(&pane.focus_handle(cx));
-                    cx.notify();
-                });
+                self.sessions.push(session_item.clone());
+                self.activate_session(session_item, window, cx);
             }
             dap_store::DapStoreEvent::RunInTerminal {
                 title,
@@ -362,63 +336,9 @@ impl DebugPanel {
         })
     }
 
-    fn handle_pane_event(
-        &mut self,
-        _: &Entity<Pane>,
-        event: &pane::Event,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        match event {
-            pane::Event::Remove { .. } => cx.emit(PanelEvent::Close),
-            pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
-            pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
-            pane::Event::AddItem { item } => {
-                self.workspace
-                    .update(cx, |workspace, cx| {
-                        item.added_to_pane(workspace, self.pane.clone(), window, cx)
-                    })
-                    .ok();
-            }
-            pane::Event::RemovedItem { item } => {
-                if let Some(debug_session) = item.downcast::<DebugSession>() {
-                    debug_session.update(cx, |session, cx| {
-                        session.shutdown(cx);
-                    })
-                }
-            }
-            pane::Event::ActivateItem {
-                local: _,
-                focus_changed,
-            } => {
-                if *focus_changed {
-                    if let Some(debug_session) = self
-                        .pane
-                        .read(cx)
-                        .active_item()
-                        .and_then(|item| item.downcast::<DebugSession>())
-                    {
-                        if let Some(running) = debug_session
-                            .read_with(cx, |session, _| session.mode().as_running().cloned())
-                        {
-                            running.update(cx, |running, cx| {
-                                running.go_to_selected_stack_frame(window, cx);
-                            });
-                        }
-                    }
-                }
-            }
-
-            _ => {}
-        }
-    }
-
     fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
-        let active_session = self
-            .pane
-            .read(cx)
-            .active_item()
-            .and_then(|item| item.downcast::<DebugSession>());
+        let active_session = self.active_session.clone();
+
         Some(
             h_flex()
                 .border_b_1()
@@ -609,34 +529,29 @@ impl DebugPanel {
                             },
                         )
                         .when_some(active_session.as_ref(), |this, session| {
-                            let pane = self.pane.downgrade();
+                            let sessions = self.sessions.clone();
+                            let weak = cx.weak_entity();
                             let label = session.read(cx).label(cx);
                             this.child(DropdownMenu::new(
                                 "debugger-session-list",
                                 label,
                                 ContextMenu::build(window, cx, move |mut this, _, cx| {
-                                    let sessions = pane
-                                        .read_with(cx, |pane, _| {
-                                            pane.items().map(|item| item.boxed_clone()).collect()
-                                        })
-                                        .ok()
-                                        .unwrap_or_else(Vec::new);
-                                    for (index, item) in sessions.into_iter().enumerate() {
-                                        if let Some(session) = item.downcast::<DebugSession>() {
-                                            let pane = pane.clone();
-                                            this = this.entry(
-                                                session.read(cx).label(cx),
-                                                None,
-                                                move |window, cx| {
-                                                    pane.update(cx, |pane, cx| {
-                                                        pane.activate_item(
-                                                            index, true, true, window, cx,
-                                                        );
-                                                    })
-                                                    .ok();
-                                                },
-                                            );
-                                        }
+                                    for item in sessions {
+                                        let weak = weak.clone();
+                                        this = this.entry(
+                                            session.read(cx).label(cx),
+                                            None,
+                                            move |window, cx| {
+                                                weak.update(cx, |panel, cx| {
+                                                    panel.activate_session(
+                                                        item.clone(),
+                                                        window,
+                                                        cx,
+                                                    );
+                                                })
+                                                .ok();
+                                            },
+                                        );
                                     }
                                     this
                                 }),
@@ -680,6 +595,25 @@ impl DebugPanel {
                 ),
         )
     }
+
+    fn activate_session(
+        &mut self,
+        session_item: Entity<DebugSession>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        debug_assert!(self.sessions.contains(&session_item));
+        session_item.focus_handle(cx).focus(window);
+        session_item.update(cx, |this, cx| {
+            if let Some(running) = this.mode().as_running() {
+                running.update(cx, |this, cx| {
+                    this.go_to_selected_stack_frame(window, cx);
+                });
+            }
+        });
+        self.active_session = Some(session_item);
+        cx.notify();
+    }
 }
 
 impl EventEmitter<PanelEvent> for DebugPanel {}
@@ -687,16 +621,12 @@ impl EventEmitter<DebugPanelEvent> for DebugPanel {}
 impl EventEmitter<project::Event> for DebugPanel {}
 
 impl Focusable for DebugPanel {
-    fn focus_handle(&self, cx: &App) -> FocusHandle {
-        self.pane.focus_handle(cx)
+    fn focus_handle(&self, _: &App) -> FocusHandle {
+        self.focus_handle.clone()
     }
 }
 
 impl Panel for DebugPanel {
-    fn pane(&self) -> Option<Entity<Pane>> {
-        Some(self.pane.clone())
-    }
-
     fn persistent_name() -> &'static str {
         "DebugPanel"
     }
@@ -753,7 +683,9 @@ impl Panel for DebugPanel {
 
 impl Render for DebugPanel {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let has_sessions = self.pane.read(cx).items_len() > 0;
+        let has_sessions = self.sessions.len() > 0;
+        debug_assert_eq!(has_sessions, self.active_session.is_some());
+
         v_flex()
             .size_full()
             .key_context("DebugPanel")
@@ -761,7 +693,7 @@ impl Render for DebugPanel {
             .track_focus(&self.focus_handle(cx))
             .map(|this| {
                 if has_sessions {
-                    this.child(self.pane.clone())
+                    this.children(self.active_session.clone())
                 } else {
                     this.child(
                         v_flex()

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -52,7 +52,7 @@ pub fn init(cx: &mut App) {
                     if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
                         if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
                             panel
-                                .active_session(cx)
+                                .active_session()
                                 .and_then(|session| session.read(cx).mode().as_running().cloned())
                         }) {
                             active_item.update(cx, |item, cx| item.pause_thread(cx))
@@ -63,7 +63,7 @@ pub fn init(cx: &mut App) {
                     if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
                         if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
                             panel
-                                .active_session(cx)
+                                .active_session()
                                 .and_then(|session| session.read(cx).mode().as_running().cloned())
                         }) {
                             active_item.update(cx, |item, cx| item.restart_session(cx))
@@ -74,7 +74,7 @@ pub fn init(cx: &mut App) {
                     if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
                         if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
                             panel
-                                .active_session(cx)
+                                .active_session()
                                 .and_then(|session| session.read(cx).mode().as_running().cloned())
                         }) {
                             active_item.update(cx, |item, cx| item.step_in(cx))
@@ -85,7 +85,7 @@ pub fn init(cx: &mut App) {
                     if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
                         if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
                             panel
-                                .active_session(cx)
+                                .active_session()
                                 .and_then(|session| session.read(cx).mode().as_running().cloned())
                         }) {
                             active_item.update(cx, |item, cx| item.step_over(cx))
@@ -96,7 +96,7 @@ pub fn init(cx: &mut App) {
                     if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
                         if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
                             panel
-                                .active_session(cx)
+                                .active_session()
                                 .and_then(|session| session.read(cx).mode().as_running().cloned())
                         }) {
                             active_item.update(cx, |item, cx| item.step_back(cx))
@@ -107,7 +107,7 @@ pub fn init(cx: &mut App) {
                     if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
                         if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
                             panel
-                                .active_session(cx)
+                                .active_session()
                                 .and_then(|session| session.read(cx).mode().as_running().cloned())
                         }) {
                             active_item.update(cx, |item, cx| item.stop_thread(cx))
@@ -118,7 +118,7 @@ pub fn init(cx: &mut App) {
                     if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
                         if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
                             panel
-                                .active_session(cx)
+                                .active_session()
                                 .and_then(|session| session.read(cx).mode().as_running().cloned())
                         }) {
                             active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))

crates/debugger_ui/src/session.rs 🔗

@@ -43,14 +43,6 @@ pub enum DebugPanelItemEvent {
     Stopped { go_to_stack_frame: bool },
 }
 
-#[derive(Clone, Copy, PartialEq, Eq, Debug)]
-pub enum ThreadItem {
-    Console,
-    LoadedSource,
-    Modules,
-    Variables,
-}
-
 impl DebugSession {
     pub(crate) fn running(
         project: Entity<Project>,
@@ -60,7 +52,15 @@ impl DebugSession {
         window: &mut Window,
         cx: &mut App,
     ) -> Entity<Self> {
-        let mode = cx.new(|cx| RunningState::new(session.clone(), workspace.clone(), window, cx));
+        let mode = cx.new(|cx| {
+            RunningState::new(
+                session.clone(),
+                project.clone(),
+                workspace.clone(),
+                window,
+                cx,
+            )
+        });
 
         cx.new(|cx| Self {
             _subscriptions: [cx.subscribe(&mode, |_, _, _, cx| {
@@ -81,6 +81,7 @@ impl DebugSession {
         }
     }
 
+    #[expect(unused)]
     pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
         match &self.mode {
             DebugSessionState::Running(state) => state.update(cx, |state, cx| state.shutdown(cx)),

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

@@ -4,47 +4,66 @@ mod module_list;
 pub mod stack_frame_list;
 pub mod variable_list;
 
-use super::{DebugPanelItemEvent, ThreadItem};
+use std::{any::Any, ops::ControlFlow, sync::Arc};
+
+use super::DebugPanelItemEvent;
+use collections::HashMap;
 use console::Console;
 use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings};
-use gpui::{AppContext, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity};
+use gpui::{
+    Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
+    NoAction, Subscription, WeakEntity,
+};
 use loaded_source_list::LoadedSourceList;
 use module_list::ModuleList;
-use project::debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus};
+use project::{
+    Project,
+    debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
+};
 use rpc::proto::ViewId;
 use settings::Settings;
 use stack_frame_list::StackFrameList;
 use ui::{
-    ActiveTheme, AnyElement, App, Button, Context, ContextMenu, DropdownMenu, FluentBuilder,
-    Indicator, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
-    StatefulInteractiveElement, Styled, Window, div, h_flex, v_flex,
+    App, Context, ContextMenu, DropdownMenu, InteractiveElement, IntoElement, ParentElement,
+    Render, SharedString, Styled, Window, div, h_flex, v_flex,
 };
 use util::ResultExt;
 use variable_list::VariableList;
-use workspace::Workspace;
+use workspace::{
+    ActivePaneDecorator, DraggedTab, Item, Pane, PaneGroup, Workspace, move_item, pane::Event,
+};
 
 pub struct RunningState {
     session: Entity<Session>,
     thread_id: Option<ThreadId>,
-    console: Entity<console::Console>,
     focus_handle: FocusHandle,
     _remote_id: Option<ViewId>,
-    show_console_indicator: bool,
-    module_list: Entity<module_list::ModuleList>,
-    active_thread_item: ThreadItem,
     workspace: WeakEntity<Workspace>,
     session_id: SessionId,
     variable_list: Entity<variable_list::VariableList>,
     _subscriptions: Vec<Subscription>,
     stack_frame_list: Entity<stack_frame_list::StackFrameList>,
-    loaded_source_list: Entity<loaded_source_list::LoadedSourceList>,
+    _module_list: Entity<module_list::ModuleList>,
+    _console: Entity<Console>,
+    panes: PaneGroup,
+    pane_close_subscriptions: HashMap<EntityId, Subscription>,
 }
 
 impl Render for RunningState {
-    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let threads = self.session.update(cx, |this, cx| this.threads(cx));
-        self.select_current_thread(&threads, cx);
-
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let active = self.panes.panes().into_iter().next();
+        let x = if let Some(active) = active {
+            self.panes
+                .render(
+                    None,
+                    &ActivePaneDecorator::new(active, &self.workspace),
+                    window,
+                    cx,
+                )
+                .into_any_element()
+        } else {
+            div().into_any_element()
+        };
         let thread_status = self
             .thread_id
             .map(|thread_id| self.session.read(cx).thread_status(thread_id))
@@ -53,88 +72,185 @@ impl Render for RunningState {
         self.variable_list.update(cx, |this, cx| {
             this.disabled(thread_status != ThreadStatus::Stopped, cx);
         });
+        v_flex()
+            .size_full()
+            .key_context("DebugSessionItem")
+            .track_focus(&self.focus_handle(cx))
+            .child(h_flex().flex_1().child(x))
+    }
+}
 
-        let active_thread_item = &self.active_thread_item;
+struct SubView {
+    inner: AnyView,
+    pane_focus_handle: FocusHandle,
+    tab_name: SharedString,
+}
 
-        let capabilities = self.capabilities(cx);
-        h_flex()
-            .key_context("DebugPanelItem")
-            .track_focus(&self.focus_handle(cx))
-            .size_full()
-            .items_start()
-            .child(
-                v_flex().size_full().items_start().child(
-                    h_flex()
-                        .size_full()
-                        .items_start()
-                        .p_1()
-                        .gap_4()
-                        .child(self.stack_frame_list.clone()),
-                ),
-            )
-            .child(
-                v_flex()
-                    .border_l_1()
-                    .border_color(cx.theme().colors().border_variant)
-                    .size_full()
-                    .items_start()
-                    .child(
-                        h_flex()
-                            .border_b_1()
-                            .w_full()
-                            .border_color(cx.theme().colors().border_variant)
-                            .child(self.render_entry_button(
-                                &SharedString::from("Variables"),
-                                ThreadItem::Variables,
-                                cx,
-                            ))
-                            .when(
-                                capabilities.supports_modules_request.unwrap_or_default(),
-                                |this| {
-                                    this.child(self.render_entry_button(
-                                        &SharedString::from("Modules"),
-                                        ThreadItem::Modules,
-                                        cx,
-                                    ))
-                                },
-                            )
-                            .when(
-                                capabilities
-                                    .supports_loaded_sources_request
-                                    .unwrap_or_default(),
-                                |this| {
-                                    this.child(self.render_entry_button(
-                                        &SharedString::from("Loaded Sources"),
-                                        ThreadItem::LoadedSource,
-                                        cx,
-                                    ))
-                                },
-                            )
-                            .child(self.render_entry_button(
-                                &SharedString::from("Console"),
-                                ThreadItem::Console,
-                                cx,
-                            )),
-                    )
-                    .when(*active_thread_item == ThreadItem::Variables, |this| {
-                        this.child(self.variable_list.clone())
+impl SubView {
+    fn new(
+        pane_focus_handle: FocusHandle,
+        view: AnyView,
+        tab_name: SharedString,
+        cx: &mut App,
+    ) -> Entity<Self> {
+        cx.new(|_| Self {
+            tab_name,
+            inner: view,
+            pane_focus_handle,
+        })
+    }
+}
+impl Focusable for SubView {
+    fn focus_handle(&self, _: &App) -> FocusHandle {
+        self.pane_focus_handle.clone()
+    }
+}
+impl EventEmitter<()> for SubView {}
+impl Item for SubView {
+    type Event = ();
+    fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
+        Some(self.tab_name.clone())
+    }
+}
+
+impl Render for SubView {
+    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+        v_flex().size_full().child(self.inner.clone())
+    }
+}
+
+fn new_debugger_pane(
+    workspace: WeakEntity<Workspace>,
+    project: Entity<Project>,
+    window: &mut Window,
+    cx: &mut Context<RunningState>,
+) -> Entity<Pane> {
+    let weak_running = cx.weak_entity();
+    let custom_drop_handle = {
+        let workspace = workspace.clone();
+        let project = project.downgrade();
+        let weak_running = weak_running.clone();
+        move |pane: &mut Pane, any: &dyn Any, window: &mut Window, cx: &mut Context<Pane>| {
+            let Some(tab) = any.downcast_ref::<DraggedTab>() else {
+                return ControlFlow::Break(());
+            };
+            let Some(project) = project.upgrade() else {
+                return ControlFlow::Break(());
+            };
+            let this_pane = cx.entity().clone();
+            let item = if tab.pane == this_pane {
+                pane.item_for_index(tab.ix)
+            } else {
+                tab.pane.read(cx).item_for_index(tab.ix)
+            };
+            let Some(item) = item.filter(|item| item.downcast::<SubView>().is_some()) else {
+                return ControlFlow::Break(());
+            };
+
+            let source = tab.pane.clone();
+            let item_id_to_move = item.item_id();
+
+            let Ok(new_split_pane) = pane
+                .drag_split_direction()
+                .map(|split_direction| {
+                    weak_running.update(cx, |running, cx| {
+                        let new_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),
+                        );
+                        debug_assert!(_previous_subscription.is_none());
+                        running
+                            .panes
+                            .split(&this_pane, &new_pane, split_direction)?;
+                        anyhow::Ok(new_pane)
                     })
-                    .when(*active_thread_item == ThreadItem::Modules, |this| {
-                        this.size_full().child(self.module_list.clone())
+                })
+                .transpose()
+            else {
+                return ControlFlow::Break(());
+            };
+
+            match new_split_pane.transpose() {
+                // Source pane may be the one currently updated, so defer the move.
+                Ok(Some(new_pane)) => cx
+                    .spawn_in(window, async move |_, cx| {
+                        cx.update(|window, cx| {
+                            move_item(
+                                &source,
+                                &new_pane,
+                                item_id_to_move,
+                                new_pane.read(cx).active_item_index(),
+                                window,
+                                cx,
+                            );
+                        })
+                        .ok();
                     })
-                    .when(*active_thread_item == ThreadItem::LoadedSource, |this| {
-                        this.size_full().child(self.loaded_source_list.clone())
+                    .detach(),
+                // If we drop into existing pane or current pane,
+                // regular pane drop handler will take care of it,
+                // using the right tab index for the operation.
+                Ok(None) => return ControlFlow::Continue(()),
+                err @ Err(_) => {
+                    err.log_err();
+                    return ControlFlow::Break(());
+                }
+            };
+
+            ControlFlow::Break(())
+        }
+    };
+
+    let ret = cx.new(move |cx| {
+        let mut pane = Pane::new(
+            workspace.clone(),
+            project.clone(),
+            Default::default(),
+            None,
+            NoAction.boxed_clone(),
+            window,
+            cx,
+        );
+        pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| {
+            if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
+                let is_current_pane = tab.pane == cx.entity();
+                let Some(can_drag_away) = weak_running
+                    .update(cx, |running_state, _| {
+                        let current_panes = running_state.panes.panes();
+                        !current_panes.contains(&&tab.pane)
+                            || current_panes.len() > 1
+                            || (!is_current_pane || pane.items_len() > 1)
                     })
-                    .when(*active_thread_item == ThreadItem::Console, |this| {
-                        this.child(self.console.clone())
-                    }),
-            )
-    }
+                    .ok()
+                else {
+                    return false;
+                };
+                if can_drag_away {
+                    let item = if is_current_pane {
+                        pane.item_for_index(tab.ix)
+                    } else {
+                        tab.pane.read(cx).item_for_index(tab.ix)
+                    };
+                    if let Some(item) = item {
+                        return item.downcast::<SubView>().is_some();
+                    }
+                }
+            }
+            false
+        })));
+        pane.display_nav_history_buttons(None);
+        pane.set_custom_drop_handle(cx, custom_drop_handle);
+        pane
+    });
+
+    ret
 }
-
 impl RunningState {
     pub fn new(
         session: Entity<Session>,
+        project: Entity<Project>,
         workspace: WeakEntity<Workspace>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -151,6 +267,7 @@ impl RunningState {
 
         let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
 
+        #[expect(unused)]
         let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
 
         let console = cx.new(|cx| {
@@ -188,24 +305,116 @@ 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"),
+                    cx,
+                )),
+                true,
+                false,
+                None,
+                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"),
+                    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"),
+                    cx,
+                )),
+                true,
+                false,
+                None,
+                window,
+                cx,
+            );
+        });
+        let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
+        rightmost_pane.update(cx, |this, cx| {
+            this.add_item(
+                Box::new(SubView::new(
+                    this.focus_handle(cx),
+                    console.clone().into(),
+                    SharedString::new_static("Console"),
+                    cx,
+                )),
+                true,
+                false,
+                None,
+                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));
+
         Self {
             session,
-            console,
             workspace,
-            module_list,
             focus_handle,
             variable_list,
             _subscriptions,
             thread_id: None,
             _remote_id: None,
             stack_frame_list,
-            loaded_source_list,
             session_id,
-            show_console_indicator: false,
-            active_thread_item: ThreadItem::Variables,
+            panes,
+            _module_list: module_list,
+            _console: console,
+            pane_close_subscriptions,
         }
     }
 
+    fn handle_pane_event(
+        this: &mut RunningState,
+        source_pane: Entity<Pane>,
+        event: &Event,
+        cx: &mut Context<RunningState>,
+    ) {
+        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
@@ -221,12 +430,6 @@ impl RunningState {
         self.session_id
     }
 
-    #[cfg(test)]
-    pub fn set_thread_item(&mut self, thread_item: ThreadItem, cx: &mut Context<Self>) {
-        self.active_thread_item = thread_item;
-        cx.notify()
-    }
-
     #[cfg(test)]
     pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
         &self.stack_frame_list
@@ -234,14 +437,31 @@ impl RunningState {
 
     #[cfg(test)]
     pub fn console(&self) -> &Entity<Console> {
-        &self.console
+        &self._console
     }
 
     #[cfg(test)]
     pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
-        &self.module_list
+        &self._module_list
     }
 
+    #[cfg(test)]
+    pub(crate) fn activate_variable_list(&self, window: &mut Window, cx: &mut App) {
+        let (variable_list_position, pane) = self
+            .panes
+            .panes()
+            .into_iter()
+            .find_map(|pane| {
+                pane.read(cx)
+                    .items_of_type::<SubView>()
+                    .position(|view| view.read(cx).tab_name == *"Variables")
+                    .map(|view| (view, pane))
+            })
+            .unwrap();
+        pane.update(cx, |this, cx| {
+            this.activate_item(variable_list_position, true, true, window, cx);
+        })
+    }
     #[cfg(test)]
     pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
         &self.variable_list
@@ -292,41 +512,6 @@ impl RunningState {
         cx.notify();
     }
 
-    fn render_entry_button(
-        &self,
-        label: &SharedString,
-        thread_item: ThreadItem,
-        cx: &mut Context<Self>,
-    ) -> AnyElement {
-        let has_indicator =
-            matches!(thread_item, ThreadItem::Console) && self.show_console_indicator;
-
-        div()
-            .id(label.clone())
-            .px_2()
-            .py_1()
-            .cursor_pointer()
-            .border_b_2()
-            .when(self.active_thread_item == thread_item, |this| {
-                this.border_color(cx.theme().colors().border)
-            })
-            .child(
-                h_flex()
-                    .child(Button::new(label.clone(), label.clone()))
-                    .when(has_indicator, |this| this.child(Indicator::dot())),
-            )
-            .on_click(cx.listener(move |this, _, _window, cx| {
-                this.active_thread_item = thread_item;
-
-                if matches!(this.active_thread_item, ThreadItem::Console) {
-                    this.show_console_indicator = false;
-                }
-
-                cx.notify();
-            }))
-            .into_any_element()
-    }
-
     pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
         let Some(thread_id) = self.thread_id else {
             return;

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

@@ -687,6 +687,7 @@ impl VariableList {
             .child(
                 ListItem::new(SharedString::from(format!("scope-{}", var_ref)))
                     .selectable(false)
+                    .disabled(self.disabled)
                     .indent_level(state.depth + 1)
                     .indent_step_size(px(20.))
                     .always_show_disclosure_icon(true)
@@ -695,7 +696,15 @@ impl VariableList {
                         let var_path = entry.path.clone();
                         cx.listener(move |this, _, _, cx| this.toggle_entry(&var_path, cx))
                     })
-                    .child(div().text_ui(cx).w_full().child(scope.name.clone())),
+                    .child(
+                        div()
+                            .text_ui(cx)
+                            .w_full()
+                            .when(self.disabled, |this| {
+                                this.text_color(Color::Disabled.color(cx))
+                            })
+                            .child(scope.name.clone()),
+                    ),
             )
             .into_any()
     }
@@ -716,20 +725,27 @@ impl VariableList {
         };
 
         let syntax_color_for = |name| cx.theme().syntax().get(name).color;
-        let variable_name_color = match &dap
-            .presentation_hint
-            .as_ref()
-            .and_then(|hint| hint.kind.as_ref())
-            .unwrap_or(&VariablePresentationHintKind::Unknown)
-        {
-            VariablePresentationHintKind::Class
-            | VariablePresentationHintKind::BaseClass
-            | VariablePresentationHintKind::InnerClass
-            | VariablePresentationHintKind::MostDerivedClass => syntax_color_for("type"),
-            VariablePresentationHintKind::Data => syntax_color_for("variable"),
-            VariablePresentationHintKind::Unknown | _ => syntax_color_for("variable"),
+        let variable_name_color = if self.disabled {
+            Some(Color::Disabled.color(cx))
+        } else {
+            match &dap
+                .presentation_hint
+                .as_ref()
+                .and_then(|hint| hint.kind.as_ref())
+                .unwrap_or(&VariablePresentationHintKind::Unknown)
+            {
+                VariablePresentationHintKind::Class
+                | VariablePresentationHintKind::BaseClass
+                | VariablePresentationHintKind::InnerClass
+                | VariablePresentationHintKind::MostDerivedClass => syntax_color_for("type"),
+                VariablePresentationHintKind::Data => syntax_color_for("variable"),
+                VariablePresentationHintKind::Unknown | _ => syntax_color_for("variable"),
+            }
         };
-        let variable_color = syntax_color_for("variable.special");
+        let variable_color = self
+            .disabled
+            .then(|| Color::Disabled.color(cx))
+            .or_else(|| syntax_color_for("variable.special"));
 
         let var_ref = dap.variables_reference;
         let colors = get_entry_color(cx);

crates/debugger_ui/src/tests.rs 🔗

@@ -76,7 +76,7 @@ pub fn active_debug_session_panel(
         .update(cx, |workspace, _window, cx| {
             let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
             debug_panel
-                .update(cx, |this, cx| this.active_session(cx))
+                .update(cx, |this, _| this.active_session())
                 .unwrap()
         })
         .unwrap()

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

@@ -101,10 +101,6 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
                 .clone()
         });
 
-    running_state.update(cx, |state, cx| {
-        state.set_thread_item(session::ThreadItem::Console, cx);
-        cx.refresh_windows();
-    });
     cx.run_until_parked();
 
     // assert we have output from before the thread stopped
@@ -112,7 +108,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
         .update(cx, |workspace, _window, cx| {
             let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
             let active_debug_session_panel = debug_panel
-                .update(cx, |this, cx| this.active_session(cx))
+                .update(cx, |this, _| this.active_session())
                 .unwrap();
 
             assert_eq!(
@@ -151,8 +147,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
         .await;
 
     cx.run_until_parked();
-    running_state.update(cx, |state, cx| {
-        state.set_thread_item(session::ThreadItem::Console, cx);
+    running_state.update(cx, |_, cx| {
         cx.refresh_windows();
     });
     cx.run_until_parked();
@@ -162,7 +157,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp
         .update(cx, |workspace, _window, cx| {
             let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
             let active_session_panel = debug_panel
-                .update(cx, |this, cx| this.active_session(cx))
+                .update(cx, |this, _| this.active_session())
                 .unwrap();
 
             assert_eq!(

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

@@ -85,9 +85,8 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
     workspace
         .update(cx, |workspace, _window, cx| {
             let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
-            let active_session = debug_panel.update(cx, |debug_panel, cx| {
-                debug_panel.active_session(cx).unwrap()
-            });
+            let active_session =
+                debug_panel.update(cx, |debug_panel, _| debug_panel.active_session().unwrap());
 
             let running_state = active_session.update(cx, |active_session, _| {
                 active_session
@@ -98,9 +97,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
             });
 
             debug_panel.update(cx, |this, cx| {
-                assert!(this.active_session(cx).is_some());
-                // we have one active session
-                assert_eq!(1, this.pane().unwrap().read(cx).items_len());
+                assert!(this.active_session().is_some());
                 assert!(running_state.read(cx).selected_thread_id().is_none());
             });
         })
@@ -124,7 +121,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
         .update(cx, |workspace, _window, cx| {
             let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
             let active_session = debug_panel
-                .update(cx, |this, cx| this.active_session(cx))
+                .update(cx, |this, _| this.active_session())
                 .unwrap();
 
             let running_state = active_session.update(cx, |active_session, _| {
@@ -135,11 +132,6 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
                     .clone()
             });
 
-            // we have one active session
-            assert_eq!(
-                1,
-                debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
-            );
             assert_eq!(client.id(), running_state.read(cx).session_id());
             assert_eq!(
                 ThreadId(1),
@@ -162,7 +154,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
             let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 
             let active_session = debug_panel
-                .update(cx, |this, cx| this.active_session(cx))
+                .update(cx, |this, _| this.active_session())
                 .unwrap();
 
             let running_state = active_session.update(cx, |active_session, _| {
@@ -174,8 +166,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
             });
 
             debug_panel.update(cx, |this, cx| {
-                assert!(this.active_session(cx).is_some());
-                assert_eq!(1, this.pane().unwrap().read(cx).items_len());
+                assert!(this.active_session().is_some());
                 assert_eq!(
                     ThreadId(1),
                     running_state.read(cx).selected_thread_id().unwrap()
@@ -243,10 +234,8 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
         .update(cx, |workspace, _window, cx| {
             let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 
-            debug_panel.update(cx, |this, cx| {
-                assert!(this.active_session(cx).is_some());
-                // we have one active session
-                assert_eq!(1, this.pane().unwrap().read(cx).items_len());
+            debug_panel.update(cx, |this, _| {
+                assert!(this.active_session().is_some());
             });
         })
         .unwrap();
@@ -270,7 +259,7 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
         .update(cx, |workspace, _window, cx| {
             let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
             let active_session = debug_panel
-                .update(cx, |this, cx| this.active_session(cx))
+                .update(cx, |this, _| this.active_session())
                 .unwrap();
 
             let running_state = active_session.update(cx, |active_session, _| {
@@ -281,11 +270,6 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
                     .clone()
             });
 
-            // we have one active session
-            assert_eq!(
-                1,
-                debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
-            );
             assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap());
             assert_eq!(
                 ThreadId(1),
@@ -312,7 +296,7 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
         .update(cx, |workspace, _window, cx| {
             let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
             let active_session = debug_panel
-                .update(cx, |this, cx| this.active_session(cx))
+                .update(cx, |this, _| this.active_session())
                 .unwrap();
 
             let running_state = active_session.update(cx, |active_session, _| {
@@ -323,11 +307,6 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
                     .clone()
             });
 
-            // we have one active session
-            assert_eq!(
-                1,
-                debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
-            );
             assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap());
             assert_eq!(
                 ThreadId(1),
@@ -349,7 +328,7 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
         .update(cx, |workspace, _window, cx| {
             let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
             let active_session = debug_panel
-                .update(cx, |this, cx| this.active_session(cx))
+                .update(cx, |this, _| this.active_session())
                 .unwrap();
 
             let running_state = active_session.update(cx, |active_session, _| {
@@ -361,8 +340,7 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
             });
 
             debug_panel.update(cx, |this, cx| {
-                assert!(this.active_session(cx).is_some());
-                assert_eq!(1, this.pane().unwrap().read(cx).items_len());
+                assert!(this.active_session().is_some());
                 assert_eq!(
                     ThreadId(1),
                     running_state.read(cx).selected_thread_id().unwrap()

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

@@ -1,6 +1,5 @@
 use crate::{
     debugger_panel::DebugPanel,
-    session::ThreadItem,
     tests::{active_debug_session_panel, init_test, init_test_workspace},
 };
 use dap::{
@@ -139,13 +138,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
                 .clone()
         });
 
-    assert!(
-        !called_modules.load(std::sync::atomic::Ordering::SeqCst),
-        "Request Modules shouldn't be called before it's needed"
-    );
-
-    running_state.update(cx, |state, cx| {
-        state.set_thread_item(ThreadItem::Modules, cx);
+    running_state.update(cx, |_, cx| {
         cx.refresh_windows();
     });
 
@@ -157,9 +150,6 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
     );
 
     active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
-        running_state.update(cx, |state, cx| {
-            state.set_thread_item(ThreadItem::Modules, cx)
-        });
         let actual_modules = running_state.update(cx, |state, cx| {
             state.module_list().update(cx, |list, cx| list.modules(cx))
         });

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

@@ -410,7 +410,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
         .update(cx, |workspace, _window, cx| {
             let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
             let active_debug_panel_item = debug_panel
-                .update(cx, |this, cx| this.active_session(cx))
+                .update(cx, |this, _| this.active_session())
                 .unwrap();
 
             active_debug_panel_item

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

@@ -207,7 +207,9 @@ async fn test_basic_fetch_initial_scope_and_variables(
                 .expect("Session should be running by this point")
                 .clone()
         });
-
+    running_state.update_in(cx, |this, window, cx| {
+        this.activate_variable_list(window, cx);
+    });
     cx.run_until_parked();
 
     running_state.update(cx, |running_state, cx| {
@@ -222,7 +224,6 @@ async fn test_basic_fetch_initial_scope_and_variables(
         running_state
             .variable_list()
             .update(cx, |variable_list, _| {
-                assert_eq!(1, variable_list.scopes().len());
                 assert_eq!(scopes, variable_list.scopes());
                 assert_eq!(
                     vec![variables[0].clone(), variables[1].clone(),],
@@ -480,7 +481,9 @@ async fn test_fetch_variables_for_multiple_scopes(
                 .expect("Session should be running by this point")
                 .clone()
         });
-
+    running_state.update_in(cx, |this, window, cx| {
+        this.activate_variable_list(window, cx);
+    });
     cx.run_until_parked();
 
     running_state.update(cx, |running_state, cx| {
@@ -797,7 +800,11 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp
             variable_list.update(cx, |_, cx| cx.focus_self(window));
             running
         });
-
+    running_state.update_in(cx, |this, window, cx| {
+        this.activate_variable_list(window, cx);
+    });
+    cx.run_until_parked();
+    cx.dispatch_action(SelectFirst);
     cx.dispatch_action(SelectFirst);
     cx.run_until_parked();
 
@@ -1541,16 +1548,13 @@ async fn test_variable_list_only_sends_requests_when_rendering(
         })
         .await;
 
-    let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, cx| {
+    let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| {
         let state = item
             .mode()
             .as_running()
             .expect("Session should be running by this point")
             .clone();
 
-        state.update(cx, |state, cx| {
-            state.set_thread_item(crate::session::ThreadItem::Modules, cx)
-        });
         state
     });
 
@@ -1577,9 +1581,10 @@ async fn test_variable_list_only_sends_requests_when_rendering(
         assert!(!made_scopes_request.load(Ordering::SeqCst));
 
         cx.focus_self(window);
-        running_state.set_thread_item(crate::session::ThreadItem::Variables, cx);
     });
-
+    running_state.update_in(cx, |this, window, cx| {
+        this.activate_variable_list(window, cx);
+    });
     cx.run_until_parked();
 
     running_state.update(cx, |running_state, cx| {
@@ -1893,7 +1898,9 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
                 .expect("Session should be running by this point")
                 .clone()
         });
-
+    running_state.update_in(cx, |this, window, cx| {
+        this.activate_variable_list(window, cx);
+    });
     cx.run_until_parked();
 
     running_state.update(cx, |running_state, cx| {

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -1202,12 +1202,15 @@ impl Render for TerminalPanel {
         self.workspace
             .update(cx, |workspace, cx| {
                 registrar.size_full().child(self.center.render(
-                    workspace.project(),
-                    &HashMap::default(),
-                    None,
-                    &self.active_pane,
                     workspace.zoomed_item(),
-                    workspace.app_state(),
+                    &workspace::PaneRenderContext {
+                        follower_states: &&HashMap::default(),
+                        active_call: workspace.active_call(),
+                        active_pane: &self.active_pane,
+                        app_state: &workspace.app_state(),
+                        project: workspace.project(),
+                        workspace: &workspace.weak_handle(),
+                    },
                     window,
                     cx,
                 ))

crates/workspace/src/pane_group.rs 🔗

@@ -8,8 +8,8 @@ use call::{ActiveCall, ParticipantLocation};
 use client::proto::PeerId;
 use collections::HashMap;
 use gpui::{
-    Along, AnyView, AnyWeakView, Axis, Bounds, Context, Entity, IntoElement, MouseButton, Pixels,
-    Point, StyleRefinement, Window, point, size,
+    Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, MouseButton, Pixels,
+    Point, StyleRefinement, WeakEntity, Window, point, size,
 };
 use parking_lot::Mutex;
 use project::Project;
@@ -124,26 +124,12 @@ impl PaneGroup {
 
     pub fn render(
         &self,
-        project: &Entity<Project>,
-        follower_states: &HashMap<PeerId, FollowerState>,
-        active_call: Option<&Entity<ActiveCall>>,
-        active_pane: &Entity<Pane>,
         zoomed: Option<&AnyWeakView>,
-        app_state: &Arc<AppState>,
+        render_cx: &dyn PaneLeaderDecorator,
         window: &mut Window,
-        cx: &mut Context<Workspace>,
+        cx: &mut App,
     ) -> impl IntoElement {
-        self.root.render(
-            project,
-            0,
-            follower_states,
-            active_call,
-            active_pane,
-            zoomed,
-            app_state,
-            window,
-            cx,
-        )
+        self.root.render(0, zoomed, render_cx, window, cx)
     }
 
     pub fn panes(&self) -> Vec<&Entity<Pane>> {
@@ -195,6 +181,160 @@ pub enum Member {
     Pane(Entity<Pane>),
 }
 
+#[derive(Clone, Copy)]
+pub struct PaneRenderContext<'a> {
+    pub project: &'a Entity<Project>,
+    pub follower_states: &'a HashMap<PeerId, FollowerState>,
+    pub active_call: Option<&'a Entity<ActiveCall>>,
+    pub active_pane: &'a Entity<Pane>,
+    pub app_state: &'a Arc<AppState>,
+    pub workspace: &'a WeakEntity<Workspace>,
+}
+
+#[derive(Default)]
+pub struct LeaderDecoration {
+    border: Option<Hsla>,
+    status_box: Option<AnyElement>,
+}
+
+pub trait PaneLeaderDecorator {
+    fn decorate(&self, pane: &Entity<Pane>, cx: &App) -> LeaderDecoration;
+    fn active_pane(&self) -> &Entity<Pane>;
+    fn workspace(&self) -> &WeakEntity<Workspace>;
+}
+
+pub struct ActivePaneDecorator<'a> {
+    active_pane: &'a Entity<Pane>,
+    workspace: &'a WeakEntity<Workspace>,
+}
+
+impl<'a> ActivePaneDecorator<'a> {
+    pub fn new(active_pane: &'a Entity<Pane>, workspace: &'a WeakEntity<Workspace>) -> Self {
+        Self {
+            active_pane,
+            workspace,
+        }
+    }
+}
+
+impl PaneLeaderDecorator for ActivePaneDecorator<'_> {
+    fn decorate(&self, _: &Entity<Pane>, _: &App) -> LeaderDecoration {
+        LeaderDecoration::default()
+    }
+    fn active_pane(&self) -> &Entity<Pane> {
+        self.active_pane
+    }
+
+    fn workspace(&self) -> &WeakEntity<Workspace> {
+        self.workspace
+    }
+}
+
+impl PaneLeaderDecorator for PaneRenderContext<'_> {
+    fn decorate(&self, pane: &Entity<Pane>, cx: &App) -> LeaderDecoration {
+        let follower_state = self.follower_states.iter().find_map(|(leader_id, state)| {
+            if state.center_pane == *pane {
+                Some((*leader_id, state))
+            } else {
+                None
+            }
+        });
+        let leader = follower_state.as_ref().and_then(|(leader_id, _)| {
+            let room = self.active_call?.read(cx).room()?.read(cx);
+            room.remote_participant_for_peer_id(*leader_id)
+        });
+        let Some(leader) = leader else {
+            return LeaderDecoration::default();
+        };
+        let is_in_unshared_view = follower_state.as_ref().map_or(false, |(_, state)| {
+            state
+                .active_view_id
+                .is_some_and(|view_id| !state.items_by_leader_view_id.contains_key(&view_id))
+        });
+        let is_in_panel = follower_state
+            .as_ref()
+            .map_or(false, |(_, state)| state.dock_pane.is_some());
+
+        let mut leader_join_data = None;
+        let leader_status_box = match leader.location {
+            ParticipantLocation::SharedProject {
+                project_id: leader_project_id,
+            } => {
+                if Some(leader_project_id) == self.project.read(cx).remote_id() {
+                    is_in_unshared_view.then(|| {
+                        Label::new(format!(
+                            "{} is in an unshared pane",
+                            leader.user.github_login
+                        ))
+                    })
+                } else {
+                    leader_join_data = Some((leader_project_id, leader.user.id));
+                    Some(Label::new(format!(
+                        "Follow {} to their active project",
+                        leader.user.github_login,
+                    )))
+                }
+            }
+            ParticipantLocation::UnsharedProject => Some(Label::new(format!(
+                "{} is viewing an unshared Zed project",
+                leader.user.github_login
+            ))),
+            ParticipantLocation::External => Some(Label::new(format!(
+                "{} is viewing a window outside of Zed",
+                leader.user.github_login
+            ))),
+        };
+        let mut leader_color = cx
+            .theme()
+            .players()
+            .color_for_participant(leader.participant_index.0)
+            .cursor;
+        if is_in_panel {
+            leader_color.fade_out(0.75);
+        } else {
+            leader_color.fade_out(0.3);
+        }
+        let status_box = leader_status_box.map(|status| {
+            div()
+                .absolute()
+                .w_96()
+                .bottom_3()
+                .right_3()
+                .elevation_2(cx)
+                .p_1()
+                .child(status)
+                .when_some(
+                    leader_join_data,
+                    |this, (leader_project_id, leader_user_id)| {
+                        let app_state = self.app_state.clone();
+                        this.cursor_pointer()
+                            .on_mouse_down(MouseButton::Left, move |_, _, cx| {
+                                crate::join_in_room_project(
+                                    leader_project_id,
+                                    leader_user_id,
+                                    app_state.clone(),
+                                    cx,
+                                )
+                                .detach_and_log_err(cx);
+                            })
+                    },
+                )
+                .into_any_element()
+        });
+        LeaderDecoration {
+            status_box,
+            border: Some(leader_color),
+        }
+    }
+
+    fn active_pane(&self) -> &Entity<Pane> {
+        self.active_pane
+    }
+
+    fn workspace(&self) -> &WeakEntity<Workspace> {
+        self.workspace
+    }
+}
 impl Member {
     fn new_axis(old_pane: Entity<Pane>, new_pane: Entity<Pane>, direction: SplitDirection) -> Self {
         use Axis::*;
@@ -222,15 +362,11 @@ impl Member {
 
     pub fn render(
         &self,
-        project: &Entity<Project>,
         basis: usize,
-        follower_states: &HashMap<PeerId, FollowerState>,
-        active_call: Option<&Entity<ActiveCall>>,
-        active_pane: &Entity<Pane>,
         zoomed: Option<&AnyWeakView>,
-        app_state: &Arc<AppState>,
+        render_cx: &dyn PaneLeaderDecorator,
         window: &mut Window,
-        cx: &mut Context<Workspace>,
+        cx: &mut App,
     ) -> impl IntoElement {
         match self {
             Member::Pane(pane) => {
@@ -238,76 +374,7 @@ impl Member {
                     return div().into_any();
                 }
 
-                let follower_state = follower_states.iter().find_map(|(leader_id, state)| {
-                    if state.center_pane == *pane {
-                        Some((*leader_id, state))
-                    } else {
-                        None
-                    }
-                });
-
-                let leader = follower_state.as_ref().and_then(|(leader_id, _)| {
-                    let room = active_call?.read(cx).room()?.read(cx);
-                    room.remote_participant_for_peer_id(*leader_id)
-                });
-
-                let is_in_unshared_view = follower_state.as_ref().map_or(false, |(_, state)| {
-                    state.active_view_id.is_some_and(|view_id| {
-                        !state.items_by_leader_view_id.contains_key(&view_id)
-                    })
-                });
-
-                let is_in_panel = follower_state
-                    .as_ref()
-                    .map_or(false, |(_, state)| state.dock_pane.is_some());
-
-                let mut leader_border = None;
-                let mut leader_status_box = None;
-                let mut leader_join_data = None;
-                if let Some(leader) = &leader {
-                    let mut leader_color = cx
-                        .theme()
-                        .players()
-                        .color_for_participant(leader.participant_index.0)
-                        .cursor;
-                    if is_in_panel {
-                        leader_color.fade_out(0.75);
-                    } else {
-                        leader_color.fade_out(0.3);
-                    }
-                    leader_border = Some(leader_color);
-
-                    leader_status_box = match leader.location {
-                        ParticipantLocation::SharedProject {
-                            project_id: leader_project_id,
-                        } => {
-                            if Some(leader_project_id) == project.read(cx).remote_id() {
-                                if is_in_unshared_view {
-                                    Some(Label::new(format!(
-                                        "{} is in an unshared pane",
-                                        leader.user.github_login
-                                    )))
-                                } else {
-                                    None
-                                }
-                            } else {
-                                leader_join_data = Some((leader_project_id, leader.user.id));
-                                Some(Label::new(format!(
-                                    "Follow {} to their active project",
-                                    leader.user.github_login,
-                                )))
-                            }
-                        }
-                        ParticipantLocation::UnsharedProject => Some(Label::new(format!(
-                            "{} is viewing an unshared Zed project",
-                            leader.user.github_login
-                        ))),
-                        ParticipantLocation::External => Some(Label::new(format!(
-                            "{} is viewing a window outside of Zed",
-                            leader.user.github_login
-                        ))),
-                    };
-                }
+                let decoration = render_cx.decorate(pane, cx);
 
                 div()
                     .relative()
@@ -317,7 +384,7 @@ impl Member {
                         AnyView::from(pane.clone())
                             .cached(StyleRefinement::default().v_flex().size_full()),
                     )
-                    .when_some(leader_border, |this, color| {
+                    .when_some(decoration.border, |this, color| {
                         this.child(
                             div()
                                 .absolute()
@@ -328,49 +395,11 @@ impl Member {
                                 .border_color(color),
                         )
                     })
-                    .when_some(leader_status_box, |this, status_box| {
-                        this.child(
-                            div()
-                                .absolute()
-                                .w_96()
-                                .bottom_3()
-                                .right_3()
-                                .elevation_2(cx)
-                                .p_1()
-                                .child(status_box)
-                                .when_some(
-                                    leader_join_data,
-                                    |this, (leader_project_id, leader_user_id)| {
-                                        this.cursor_pointer().on_mouse_down(
-                                            MouseButton::Left,
-                                            cx.listener(move |this, _, _, cx| {
-                                                crate::join_in_room_project(
-                                                    leader_project_id,
-                                                    leader_user_id,
-                                                    this.app_state().clone(),
-                                                    cx,
-                                                )
-                                                .detach_and_log_err(cx);
-                                            }),
-                                        )
-                                    },
-                                ),
-                        )
-                    })
+                    .children(decoration.status_box)
                     .into_any()
             }
             Member::Axis(axis) => axis
-                .render(
-                    project,
-                    basis + 1,
-                    follower_states,
-                    active_call,
-                    active_pane,
-                    zoomed,
-                    app_state,
-                    window,
-                    cx,
-                )
+                .render(basis + 1, zoomed, render_cx, window, cx)
                 .into_any(),
         }
     }
@@ -671,15 +700,11 @@ impl PaneAxis {
 
     fn render(
         &self,
-        project: &Entity<Project>,
         basis: usize,
-        follower_states: &HashMap<PeerId, FollowerState>,
-        active_call: Option<&Entity<ActiveCall>>,
-        active_pane: &Entity<Pane>,
         zoomed: Option<&AnyWeakView>,
-        app_state: &Arc<AppState>,
+        render_cx: &dyn PaneLeaderDecorator,
         window: &mut Window,
-        cx: &mut Context<Workspace>,
+        cx: &mut App,
     ) -> gpui::AnyElement {
         debug_assert!(self.members.len() == self.flexes.lock().len());
         let mut active_pane_ix = None;
@@ -689,24 +714,14 @@ impl PaneAxis {
             basis,
             self.flexes.clone(),
             self.bounding_boxes.clone(),
-            cx.entity().downgrade(),
+            render_cx.workspace().clone(),
         )
         .children(self.members.iter().enumerate().map(|(ix, member)| {
-            if matches!(member, Member::Pane(pane) if pane == active_pane) {
+            if matches!(member, Member::Pane(pane) if pane == render_cx.active_pane()) {
                 active_pane_ix = Some(ix);
             }
             member
-                .render(
-                    project,
-                    (basis + ix) * 10,
-                    follower_states,
-                    active_call,
-                    active_pane,
-                    zoomed,
-                    app_state,
-                    window,
-                    cx,
-                )
+                .render((basis + ix) * 10, zoomed, render_cx, window, cx)
                 .into_any_element()
         }))
         .with_active_pane(active_pane_ix)

crates/workspace/src/workspace.rs 🔗

@@ -5561,12 +5561,16 @@ impl Render for Workspace {
                                                             this.child(p.border_r_1())
                                                         })
                                                         .child(self.center.render(
-                                                            &self.project,
-                                                            &self.follower_states,
-                                                            self.active_call(),
-                                                            &self.active_pane,
                                                             self.zoomed.as_ref(),
-                                                            &self.app_state,
+                                                            &PaneRenderContext {
+                                                                follower_states:
+                                                                    &self.follower_states,
+                                                                active_call: self.active_call(),
+                                                                active_pane: &self.active_pane,
+                                                                app_state: &self.app_state,
+                                                                project: &self.project,
+                                                                workspace: &self.weak_self,
+                                                            },
                                                             window,
                                                             cx,
                                                         ))