Skip to content

Commit 90f3242

Browse files
committed
Implement view swapping
* add Tree::swap_split_in_direction() * add swap_view_{left,down,up,right} commands, bound to H,J,K,L respectively in the Window menu(s) * add test for view swapping
1 parent ae19aaf commit 90f3242

File tree

4 files changed

+191
-0
lines changed

4 files changed

+191
-0
lines changed

helix-term/src/commands.rs

+20
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,10 @@ impl MappableCommand {
360360
jump_view_left, "Jump to the split to the left",
361361
jump_view_up, "Jump to the split above",
362362
jump_view_down, "Jump to the split below",
363+
swap_view_right, "Swap with the split to the right",
364+
swap_view_left, "Swap with the split to the left",
365+
swap_view_up, "Swap with the split above",
366+
swap_view_down, "Swap with the split below",
363367
rotate_view, "Goto next window",
364368
hsplit, "Horizontal bottom split",
365369
hsplit_new, "Horizontal bottom split scratch buffer",
@@ -3836,6 +3840,22 @@ fn jump_view_down(cx: &mut Context) {
38363840
cx.editor.focus_down()
38373841
}
38383842

3843+
fn swap_view_right(cx: &mut Context) {
3844+
cx.editor.swap_right()
3845+
}
3846+
3847+
fn swap_view_left(cx: &mut Context) {
3848+
cx.editor.swap_left()
3849+
}
3850+
3851+
fn swap_view_up(cx: &mut Context) {
3852+
cx.editor.swap_up()
3853+
}
3854+
3855+
fn swap_view_down(cx: &mut Context) {
3856+
cx.editor.swap_down()
3857+
}
3858+
38393859
// split helper, clear it later
38403860
fn split(cx: &mut Context, action: Action) {
38413861
let (view, doc) = current!(cx.editor);

helix-term/src/keymap/default.rs

+8
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,10 @@ pub fn default() -> HashMap<Mode, Keymap> {
179179
"C-j" | "j" | "down" => jump_view_down,
180180
"C-k" | "k" | "up" => jump_view_up,
181181
"C-l" | "l" | "right" => jump_view_right,
182+
"L" => swap_view_right,
183+
"K" => swap_view_up,
184+
"H" => swap_view_left,
185+
"J" => swap_view_down,
182186
"n" => { "New split scratch buffer"
183187
"C-s" | "s" => hsplit_new,
184188
"C-v" | "v" => vsplit_new,
@@ -234,6 +238,10 @@ pub fn default() -> HashMap<Mode, Keymap> {
234238
"C-j" | "j" | "down" => jump_view_down,
235239
"C-k" | "k" | "up" => jump_view_up,
236240
"C-l" | "l" | "right" => jump_view_right,
241+
"H" => swap_view_left,
242+
"J" => swap_view_down,
243+
"K" => swap_view_up,
244+
"L" => swap_view_right,
237245
"n" => { "New split scratch buffer"
238246
"C-s" | "s" => hsplit_new,
239247
"C-v" | "v" => vsplit_new,

helix-view/src/editor.rs

+16
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,22 @@ impl Editor {
885885
self.tree.focus_direction(tree::Direction::Down);
886886
}
887887

888+
pub fn swap_right(&mut self) {
889+
self.tree.swap_split_in_direction(tree::Direction::Right);
890+
}
891+
892+
pub fn swap_left(&mut self) {
893+
self.tree.swap_split_in_direction(tree::Direction::Left);
894+
}
895+
896+
pub fn swap_up(&mut self) {
897+
self.tree.swap_split_in_direction(tree::Direction::Up);
898+
}
899+
900+
pub fn swap_down(&mut self) {
901+
self.tree.swap_split_in_direction(tree::Direction::Down);
902+
}
903+
888904
pub fn should_close(&self) -> bool {
889905
self.tree.is_empty()
890906
}

helix-view/src/tree.rs

+147
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,24 @@ impl Tree {
526526
}
527527
}
528528

529+
pub fn swap_split_in_direction(&mut self, direction: Direction) {
530+
if let Some(id) = self.find_split_in_direction(self.focus, direction) {
531+
if let Some([focused, target]) = self.nodes.get_disjoint_mut([self.focus, id]) {
532+
match (&mut focused.content, &mut target.content) {
533+
(Content::View(focused), Content::View(target)) => {
534+
std::mem::swap(&mut focused.doc, &mut target.doc);
535+
std::mem::swap(&mut focused.id, &mut target.id);
536+
self.focus = id;
537+
}
538+
// self.focus always points to a view which has a content of Content::View
539+
// and find_split_in_direction() only returns a view which has content of
540+
// Content::View.
541+
_ => unreachable!(),
542+
}
543+
}
544+
}
545+
}
546+
529547
pub fn area(&self) -> Rect {
530548
self.area
531549
}
@@ -637,4 +655,133 @@ mod test {
637655
assert_eq!(None, tree.find_split_in_direction(r0, Direction::Right));
638656
assert_eq!(None, tree.find_split_in_direction(r0, Direction::Up));
639657
}
658+
659+
#[test]
660+
fn swap_split_in_direction() {
661+
let mut tree = Tree::new(Rect {
662+
x: 0,
663+
y: 0,
664+
width: 180,
665+
height: 80,
666+
});
667+
668+
let doc_l0 = DocumentId::default();
669+
let mut view = View::new(
670+
doc_l0,
671+
vec![GutterType::Diagnostics, GutterType::LineNumbers],
672+
);
673+
view.area = Rect::new(0, 0, 180, 80);
674+
tree.insert(view);
675+
676+
let l0 = tree.focus;
677+
678+
let doc_r0 = DocumentId::default();
679+
let view = View::new(
680+
doc_r0,
681+
vec![GutterType::Diagnostics, GutterType::LineNumbers],
682+
);
683+
tree.split(view, Layout::Vertical);
684+
let r0 = tree.focus;
685+
686+
tree.focus = l0;
687+
688+
let doc_l1 = DocumentId::default();
689+
let view = View::new(
690+
doc_l1,
691+
vec![GutterType::Diagnostics, GutterType::LineNumbers],
692+
);
693+
tree.split(view, Layout::Horizontal);
694+
let l1 = tree.focus;
695+
696+
tree.focus = l0;
697+
698+
let doc_l2 = DocumentId::default();
699+
let view = View::new(
700+
doc_l2,
701+
vec![GutterType::Diagnostics, GutterType::LineNumbers],
702+
);
703+
tree.split(view, Layout::Vertical);
704+
let l2 = tree.focus;
705+
706+
// Views in test
707+
// | L0 | L2 | |
708+
// | L1 | R0 |
709+
710+
// Document IDs in test
711+
// | l0 | l2 | |
712+
// | l1 | r0 |
713+
714+
fn doc_id(tree: &Tree, view_id: ViewId) -> Option<DocumentId> {
715+
if let Content::View(view) = &tree.nodes[view_id].content {
716+
Some(view.doc)
717+
} else {
718+
None
719+
}
720+
}
721+
722+
tree.focus = l0;
723+
// `*` marks the view in focus from view table (here L0)
724+
// | l0* | l2 | |
725+
// | l1 | r0 |
726+
tree.swap_split_in_direction(Direction::Down);
727+
// | l1 | l2 | |
728+
// | l0* | r0 |
729+
assert_eq!(tree.focus, l1);
730+
assert_eq!(doc_id(&tree, l0), Some(doc_l1));
731+
assert_eq!(doc_id(&tree, l1), Some(doc_l0));
732+
assert_eq!(doc_id(&tree, l2), Some(doc_l2));
733+
assert_eq!(doc_id(&tree, r0), Some(doc_r0));
734+
735+
tree.swap_split_in_direction(Direction::Right);
736+
737+
// | l1 | l2 | |
738+
// | r0 | l0* |
739+
assert_eq!(tree.focus, r0);
740+
assert_eq!(doc_id(&tree, l0), Some(doc_l1));
741+
assert_eq!(doc_id(&tree, l1), Some(doc_r0));
742+
assert_eq!(doc_id(&tree, l2), Some(doc_l2));
743+
assert_eq!(doc_id(&tree, r0), Some(doc_l0));
744+
745+
// cannot swap, nothing changes
746+
tree.swap_split_in_direction(Direction::Up);
747+
// | l1 | l2 | |
748+
// | r0 | l0* |
749+
assert_eq!(tree.focus, r0);
750+
assert_eq!(doc_id(&tree, l0), Some(doc_l1));
751+
assert_eq!(doc_id(&tree, l1), Some(doc_r0));
752+
assert_eq!(doc_id(&tree, l2), Some(doc_l2));
753+
assert_eq!(doc_id(&tree, r0), Some(doc_l0));
754+
755+
// cannot swap, nothing changes
756+
tree.swap_split_in_direction(Direction::Down);
757+
// | l1 | l2 | |
758+
// | r0 | l0* |
759+
assert_eq!(tree.focus, r0);
760+
assert_eq!(doc_id(&tree, l0), Some(doc_l1));
761+
assert_eq!(doc_id(&tree, l1), Some(doc_r0));
762+
assert_eq!(doc_id(&tree, l2), Some(doc_l2));
763+
assert_eq!(doc_id(&tree, r0), Some(doc_l0));
764+
765+
tree.focus = l2;
766+
// | l1 | l2* | |
767+
// | r0 | l0 |
768+
769+
tree.swap_split_in_direction(Direction::Down);
770+
// | l1 | r0 | |
771+
// | l2* | l0 |
772+
assert_eq!(tree.focus, l1);
773+
assert_eq!(doc_id(&tree, l0), Some(doc_l1));
774+
assert_eq!(doc_id(&tree, l1), Some(doc_l2));
775+
assert_eq!(doc_id(&tree, l2), Some(doc_r0));
776+
assert_eq!(doc_id(&tree, r0), Some(doc_l0));
777+
778+
tree.swap_split_in_direction(Direction::Up);
779+
// | l2* | r0 | |
780+
// | l1 | l0 |
781+
assert_eq!(tree.focus, l0);
782+
assert_eq!(doc_id(&tree, l0), Some(doc_l2));
783+
assert_eq!(doc_id(&tree, l1), Some(doc_l1));
784+
assert_eq!(doc_id(&tree, l2), Some(doc_r0));
785+
assert_eq!(doc_id(&tree, r0), Some(doc_l0));
786+
}
640787
}

0 commit comments

Comments
 (0)