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
This commit is contained in:
parent
66afbc9fff
commit
2e02a1d6bc
6 changed files with 133 additions and 3 deletions
|
@ -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` |
|
| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
|
||||||
| `[space` | Add newline above | `add_newline_above` |
|
| `[space` | Add newline above | `add_newline_above` |
|
||||||
| `]space` | Add newline below | `add_newline_below` |
|
| `]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
|
## Insert Mode
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
use crate::{Range, RopeSlice, Selection, Syntax};
|
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 {
|
pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection {
|
||||||
let tree = syntax.tree();
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -140,6 +140,11 @@ impl Range {
|
||||||
self.from() == other.from() || (self.to() > other.from() && other.to() > self.from())
|
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 {
|
pub fn contains(&self, pos: usize) -> bool {
|
||||||
self.from() <= pos && pos < self.to()
|
self.from() <= pos && pos < self.to()
|
||||||
}
|
}
|
||||||
|
@ -544,6 +549,39 @@ impl Selection {
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.ranges.len()
|
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 {
|
impl<'a> IntoIterator for &'a Selection {
|
||||||
|
@ -982,4 +1020,30 @@ mod test {
|
||||||
&["", "abcd", "efg", "rs", "xyz"]
|
&["", "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))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -362,6 +362,7 @@ impl MappableCommand {
|
||||||
rotate_selection_contents_forward, "Rotate selection contents forward",
|
rotate_selection_contents_forward, "Rotate selection contents forward",
|
||||||
rotate_selection_contents_backward, "Rotate selections contents backward",
|
rotate_selection_contents_backward, "Rotate selections contents backward",
|
||||||
expand_selection, "Expand selection to parent syntax node",
|
expand_selection, "Expand selection to parent syntax node",
|
||||||
|
shrink_selection, "Shrink selection to previously expanded syntax node",
|
||||||
jump_forward, "Jump forward on jumplist",
|
jump_forward, "Jump forward on jumplist",
|
||||||
jump_backward, "Jump backward on jumplist",
|
jump_backward, "Jump backward on jumplist",
|
||||||
save_selection, "Save the current selection to the 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.apply(&transaction, view.id);
|
||||||
doc.append_changes_to_history(view.id);
|
doc.append_changes_to_history(view.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rotate_selection_contents_forward(cx: &mut Context) {
|
fn rotate_selection_contents_forward(cx: &mut Context) {
|
||||||
rotate_selection_contents(cx, Direction::Forward)
|
rotate_selection_contents(cx, Direction::Forward)
|
||||||
}
|
}
|
||||||
|
@ -5482,7 +5484,39 @@ fn expand_selection(cx: &mut Context) {
|
||||||
|
|
||||||
if let Some(syntax) = doc.syntax() {
|
if let Some(syntax) = doc.syntax() {
|
||||||
let text = doc.text().slice(..);
|
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);
|
doc.set_selection(view.id, selection);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -569,11 +569,13 @@ impl Default for Keymaps {
|
||||||
"d" => goto_prev_diag,
|
"d" => goto_prev_diag,
|
||||||
"D" => goto_first_diag,
|
"D" => goto_first_diag,
|
||||||
"space" => add_newline_above,
|
"space" => add_newline_above,
|
||||||
|
"o" => shrink_selection,
|
||||||
},
|
},
|
||||||
"]" => { "Right bracket"
|
"]" => { "Right bracket"
|
||||||
"d" => goto_next_diag,
|
"d" => goto_next_diag,
|
||||||
"D" => goto_last_diag,
|
"D" => goto_last_diag,
|
||||||
"space" => add_newline_below,
|
"space" => add_newline_below,
|
||||||
|
"o" => expand_selection,
|
||||||
},
|
},
|
||||||
|
|
||||||
"/" => search,
|
"/" => search,
|
||||||
|
|
|
@ -80,6 +80,8 @@ pub struct View {
|
||||||
// uses two docs because we want to be able to swap between the
|
// 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
|
// two last modified docs which we need to manually keep track of
|
||||||
pub last_modified_docs: [Option<DocumentId>; 2],
|
pub last_modified_docs: [Option<DocumentId>; 2],
|
||||||
|
/// used to store previous selections of tree-sitter objecs
|
||||||
|
pub object_selections: Vec<Selection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl View {
|
impl View {
|
||||||
|
@ -92,6 +94,7 @@ impl View {
|
||||||
jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
|
jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
|
||||||
last_accessed_doc: None,
|
last_accessed_doc: None,
|
||||||
last_modified_docs: [None, None],
|
last_modified_docs: [None, None],
|
||||||
|
object_selections: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue