on master help sign in
feat(commands): shrink_selection (#1340)

* feat(commands): shrink_selection Add `shrink_selection` command that can be used to shrink previously expanded selection. To make `shrink_selection` work it was necessary to add selection history to the Document since we want to shrink the selection towards the syntax tree node that was initially selected. Selection history is cleared any time the user changes selection other way than by `expand_selection`. This ensures that we don't get some funky edge cases when user calls `shrink_selection`. Related: https://github.com/helix-editor/helix/discussions/1328 * Refactor shrink_selection, move history to view * Remove useless comment * Add default key mapping for extend&shrink selection * Rework contains_selection method * Shrink selection without expand selects first child

commit2e02a1d6bc004212033b9c4e5ed0de0fd880796c authorMatouš Dzivjak <matousdzivjak@gmail.com> dateThu 6 Jan 2022, 03:12 (5 years ago) parent66afbc9
6 files changed+133−3
book/src/keymap.md+2 helix-core/src/object.rs+27−2 helix-core/src/selection.rs+64 helix-term/src/commands.rs+35−1 helix-term/src/keymap.rs+2 helix-view/src/view.rs+3
book/src/keymap.md+2
@@ -261,6 +261,8 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire
261261| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
262262| `[space` | Add newline above | `add_newline_above` |
263263| `]space` | Add newline below | `add_newline_below` |
264+| `]o` | Expand syntax tree object selection. | `expand_selection` |
265+| `[o` | Shrink syntax tree object selection. | `shrink_selection` |
264266
265267## Insert Mode
266268
helix-core/src/object.rs+27−2
@@ -1,7 +1,5 @@
11use crate::{Range, RopeSlice, Selection, Syntax};
22
3−// TODO: to contract_selection we'd need to store the previous ranges before expand.
4−// Maybe just contract to the first child node?
53pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection {
64 let tree = syntax.tree();
75
@@ -34,3 +32,30 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection)
3432 }
3533 })
3634}
35+
36+pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection {
37+ let tree = syntax.tree();
38+
39+ selection.clone().transform(|range| {
40+ let from = text.char_to_byte(range.from());
41+ let to = text.char_to_byte(range.to());
42+
43+ let descendant = match tree.root_node().descendant_for_byte_range(from, to) {
44+ // find first child, if not possible, fallback to the node that contains selection
45+ Some(descendant) => match descendant.child(0) {
46+ Some(child) => child,
47+ None => descendant,
48+ },
49+ None => return range,
50+ };
51+
52+ let from = text.byte_to_char(descendant.start_byte());
53+ let to = text.byte_to_char(descendant.end_byte());
54+
55+ if range.head < range.anchor {
56+ Range::new(to, from)
57+ } else {
58+ Range::new(from, to)
59+ }
60+ })
61+}
helix-core/src/selection.rs+64
@@ -140,6 +140,11 @@ impl Range {
140140 self.from() == other.from() || (self.to() > other.from() && other.to() > self.from())
141141 }
142142
143+ #[inline]
144+ pub fn contains_range(&self, other: &Self) -> bool {
145+ self.from() <= other.from() && self.to() >= other.to()
146+ }
147+
143148 pub fn contains(&self, pos: usize) -> bool {
144149 self.from() <= pos && pos < self.to()
145150 }
@@ -544,6 +549,39 @@ impl Selection {
544549 pub fn len(&self) -> usize {
545550 self.ranges.len()
546551 }
552+
553+ // returns true if self ⊇ other
554+ pub fn contains(&self, other: &Selection) -> bool {
555+ // can't contain other if it is larger
556+ if other.len() > self.len() {
557+ return false;
558+ }
559+
560+ let (mut iter_self, mut iter_other) = (self.iter(), other.iter());
561+ let (mut ele_self, mut ele_other) = (iter_self.next(), iter_other.next());
562+
563+ loop {
564+ match (ele_self, ele_other) {
565+ (Some(ra), Some(rb)) => {
566+ if !ra.contains_range(rb) {
567+ // `self` doesn't contain next element from `other`, advance `self`, we need to match all from `other`
568+ ele_self = iter_self.next();
569+ } else {
570+ // matched element from `other`, advance `other`
571+ ele_other = iter_other.next();
572+ };
573+ }
574+ (None, Some(_)) => {
575+ // exhausted `self`, we can't match the reminder of `other`
576+ return false;
577+ }
578+ (_, None) => {
579+ // no elements from `other` left to match, `self` contains `other`
580+ return true;
581+ }
582+ }
583+ }
584+ }
547585}
548586
549587impl<'a> IntoIterator for &'a Selection {
@@ -982,4 +1020,30 @@ mod test {
9821020 &["", "abcd", "efg", "rs", "xyz"]
9831021 );
9841022 }
1023+ #[test]
1024+ fn test_selection_contains() {
1025+ fn contains(a: Vec<(usize, usize)>, b: Vec<(usize, usize)>) -> bool {
1026+ let sela = Selection::new(a.iter().map(|a| Range::new(a.0, a.1)).collect(), 0);
1027+ let selb = Selection::new(b.iter().map(|b| Range::new(b.0, b.1)).collect(), 0);
1028+ sela.contains(&selb)
1029+ }
1030+
1031+ // exact match
1032+ assert!(contains(vec!((1, 1)), vec!((1, 1))));
1033+
1034+ // larger set contains smaller
1035+ assert!(contains(vec!((1, 1), (2, 2), (3, 3)), vec!((2, 2))));
1036+
1037+ // multiple matches
1038+ assert!(contains(vec!((1, 1), (2, 2)), vec!((1, 1), (2, 2))));
1039+
1040+ // smaller set can't contain bigger
1041+ assert!(!contains(vec!((1, 1)), vec!((1, 1), (2, 2))));
1042+
1043+ assert!(contains(
1044+ vec!((1, 1), (2, 4), (5, 6), (7, 9), (10, 13)),
1045+ vec!((3, 4), (7, 9))
1046+ ));
1047+ assert!(!contains(vec!((1, 1), (5, 6)), vec!((1, 6))));
1048+ }
9851049}
helix-term/src/commands.rs+35−1
@@ -362,6 +362,7 @@ impl MappableCommand {
362362 rotate_selection_contents_forward, "Rotate selection contents forward",
363363 rotate_selection_contents_backward, "Rotate selections contents backward",
364364 expand_selection, "Expand selection to parent syntax node",
365+ shrink_selection, "Shrink selection to previously expanded syntax node",
365366 jump_forward, "Jump forward on jumplist",
366367 jump_backward, "Jump backward on jumplist",
367368 save_selection, "Save the current selection to the jumplist",
@@ -5467,6 +5468,7 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
54675468 doc.apply(&transaction, view.id);
54685469 doc.append_changes_to_history(view.id);
54695470}
5471+
54705472fn rotate_selection_contents_forward(cx: &mut Context) {
54715473 rotate_selection_contents(cx, Direction::Forward)
54725474}
@@ -5482,7 +5484,39 @@ fn expand_selection(cx: &mut Context) {
54825484
54835485 if let Some(syntax) = doc.syntax() {
54845486 let text = doc.text().slice(..);
5485− let selection = object::expand_selection(syntax, text, doc.selection(view.id));
5487+
5488+ let current_selection = doc.selection(view.id);
5489+
5490+ // save current selection so it can be restored using shrink_selection
5491+ view.object_selections.push(current_selection.clone());
5492+
5493+ let selection = object::expand_selection(syntax, text, current_selection);
5494+ doc.set_selection(view.id, selection);
5495+ }
5496+ };
5497+ motion(cx.editor);
5498+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
5499+}
5500+
5501+fn shrink_selection(cx: &mut Context) {
5502+ let motion = |editor: &mut Editor| {
5503+ let (view, doc) = current!(editor);
5504+ let current_selection = doc.selection(view.id);
5505+ // try to restore previous selection
5506+ if let Some(prev_selection) = view.object_selections.pop() {
5507+ if current_selection.contains(&prev_selection) {
5508+ // allow shrinking the selection only if current selection contains the previous object selection
5509+ doc.set_selection(view.id, prev_selection);
5510+ return;
5511+ } else {
5512+ // clear existing selection as they can't be shrinked to anyway
5513+ view.object_selections.clear();
5514+ }
5515+ }
5516+ // if not previous selection, shrink to first child
5517+ if let Some(syntax) = doc.syntax() {
5518+ let text = doc.text().slice(..);
5519+ let selection = object::shrink_selection(syntax, text, current_selection);
54865520 doc.set_selection(view.id, selection);
54875521 }
54885522 };
helix-term/src/keymap.rs+2
@@ -569,11 +569,13 @@ impl Default for Keymaps {
569569 "d" => goto_prev_diag,
570570 "D" => goto_first_diag,
571571 "space" => add_newline_above,
572+ "o" => shrink_selection,
572573 },
573574 "]" => { "Right bracket"
574575 "d" => goto_next_diag,
575576 "D" => goto_last_diag,
576577 "space" => add_newline_below,
578+ "o" => expand_selection,
577579 },
578580
579581 "/" => search,
helix-view/src/view.rs+3
@@ -80,6 +80,8 @@ pub struct View {
8080 // uses two docs because we want to be able to swap between the
8181 // two last modified docs which we need to manually keep track of
8282 pub last_modified_docs: [Option<DocumentId>; 2],
83+ /// used to store previous selections of tree-sitter objecs
84+ pub object_selections: Vec<Selection>,
8385}
8486
8587impl View {
@@ -92,6 +94,7 @@ impl View {
9294 jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
9395 last_accessed_doc: None,
9496 last_modified_docs: [None, None],
97+ object_selections: Vec::new(),
9598 }
9699 }
97100