on master help sign in
Add commands for moving between splits with a direction (#860)

* Add commands for moving between splits with a direction * Update keymaps * Change picker mapping * Add test and clean up some comments

commit0f886af4b993c836bb2d522f6e036362593ff8b8 authorOskar Nehlin <oskar.d.nehlin@gmail.com> dateSat 23 Oct 2021, 13:06 (5 years ago) parent4ee92ca
6 files changed+236−19
book/src/keymap.md+11−7 helix-term/src/commands.rs+20 helix-term/src/keymap.rs+5−1 helix-term/src/ui/picker.rs+1−1 helix-view/src/editor.rs+17−1 helix-view/src/tree.rs+182−9
book/src/keymap.md+11−7
@@ -181,12 +181,16 @@ TODO: Mappings for selecting syntax nodes (a superset of `[`).
181181
182182This layer is similar to vim keybindings as kakoune does not support window.
183183
184−| Key | Description | Command |
185−| ----- | ------------- | ------- |
186−| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
187−| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
188−| `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` |
189−| `q`, `Ctrl-q` | Close current window | `wclose` |
184+| Key | Description | Command |
185+| ----- | ------------- | ------- |
186+| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
187+| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
188+| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
189+| `h`, `Ctrl-h` | Move to left split | `jump_view_left` |
190+| `j`, `Ctrl-j` | Move to split below | `jump_view_down` |
191+| `k`, `Ctrl-k` | Move to split above | `jump_view_up` |
192+| `l`, `Ctrl-l` | Move to right split | `jump_view_right` |
193+| `q`, `Ctrl-q` | Close current window | `wclose` |
190194
191195#### Space mode
192196
@@ -249,6 +253,6 @@ Keys to use within picker. Remapping currently not supported.
249253| `Down`, `Ctrl-j`, `Ctrl-n` | Next entry |
250254| `Ctrl-space` | Filter options |
251255| `Enter` | Open selected |
252−| `Ctrl-h` | Open horizontally |
256+| `Ctrl-s` | Open horizontally |
253257| `Ctrl-v` | Open vertically |
254258| `Escape`, `Ctrl-c` | Close picker |
helix-term/src/commands.rs+20
@@ -301,6 +301,10 @@ impl Command {
301301 expand_selection, "Expand selection to parent syntax node",
302302 jump_forward, "Jump forward on jumplist",
303303 jump_backward, "Jump backward on jumplist",
304+ jump_view_right, "Jump to the split to the right",
305+ jump_view_left, "Jump to the split to the left",
306+ jump_view_up, "Jump to the split above",
307+ jump_view_down, "Jump to the split below",
304308 rotate_view, "Goto next window",
305309 hsplit, "Horizontal bottom split",
306310 vsplit, "Vertical right split",
@@ -4373,6 +4377,22 @@ fn rotate_view(cx: &mut Context) {
43734377 cx.editor.focus_next()
43744378}
43754379
4380+fn jump_view_right(cx: &mut Context) {
4381+ cx.editor.focus_right()
4382+}
4383+
4384+fn jump_view_left(cx: &mut Context) {
4385+ cx.editor.focus_left()
4386+}
4387+
4388+fn jump_view_up(cx: &mut Context) {
4389+ cx.editor.focus_up()
4390+}
4391+
4392+fn jump_view_down(cx: &mut Context) {
4393+ cx.editor.focus_down()
4394+}
4395+
43764396// split helper, clear it later
43774397fn split(cx: &mut Context, action: Action) {
43784398 let (view, doc) = current!(cx.editor);
helix-term/src/keymap.rs+5−1
@@ -520,9 +520,13 @@ impl Default for Keymaps {
520520
521521 "C-w" => { "Window"
522522 "C-w" | "w" => rotate_view,
523− "C-h" | "h" => hsplit,
523+ "C-s" | "s" => hsplit,
524524 "C-v" | "v" => vsplit,
525525 "C-q" | "q" => wclose,
526+ "C-h" | "h" => jump_view_left,
527+ "C-j" | "j" => jump_view_down,
528+ "C-k" | "k" => jump_view_up,
529+ "C-l" | "l" => jump_view_right,
526530 },
527531
528532 // move under <space>c
helix-term/src/ui/picker.rs+1−1
@@ -373,7 +373,7 @@ impl<T: 'static> Component for Picker<T> {
373373 return close_fn;
374374 }
375375 KeyEvent {
376− code: KeyCode::Char('h'),
376+ code: KeyCode::Char('s'),
377377 modifiers: KeyModifiers::CONTROL,
378378 } => {
379379 if let Some(option) = self.selection() {
helix-view/src/editor.rs+17−1
@@ -2,7 +2,7 @@ use crate::{
22 clipboard::{get_clipboard_provider, ClipboardProvider},
33 graphics::{CursorKind, Rect},
44 theme::{self, Theme},
5− tree::Tree,
5+ tree::{self, Tree},
66 Document, DocumentId, View, ViewId,
77};
88
@@ -355,6 +355,22 @@ impl Editor {
355355 self.tree.focus_next();
356356 }
357357
358+ pub fn focus_right(&mut self) {
359+ self.tree.focus_direction(tree::Direction::Right);
360+ }
361+
362+ pub fn focus_left(&mut self) {
363+ self.tree.focus_direction(tree::Direction::Left);
364+ }
365+
366+ pub fn focus_up(&mut self) {
367+ self.tree.focus_direction(tree::Direction::Up);
368+ }
369+
370+ pub fn focus_down(&mut self) {
371+ self.tree.focus_direction(tree::Direction::Down);
372+ }
373+
358374 pub fn should_close(&self) -> bool {
359375 self.tree.is_empty()
360376 }
helix-view/src/tree.rs+182−9
@@ -47,13 +47,21 @@ impl Node {
4747
4848// TODO: screen coord to container + container coordinate helpers
4949
50−#[derive(Debug, PartialEq, Eq)]
50+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5151pub enum Layout {
5252 Horizontal,
5353 Vertical,
5454 // could explore stacked/tabbed
5555}
5656
57+#[derive(Debug, Clone, Copy)]
58+pub enum Direction {
59+ Up,
60+ Down,
61+ Left,
62+ Right,
63+}
64+
5765#[derive(Debug)]
5866pub struct Container {
5967 layout: Layout,
@@ -150,7 +158,6 @@ impl Tree {
150158 } => container,
151159 _ => unreachable!(),
152160 };
153−
154161 if container.layout == layout {
155162 // insert node after the current item if there is children already
156163 let pos = if container.children.is_empty() {
@@ -393,6 +400,112 @@ impl Tree {
393400 Traverse::new(self)
394401 }
395402
403+ // Finds the split in the given direction if it exists
404+ pub fn find_split_in_direction(&self, id: ViewId, direction: Direction) -> Option<ViewId> {
405+ let parent = self.nodes[id].parent;
406+ // Base case, we found the root of the tree
407+ if parent == id {
408+ return None;
409+ }
410+ // Parent must always be a container
411+ let parent_container = match &self.nodes[parent].content {
412+ Content::Container(container) => container,
413+ Content::View(_) => unreachable!(),
414+ };
415+
416+ match (direction, parent_container.layout) {
417+ (Direction::Up, Layout::Vertical)
418+ | (Direction::Left, Layout::Horizontal)
419+ | (Direction::Right, Layout::Horizontal)
420+ | (Direction::Down, Layout::Vertical) => {
421+ // The desired direction of movement is not possible within
422+ // the parent container so the search must continue closer to
423+ // the root of the split tree.
424+ self.find_split_in_direction(parent, direction)
425+ }
426+ (Direction::Up, Layout::Horizontal)
427+ | (Direction::Down, Layout::Horizontal)
428+ | (Direction::Left, Layout::Vertical)
429+ | (Direction::Right, Layout::Vertical) => {
430+ // It's possible to move in the desired direction within
431+ // the parent container so an attempt is made to find the
432+ // correct child.
433+ match self.find_child(id, &parent_container.children, direction) {
434+ // Child is found, search is ended
435+ Some(id) => Some(id),
436+ // A child is not found. This could be because of either two scenarios
437+ // 1. Its not possible to move in the desired direction, and search should end
438+ // 2. A layout like the following with focus at X and desired direction Right
439+ // | _ | x | |
440+ // | _ _ _ | |
441+ // | _ _ _ | |
442+ // The container containing X ends at X so no rightward movement is possible
443+ // however there still exists another view/container to the right that hasn't
444+ // been explored. Thus another search is done here in the parent container
445+ // before concluding it's not possible to move in the desired direction.
446+ None => self.find_split_in_direction(parent, direction),
447+ }
448+ }
449+ }
450+ }
451+
452+ fn find_child(&self, id: ViewId, children: &[ViewId], direction: Direction) -> Option<ViewId> {
453+ let mut child_id = match direction {
454+ // index wise in the child list the Up and Left represents a -1
455+ // thus reversed iterator.
456+ Direction::Up | Direction::Left => children
457+ .iter()
458+ .rev()
459+ .skip_while(|i| **i != id)
460+ .copied()
461+ .nth(1)?,
462+ // Down and Right => +1 index wise in the child list
463+ Direction::Down | Direction::Right => {
464+ children.iter().skip_while(|i| **i != id).copied().nth(1)?
465+ }
466+ };
467+ let (current_x, current_y) = match &self.nodes[self.focus].content {
468+ Content::View(current_view) => (current_view.area.left(), current_view.area.top()),
469+ Content::Container(_) => unreachable!(),
470+ };
471+
472+ // If the child is a container the search finds the closest container child
473+ // visually based on screen location.
474+ while let Content::Container(container) = &self.nodes[child_id].content {
475+ match (direction, container.layout) {
476+ (_, Layout::Vertical) => {
477+ // find closest split based on x because y is irrelevant
478+ // in a vertical container (and already correct based on previous search)
479+ child_id = *container.children.iter().min_by_key(|id| {
480+ let x = match &self.nodes[**id].content {
481+ Content::View(view) => view.inner_area().left(),
482+ Content::Container(container) => container.area.left(),
483+ };
484+ (current_x as i16 - x as i16).abs()
485+ })?;
486+ }
487+ (_, Layout::Horizontal) => {
488+ // find closest split based on y because x is irrelevant
489+ // in a horizontal container (and already correct based on previous search)
490+ child_id = *container.children.iter().min_by_key(|id| {
491+ let y = match &self.nodes[**id].content {
492+ Content::View(view) => view.inner_area().top(),
493+ Content::Container(container) => container.area.top(),
494+ };
495+ (current_y as i16 - y as i16).abs()
496+ })?;
497+ }
498+ }
499+ }
500+ Some(child_id)
501+ }
502+
503+ pub fn focus_direction(&mut self, direction: Direction) {
504+ if let Some(id) = self.find_split_in_direction(self.focus, direction) {
505+ self.focus = id;
506+ }
507+ }
508+
396509 pub fn focus_next(&mut self) {
397510 // This function is very dumb, but that's because we don't store any parent links.
398511 // (we'd be able to go parent.next_sibling() recursively until we find something)
@@ -420,13 +533,12 @@ impl Tree {
420533 // if found = container -> found = first child
421534 // }
422535
423− let iter = self.traverse();
424−
425− let mut iter = iter.skip_while(|&(key, _view)| key != self.focus);
426− iter.next(); // take the focused value
427−
428− if let Some((key, _)) = iter.next() {
429− self.focus = key;
536+ let mut views = self
537+ .traverse()
538+ .skip_while(|&(id, _view)| id != self.focus)
539+ .skip(1); // Skip focused value
540+ if let Some((id, _)) = views.next() {
541+ self.focus = id;
430542 } else {
431543 // extremely crude, take the first item again
432544 let (key, _) = self.traverse().next().unwrap();
@@ -472,3 +584,64 @@ impl<'a> Iterator for Traverse<'a> {
472584 }
473585 }
474586}
587+
588+#[cfg(test)]
589+mod test {
590+ use super::*;
591+ use crate::DocumentId;
592+
593+ #[test]
594+ fn find_split_in_direction() {
595+ let mut tree = Tree::new(Rect {
596+ x: 0,
597+ y: 0,
598+ width: 180,
599+ height: 80,
600+ });
601+ let mut view = View::new(DocumentId::default());
602+ view.area = Rect::new(0, 0, 180, 80);
603+ tree.insert(view);
604+
605+ let l0 = tree.focus;
606+ let view = View::new(DocumentId::default());
607+ tree.split(view, Layout::Vertical);
608+ let r0 = tree.focus;
609+
610+ tree.focus = l0;
611+ let view = View::new(DocumentId::default());
612+ tree.split(view, Layout::Horizontal);
613+ let l1 = tree.focus;
614+
615+ tree.focus = l0;
616+ let view = View::new(DocumentId::default());
617+ tree.split(view, Layout::Vertical);
618+ let l2 = tree.focus;
619+
620+ // Tree in test
621+ // | L0 | L2 | |
622+ // | L1 | R0 |
623+ tree.focus = l2;
624+ assert_eq!(Some(l0), tree.find_split_in_direction(l2, Direction::Left));
625+ assert_eq!(Some(l1), tree.find_split_in_direction(l2, Direction::Down));
626+ assert_eq!(Some(r0), tree.find_split_in_direction(l2, Direction::Right));
627+ assert_eq!(None, tree.find_split_in_direction(l2, Direction::Up));
628+
629+ tree.focus = l1;
630+ assert_eq!(None, tree.find_split_in_direction(l1, Direction::Left));
631+ assert_eq!(None, tree.find_split_in_direction(l1, Direction::Down));
632+ assert_eq!(Some(r0), tree.find_split_in_direction(l1, Direction::Right));
633+ assert_eq!(Some(l0), tree.find_split_in_direction(l1, Direction::Up));
634+
635+ tree.focus = l0;
636+ assert_eq!(None, tree.find_split_in_direction(l0, Direction::Left));
637+ assert_eq!(Some(l1), tree.find_split_in_direction(l0, Direction::Down));
638+ assert_eq!(Some(l2), tree.find_split_in_direction(l0, Direction::Right));
639+ assert_eq!(None, tree.find_split_in_direction(l0, Direction::Up));
640+
641+ tree.focus = r0;
642+ assert_eq!(Some(l2), tree.find_split_in_direction(r0, Direction::Left));
643+ assert_eq!(None, tree.find_split_in_direction(r0, Direction::Down));
644+ assert_eq!(None, tree.find_split_in_direction(r0, Direction::Right));
645+ assert_eq!(None, tree.find_split_in_direction(r0, Direction::Up));
646+ }
647+}