From 2df5993eb0a38c7f1935f6ea1de06fc9cb84f262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Fr=C3=B6hlich?= <65558856+mfroeh@users.noreply.github.com> Date: Tue, 2 Dec 2025 04:36:06 +0100 Subject: [PATCH] workspace: Add `ctrl-w x` support for vim (#42792) Adds support for the vim CTRL-W x keybinding, which swaps the active pane with the next adjacent one, prioritizing column over row and next over previous. Upon swap, the pane which was swapped with is activated (this is the vim behavior). See also https://github.com/vim/vim/blob/ca6a260ef1a4b4ae94bc71c17cbabf8f12bf0f8c/runtime/doc/windows.txt#L514C1-L521C24 Release Notes: - Added ctrl-w x keybinding in Vim mode, which swaps the active window with the next adjacent one (aligning with Vim behavior) **Vim behavior** https://github.com/user-attachments/assets/435a8b52-5d1c-4d4b-964e-4f0f3c9aca31 https://github.com/user-attachments/assets/7aa40014-1eac-4cce-858f-516cd06d13f6 **Zed behavior** https://github.com/user-attachments/assets/2431e860-4e11-45c6-a3f2-08f1a9b610c1 https://github.com/user-attachments/assets/30432d9d-5db1-4650-af30-232b1340229c Note: There is a discrepancy where in Vim, if vertical and horizontal splits are mixed, swapping from a column with a single window does not work (see the vertical video), whilst in Zed it does. However, I don't see a good reason as to why this should not be supported and would argue that it makes more sense to keep the clear priority swap behavior, instead of adding a workaround to supports such cases. --------- Co-authored-by: Conrad Irwin --- assets/keymaps/vim.json | 2 ++ crates/workspace/src/pane_group.rs | 9 +++++++++ crates/workspace/src/workspace.rs | 17 +++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 34228530d2d398348d57d71ae41654dacb479712..5ff1dc196a82d0c3226253c4b8d892058598b4e3 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -857,6 +857,8 @@ "ctrl-w shift-right": "workspace::SwapPaneRight", "ctrl-w shift-up": "workspace::SwapPaneUp", "ctrl-w shift-down": "workspace::SwapPaneDown", + "ctrl-w x": "workspace::SwapPaneAdjacent", + "ctrl-w ctrl-x": "workspace::SwapPaneAdjacent", "ctrl-w shift-h": "workspace::MovePaneLeft", "ctrl-w shift-l": "workspace::MovePaneRight", "ctrl-w shift-k": "workspace::MovePaneUp", diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 36898b127bdd749a9c1867a97bd72dfd6f4e15ea..c9d98977139ed644cf5f3bfb7eb26d94ca081d19 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -963,6 +963,15 @@ impl SplitDirection { Self::Down | Self::Right => true, } } + + pub fn opposite(&self) -> SplitDirection { + match self { + Self::Down => Self::Up, + Self::Up => Self::Down, + Self::Left => Self::Right, + Self::Right => Self::Left, + } + } } mod element { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 96fed9f65517bd0005ff27907e6f888edd7a48f9..cf102fc16109555971defbdc279e89738582ab74 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -437,6 +437,8 @@ actions!( SwapPaneUp, /// Swaps the current pane with the one below. SwapPaneDown, + // Swaps the current pane with the first available adjacent pane (searching in order: below, above, right, left) and activates that pane. + SwapPaneAdjacent, /// Move the current pane to be at the far left. MovePaneLeft, /// Move the current pane to be at the far right. @@ -5823,6 +5825,21 @@ impl Workspace { .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| { workspace.swap_pane_in_direction(SplitDirection::Down, cx) })) + .on_action(cx.listener(|workspace, _: &SwapPaneAdjacent, window, cx| { + const DIRECTION_PRIORITY: [SplitDirection; 4] = [ + SplitDirection::Down, + SplitDirection::Up, + SplitDirection::Right, + SplitDirection::Left, + ]; + for dir in DIRECTION_PRIORITY { + if workspace.find_pane_in_direction(dir, cx).is_some() { + workspace.swap_pane_in_direction(dir, cx); + workspace.activate_pane_in_direction(dir.opposite(), window, cx); + break; + } + } + })) .on_action(cx.listener(|workspace, _: &MovePaneLeft, _, cx| { workspace.move_pane_to_border(SplitDirection::Left, cx) }))