on master help sign in
Add commands for moving between splits with a direction (#860)
0f886afOskar Nehlin
book/src/keymap.md+11−7
@@ -181,12 +181,16 @@ TODO: Mappings for selecting syntax nodes (a superset of `[`).
181
181
182This layer is similar to vim keybindings as kakoune does not support window.
182This layer is similar to vim keybindings as kakoune does not support window.
183
183
184−| Key | Description | Command |
184+| Key | Description | Command |
185−| ----- | ------------- | ------- |
185+| ----- | ------------- | ------- |
186−| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
186+| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
187−| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
187+| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
188−| `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` |
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` |
189−| `q`, `Ctrl-q` | Close current window | `wclose` |
193+| `q`, `Ctrl-q` | Close current window | `wclose` |
190
194
191#### Space mode
195#### Space mode
192
196
@@ -249,6 +253,6 @@ Keys to use within picker. Remapping currently not supported.
249| `Down`, `Ctrl-j`, `Ctrl-n` | Next entry |
253| `Down`, `Ctrl-j`, `Ctrl-n` | Next entry |
250| `Ctrl-space` | Filter options |
254| `Ctrl-space` | Filter options |
251| `Enter` | Open selected |
255| `Enter` | Open selected |
252−| `Ctrl-h` | Open horizontally |
256+| `Ctrl-s` | Open horizontally |
253| `Ctrl-v` | Open vertically |
257| `Ctrl-v` | Open vertically |
254| `Escape`, `Ctrl-c` | Close picker |
258| `Escape`, `Ctrl-c` | Close picker |
helix-term/src/commands.rs+20
@@ -301,6 +301,10 @@ impl Command {
301 expand_selection, "Expand selection to parent syntax node",
301 expand_selection, "Expand selection to parent syntax node",
302 jump_forward, "Jump forward on jumplist",
302 jump_forward, "Jump forward on jumplist",
303 jump_backward, "Jump backward on jumplist",
303 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",
304 rotate_view, "Goto next window",
308 rotate_view, "Goto next window",
305 hsplit, "Horizontal bottom split",
309 hsplit, "Horizontal bottom split",
306 vsplit, "Vertical right split",
310 vsplit, "Vertical right split",
@@ -4373,6 +4377,22 @@ fn rotate_view(cx: &mut Context) {
4373 cx.editor.focus_next()
4377 cx.editor.focus_next()
4374}
4378}
4375
4379
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+
4376// split helper, clear it later
4396// split helper, clear it later
4377fn split(cx: &mut Context, action: Action) {
4397fn split(cx: &mut Context, action: Action) {
4378 let (view, doc) = current!(cx.editor);
4398 let (view, doc) = current!(cx.editor);
helix-term/src/keymap.rs+5−1
@@ -520,9 +520,13 @@ impl Default for Keymaps {
520
520
521 "C-w" => { "Window"
521 "C-w" => { "Window"
522 "C-w" | "w" => rotate_view,
522 "C-w" | "w" => rotate_view,
523− "C-h" | "h" => hsplit,
523+ "C-s" | "s" => hsplit,
524 "C-v" | "v" => vsplit,
524 "C-v" | "v" => vsplit,
525 "C-q" | "q" => wclose,
525 "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,
526 },
530 },
527
531
528 // move under <space>c
532 // move under <space>c
helix-term/src/ui/picker.rs+1−1
@@ -373,7 +373,7 @@ impl<T: 'static> Component for Picker<T> {
373 return close_fn;
373 return close_fn;
374 }
374 }
375 KeyEvent {
375 KeyEvent {
376− code: KeyCode::Char('h'),
376+ code: KeyCode::Char('s'),
377 modifiers: KeyModifiers::CONTROL,
377 modifiers: KeyModifiers::CONTROL,
378 } => {
378 } => {
379 if let Some(option) = self.selection() {
379 if let Some(option) = self.selection() {
helix-view/src/editor.rs+17−1
@@ -2,7 +2,7 @@ use crate::{
2 clipboard::{get_clipboard_provider, ClipboardProvider},
2 clipboard::{get_clipboard_provider, ClipboardProvider},
3 graphics::{CursorKind, Rect},
3 graphics::{CursorKind, Rect},
4 theme::{self, Theme},
4 theme::{self, Theme},
5− tree::Tree,
5+ tree::{self, Tree},
6 Document, DocumentId, View, ViewId,
6 Document, DocumentId, View, ViewId,
7};
7};
8
8
@@ -355,6 +355,22 @@ impl Editor {
355 self.tree.focus_next();
355 self.tree.focus_next();
356 }
356 }
357
357
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+
358 pub fn should_close(&self) -> bool {
374 pub fn should_close(&self) -> bool {
359 self.tree.is_empty()
375 self.tree.is_empty()
360 }
376 }
helix-view/src/tree.rs+182−9
@@ -47,13 +47,21 @@ impl Node {
47
47
48// TODO: screen coord to container + container coordinate helpers
48// TODO: screen coord to container + container coordinate helpers
49
49
50−#[derive(Debug, PartialEq, Eq)]
50+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum Layout {
51pub enum Layout {
52 Horizontal,
52 Horizontal,
53 Vertical,
53 Vertical,
54 // could explore stacked/tabbed
54 // could explore stacked/tabbed
55}
55}
56
56
57+#[derive(Debug, Clone, Copy)]
58+pub enum Direction {
59+ Up,
60+ Down,
61+ Left,
62+ Right,
63+}
64+
57#[derive(Debug)]
65#[derive(Debug)]
58pub struct Container {
66pub struct Container {
59 layout: Layout,
67 layout: Layout,
@@ -150,7 +158,6 @@ impl Tree {
150 } => container,
158 } => container,
151 _ => unreachable!(),
159 _ => unreachable!(),
152 };
160 };
153−
154 if container.layout == layout {
161 if container.layout == layout {
155 // insert node after the current item if there is children already
162 // insert node after the current item if there is children already
156 let pos = if container.children.is_empty() {
163 let pos = if container.children.is_empty() {
@@ -393,6 +400,112 @@ impl Tree {
393 Traverse::new(self)
400 Traverse::new(self)
394 }
401 }
395
402
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+
396 pub fn focus_next(&mut self) {
509 pub fn focus_next(&mut self) {
397 // This function is very dumb, but that's because we don't store any parent links.
510 // This function is very dumb, but that's because we don't store any parent links.
398 // (we'd be able to go parent.next_sibling() recursively until we find something)
511 // (we'd be able to go parent.next_sibling() recursively until we find something)
@@ -420,13 +533,12 @@ impl Tree {
420 // if found = container -> found = first child
533 // if found = container -> found = first child
421 // }
534 // }
422
535
536+ let mut views = self
423− let iter = self.traverse();
537+ .traverse()
424−
425− let mut iter = iter.skip_while(|&(key, _view)| key != self.focus);
538+ .skip_while(|&(id, _view)| id != self.focus)
426− iter.next(); // take the focused value
539+ .skip(1); // Skip focused value
427−
428− if let Some((key, _)) = iter.next() {
540+ if let Some((id, _)) = views.next() {
429− self.focus = key;
541+ self.focus = id;
430 } else {
542 } else {
431 // extremely crude, take the first item again
543 // extremely crude, take the first item again
432 let (key, _) = self.traverse().next().unwrap();
544 let (key, _) = self.traverse().next().unwrap();
@@ -472,3 +584,64 @@ impl<'a> Iterator for Traverse<'a> {
472 }
584 }
473 }
585 }
474}
586}
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+}