diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c3c7d224e..2839f495c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -360,6 +360,10 @@ pub fn doc(&self) -> &str { jump_view_left, "Jump to the split to the left", jump_view_up, "Jump to the split above", jump_view_down, "Jump to the split below", + swap_view_right, "Swap with the split to the right", + swap_view_left, "Swap with the split to the left", + swap_view_up, "Swap with the split above", + swap_view_down, "Swap with the split below", transpose_view, "Transpose splits", rotate_view, "Goto next window", hsplit, "Horizontal bottom split", @@ -3864,6 +3868,22 @@ fn jump_view_down(cx: &mut Context) { cx.editor.focus_down() } +fn swap_view_right(cx: &mut Context) { + cx.editor.swap_right() +} + +fn swap_view_left(cx: &mut Context) { + cx.editor.swap_left() +} + +fn swap_view_up(cx: &mut Context) { + cx.editor.swap_up() +} + +fn swap_view_down(cx: &mut Context) { + cx.editor.swap_down() +} + fn transpose_view(cx: &mut Context) { cx.editor.transpose_view() } diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 124517d4a..a887f4b3f 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -180,6 +180,10 @@ pub fn default() -> HashMap { "C-j" | "j" | "down" => jump_view_down, "C-k" | "k" | "up" => jump_view_up, "C-l" | "l" | "right" => jump_view_right, + "L" => swap_view_right, + "K" => swap_view_up, + "H" => swap_view_left, + "J" => swap_view_down, "n" => { "New split scratch buffer" "C-s" | "s" => hsplit_new, "C-v" | "v" => vsplit_new, @@ -236,6 +240,10 @@ pub fn default() -> HashMap { "C-j" | "j" | "down" => jump_view_down, "C-k" | "k" | "up" => jump_view_up, "C-l" | "l" | "right" => jump_view_right, + "H" => swap_view_left, + "J" => swap_view_down, + "K" => swap_view_up, + "L" => swap_view_right, "n" => { "New split scratch buffer" "C-s" | "s" => hsplit_new, "C-v" | "v" => vsplit_new, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 1ad210592..3ba6fea87 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -885,6 +885,22 @@ pub fn focus_down(&mut self) { self.tree.focus_direction(tree::Direction::Down); } + pub fn swap_right(&mut self) { + self.tree.swap_split_in_direction(tree::Direction::Right); + } + + pub fn swap_left(&mut self) { + self.tree.swap_split_in_direction(tree::Direction::Left); + } + + pub fn swap_up(&mut self) { + self.tree.swap_split_in_direction(tree::Direction::Up); + } + + pub fn swap_down(&mut self) { + self.tree.swap_split_in_direction(tree::Direction::Down); + } + pub fn transpose_view(&mut self) { self.tree.transpose(); } diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index 522a79d78..2aa35dee8 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -538,6 +538,24 @@ pub fn transpose(&mut self) { } } + pub fn swap_split_in_direction(&mut self, direction: Direction) { + if let Some(id) = self.find_split_in_direction(self.focus, direction) { + if let Some([focused, target]) = self.nodes.get_disjoint_mut([self.focus, id]) { + match (&mut focused.content, &mut target.content) { + (Content::View(focused), Content::View(target)) => { + std::mem::swap(&mut focused.doc, &mut target.doc); + std::mem::swap(&mut focused.id, &mut target.id); + self.focus = id; + } + // self.focus always points to a view which has a content of Content::View + // and find_split_in_direction() only returns a view which has content of + // Content::View. + _ => unreachable!(), + } + } + } + } + pub fn area(&self) -> Rect { self.area } @@ -649,4 +667,133 @@ fn find_split_in_direction() { assert_eq!(None, tree.find_split_in_direction(r0, Direction::Right)); assert_eq!(None, tree.find_split_in_direction(r0, Direction::Up)); } + + #[test] + fn swap_split_in_direction() { + let mut tree = Tree::new(Rect { + x: 0, + y: 0, + width: 180, + height: 80, + }); + + let doc_l0 = DocumentId::default(); + let mut view = View::new( + doc_l0, + vec![GutterType::Diagnostics, GutterType::LineNumbers], + ); + view.area = Rect::new(0, 0, 180, 80); + tree.insert(view); + + let l0 = tree.focus; + + let doc_r0 = DocumentId::default(); + let view = View::new( + doc_r0, + vec![GutterType::Diagnostics, GutterType::LineNumbers], + ); + tree.split(view, Layout::Vertical); + let r0 = tree.focus; + + tree.focus = l0; + + let doc_l1 = DocumentId::default(); + let view = View::new( + doc_l1, + vec![GutterType::Diagnostics, GutterType::LineNumbers], + ); + tree.split(view, Layout::Horizontal); + let l1 = tree.focus; + + tree.focus = l0; + + let doc_l2 = DocumentId::default(); + let view = View::new( + doc_l2, + vec![GutterType::Diagnostics, GutterType::LineNumbers], + ); + tree.split(view, Layout::Vertical); + let l2 = tree.focus; + + // Views in test + // | L0 | L2 | | + // | L1 | R0 | + + // Document IDs in test + // | l0 | l2 | | + // | l1 | r0 | + + fn doc_id(tree: &Tree, view_id: ViewId) -> Option { + if let Content::View(view) = &tree.nodes[view_id].content { + Some(view.doc) + } else { + None + } + } + + tree.focus = l0; + // `*` marks the view in focus from view table (here L0) + // | l0* | l2 | | + // | l1 | r0 | + tree.swap_split_in_direction(Direction::Down); + // | l1 | l2 | | + // | l0* | r0 | + assert_eq!(tree.focus, l1); + assert_eq!(doc_id(&tree, l0), Some(doc_l1)); + assert_eq!(doc_id(&tree, l1), Some(doc_l0)); + assert_eq!(doc_id(&tree, l2), Some(doc_l2)); + assert_eq!(doc_id(&tree, r0), Some(doc_r0)); + + tree.swap_split_in_direction(Direction::Right); + + // | l1 | l2 | | + // | r0 | l0* | + assert_eq!(tree.focus, r0); + assert_eq!(doc_id(&tree, l0), Some(doc_l1)); + assert_eq!(doc_id(&tree, l1), Some(doc_r0)); + assert_eq!(doc_id(&tree, l2), Some(doc_l2)); + assert_eq!(doc_id(&tree, r0), Some(doc_l0)); + + // cannot swap, nothing changes + tree.swap_split_in_direction(Direction::Up); + // | l1 | l2 | | + // | r0 | l0* | + assert_eq!(tree.focus, r0); + assert_eq!(doc_id(&tree, l0), Some(doc_l1)); + assert_eq!(doc_id(&tree, l1), Some(doc_r0)); + assert_eq!(doc_id(&tree, l2), Some(doc_l2)); + assert_eq!(doc_id(&tree, r0), Some(doc_l0)); + + // cannot swap, nothing changes + tree.swap_split_in_direction(Direction::Down); + // | l1 | l2 | | + // | r0 | l0* | + assert_eq!(tree.focus, r0); + assert_eq!(doc_id(&tree, l0), Some(doc_l1)); + assert_eq!(doc_id(&tree, l1), Some(doc_r0)); + assert_eq!(doc_id(&tree, l2), Some(doc_l2)); + assert_eq!(doc_id(&tree, r0), Some(doc_l0)); + + tree.focus = l2; + // | l1 | l2* | | + // | r0 | l0 | + + tree.swap_split_in_direction(Direction::Down); + // | l1 | r0 | | + // | l2* | l0 | + assert_eq!(tree.focus, l1); + assert_eq!(doc_id(&tree, l0), Some(doc_l1)); + assert_eq!(doc_id(&tree, l1), Some(doc_l2)); + assert_eq!(doc_id(&tree, l2), Some(doc_r0)); + assert_eq!(doc_id(&tree, r0), Some(doc_l0)); + + tree.swap_split_in_direction(Direction::Up); + // | l2* | r0 | | + // | l1 | l0 | + assert_eq!(tree.focus, l0); + assert_eq!(doc_id(&tree, l0), Some(doc_l2)); + assert_eq!(doc_id(&tree, l1), Some(doc_l1)); + assert_eq!(doc_id(&tree, l2), Some(doc_r0)); + assert_eq!(doc_id(&tree, r0), Some(doc_l0)); + } }