From 1b43217c0541d717d4facda75df9304fa9d1e2aa Mon Sep 17 00:00:00 2001 From: Adir Shemesh <50236379+Adir-Shemesh@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:54:05 +0200 Subject: [PATCH] Add a jetbrains-like Toggle All Docks action (#40567) The current Jetbrains keymap has `ctrl-shift-f12` set to `CloseAllDocks`. On Jetbrains IDEs this hotkey actually toggles the docks, which is very convenient: You press it once to hide all docks and just focus on the code, and then you can press it again to toggle your docks right back to how they were. Unlike `CloseAllDocks`, a toggle means the editor needs to remember the previous docks state so this necessitated some code changes. Release Notes: - Added a `Toggle All Docks` editor action and updated the keymaps to use it --- assets/keymaps/default-linux.json | 2 +- assets/keymaps/default-macos.json | 2 +- assets/keymaps/default-windows.json | 2 +- assets/keymaps/linux/jetbrains.json | 2 +- assets/keymaps/macos/jetbrains.json | 2 +- crates/workspace/src/workspace.rs | 313 +++++++++++++++++++++++++++- crates/zed/src/zed/app_menus.rs | 2 +- 7 files changed, 315 insertions(+), 10 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 3d94edafcd..2c5f25a29c 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -609,7 +609,7 @@ "ctrl-alt-b": "workspace::ToggleRightDock", "ctrl-b": "workspace::ToggleLeftDock", "ctrl-j": "workspace::ToggleBottomDock", - "ctrl-alt-y": "workspace::CloseAllDocks", + "ctrl-alt-y": "workspace::ToggleAllDocks", "ctrl-alt-0": "workspace::ResetActiveDockSize", // For 0px parameter, uses UI font size value. "ctrl-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }], diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 6c3f47cb45..f0a165e462 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -679,7 +679,7 @@ "cmd-alt-b": "workspace::ToggleRightDock", "cmd-r": "workspace::ToggleRightDock", "cmd-j": "workspace::ToggleBottomDock", - "alt-cmd-y": "workspace::CloseAllDocks", + "alt-cmd-y": "workspace::ToggleAllDocks", // For 0px parameter, uses UI font size value. "ctrl-alt-0": "workspace::ResetActiveDockSize", "ctrl-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }], diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 5b96d20633..5c84bb182a 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -614,7 +614,7 @@ "ctrl-alt-b": "workspace::ToggleRightDock", "ctrl-b": "workspace::ToggleLeftDock", "ctrl-j": "workspace::ToggleBottomDock", - "ctrl-shift-y": "workspace::CloseAllDocks", + "ctrl-shift-y": "workspace::ToggleAllDocks", "alt-r": "workspace::ResetActiveDockSize", // For 0px parameter, uses UI font size value. "shift-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }], diff --git a/assets/keymaps/linux/jetbrains.json b/assets/keymaps/linux/jetbrains.json index a5e387c014..cf28c43dbd 100644 --- a/assets/keymaps/linux/jetbrains.json +++ b/assets/keymaps/linux/jetbrains.json @@ -91,7 +91,7 @@ { "context": "Workspace", "bindings": { - "ctrl-shift-f12": "workspace::CloseAllDocks", + "ctrl-shift-f12": "workspace::ToggleAllDocks", "ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }], "alt-shift-f10": "task::Spawn", "ctrl-e": "file_finder::Toggle", diff --git a/assets/keymaps/macos/jetbrains.json b/assets/keymaps/macos/jetbrains.json index 2c757c3a30..e5e5aeb0b8 100644 --- a/assets/keymaps/macos/jetbrains.json +++ b/assets/keymaps/macos/jetbrains.json @@ -93,7 +93,7 @@ { "context": "Workspace", "bindings": { - "cmd-shift-f12": "workspace::CloseAllDocks", + "cmd-shift-f12": "workspace::ToggleAllDocks", "cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }], "ctrl-alt-r": "task::Spawn", "cmd-e": "file_finder::Toggle", diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e34f9f6285..a548a04aa7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -203,6 +203,8 @@ actions!( CloseActiveDock, /// Closes all docks. CloseAllDocks, + /// Toggles all docks. + ToggleAllDocks, /// Closes the current window. CloseWindow, /// Opens the feedback dialog. @@ -1176,6 +1178,7 @@ pub struct Workspace { _items_serializer: Task>, session_id: Option, scheduled_tasks: Vec>, + last_open_dock_positions: Vec, } impl EventEmitter for Workspace {} @@ -1518,6 +1521,7 @@ impl Workspace { session_id: Some(session_id), scheduled_tasks: Vec::new(), + last_open_dock_positions: Vec::new(), } } @@ -2987,12 +2991,17 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) { - let dock = self.dock_at_position(dock_side); let mut focus_center = false; let mut reveal_dock = false; + + let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side); + let was_visible = self.is_dock_at_position_open(dock_side, cx) && !other_is_zoomed; + if was_visible { + self.save_open_dock_positions(cx); + } + + let dock = self.dock_at_position(dock_side); dock.update(cx, |dock, cx| { - let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side); - let was_visible = dock.is_open() && !other_is_zoomed; dock.set_open(!was_visible, window, cx); if dock.active_panel().is_none() { @@ -3041,7 +3050,8 @@ impl Workspace { } fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context) -> bool { - if let Some(dock) = self.active_dock(window, cx) { + if let Some(dock) = self.active_dock(window, cx).cloned() { + self.save_open_dock_positions(cx); dock.update(cx, |dock, cx| { dock.set_open(false, window, cx); }); @@ -3051,6 +3061,7 @@ impl Workspace { } pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context) { + self.save_open_dock_positions(cx); for dock in self.all_docks() { dock.update(cx, |dock, cx| { dock.set_open(false, window, cx); @@ -3062,6 +3073,67 @@ impl Workspace { self.serialize_workspace(window, cx); } + fn get_open_dock_positions(&self, cx: &Context) -> Vec { + self.all_docks() + .into_iter() + .filter_map(|dock| { + let dock_ref = dock.read(cx); + if dock_ref.is_open() { + Some(dock_ref.position()) + } else { + None + } + }) + .collect() + } + + /// Saves the positions of currently open docks. + /// + /// Updates `last_open_dock_positions` with positions of all currently open + /// docks, to later be restored by the 'Toggle All Docks' action. + fn save_open_dock_positions(&mut self, cx: &mut Context) { + let open_dock_positions = self.get_open_dock_positions(cx); + if !open_dock_positions.is_empty() { + self.last_open_dock_positions = open_dock_positions; + } + } + + /// Toggles all docks between open and closed states. + /// + /// If any docks are open, closes all and remembers their positions. If all + /// docks are closed, restores the last remembered dock configuration. + fn toggle_all_docks( + &mut self, + _: &ToggleAllDocks, + window: &mut Window, + cx: &mut Context, + ) { + let open_dock_positions = self.get_open_dock_positions(cx); + + if !open_dock_positions.is_empty() { + self.close_all_docks(window, cx); + } else if !self.last_open_dock_positions.is_empty() { + self.restore_last_open_docks(window, cx); + } + } + + /// Reopens docks from the most recently remembered configuration. + /// + /// Opens all docks whose positions are stored in `last_open_dock_positions` + /// and clears the stored positions. + fn restore_last_open_docks(&mut self, window: &mut Window, cx: &mut Context) { + let positions_to_open = std::mem::take(&mut self.last_open_dock_positions); + + for position in positions_to_open { + let dock = self.dock_at_position(position); + dock.update(cx, |dock, cx| dock.set_open(true, window, cx)); + } + + cx.focus_self(window); + cx.notify(); + self.serialize_workspace(window, cx); + } + /// Transfer focus to the panel of the given type. pub fn focus_panel( &mut self, @@ -5761,6 +5833,7 @@ impl Workspace { workspace.close_all_docks(window, cx); }), ) + .on_action(cx.listener(Self::toggle_all_docks)) .on_action(cx.listener( |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| { workspace.clear_all_notifications(cx); @@ -9206,6 +9279,238 @@ mod tests { }); } + #[gpui::test] + async fn test_toggle_all_docks(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| { + // Open two docks + let left_dock = workspace.dock_at_position(DockPosition::Left); + let right_dock = workspace.dock_at_position(DockPosition::Right); + + left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx)); + right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx)); + + assert!(left_dock.read(cx).is_open()); + assert!(right_dock.read(cx).is_open()); + }); + + workspace.update_in(cx, |workspace, window, cx| { + // Toggle all docks - should close both + workspace.toggle_all_docks(&ToggleAllDocks, window, cx); + + let left_dock = workspace.dock_at_position(DockPosition::Left); + let right_dock = workspace.dock_at_position(DockPosition::Right); + assert!(!left_dock.read(cx).is_open()); + assert!(!right_dock.read(cx).is_open()); + }); + + workspace.update_in(cx, |workspace, window, cx| { + // Toggle again - should reopen both + workspace.toggle_all_docks(&ToggleAllDocks, window, cx); + + let left_dock = workspace.dock_at_position(DockPosition::Left); + let right_dock = workspace.dock_at_position(DockPosition::Right); + assert!(left_dock.read(cx).is_open()); + assert!(right_dock.read(cx).is_open()); + }); + } + + #[gpui::test] + async fn test_toggle_all_with_manual_close(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| { + // Open two docks + let left_dock = workspace.dock_at_position(DockPosition::Left); + let right_dock = workspace.dock_at_position(DockPosition::Right); + + left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx)); + right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx)); + + assert!(left_dock.read(cx).is_open()); + assert!(right_dock.read(cx).is_open()); + }); + + workspace.update_in(cx, |workspace, window, cx| { + // Close them manually + workspace.toggle_dock(DockPosition::Left, window, cx); + workspace.toggle_dock(DockPosition::Right, window, cx); + + let left_dock = workspace.dock_at_position(DockPosition::Left); + let right_dock = workspace.dock_at_position(DockPosition::Right); + assert!(!left_dock.read(cx).is_open()); + assert!(!right_dock.read(cx).is_open()); + }); + + workspace.update_in(cx, |workspace, window, cx| { + // Toggle all docks - only last closed (right dock) should reopen + workspace.toggle_all_docks(&ToggleAllDocks, window, cx); + + let left_dock = workspace.dock_at_position(DockPosition::Left); + let right_dock = workspace.dock_at_position(DockPosition::Right); + assert!(!left_dock.read(cx).is_open()); + assert!(right_dock.read(cx).is_open()); + }); + } + + #[gpui::test] + async fn test_toggle_all_docks_after_dock_move(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)); + + // Open two docks (left and right) with one panel each + let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| { + let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, cx)); + workspace.add_panel(left_panel.clone(), window, cx); + + let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx)); + workspace.add_panel(right_panel.clone(), window, cx); + + workspace.toggle_dock(DockPosition::Left, window, cx); + workspace.toggle_dock(DockPosition::Right, window, cx); + + // Verify initial state + assert!( + workspace.left_dock().read(cx).is_open(), + "Left dock should be open" + ); + assert_eq!( + workspace + .left_dock() + .read(cx) + .visible_panel() + .unwrap() + .panel_id(), + left_panel.panel_id(), + "Left panel should be visible in left dock" + ); + assert!( + workspace.right_dock().read(cx).is_open(), + "Right dock should be open" + ); + assert_eq!( + workspace + .right_dock() + .read(cx) + .visible_panel() + .unwrap() + .panel_id(), + right_panel.panel_id(), + "Right panel should be visible in right dock" + ); + assert!( + !workspace.bottom_dock().read(cx).is_open(), + "Bottom dock should be closed" + ); + + (left_panel, right_panel) + }); + + // Focus the left panel and move it to the next position (bottom dock) + workspace.update_in(cx, |workspace, window, cx| { + workspace.toggle_panel_focus::(window, cx); // Focus left panel + assert!( + left_panel.read(cx).focus_handle(cx).is_focused(window), + "Left panel should be focused" + ); + }); + + cx.dispatch_action(MoveFocusedPanelToNextPosition); + + // Verify the left panel has moved to the bottom dock, and the bottom dock is now open + workspace.update(cx, |workspace, cx| { + assert!( + !workspace.left_dock().read(cx).is_open(), + "Left dock should be closed" + ); + assert!( + workspace.bottom_dock().read(cx).is_open(), + "Bottom dock should now be open" + ); + assert_eq!( + left_panel.read(cx).position, + DockPosition::Bottom, + "Left panel should now be in the bottom dock" + ); + assert_eq!( + workspace + .bottom_dock() + .read(cx) + .visible_panel() + .unwrap() + .panel_id(), + left_panel.panel_id(), + "Left panel should be the visible panel in the bottom dock" + ); + }); + + // Toggle all docks off + workspace.update_in(cx, |workspace, window, cx| { + workspace.toggle_all_docks(&ToggleAllDocks, window, cx); + assert!( + !workspace.left_dock().read(cx).is_open(), + "Left dock should be closed" + ); + assert!( + !workspace.right_dock().read(cx).is_open(), + "Right dock should be closed" + ); + assert!( + !workspace.bottom_dock().read(cx).is_open(), + "Bottom dock should be closed" + ); + }); + + // Toggle all docks back on and verify positions are restored + workspace.update_in(cx, |workspace, window, cx| { + workspace.toggle_all_docks(&ToggleAllDocks, window, cx); + assert!( + !workspace.left_dock().read(cx).is_open(), + "Left dock should remain closed" + ); + assert!( + workspace.right_dock().read(cx).is_open(), + "Right dock should remain open" + ); + assert!( + workspace.bottom_dock().read(cx).is_open(), + "Bottom dock should remain open" + ); + assert_eq!( + left_panel.read(cx).position, + DockPosition::Bottom, + "Left panel should remain in the bottom dock" + ); + assert_eq!( + right_panel.read(cx).position, + DockPosition::Right, + "Right panel should remain in the right dock" + ); + assert_eq!( + workspace + .bottom_dock() + .read(cx) + .visible_panel() + .unwrap() + .panel_id(), + left_panel.panel_id(), + "Left panel should be the visible panel in the right dock" + ); + }); + } + #[gpui::test] async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) { init_test(cx); diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index ac22f97236..af68cbbbe9 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -28,7 +28,7 @@ pub fn app_menus(cx: &mut App) -> Vec { MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock), MenuItem::action("Toggle Right Dock", workspace::ToggleRightDock), MenuItem::action("Toggle Bottom Dock", workspace::ToggleBottomDock), - MenuItem::action("Close All Docks", workspace::CloseAllDocks), + MenuItem::action("Toggle All Docks", workspace::ToggleAllDocks), MenuItem::submenu(Menu { name: "Editor Layout".into(), items: vec![