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
This commit is contained in:
Adir Shemesh
2025-10-28 10:54:05 +02:00
committed by GitHub
parent 1b6cde7032
commit 1b43217c05
7 changed files with 315 additions and 10 deletions

View File

@@ -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 }],

View File

@@ -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 }],

View File

@@ -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 }],

View File

@@ -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",

View File

@@ -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",

View File

@@ -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<Result<()>>,
session_id: Option<String>,
scheduled_tasks: Vec<Task<()>>,
last_open_dock_positions: Vec<DockPosition>,
}
impl EventEmitter<Event> 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<Self>,
) {
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<Self>) -> 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>) {
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<Self>) -> Vec<DockPosition> {
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<Self>) {
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<Self>,
) {
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<Self>) {
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<T: 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::<TestPanel>(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);

View File

@@ -28,7 +28,7 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
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![