From 269b03f4a3219d54e19354484ad44d928ce740a7 Mon Sep 17 00:00:00 2001 From: xj Date: Tue, 24 Feb 2026 08:23:02 -0800 Subject: [PATCH] workspace: Add ActivateLastPane action (#49853) ## Summary Add `workspace::ActivateLastPane` so users can bind a shortcut (for example `cmd-9`) to focus the last pane. ## Why Today, the closest option is `workspace::ActivatePane` with an index (for example `8`), but that has side effects: when the index does not exist, it creates/splits panes (`activate_pane_at_index` fallback). `ActivateLastPane` gives a stable, no-surprises target: focus the rightmost/last pane in current pane order, never create a new pane. ## Context This capability has been requested by users before: - https://github.com/zed-industries/zed/issues/17503#event-22959656321 ## Prior art VS Code exposes explicit editor-group focus commands and index-based focus patterns (e.g. `workbench.action.focusSecondEditorGroup` ... `focusEighthEditorGroup`) in its workbench commands: - https://github.com/microsoft/vscode/blob/main/src/vs/workbench/browser/parts/editor/editorCommands.ts#L675-L724 Zed already follows numbered pane focus in default keymaps (`ActivatePane` 1..9 on macOS/Linux/Windows), so adding a dedicated "last pane" action is a small, natural extension: - `assets/keymaps/default-macos.json` - `assets/keymaps/default-linux.json` - `assets/keymaps/default-windows.json` ## Change - Added `workspace::ActivateLastPane` - Implemented `Workspace::activate_last_pane(...)` - Wired action handler in workspace listeners - Added `test_activate_last_pane` ## Validation - `cargo test -p workspace test_activate_last_pane -- --nocapture` - `cargo test -p workspace test_pane_navigation -- --nocapture` - `cargo fmt --all -- --check` ## Risk Low: focus-only behavior, no layout/data changes, no default keymap changes. Release Notes: - Added `workspace::ActivateLastPane` action for keybindings that focus the last pane. --------- Co-authored-by: xj --- crates/workspace/src/workspace.rs | 64 +++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c12525bb2a5c6b46cd6b4fabc9599e3b6cdfd25d..c1d26476544ecf5db51a9c7b358ad12c84aa168f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -210,6 +210,8 @@ actions!( ActivateNextPane, /// Activates the previous pane in the workspace. ActivatePreviousPane, + /// Activates the last pane in the workspace. + ActivateLastPane, /// Switches to the next window. ActivateNextWindow, /// Switches to the previous window. @@ -4331,6 +4333,11 @@ impl Workspace { } } + pub fn activate_last_pane(&mut self, window: &mut Window, cx: &mut App) { + let last_pane = self.center.last_pane(); + window.focus(&last_pane.focus_handle(cx), cx); + } + pub fn activate_pane_in_direction( &mut self, direction: SplitDirection, @@ -6381,6 +6388,9 @@ impl Workspace { .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| { workspace.activate_next_pane(window, cx) })) + .on_action(cx.listener(|workspace, _: &ActivateLastPane, window, cx| { + workspace.activate_last_pane(window, cx) + })) .on_action( cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| { workspace.activate_next_window(cx) @@ -6403,9 +6413,6 @@ impl Workspace { .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| { workspace.activate_pane_in_direction(SplitDirection::Down, window, cx) })) - .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| { - workspace.activate_next_pane(window, cx) - })) .on_action(cx.listener( |workspace, action: &MoveItemToPaneInDirection, window, cx| { workspace.move_item_to_pane_in_direction(action, window, cx) @@ -10552,6 +10559,57 @@ mod tests { }); } + #[gpui::test] + async fn test_activate_last_pane(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + + workspace.update_in(cx, |workspace, window, cx| { + let first_item = cx.new(|cx| { + TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) + }); + workspace.add_item_to_active_pane(Box::new(first_item), None, true, window, cx); + workspace.split_pane( + workspace.active_pane().clone(), + SplitDirection::Right, + window, + cx, + ); + workspace.split_pane( + workspace.active_pane().clone(), + SplitDirection::Right, + window, + cx, + ); + }); + + let (first_pane_id, target_last_pane_id) = workspace.update(cx, |workspace, _cx| { + let panes = workspace.center.panes(); + assert!(panes.len() >= 2); + ( + panes.first().expect("at least one pane").entity_id(), + panes.last().expect("at least one pane").entity_id(), + ) + }); + + workspace.update_in(cx, |workspace, window, cx| { + workspace.activate_pane_at_index(&ActivatePane(0), window, cx); + }); + workspace.update(cx, |workspace, _| { + assert_eq!(workspace.active_pane().entity_id(), first_pane_id); + assert_ne!(workspace.active_pane().entity_id(), target_last_pane_id); + }); + + cx.dispatch_action(ActivateLastPane); + + workspace.update(cx, |workspace, _| { + assert_eq!(workspace.active_pane().entity_id(), target_last_pane_id); + }); + } + #[gpui::test] async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) { init_test(cx);