Add SwapPaneInDirection (#3043)

Conrad Irwin created

- Add cmd-k shift-{left,right,up,down} to swap panes in that direction
- vim: Add ctrl-w shift-{h,j,k,l} to swap panes in that direction
([#278](https://github.com/zed-industries/community/issues/278))

Change summary

assets/keymaps/default.json        | 16 +++++++++++
assets/keymaps/vim.json            | 32 ++++++++++++++++++++++
crates/workspace/src/pane_group.rs | 22 +++++++++++++++
crates/workspace/src/workspace.rs  | 46 +++++++++++++++++++++++++------
4 files changed, 107 insertions(+), 9 deletions(-)

Detailed changes

assets/keymaps/default.json 🔗

@@ -506,6 +506,22 @@
       "cmd-k cmd-down": [
         "workspace::ActivatePaneInDirection",
         "Down"
+      ],
+      "cmd-k shift-left": [
+        "workspace::SwapPaneInDirection",
+        "Left"
+      ],
+      "cmd-k shift-right": [
+        "workspace::SwapPaneInDirection",
+        "Right"
+      ],
+      "cmd-k shift-up": [
+        "workspace::SwapPaneInDirection",
+        "Up"
+      ],
+      "cmd-k shift-down": [
+        "workspace::SwapPaneInDirection",
+        "Down"
       ]
     }
   },

assets/keymaps/vim.json 🔗

@@ -316,6 +316,38 @@
         "workspace::ActivatePaneInDirection",
         "Down"
       ],
+      "ctrl-w shift-left": [
+        "workspace::SwapPaneInDirection",
+        "Left"
+      ],
+      "ctrl-w shift-right": [
+        "workspace::SwapPaneInDirection",
+        "Right"
+      ],
+      "ctrl-w shift-up": [
+        "workspace::SwapPaneInDirection",
+        "Up"
+      ],
+      "ctrl-w shift-down": [
+        "workspace::SwapPaneInDirection",
+        "Down"
+      ],
+      "ctrl-w shift-h": [
+        "workspace::SwapPaneInDirection",
+        "Left"
+      ],
+      "ctrl-w shift-l": [
+        "workspace::SwapPaneInDirection",
+        "Right"
+      ],
+      "ctrl-w shift-k": [
+        "workspace::SwapPaneInDirection",
+        "Up"
+      ],
+      "ctrl-w shift-j": [
+        "workspace::SwapPaneInDirection",
+        "Down"
+      ],
       "ctrl-w g t": "pane::ActivateNextItem",
       "ctrl-w ctrl-g t": "pane::ActivateNextItem",
       "ctrl-w g shift-t": "pane::ActivatePrevItem",

crates/workspace/src/pane_group.rs 🔗

@@ -84,6 +84,13 @@ impl PaneGroup {
         }
     }
 
+    pub fn swap(&mut self, from: &ViewHandle<Pane>, to: &ViewHandle<Pane>) {
+        match &mut self.root {
+            Member::Pane(_) => {}
+            Member::Axis(axis) => axis.swap(from, to),
+        };
+    }
+
     pub(crate) fn render(
         &self,
         project: &ModelHandle<Project>,
@@ -428,6 +435,21 @@ impl PaneAxis {
         }
     }
 
+    fn swap(&mut self, from: &ViewHandle<Pane>, to: &ViewHandle<Pane>) {
+        for member in self.members.iter_mut() {
+            match member {
+                Member::Axis(axis) => axis.swap(from, to),
+                Member::Pane(pane) => {
+                    if pane == from {
+                        *member = Member::Pane(to.clone());
+                    } else if pane == to {
+                        *member = Member::Pane(from.clone())
+                    }
+                }
+            }
+        }
+    }
+
     fn bounding_box_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<RectF> {
         debug_assert!(self.members.len() == self.bounding_boxes.borrow().len());
 

crates/workspace/src/workspace.rs 🔗

@@ -157,6 +157,9 @@ pub struct ActivatePane(pub usize);
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct ActivatePaneInDirection(pub SplitDirection);
 
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct SwapPaneInDirection(pub SplitDirection);
+
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct NewFileInDirection(pub SplitDirection);
 
@@ -233,6 +236,7 @@ impl_actions!(
     [
         ActivatePane,
         ActivatePaneInDirection,
+        SwapPaneInDirection,
         NewFileInDirection,
         Toast,
         OpenTerminal,
@@ -318,6 +322,12 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
         },
     );
 
+    cx.add_action(
+        |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| {
+            workspace.swap_pane_in_direction(action.0, cx)
+        },
+    );
+
     cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
         workspace.toggle_dock(DockPosition::Left, cx);
     });
@@ -2236,11 +2246,32 @@ impl Workspace {
         direction: SplitDirection,
         cx: &mut ViewContext<Self>,
     ) {
-        let bounding_box = match self.center.bounding_box_for_pane(&self.active_pane) {
-            Some(coordinates) => coordinates,
-            None => {
-                return;
-            }
+        if let Some(pane) = self.find_pane_in_direction(direction, cx) {
+            cx.focus(pane);
+        }
+    }
+
+    pub fn swap_pane_in_direction(
+        &mut self,
+        direction: SplitDirection,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(to) = self
+            .find_pane_in_direction(direction, cx)
+            .map(|pane| pane.clone())
+        {
+            self.center.swap(&self.active_pane.clone(), &to);
+            cx.notify();
+        }
+    }
+
+    fn find_pane_in_direction(
+        &mut self,
+        direction: SplitDirection,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<&ViewHandle<Pane>> {
+        let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
+            return None;
         };
         let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
         let center = match cursor {
@@ -2256,10 +2287,7 @@ impl Workspace {
             SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next),
             SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next),
         };
-
-        if let Some(pane) = self.center.pane_at_pixel_position(target) {
-            cx.focus(pane);
-        }
+        self.center.pane_at_pixel_position(target)
     }
 
     fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {