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 3d94edafcdfc1d9acec5328cade996459547996b..2c5f25a29ca3e54e232cb54fbe54080ac37b2419 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 6c3f47cb45909c1e014e76c9d414b68f23632a14..f0a165e462a009b826302469e1fc32182c9a4d27 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 5b96d20633b573d939e49a3ea60c4afc5d7ca721..5c84bb182adf7163d8330828005276405c918f9c 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 a5e387c014e1315bf51cfdf7c5226adaa8a20b27..cf28c43dbd7f8335f30ef7702e584bea5c0ba5e0 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 2c757c3a30a08eb55e8344945ab66baf91ce0c6b..e5e5aeb0b8516285136438d40b57fb17fc9a9777 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 e34f9f628507681b4977c2abbe716d83d8bf97c9..a548a04aa7be55d44a0d30af5dbb49eeba54ade5 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 ac22f972368f61fa518ac74a5ac23e593433c75b..af68cbbbe9c5178db80f1fc9adc0a922e634c82a 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![