diff --git a/book/src/keymap.md b/book/src/keymap.md index 581e70e9..70ec13b3 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -261,6 +261,8 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire | `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` | | `[space` | Add newline above | `add_newline_above` | | `]space` | Add newline below | `add_newline_below` | +| `]o` | Expand syntax tree object selection. | `expand_selection` | +| `[o` | Shrink syntax tree object selection. | `shrink_selection` | ## Insert Mode diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index 717c5994..21fa24fb 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -1,7 +1,5 @@ use crate::{Range, RopeSlice, Selection, Syntax}; -// TODO: to contract_selection we'd need to store the previous ranges before expand. -// Maybe just contract to the first child node? pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection { let tree = syntax.tree(); @@ -34,3 +32,30 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) } }) } + +pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection { + let tree = syntax.tree(); + + selection.clone().transform(|range| { + let from = text.char_to_byte(range.from()); + let to = text.char_to_byte(range.to()); + + let descendant = match tree.root_node().descendant_for_byte_range(from, to) { + // find first child, if not possible, fallback to the node that contains selection + Some(descendant) => match descendant.child(0) { + Some(child) => child, + None => descendant, + }, + None => return range, + }; + + let from = text.byte_to_char(descendant.start_byte()); + let to = text.byte_to_char(descendant.end_byte()); + + if range.head < range.anchor { + Range::new(to, from) + } else { + Range::new(from, to) + } + }) +} diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 06ea9d67..1515c4fc 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -140,6 +140,11 @@ impl Range { self.from() == other.from() || (self.to() > other.from() && other.to() > self.from()) } + #[inline] + pub fn contains_range(&self, other: &Self) -> bool { + self.from() <= other.from() && self.to() >= other.to() + } + pub fn contains(&self, pos: usize) -> bool { self.from() <= pos && pos < self.to() } @@ -544,6 +549,39 @@ impl Selection { pub fn len(&self) -> usize { self.ranges.len() } + + // returns true if self ⊇ other + pub fn contains(&self, other: &Selection) -> bool { + // can't contain other if it is larger + if other.len() > self.len() { + return false; + } + + let (mut iter_self, mut iter_other) = (self.iter(), other.iter()); + let (mut ele_self, mut ele_other) = (iter_self.next(), iter_other.next()); + + loop { + match (ele_self, ele_other) { + (Some(ra), Some(rb)) => { + if !ra.contains_range(rb) { + // `self` doesn't contain next element from `other`, advance `self`, we need to match all from `other` + ele_self = iter_self.next(); + } else { + // matched element from `other`, advance `other` + ele_other = iter_other.next(); + }; + } + (None, Some(_)) => { + // exhausted `self`, we can't match the reminder of `other` + return false; + } + (_, None) => { + // no elements from `other` left to match, `self` contains `other` + return true; + } + } + } + } } impl<'a> IntoIterator for &'a Selection { @@ -982,4 +1020,30 @@ mod test { &["", "abcd", "efg", "rs", "xyz"] ); } + #[test] + fn test_selection_contains() { + fn contains(a: Vec<(usize, usize)>, b: Vec<(usize, usize)>) -> bool { + let sela = Selection::new(a.iter().map(|a| Range::new(a.0, a.1)).collect(), 0); + let selb = Selection::new(b.iter().map(|b| Range::new(b.0, b.1)).collect(), 0); + sela.contains(&selb) + } + + // exact match + assert!(contains(vec!((1, 1)), vec!((1, 1)))); + + // larger set contains smaller + assert!(contains(vec!((1, 1), (2, 2), (3, 3)), vec!((2, 2)))); + + // multiple matches + assert!(contains(vec!((1, 1), (2, 2)), vec!((1, 1), (2, 2)))); + + // smaller set can't contain bigger + assert!(!contains(vec!((1, 1)), vec!((1, 1), (2, 2)))); + + assert!(contains( + vec!((1, 1), (2, 4), (5, 6), (7, 9), (10, 13)), + vec!((3, 4), (7, 9)) + )); + assert!(!contains(vec!((1, 1), (5, 6)), vec!((1, 6)))); + } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index bf12b122..5c26a5b2 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -362,6 +362,7 @@ impl MappableCommand { rotate_selection_contents_forward, "Rotate selection contents forward", rotate_selection_contents_backward, "Rotate selections contents backward", expand_selection, "Expand selection to parent syntax node", + shrink_selection, "Shrink selection to previously expanded syntax node", jump_forward, "Jump forward on jumplist", jump_backward, "Jump backward on jumplist", save_selection, "Save the current selection to the jumplist", @@ -5467,6 +5468,7 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) { doc.apply(&transaction, view.id); doc.append_changes_to_history(view.id); } + fn rotate_selection_contents_forward(cx: &mut Context) { rotate_selection_contents(cx, Direction::Forward) } @@ -5482,7 +5484,39 @@ fn expand_selection(cx: &mut Context) { if let Some(syntax) = doc.syntax() { let text = doc.text().slice(..); - let selection = object::expand_selection(syntax, text, doc.selection(view.id)); + + let current_selection = doc.selection(view.id); + + // save current selection so it can be restored using shrink_selection + view.object_selections.push(current_selection.clone()); + + let selection = object::expand_selection(syntax, text, current_selection); + doc.set_selection(view.id, selection); + } + }; + motion(cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(motion))); +} + +fn shrink_selection(cx: &mut Context) { + let motion = |editor: &mut Editor| { + let (view, doc) = current!(editor); + let current_selection = doc.selection(view.id); + // try to restore previous selection + if let Some(prev_selection) = view.object_selections.pop() { + if current_selection.contains(&prev_selection) { + // allow shrinking the selection only if current selection contains the previous object selection + doc.set_selection(view.id, prev_selection); + return; + } else { + // clear existing selection as they can't be shrinked to anyway + view.object_selections.clear(); + } + } + // if not previous selection, shrink to first child + if let Some(syntax) = doc.syntax() { + let text = doc.text().slice(..); + let selection = object::shrink_selection(syntax, text, current_selection); doc.set_selection(view.id, selection); } }; diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 72833212..49f8469a 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -569,11 +569,13 @@ impl Default for Keymaps { "d" => goto_prev_diag, "D" => goto_first_diag, "space" => add_newline_above, + "o" => shrink_selection, }, "]" => { "Right bracket" "d" => goto_next_diag, "D" => goto_last_diag, "space" => add_newline_below, + "o" => expand_selection, }, "/" => search, diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 94d67acd..89a6c196 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -80,6 +80,8 @@ pub struct View { // uses two docs because we want to be able to swap between the // two last modified docs which we need to manually keep track of pub last_modified_docs: [Option; 2], + /// used to store previous selections of tree-sitter objecs + pub object_selections: Vec, } impl View { @@ -92,6 +94,7 @@ impl View { jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel last_accessed_doc: None, last_modified_docs: [None, None], + object_selections: Vec::new(), } }