aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--book/src/keymap.md2
-rw-r--r--helix-core/src/object.rs29
-rw-r--r--helix-core/src/selection.rs64
-rw-r--r--helix-term/src/commands.rs36
-rw-r--r--helix-term/src/keymap.rs2
-rw-r--r--helix-view/src/view.rs3
6 files changed, 133 insertions, 3 deletions
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<DocumentId>; 2],
+ /// used to store previous selections of tree-sitter objecs
+ pub object_selections: Vec<Selection>,
}
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(),
}
}