Terminal in debugger (#29328)

Conrad Irwin created

- **debug-terminal**
- **Use terminal inside debugger to spawn commands**

Closes #ISSUE

Release Notes:

- N/A

Change summary

crates/debugger_ui/src/debugger_panel.rs       | 135 +++++++++-----
crates/debugger_ui/src/persistence.rs          |  14 +
crates/debugger_ui/src/session.rs              |   6 
crates/debugger_ui/src/session/running.rs      | 179 ++++++++++++++-----
crates/debugger_ui/src/tests/debugger_panel.rs |  27 +-
crates/project/src/terminals.rs                |  33 ---
crates/terminal/src/terminal.rs                |   7 
crates/terminal_view/src/terminal_view.rs      |   5 
crates/workspace/src/pane_group.rs             |  11 +
9 files changed, 251 insertions(+), 166 deletions(-)

Detailed changes

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -1,3 +1,4 @@
+use crate::persistence::DebuggerPaneItem;
 use crate::{
     ClearAllBreakpoints, Continue, CreateDebuggingSession, Disconnect, Pause, Restart, StepBack,
     StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, persistence,
@@ -32,8 +33,10 @@ use settings::Settings;
 use std::any::TypeId;
 use std::path::Path;
 use std::sync::Arc;
-use task::{DebugTaskDefinition, DebugTaskTemplate};
-use terminal_view::terminal_panel::TerminalPanel;
+use task::{
+    DebugTaskDefinition, DebugTaskTemplate, HideStrategy, RevealStrategy, RevealTarget, TaskId,
+};
+use terminal_view::TerminalView;
 use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
 use workspace::{
     Workspace,
@@ -293,23 +296,21 @@ impl DebugPanel {
 
                 (session.clone(), dap_store.boot_session(session, cx))
             })?;
+            Self::register_session(this.clone(), session.clone(), cx).await?;
 
-            match task.await {
-                Err(e) => {
-                    this.update(cx, |this, cx| {
-                        this.workspace
-                            .update(cx, |workspace, cx| {
-                                workspace.show_error(&e, cx);
-                            })
-                            .ok();
-                    })
-                    .ok();
+            if let Err(e) = task.await {
+                this.update(cx, |this, cx| {
+                    this.workspace
+                        .update(cx, |workspace, cx| {
+                            workspace.show_error(&e, cx);
+                        })
+                        .ok();
+                })
+                .ok();
 
-                    session
-                        .update(cx, |session, cx| session.shutdown(cx))?
-                        .await;
-                }
-                Ok(_) => Self::register_session(this, session, cx).await?,
+                session
+                    .update(cx, |session, cx| session.shutdown(cx))?
+                    .await;
             }
 
             anyhow::Ok(())
@@ -467,6 +468,7 @@ impl DebugPanel {
     ) {
         match event {
             dap_store::DapStoreEvent::RunInTerminal {
+                session_id,
                 title,
                 cwd,
                 command,
@@ -476,6 +478,7 @@ impl DebugPanel {
                 ..
             } => {
                 self.handle_run_in_terminal_request(
+                    *session_id,
                     title.clone(),
                     cwd.clone(),
                     command.clone(),
@@ -499,6 +502,7 @@ impl DebugPanel {
 
     fn handle_run_in_terminal_request(
         &self,
+        session_id: SessionId,
         title: Option<String>,
         cwd: Option<Arc<Path>>,
         command: Option<String>,
@@ -506,56 +510,83 @@ impl DebugPanel {
         envs: HashMap<String, String>,
         mut sender: mpsc::Sender<Result<u32>>,
         window: &mut Window,
-        cx: &mut App,
+        cx: &mut Context<Self>,
     ) -> Task<Result<()>> {
-        let terminal_task = self.workspace.update(cx, |workspace, cx| {
-            let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| {
-                anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found")
-            });
+        let Some(session) = self
+            .sessions
+            .iter()
+            .find(|s| s.read(cx).session_id(cx) == session_id)
+        else {
+            return Task::ready(Err(anyhow!("no session {:?} found", session_id)));
+        };
+        let running = session.read(cx).running_state();
+        let cwd = cwd.map(|p| p.to_path_buf());
+        let shell = self
+            .project
+            .read(cx)
+            .terminal_settings(&cwd, cx)
+            .shell
+            .clone();
+        let kind = if let Some(command) = command {
+            let title = title.clone().unwrap_or(command.clone());
+            TerminalKind::Task(task::SpawnInTerminal {
+                id: TaskId("debug".to_string()),
+                full_label: title.clone(),
+                label: title.clone(),
+                command: command.clone(),
+                args,
+                command_label: title.clone(),
+                cwd,
+                env: envs,
+                use_new_terminal: true,
+                allow_concurrent_runs: true,
+                reveal: RevealStrategy::NoFocus,
+                reveal_target: RevealTarget::Dock,
+                hide: HideStrategy::Never,
+                shell,
+                show_summary: false,
+                show_command: false,
+                show_rerun: false,
+            })
+        } else {
+            TerminalKind::Shell(cwd.map(|c| c.to_path_buf()))
+        };
 
-            let terminal_panel = match terminal_panel {
-                Ok(panel) => panel,
-                Err(err) => return Task::ready(Err(err)),
-            };
+        let workspace = self.workspace.clone();
+        let project = self.project.downgrade();
 
-            terminal_panel.update(cx, |terminal_panel, cx| {
-                let terminal_task = terminal_panel.add_terminal(
-                    TerminalKind::Debug {
-                        command,
-                        args,
-                        envs,
-                        cwd,
-                        title,
-                    },
-                    task::RevealStrategy::Never,
-                    window,
-                    cx,
-                );
+        let terminal_task = self.project.update(cx, |project, cx| {
+            project.create_terminal(kind, window.window_handle(), cx)
+        });
+        let terminal_task = cx.spawn_in(window, async move |_, cx| {
+            let terminal = terminal_task.await?;
 
-                cx.spawn(async move |_, cx| {
-                    let pid_task = async move {
-                        let terminal = terminal_task.await?;
+            let terminal_view = cx.new_window_entity(|window, cx| {
+                TerminalView::new(terminal.clone(), workspace, None, project, window, cx)
+            })?;
 
-                        terminal.read_with(cx, |terminal, _| terminal.pty_info.pid())
-                    };
+            running.update_in(cx, |running, window, cx| {
+                running.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx);
+                running.debug_terminal.update(cx, |debug_terminal, cx| {
+                    debug_terminal.terminal = Some(terminal_view);
+                    cx.notify();
+                });
+            })?;
 
-                    pid_task.await
-                })
-            })
+            anyhow::Ok(terminal.read_with(cx, |terminal, _| terminal.pty_info.pid())?)
         });
 
         cx.background_spawn(async move {
-            match terminal_task {
-                Ok(pid_task) => match pid_task.await {
-                    Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?,
-                    Ok(None) => {
+            match terminal_task.await {
+                Ok(pid_task) => match pid_task {
+                    Some(pid) => sender.send(Ok(pid.as_u32())).await?,
+                    None => {
                         sender
                             .send(Err(anyhow!(
                                 "Terminal was spawned but PID was not available"
                             )))
                             .await?
                     }
-                    Err(error) => sender.send(Err(anyhow!(error))).await?,
                 },
                 Err(error) => sender.send(Err(anyhow!(error))).await?,
             };

crates/debugger_ui/src/persistence.rs 🔗

@@ -9,7 +9,7 @@ use util::ResultExt;
 use workspace::{Member, Pane, PaneAxis, Workspace};
 
 use crate::session::running::{
-    self, RunningState, SubView, breakpoint_list::BreakpointList, console::Console,
+    self, DebugTerminal, RunningState, SubView, breakpoint_list::BreakpointList, console::Console,
     loaded_source_list::LoadedSourceList, module_list::ModuleList,
     stack_frame_list::StackFrameList, variable_list::VariableList,
 };
@@ -22,6 +22,7 @@ pub(crate) enum DebuggerPaneItem {
     Frames,
     Modules,
     LoadedSources,
+    Terminal,
 }
 
 impl DebuggerPaneItem {
@@ -33,6 +34,7 @@ impl DebuggerPaneItem {
             DebuggerPaneItem::Frames,
             DebuggerPaneItem::Modules,
             DebuggerPaneItem::LoadedSources,
+            DebuggerPaneItem::Terminal,
         ];
         VARIANTS
     }
@@ -55,6 +57,7 @@ impl DebuggerPaneItem {
             DebuggerPaneItem::Frames => SharedString::new_static("Frames"),
             DebuggerPaneItem::Modules => SharedString::new_static("Modules"),
             DebuggerPaneItem::LoadedSources => SharedString::new_static("Sources"),
+            DebuggerPaneItem::Terminal => SharedString::new_static("Terminal"),
         }
     }
 }
@@ -169,6 +172,7 @@ pub(crate) fn deserialize_pane_layout(
     console: &Entity<Console>,
     breakpoint_list: &Entity<BreakpointList>,
     loaded_sources: &Entity<LoadedSourceList>,
+    terminal: &Entity<DebugTerminal>,
     subscriptions: &mut HashMap<EntityId, Subscription>,
     window: &mut Window,
     cx: &mut Context<RunningState>,
@@ -191,6 +195,7 @@ pub(crate) fn deserialize_pane_layout(
                     console,
                     breakpoint_list,
                     loaded_sources,
+                    terminal,
                     subscriptions,
                     window,
                     cx,
@@ -273,6 +278,13 @@ pub(crate) fn deserialize_pane_layout(
                         })),
                         cx,
                     )),
+                    DebuggerPaneItem::Terminal => Box::new(SubView::new(
+                        pane.focus_handle(cx),
+                        terminal.clone().into(),
+                        DebuggerPaneItem::Terminal,
+                        None,
+                        cx,
+                    )),
                 })
                 .collect();
 

crates/debugger_ui/src/session.rs 🔗

@@ -104,6 +104,12 @@ impl DebugSession {
         &self.mode
     }
 
+    pub(crate) fn running_state(&self) -> Entity<RunningState> {
+        match &self.mode {
+            DebugSessionState::Running(running_state) => running_state.clone(),
+        }
+    }
+
     pub(crate) fn label(&self, cx: &App) -> String {
         if let Some(label) = self.label.get() {
             return label.to_owned();

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

@@ -27,6 +27,7 @@ use project::{
 use rpc::proto::ViewId;
 use settings::Settings;
 use stack_frame_list::StackFrameList;
+use terminal_view::TerminalView;
 use ui::{
     ActiveTheme, AnyElement, App, Context, ContextMenu, DropdownMenu, FluentBuilder,
     InteractiveElement, IntoElement, Label, LabelCommon as _, ParentElement, Render, SharedString,
@@ -35,7 +36,7 @@ use ui::{
 use util::ResultExt;
 use variable_list::VariableList;
 use workspace::{
-    ActivePaneDecorator, DraggedTab, Item, Member, Pane, PaneGroup, Workspace,
+    ActivePaneDecorator, DraggedTab, Item, ItemHandle, Member, Pane, PaneGroup, Workspace,
     item::TabContentParams, move_item, pane::Event,
 };
 
@@ -50,6 +51,7 @@ pub struct RunningState {
     _subscriptions: Vec<Subscription>,
     stack_frame_list: Entity<stack_frame_list::StackFrameList>,
     loaded_sources_list: Entity<LoadedSourceList>,
+    pub debug_terminal: Entity<DebugTerminal>,
     module_list: Entity<module_list::ModuleList>,
     _console: Entity<Console>,
     breakpoint_list: Entity<BreakpointList>,
@@ -364,6 +366,40 @@ pub(crate) fn new_debugger_pane(
 
     ret
 }
+
+pub struct DebugTerminal {
+    pub terminal: Option<Entity<TerminalView>>,
+    focus_handle: FocusHandle,
+}
+
+impl DebugTerminal {
+    fn empty(cx: &mut Context<Self>) -> Self {
+        Self {
+            terminal: None,
+            focus_handle: cx.focus_handle(),
+        }
+    }
+}
+
+impl gpui::Render for DebugTerminal {
+    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+        if let Some(terminal) = self.terminal.clone() {
+            terminal.into_any_element()
+        } else {
+            div().track_focus(&self.focus_handle).into_any_element()
+        }
+    }
+}
+impl Focusable for DebugTerminal {
+    fn focus_handle(&self, cx: &App) -> FocusHandle {
+        if let Some(terminal) = self.terminal.as_ref() {
+            return terminal.focus_handle(cx);
+        } else {
+            self.focus_handle.clone()
+        }
+    }
+}
+
 impl RunningState {
     pub fn new(
         session: Entity<Session>,
@@ -380,6 +416,8 @@ impl RunningState {
             StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
         });
 
+        let debug_terminal = cx.new(DebugTerminal::empty);
+
         let variable_list =
             cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
 
@@ -452,6 +490,7 @@ impl RunningState {
                 &console,
                 &breakpoint_list,
                 &loaded_source_list,
+                &debug_terminal,
                 &mut pane_close_subscriptions,
                 window,
                 cx,
@@ -494,6 +533,7 @@ impl RunningState {
             breakpoint_list,
             loaded_sources_list: loaded_source_list,
             pane_close_subscriptions,
+            debug_terminal,
             _schedule_serialize: None,
         }
     }
@@ -525,6 +565,90 @@ impl RunningState {
         self.panes.pane_at_pixel_position(position).is_some()
     }
 
+    fn create_sub_view(
+        &self,
+        item_kind: DebuggerPaneItem,
+        pane: &Entity<Pane>,
+        cx: &mut Context<Self>,
+    ) -> Box<dyn ItemHandle> {
+        match item_kind {
+            DebuggerPaneItem::Console => {
+                let weak_console = self._console.clone().downgrade();
+
+                Box::new(SubView::new(
+                    pane.focus_handle(cx),
+                    self._console.clone().into(),
+                    item_kind,
+                    Some(Box::new(move |cx| {
+                        weak_console
+                            .read_with(cx, |console, cx| console.show_indicator(cx))
+                            .unwrap_or_default()
+                    })),
+                    cx,
+                ))
+            }
+            DebuggerPaneItem::Variables => Box::new(SubView::new(
+                self.variable_list.focus_handle(cx),
+                self.variable_list.clone().into(),
+                item_kind,
+                None,
+                cx,
+            )),
+            DebuggerPaneItem::BreakpointList => Box::new(SubView::new(
+                self.breakpoint_list.focus_handle(cx),
+                self.breakpoint_list.clone().into(),
+                item_kind,
+                None,
+                cx,
+            )),
+            DebuggerPaneItem::Frames => Box::new(SubView::new(
+                self.stack_frame_list.focus_handle(cx),
+                self.stack_frame_list.clone().into(),
+                item_kind,
+                None,
+                cx,
+            )),
+            DebuggerPaneItem::Modules => Box::new(SubView::new(
+                self.module_list.focus_handle(cx),
+                self.module_list.clone().into(),
+                item_kind,
+                None,
+                cx,
+            )),
+            DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
+                self.loaded_sources_list.focus_handle(cx),
+                self.loaded_sources_list.clone().into(),
+                item_kind,
+                None,
+                cx,
+            )),
+            DebuggerPaneItem::Terminal => Box::new(SubView::new(
+                self.debug_terminal.focus_handle(cx),
+                self.debug_terminal.clone().into(),
+                item_kind,
+                None,
+                cx,
+            )),
+        }
+    }
+
+    pub(crate) fn ensure_pane_item(
+        &mut self,
+        item_kind: DebuggerPaneItem,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if self.pane_items_status(cx).get(&item_kind) == Some(&true) {
+            return;
+        };
+        let pane = self.panes.last_pane();
+        let sub_view = self.create_sub_view(item_kind, &pane, cx);
+
+        pane.update(cx, |pane, cx| {
+            pane.add_item_inner(sub_view, false, false, false, None, window, cx);
+        })
+    }
+
     pub(crate) fn add_pane_item(
         &mut self,
         item_kind: DebuggerPaneItem,
@@ -538,58 +662,7 @@ impl RunningState {
         );
 
         if let Some(pane) = self.panes.pane_at_pixel_position(position) {
-            let sub_view = match item_kind {
-                DebuggerPaneItem::Console => {
-                    let weak_console = self._console.clone().downgrade();
-
-                    Box::new(SubView::new(
-                        pane.focus_handle(cx),
-                        self._console.clone().into(),
-                        item_kind,
-                        Some(Box::new(move |cx| {
-                            weak_console
-                                .read_with(cx, |console, cx| console.show_indicator(cx))
-                                .unwrap_or_default()
-                        })),
-                        cx,
-                    ))
-                }
-                DebuggerPaneItem::Variables => Box::new(SubView::new(
-                    self.variable_list.focus_handle(cx),
-                    self.variable_list.clone().into(),
-                    item_kind,
-                    None,
-                    cx,
-                )),
-                DebuggerPaneItem::BreakpointList => Box::new(SubView::new(
-                    self.breakpoint_list.focus_handle(cx),
-                    self.breakpoint_list.clone().into(),
-                    item_kind,
-                    None,
-                    cx,
-                )),
-                DebuggerPaneItem::Frames => Box::new(SubView::new(
-                    self.stack_frame_list.focus_handle(cx),
-                    self.stack_frame_list.clone().into(),
-                    item_kind,
-                    None,
-                    cx,
-                )),
-                DebuggerPaneItem::Modules => Box::new(SubView::new(
-                    self.module_list.focus_handle(cx),
-                    self.module_list.clone().into(),
-                    item_kind,
-                    None,
-                    cx,
-                )),
-                DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
-                    self.loaded_sources_list.focus_handle(cx),
-                    self.loaded_sources_list.clone().into(),
-                    item_kind,
-                    None,
-                    cx,
-                )),
-            };
+            let sub_view = self.create_sub_view(item_kind, pane, cx);
 
             pane.update(cx, |pane, cx| {
                 pane.add_item(sub_view, false, false, None, window, cx);

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

@@ -1,4 +1,4 @@
-use crate::{tests::start_debug_session, *};
+use crate::{persistence::DebuggerPaneItem, tests::start_debug_session, *};
 use dap::{
     ErrorResponse, Message, RunInTerminalRequestArguments, SourceBreakpoint,
     StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
@@ -25,7 +25,7 @@ use std::{
         atomic::{AtomicBool, Ordering},
     },
 };
-use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
+use terminal_view::terminal_panel::TerminalPanel;
 use tests::{active_debug_session_panel, init_test, init_test_workspace};
 use util::path;
 use workspace::{Item, dock::Panel};
@@ -385,22 +385,17 @@ async fn test_handle_successful_run_in_terminal_reverse_request(
 
     workspace
         .update(cx, |workspace, _window, cx| {
-            let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
-
-            let panel = terminal_panel.read(cx).pane().unwrap().read(cx);
-
-            assert_eq!(1, panel.items_len());
-            assert!(
-                panel
-                    .active_item()
-                    .unwrap()
-                    .downcast::<TerminalView>()
-                    .unwrap()
-                    .read(cx)
-                    .terminal()
+            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
+            let session = debug_panel.read(cx).active_session().unwrap();
+            let running = session.read(cx).running_state();
+            assert_eq!(
+                running
                     .read(cx)
-                    .debug_terminal()
+                    .pane_items_status(cx)
+                    .get(&DebuggerPaneItem::Terminal),
+                Some(&true)
             );
+            assert!(running.read(cx).debug_terminal.read(cx).terminal.is_some());
         })
         .unwrap();
 

crates/project/src/terminals.rs 🔗

@@ -31,14 +31,6 @@ pub enum TerminalKind {
     Shell(Option<PathBuf>),
     /// Run a task.
     Task(SpawnInTerminal),
-    /// Run a debug terminal.
-    Debug {
-        command: Option<String>,
-        args: Vec<String>,
-        envs: HashMap<String, String>,
-        cwd: Option<Arc<Path>>,
-        title: Option<String>,
-    },
 }
 
 /// SshCommand describes how to connect to a remote server
@@ -105,7 +97,6 @@ impl Project {
                     self.active_project_directory(cx)
                 }
             }
-            TerminalKind::Debug { cwd, .. } => cwd.clone(),
         };
 
         let mut settings_location = None;
@@ -209,7 +200,6 @@ impl Project {
                     this.active_project_directory(cx)
                 }
             }
-            TerminalKind::Debug { cwd, .. } => cwd.clone(),
         };
         let ssh_details = this.ssh_details(cx);
 
@@ -243,7 +233,6 @@ impl Project {
         };
 
         let mut python_venv_activate_command = None;
-        let debug_terminal = matches!(kind, TerminalKind::Debug { .. });
 
         let (spawn_task, shell) = match kind {
             TerminalKind::Shell(_) => {
@@ -339,27 +328,6 @@ impl Project {
                     }
                 }
             }
-            TerminalKind::Debug {
-                command,
-                args,
-                envs,
-                title,
-                ..
-            } => {
-                env.extend(envs);
-
-                let shell = if let Some(program) = command {
-                    Shell::WithArguments {
-                        program,
-                        args,
-                        title_override: Some(title.unwrap_or("Debug Terminal".into()).into()),
-                    }
-                } else {
-                    settings.shell.clone()
-                };
-
-                (None, shell)
-            }
         };
         TerminalBuilder::new(
             local_path.map(|path| path.to_path_buf()),
@@ -373,7 +341,6 @@ impl Project {
             ssh_details.is_some(),
             window,
             completion_tx,
-            debug_terminal,
             cx,
         )
         .map(|builder| {

crates/terminal/src/terminal.rs 🔗

@@ -352,7 +352,6 @@ impl TerminalBuilder {
         is_ssh_terminal: bool,
         window: AnyWindowHandle,
         completion_tx: Sender<Option<ExitStatus>>,
-        debug_terminal: bool,
         cx: &App,
     ) -> Result<TerminalBuilder> {
         // If the parent environment doesn't have a locale set
@@ -502,7 +501,6 @@ impl TerminalBuilder {
             word_regex: RegexSearch::new(WORD_REGEX).unwrap(),
             python_file_line_regex: RegexSearch::new(PYTHON_FILE_LINE_REGEX).unwrap(),
             vi_mode_enabled: false,
-            debug_terminal,
             is_ssh_terminal,
             python_venv_directory,
         };
@@ -660,7 +658,6 @@ pub struct Terminal {
     python_file_line_regex: RegexSearch,
     task: Option<TaskState>,
     vi_mode_enabled: bool,
-    debug_terminal: bool,
     is_ssh_terminal: bool,
 }
 
@@ -1855,10 +1852,6 @@ impl Terminal {
         self.task.as_ref()
     }
 
-    pub fn debug_terminal(&self) -> bool {
-        self.debug_terminal
-    }
-
     pub fn wait_for_completed_task(&self, cx: &App) -> Task<Option<ExitStatus>> {
         if let Some(task) = self.task() {
             if task.status == TaskStatus::Running {

crates/terminal_view/src/terminal_view.rs 🔗

@@ -1420,9 +1420,6 @@ impl Item for TerminalView {
                     }
                 }
             },
-            None if self.terminal.read(cx).debug_terminal() => {
-                (IconName::Debug, Color::Muted, None)
-            }
             None => (IconName::Terminal, Color::Muted, None),
         };
 
@@ -1583,7 +1580,7 @@ impl SerializableItem for TerminalView {
         cx: &mut Context<Self>,
     ) -> Option<Task<gpui::Result<()>>> {
         let terminal = self.terminal().read(cx);
-        if terminal.task().is_some() || terminal.debug_terminal() {
+        if terminal.task().is_some() {
             return None;
         }
 

crates/workspace/src/pane_group.rs 🔗

@@ -142,6 +142,10 @@ impl PaneGroup {
         self.root.first_pane()
     }
 
+    pub fn last_pane(&self) -> Entity<Pane> {
+        self.root.last_pane()
+    }
+
     pub fn find_pane_in_direction(
         &mut self,
         active_pane: &Entity<Pane>,
@@ -360,6 +364,13 @@ impl Member {
         }
     }
 
+    fn last_pane(&self) -> Entity<Pane> {
+        match self {
+            Member::Axis(axis) => axis.members.last().unwrap().last_pane(),
+            Member::Pane(pane) => pane.clone(),
+        }
+    }
+
     pub fn render(
         &self,
         basis: usize,