Compare commits

...

7 Commits

Author SHA1 Message Date
Skyler Hawthorne d51abbe4f0
Merge 70650ff55a into 4281228da3 2025-07-24 18:17:28 -07:00
Valtteri Koskivuori 4281228da3
fix(queries): Fix filesystem permissions for snakemake (#14061) 2025-07-24 13:09:40 -04:00
Skyler Hawthorne 70650ff55a feat(command): expand_selection_around
Introduces a new command `expand_selection_around` that expands the
selection to the parent node, like `expand_selection`, except it splits
on the selection you start with and continues expansion around this
initial selection.
2025-05-26 19:22:20 -04:00
Skyler Hawthorne 57d7b4fb4a Generalize View::object_selections into map
This allows using multiple distinct state histories. By default, all
history is also cleared any time a view's selection is set, unless
explicitly told to save the state. This way, we can have control over
when selection history is saved. They are also cleared on any text
edit, since an edit could invalidate the previous selection, potentially
causing a panic.

Additionally, the object selections have been moved into `Document`
so that it is easier to manipulate them when changes to the document
happen. They have been put into a wrapper struct named `ViewData`, where
the intention is that any further fields that we want to add in the
future that must be associated with a view, but are more convenient to
store in a document, can be added here, instead of further polluting the
core `Document` type.
2025-05-26 19:22:20 -04:00
Skyler Hawthorne 4b3aad7830 Add Selection::{overlaps,without} and fix contains
Adds two helper functions to `Selection`:

* `overlaps`: tests whether two `Selection`s contain any ranges which
  overlap with each other
* `without`: Computes a new `Selection` that is the set difference
  of two `Selection`s, i.e. everything in the first `Selection`
  with everything that overlaps in the second `Selection` removed,
  potentially leaving holes in the original ranges.

It also fixes a bug with `Selection::contains`: it assumes that if the
second `Selection` has a greater number of ranges than the first, then
the first cannot contain the second; but this is false, since one range
from the first could contain multiple smaller ranges in the second.
2025-05-26 19:22:20 -04:00
Skyler Hawthorne 8fbb9566fb shrink_selection without history selects first contained child
This changes the behavior of `shrink_selection` to iterate through child
nodes until it finds one that is contained within the selection, with
at least one of the ends of the selection being exclusively inside the
starting selection (though not necessarily both ends). This produces
more intuitive behavior for selecting the "first logical thing" inside
the selection.
2025-05-26 19:22:20 -04:00
Skyler Hawthorne f6e1a2f599 backfill expand/shrink tests 2025-05-26 19:22:20 -04:00
13 changed files with 855 additions and 99 deletions

View File

@ -25,38 +25,59 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection)
}
pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(
syntax,
text,
selection,
|cursor| {
cursor.goto_first_child();
},
None,
)
selection.transform(move |range| {
let (from, to) = range.into_byte_range(text);
let mut cursor = syntax.walk();
cursor.reset_to_byte_range(from, to);
if let Some(node) = cursor
.into_iter()
.find(|node| node.is_named() && node.is_contained_within(from..to))
{
return Range::from_node(node, text, range.direction());
}
range
})
}
pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(
syntax,
text,
selection,
|cursor| {
while !cursor.goto_next_sibling() {
if !cursor.goto_parent() {
break;
}
selection.transform(move |range| {
let (from, to) = range.into_byte_range(text);
let mut cursor = syntax.walk();
cursor.reset_to_byte_range(from, to);
while !cursor.goto_next_sibling() {
if !cursor.goto_parent() {
return range;
}
},
Some(Direction::Forward),
)
}
Range::from_node(cursor.node(), text, Direction::Forward)
})
}
pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
selection.transform(move |range| {
let (from, to) = range.into_byte_range(text);
let mut cursor = syntax.walk();
cursor.reset_to_byte_range(from, to);
while !cursor.goto_previous_sibling() {
if !cursor.goto_parent() {
return range;
}
}
Range::from_node(cursor.node(), text, Direction::Backward)
})
}
pub fn select_all_siblings(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
let mut cursor = syntax.walk();
selection.transform_iter(move |range| {
let (from, to) = range.into_byte_range(text);
cursor.reset_to_byte_range(from as u32, to as u32);
cursor.reset_to_byte_range(from, to);
if !cursor.goto_parent_with(|parent| parent.child_count() > 1) {
return vec![range].into_iter();
@ -70,7 +91,7 @@ pub fn select_all_children(syntax: &Syntax, text: RopeSlice, selection: Selectio
let mut cursor = syntax.walk();
selection.transform_iter(move |range| {
let (from, to) = range.into_byte_range(text);
cursor.reset_to_byte_range(from as u32, to as u32);
cursor.reset_to_byte_range(from, to);
select_children(&mut cursor, text, range).into_iter()
})
}
@ -88,47 +109,3 @@ fn select_children(cursor: &mut TreeCursor, text: RopeSlice, range: Range) -> Ve
vec![range]
}
}
pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(
syntax,
text,
selection,
|cursor| {
while !cursor.goto_previous_sibling() {
if !cursor.goto_parent() {
break;
}
}
},
Some(Direction::Backward),
)
}
fn select_node_impl<F>(
syntax: &Syntax,
text: RopeSlice,
selection: Selection,
motion: F,
direction: Option<Direction>,
) -> Selection
where
F: Fn(&mut TreeCursor),
{
let cursor = &mut syntax.walk();
selection.transform(|range| {
let from = text.char_to_byte(range.from()) as u32;
let to = text.char_to_byte(range.to()) as u32;
cursor.reset_to_byte_range(from, to);
motion(cursor);
let node = cursor.node();
let from = text.byte_to_char(node.start_byte() as usize);
let to = text.byte_to_char(node.end_byte() as usize);
Range::new(from, to).with_direction(direction.unwrap_or_else(|| range.direction()))
})
}

View File

@ -387,8 +387,11 @@ impl Range {
/// Converts this char range into an in order byte range, discarding
/// direction.
pub fn into_byte_range(&self, text: RopeSlice) -> (usize, usize) {
(text.char_to_byte(self.from()), text.char_to_byte(self.to()))
pub fn into_byte_range(&self, text: RopeSlice) -> (u32, u32) {
(
text.char_to_byte(self.from()) as u32,
text.char_to_byte(self.to()) as u32,
)
}
}
@ -700,22 +703,161 @@ impl Selection {
pub fn contains(&self, other: &Selection) -> bool {
is_subset::<true>(self.range_bounds(), other.range_bounds())
}
/// returns true if self has at least one range that overlaps with at least one range from other
pub fn overlaps(&self, other: &Selection) -> bool {
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.overlaps(rb) {
return true;
} else if ra.from() > rb.from() {
ele_self = iter_self.next();
} else {
ele_other = iter_other.next();
}
}
_ => return false,
}
}
}
/// Returns the given selection with the overlapping portions of `other`
/// cut out. If one range from this selection is equal to one from `other`,
/// this range is removed. If this results in an entirely empty selection,
/// `None` is returned.
pub fn without(self, other: &Selection) -> Option<Self> {
if other.contains(&self) {
return None;
}
let mut primary_index = self.primary_index;
let mut ranges = smallvec!();
let (mut iter_self, mut iter_other) = (self.into_iter(), other.iter());
let (mut ele_self, mut ele_other) = (iter_self.next(), iter_other.next());
let mut cur_index = 0;
loop {
match (ele_self, ele_other) {
(Some(ra), Some(rb)) => {
if !ra.overlaps(rb) {
// there's no overlap and it's on the left of rb
if ra.to() <= rb.from() {
ranges.push(ra);
ele_self = iter_self.next();
cur_index += 1;
// otherwise it must be on the right, so move rb forward
} else {
ele_other = iter_other.next();
}
continue;
}
// otherwise there is overlap, so truncate or split
if rb.contains_range(&ra) {
ele_self = iter_self.next();
cur_index += 1;
continue;
}
// [ ra ]
// [ rb ]
if ra.from() <= rb.from() && ra.to() <= rb.to() && ra.to() >= rb.from() {
let new = if ra.direction() == Direction::Backward {
Range::new(rb.from(), ra.head)
} else {
Range::new(ra.anchor, rb.from())
};
ranges.push(new);
ele_self = iter_self.next();
cur_index += 1;
// [ ra ]
// [ rb ]
} else if ra.from() >= rb.from() && ra.to() >= rb.to() && ra.from() <= rb.to() {
let new = if ra.direction() == Direction::Backward {
Range::new(ra.anchor, rb.to() + 1)
} else {
Range::new(rb.to(), ra.head)
};
// don't settle on the new range yet because the next
// rb could chop off the other end of ra
ele_self = Some(new);
ele_other = iter_other.next();
// [ ra ]
// [ rb ]
} else if ra.from() < rb.from() && ra.to() > rb.to() {
// we must split the range into two
let left = if ra.direction() == Direction::Backward {
Range::new(rb.from(), ra.head)
} else {
Range::new(ra.anchor, rb.from())
};
let right = if ra.direction() == Direction::Backward {
Range::new(ra.anchor, rb.to())
} else {
Range::new(rb.to(), ra.head)
};
// We do NOT push right onto the results right away.
// We must put it back into the iterator and check it
// again in case a further range splits it again.
ranges.push(left);
ele_other = iter_other.next();
// offset the primary index whenever we split
if cur_index < primary_index {
primary_index += 1;
}
cur_index += 1;
ele_self = Some(right);
}
}
// the rest just get included as is
(Some(range), None) => {
ranges.push(range);
ele_self = iter_self.next();
cur_index += 1;
}
// exhausted `self`, nothing left to do
(None, _) => {
break;
}
}
}
if ranges.is_empty() {
return None;
}
Some(Selection::new(ranges, primary_index))
}
}
impl<'a> IntoIterator for &'a Selection {
type Item = &'a Range;
type IntoIter = std::slice::Iter<'a, Range>;
fn into_iter(self) -> std::slice::Iter<'a, Range> {
fn into_iter(self) -> Self::IntoIter {
self.ranges().iter()
}
}
impl IntoIterator for Selection {
type Item = Range;
type IntoIter = smallvec::IntoIter<[Range; 1]>;
type IntoIter = smallvec::IntoIter<[Self::Item; 1]>;
fn into_iter(self) -> smallvec::IntoIter<[Range; 1]> {
fn into_iter(self) -> Self::IntoIter {
self.ranges.into_iter()
}
}
@ -882,6 +1024,7 @@ pub fn split_on_matches(text: RopeSlice, selection: &Selection, regex: &rope::Re
#[cfg(test)]
mod test {
use super::*;
use crate::test;
use crate::Rope;
#[test]
@ -972,7 +1115,7 @@ mod test {
}
#[test]
fn test_overlaps() {
fn test_range_overlaps() {
fn overlaps(a: (usize, usize), b: (usize, usize)) -> bool {
Range::new(a.0, a.1).overlaps(&Range::new(b.0, b.1))
}
@ -1022,6 +1165,160 @@ mod test {
assert!(overlaps((1, 1), (1, 1)));
}
#[test]
fn test_selection_overlaps() {
fn overlaps(a: &[(usize, usize)], b: &[(usize, usize)]) -> bool {
let a = Selection::new(
a.iter()
.map(|(anchor, head)| Range::new(*anchor, *head))
.collect(),
0,
);
let b = Selection::new(
b.iter()
.map(|(anchor, head)| Range::new(*anchor, *head))
.collect(),
0,
);
a.overlaps(&b)
}
// Two non-zero-width ranges, no overlap.
assert!(!overlaps(&[(0, 3)], &[(3, 6)]));
assert!(!overlaps(&[(0, 3)], &[(6, 3)]));
assert!(!overlaps(&[(3, 0)], &[(3, 6)]));
assert!(!overlaps(&[(3, 0)], &[(6, 3)]));
assert!(!overlaps(&[(3, 6)], &[(0, 3)]));
assert!(!overlaps(&[(3, 6)], &[(3, 0)]));
assert!(!overlaps(&[(6, 3)], &[(0, 3)]));
assert!(!overlaps(&[(6, 3)], &[(3, 0)]));
// more ranges in one or the other, no overlap
assert!(!overlaps(&[(6, 3), (9, 6)], &[(3, 0)]));
assert!(!overlaps(&[(6, 3), (6, 9)], &[(3, 0)]));
assert!(!overlaps(&[(3, 6), (9, 6)], &[(3, 0)]));
assert!(!overlaps(&[(3, 6), (6, 9)], &[(3, 0)]));
assert!(!overlaps(&[(6, 3), (9, 6)], &[(0, 3)]));
assert!(!overlaps(&[(6, 3), (6, 9)], &[(0, 3)]));
assert!(!overlaps(&[(3, 6), (9, 6)], &[(0, 3)]));
assert!(!overlaps(&[(3, 6), (6, 9)], &[(0, 3)]));
assert!(!overlaps(&[(6, 3)], &[(3, 0), (9, 6)]));
assert!(!overlaps(&[(6, 3)], &[(3, 0), (6, 9)]));
assert!(!overlaps(&[(3, 6)], &[(3, 0), (9, 6)]));
assert!(!overlaps(&[(3, 6)], &[(3, 0), (6, 9)]));
assert!(!overlaps(&[(6, 3)], &[(0, 3), (9, 6)]));
assert!(!overlaps(&[(6, 3)], &[(0, 3), (6, 9)]));
assert!(!overlaps(&[(3, 6)], &[(0, 3), (9, 6)]));
assert!(!overlaps(&[(3, 6)], &[(0, 3), (6, 9)]));
// Two non-zero-width ranges, overlap.
assert!(overlaps(&[(0, 4)], &[(3, 6)]));
assert!(overlaps(&[(0, 4)], &[(6, 3)]));
assert!(overlaps(&[(4, 0)], &[(3, 6)]));
assert!(overlaps(&[(4, 0)], &[(6, 3)]));
assert!(overlaps(&[(3, 6)], &[(0, 4)]));
assert!(overlaps(&[(3, 6)], &[(4, 0)]));
assert!(overlaps(&[(6, 3)], &[(0, 4)]));
assert!(overlaps(&[(6, 3)], &[(4, 0)]));
// Two non-zero-width ranges, overlap, extra from one or the other
assert!(overlaps(&[(0, 4), (7, 10)], &[(3, 6)]));
assert!(overlaps(&[(0, 4), (7, 10)], &[(6, 3)]));
assert!(overlaps(&[(4, 0), (7, 10)], &[(3, 6)]));
assert!(overlaps(&[(4, 0), (7, 10)], &[(6, 3)]));
assert!(overlaps(&[(3, 6), (7, 10)], &[(0, 4)]));
assert!(overlaps(&[(3, 6), (7, 10)], &[(4, 0)]));
assert!(overlaps(&[(6, 3), (7, 10)], &[(0, 4)]));
assert!(overlaps(&[(6, 3), (7, 10)], &[(4, 0)]));
assert!(overlaps(&[(0, 4), (10, 7)], &[(3, 6)]));
assert!(overlaps(&[(0, 4), (10, 7)], &[(6, 3)]));
assert!(overlaps(&[(4, 0), (10, 7)], &[(3, 6)]));
assert!(overlaps(&[(4, 0), (10, 7)], &[(6, 3)]));
assert!(overlaps(&[(3, 6), (10, 7)], &[(0, 4)]));
assert!(overlaps(&[(3, 6), (10, 7)], &[(4, 0)]));
assert!(overlaps(&[(6, 3), (10, 7)], &[(0, 4)]));
assert!(overlaps(&[(6, 3), (10, 7)], &[(4, 0)]));
assert!(overlaps(&[(0, 4)], &[(3, 6), (7, 10)]));
assert!(overlaps(&[(0, 4)], &[(6, 3), (7, 10)]));
assert!(overlaps(&[(4, 0)], &[(3, 6), (7, 10)]));
assert!(overlaps(&[(4, 0)], &[(6, 3), (7, 10)]));
assert!(overlaps(&[(3, 6)], &[(0, 4), (7, 10)]));
assert!(overlaps(&[(3, 6)], &[(4, 0), (7, 10)]));
assert!(overlaps(&[(6, 3)], &[(0, 4), (7, 10)]));
assert!(overlaps(&[(6, 3)], &[(4, 0), (7, 10)]));
assert!(overlaps(&[(0, 4)], &[(3, 6), (10, 7)]));
assert!(overlaps(&[(0, 4)], &[(6, 3), (10, 7)]));
assert!(overlaps(&[(4, 0)], &[(3, 6), (10, 7)]));
assert!(overlaps(&[(4, 0)], &[(6, 3), (10, 7)]));
assert!(overlaps(&[(3, 6)], &[(0, 4), (10, 7)]));
assert!(overlaps(&[(3, 6)], &[(4, 0), (10, 7)]));
assert!(overlaps(&[(6, 3)], &[(0, 4), (10, 7)]));
assert!(overlaps(&[(6, 3)], &[(4, 0), (10, 7)]));
// Zero-width and non-zero-width range, no overlap.
assert!(!overlaps(&[(0, 3)], &[(3, 3)]));
assert!(!overlaps(&[(3, 0)], &[(3, 3)]));
assert!(!overlaps(&[(3, 3)], &[(0, 3)]));
assert!(!overlaps(&[(3, 3)], &[(3, 0)]));
assert!(!overlaps(&[(0, 3), (7, 10)], &[(3, 3)]));
assert!(!overlaps(&[(3, 0), (7, 10)], &[(3, 3)]));
assert!(!overlaps(&[(3, 3), (7, 10)], &[(0, 3)]));
assert!(!overlaps(&[(3, 3), (7, 10)], &[(3, 0)]));
assert!(!overlaps(&[(0, 3)], &[(3, 3), (7, 10)]));
assert!(!overlaps(&[(3, 0)], &[(3, 3), (7, 10)]));
assert!(!overlaps(&[(3, 3)], &[(0, 3), (7, 10)]));
assert!(!overlaps(&[(3, 3)], &[(3, 0), (7, 10)]));
// Zero-width and non-zero-width range, overlap.
assert!(overlaps(&[(1, 4)], &[(1, 1)]));
assert!(overlaps(&[(4, 1)], &[(1, 1)]));
assert!(overlaps(&[(1, 1)], &[(1, 4)]));
assert!(overlaps(&[(1, 1)], &[(4, 1)]));
assert!(overlaps(&[(1, 4)], &[(3, 3)]));
assert!(overlaps(&[(4, 1)], &[(3, 3)]));
assert!(overlaps(&[(3, 3)], &[(1, 4)]));
assert!(overlaps(&[(3, 3)], &[(4, 1)]));
assert!(overlaps(&[(1, 4), (7, 10)], &[(1, 1)]));
assert!(overlaps(&[(4, 1), (7, 10)], &[(1, 1)]));
assert!(overlaps(&[(1, 1), (7, 10)], &[(1, 4)]));
assert!(overlaps(&[(1, 1), (7, 10)], &[(4, 1)]));
assert!(overlaps(&[(1, 4), (7, 10)], &[(3, 3)]));
assert!(overlaps(&[(4, 1), (7, 10)], &[(3, 3)]));
assert!(overlaps(&[(3, 3), (7, 10)], &[(1, 4)]));
assert!(overlaps(&[(3, 3), (7, 10)], &[(4, 1)]));
assert!(overlaps(&[(1, 4)], &[(1, 1), (7, 10)]));
assert!(overlaps(&[(4, 1)], &[(1, 1), (7, 10)]));
assert!(overlaps(&[(1, 1)], &[(1, 4), (7, 10)]));
assert!(overlaps(&[(1, 1)], &[(4, 1), (7, 10)]));
assert!(overlaps(&[(1, 4)], &[(3, 3), (7, 10)]));
assert!(overlaps(&[(4, 1)], &[(3, 3), (7, 10)]));
assert!(overlaps(&[(3, 3)], &[(1, 4), (7, 10)]));
assert!(overlaps(&[(3, 3)], &[(4, 1), (7, 10)]));
// Two zero-width ranges, no overlap.
assert!(!overlaps(&[(0, 0)], &[(1, 1)]));
assert!(!overlaps(&[(1, 1)], &[(0, 0)]));
assert!(!overlaps(&[(0, 0), (2, 2)], &[(1, 1)]));
assert!(!overlaps(&[(0, 0), (2, 2)], &[(1, 1)]));
assert!(!overlaps(&[(1, 1)], &[(0, 0), (2, 2)]));
assert!(!overlaps(&[(1, 1)], &[(0, 0), (2, 2)]));
// Two zero-width ranges, overlap.
assert!(overlaps(&[(1, 1)], &[(1, 1)]));
assert!(overlaps(&[(1, 1), (2, 2)], &[(1, 1)]));
assert!(overlaps(&[(1, 1)], &[(1, 1), (2, 2)]));
}
#[test]
fn test_grapheme_aligned() {
let r = Rope::from_str("\r\nHi\r\n");
@ -1378,9 +1675,15 @@ mod test {
// multiple matches
assert!(contains(vec!((1, 1), (2, 2)), vec!((1, 1), (2, 2))));
// smaller set can't contain bigger
// extra items out of range
assert!(!contains(vec!((1, 1)), vec!((1, 1), (2, 2))));
// one big range can contain multiple smaller ranges
assert!(contains(
vec!((1, 10)),
vec!((1, 1), (2, 2), (3, 3), (3, 5), (7, 10))
));
assert!(contains(
vec!((1, 1), (2, 4), (5, 6), (7, 9), (10, 13)),
vec!((3, 4), (7, 9))
@ -1393,4 +1696,143 @@ mod test {
vec!((1, 2), (3, 4), (7, 9))
));
}
#[test]
fn test_selection_without() {
let without = |one, two, expected: Option<_>| {
println!("one: {:?}", one);
println!("two: {:?}", two);
println!("expected: {:?}", expected);
let (one_text, one_sel) = test::print(one);
let (two_text, two_sel) = test::print(two);
assert_eq!(one_text, two_text); // sanity
let actual_sel = one_sel.without(&two_sel);
let expected_sel = expected.map(|exp| {
let (expected_text, expected_sel) = test::print(exp);
assert_eq!(two_text, expected_text);
expected_sel
});
let actual = actual_sel
.as_ref()
.map(|sel| test::plain(two_text.to_string(), sel));
println!("actual: {:?}\n\n", actual);
assert_eq!(
expected_sel,
actual_sel,
"expected: {:?}, got: {:?}",
expected_sel
.as_ref()
.map(|sel| test::plain(two_text.to_string(), sel)),
actual,
);
};
without(
"#[foo bar baz|]#",
"foo #[bar|]# baz",
Some("#[foo |]#bar#( baz|)#"),
);
without("#[foo bar baz|]#", "#[foo bar baz|]#", None);
without("#[foo bar|]# baz", "#[foo bar baz|]#", None);
without("foo #[bar|]# baz", "#[foo bar baz|]#", None);
// direction is preserved
without(
"#[|foo bar baz]#",
"foo #[|bar]# baz",
Some("#[|foo ]#bar#(| baz)#"),
);
// preference for direction is given to the left
without(
"#[|foo bar baz]#",
"foo #[bar|]# baz",
Some("#[|foo ]#bar#(| baz)#"),
);
// disjoint ranges on the right are ignored
without(
"#[foo bar|]# baz",
"foo bar #[baz|]#",
Some("#[foo bar|]# baz"),
);
without(
"#[foo bar|]# baz",
"foo bar#[ baz|]#",
Some("#[foo bar|]# baz"),
);
without(
"#(foo|)# #[bar|]# baz",
"foo#[ b|]#ar ba#(z|)#",
Some("#(foo|)# b#[ar|]# baz"),
);
// ranges contained by those one on the right are removed
without(
"#[foo bar|]# #(b|)#az",
"foo bar#[ b|]#a#(z|)#",
Some("#[foo bar|]# baz"),
);
without(
"#[foo|]# bar #(baz|)#",
"foo bar#[ b|]#a#(z|)#",
Some("#[foo|]# bar b#(a|)#z"),
);
without(
"#[foo bar|]# #(b|)#az",
"foo bar #[b|]#a#(z|)#",
Some("#[foo bar|]# baz"),
);
without(
"#[foo bar|]# #(b|)#a#(z|)#",
"foo bar #[b|]#a#(z|)#",
Some("#[foo bar|]# baz"),
);
without(
"#[foo bar|]# #(b|)#a#(z|)#",
"foo bar #[b|]#a#(z|)#",
Some("#[foo bar|]# baz"),
);
// more than one range intersected by a single range on the right
without(
"#[foo bar|]# #(baz|)#",
"foo b#[ar ba|]#z",
Some("#[foo b|]#ar ba#(z|)#"),
);
// partial overlap
without(
"#[foo bar|]# baz",
"foo #[bar baz|]#",
Some("#[foo |]#bar baz"),
);
without(
"#[foo bar|]# baz",
"foo#[ bar baz|]#",
Some("#[foo|]# bar baz"),
);
without(
"#[foo bar|]# baz",
"foo ba#[r baz|]#",
Some("#[foo ba|]#r baz"),
);
without(
"foo ba#[r baz|]#",
"#[foo bar|]# baz",
Some("foo bar#[ baz|]#"),
);
// primary selection is moved - preference given to the left of a split
without(
"#(|foo)# #(|bar baz)# #[|quux]#",
"f#(o|)#o ba#[r b|]#az q#(uu|)#x",
Some("#(|f)#o#(|o)# #(|ba)#r b#(|az)# #[|q]#uu#(|x)#"),
);
}
}

View File

@ -526,6 +526,7 @@ impl MappableCommand {
select_prev_sibling, "Select previous sibling the in syntax tree",
select_all_siblings, "Select all siblings of the current node",
select_all_children, "Select all children of the current node",
expand_selection_around, "Expand selection to parent syntax node, but exclude the selection you started with",
jump_forward, "Jump forward on jumplist",
jump_backward, "Jump backward on jumplist",
save_selection, "Save current selection to jumplist",
@ -3936,6 +3937,7 @@ fn goto_first_diag(cx: &mut Context) {
Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return,
};
doc.set_selection(view.id, selection);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
@ -3947,6 +3949,7 @@ fn goto_last_diag(cx: &mut Context) {
Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return,
};
doc.set_selection(view.id, selection);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
@ -4003,6 +4006,7 @@ fn goto_prev_diag(cx: &mut Context) {
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
};
cx.editor.apply_motion(motion)
}
@ -5374,6 +5378,10 @@ fn reverse_selection_contents(cx: &mut Context) {
// tree sitter node selection
const EXPAND_KEY: &str = "expand";
const EXPAND_AROUND_BASE_KEY: &str = "expand_around_base";
const PARENTS_KEY: &str = "parents";
fn expand_selection(cx: &mut Context) {
let motion = |editor: &mut Editor| {
let (view, doc) = current!(editor);
@ -5381,42 +5389,154 @@ fn expand_selection(cx: &mut Context) {
if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
let current_selection = doc.selection(view.id);
let current_selection = doc.selection(view.id).clone();
let selection = object::expand_selection(syntax, text, current_selection.clone());
// check if selection is different from the last one
if *current_selection != selection {
// save current selection so it can be restored using shrink_selection
view.object_selections.push(current_selection.clone());
if current_selection != selection {
let prev_selections = doc
.view_data_mut(view.id)
.object_selections
.entry(EXPAND_KEY)
.or_default();
doc.set_selection(view.id, selection);
// save current selection so it can be restored using shrink_selection
prev_selections.push(current_selection);
doc.set_selection_clear(view.id, selection, false);
}
}
};
cx.editor.apply_motion(motion);
}
fn shrink_selection(cx: &mut Context) {
let motion = |editor: &mut Editor| {
let (view, doc) = current!(editor);
let current_selection = doc.selection(view.id);
let current_selection = doc.selection(view.id).clone();
let prev_expansions = doc
.view_data_mut(view.id)
.object_selections
.entry(EXPAND_KEY)
.or_default();
// try to restore previous selection
if let Some(prev_selection) = view.object_selections.pop() {
if current_selection.contains(&prev_selection) {
doc.set_selection(view.id, prev_selection);
return;
} else {
// clear existing selection as they can't be shrunk to anyway
view.object_selections.clear();
if let Some(prev_selection) = prev_expansions.pop() {
// allow shrinking the selection only if current selection contains the previous object selection
doc.set_selection_clear(view.id, prev_selection, false);
// Do a corresponding pop of the parents from `expand_selection_around`
doc.view_data_mut(view.id)
.object_selections
.entry(PARENTS_KEY)
.and_modify(|parents| {
parents.pop();
});
// need to do this again because borrowing
let prev_expansions = doc
.view_data_mut(view.id)
.object_selections
.entry(EXPAND_KEY)
.or_default();
// if we've emptied out the previous expansions, then clear out the
// base history as well so it doesn't get used again erroneously
if prev_expansions.is_empty() {
doc.view_data_mut(view.id)
.object_selections
.entry(EXPAND_AROUND_BASE_KEY)
.and_modify(|base| {
base.clear();
});
}
return;
}
// 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.clone());
doc.set_selection(view.id, selection);
let selection = object::shrink_selection(syntax, text, current_selection);
doc.set_selection_clear(view.id, selection, false);
}
};
cx.editor.apply_motion(motion);
}
fn expand_selection_around(cx: &mut Context) {
let motion = |editor: &mut Editor| {
let (view, doc) = current!(editor);
if doc.syntax().is_some() {
// [NOTE] we do this pop and push dance because if we don't take
// ownership of the objects, then we require multiple
// mutable references to the view's object selections
let mut parents_selection = doc
.view_data_mut(view.id)
.object_selections
.entry(PARENTS_KEY)
.or_default()
.pop();
let mut base_selection = doc
.view_data_mut(view.id)
.object_selections
.entry(EXPAND_AROUND_BASE_KEY)
.or_default()
.pop();
let current_selection = doc.selection(view.id).clone();
if parents_selection.is_none() || base_selection.is_none() {
parents_selection = Some(current_selection.clone());
base_selection = Some(current_selection.clone());
}
let text = doc.text().slice(..);
let syntax = doc.syntax().unwrap();
let outside_selection =
object::expand_selection(syntax, text, parents_selection.clone().unwrap());
let target_selection = match outside_selection
.clone()
.without(&base_selection.clone().unwrap())
{
Some(sel) => sel,
None => outside_selection.clone(),
};
// check if selection is different from the last one
if target_selection != current_selection {
// save current selection so it can be restored using shrink_selection
doc.view_data_mut(view.id)
.object_selections
.entry(EXPAND_KEY)
.or_default()
.push(current_selection);
doc.set_selection_clear(view.id, target_selection, false);
}
let parents = doc
.view_data_mut(view.id)
.object_selections
.entry(PARENTS_KEY)
.or_default();
parents.push(parents_selection.unwrap());
parents.push(outside_selection);
doc.view_data_mut(view.id)
.object_selections
.entry(EXPAND_AROUND_BASE_KEY)
.or_default()
.push(base_selection.unwrap());
}
};
cx.editor.apply_motion(motion);
}
@ -5434,6 +5554,7 @@ where
doc.set_selection(view.id, selection);
}
};
cx.editor.apply_motion(motion);
}
@ -5535,8 +5656,6 @@ fn match_brackets(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
//
fn jump_forward(cx: &mut Context) {
let count = cx.count();
let config = cx.editor.config();

View File

@ -87,6 +87,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
";" => collapse_selection,
"A-;" => flip_selections,
"A-o" | "A-up" => expand_selection,
"A-O" => expand_selection_around,
"A-i" | "A-down" => shrink_selection,
"A-I" | "A-S-down" => select_all_children,
"A-p" | "A-left" => select_prev_sibling,

View File

@ -948,3 +948,198 @@ async fn match_bracket() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn expand_shrink_selection() -> anyhow::Result<()> {
let tests = vec![
// single range
(
indoc! {r##"
Some(#[thing|]#)
"##},
"<A-o><A-o>",
indoc! {r##"
#[Some(thing)|]#
"##},
),
// multi range
(
indoc! {r##"
Some(#[thing|]#)
Some(#(other_thing|)#)
"##},
"<A-o>",
indoc! {r##"
Some#[(thing)|]#
Some#((other_thing)|)#
"##},
),
// multi range collision merges
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-o><A-o><A-o>",
indoc! {r##"
#[(
Some(thing),
Some(other_thing),
)|]#
"##},
),
// multi range collision merges, then shrinks back to original
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-o><A-o><A-o><A-i>",
indoc! {r##"
(
#[Some(thing)|]#,
#(Some(other_thing)|)#,
)
"##},
),
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-o><A-o><A-o><A-i><A-i>",
indoc! {r##"
(
Some#[(thing)|]#,
Some#((other_thing)|)#,
)
"##},
),
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-o><A-o><A-o><A-i><A-i><A-i>",
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
),
// shrink with no expansion history defaults to first child
(
indoc! {r##"
#[(
Some(thing),
Some(other_thing),
)|]#
"##},
"<A-i>",
indoc! {r##"
(
#[Some(thing)|]#,
Some(other_thing),
)
"##},
),
// any movement cancels selection history and falls back to first child
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-o><A-o><A-o>jkvkkk<A-i>",
indoc! {r##"
(
#[|Some(thing)]#,
Some(other_thing),
)
"##},
),
];
for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn expand_selection_around() -> anyhow::Result<()> {
let tests = vec![
// single cursor stays single cursor, first goes to end of current
// node, then parent
(
indoc! {r##"
Some(#[thing|]#)
"##},
"<A-O><A-O>",
indoc! {r##"
#[Some(|]#thing#()|)#
"##},
),
// shrinking restores previous selection
(
indoc! {r##"
Some(#[thing|]#)
"##},
"<A-O><A-O><A-i><A-i>",
indoc! {r##"
Some(#[thing|]#)
"##},
),
// multi range collision merges expand as normal, except with the
// original selection removed from the result
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-O><A-O><A-O>",
indoc! {r##"
#[(
Some(|]#thing#(),
Some(|)#other_thing#(),
)|)#
"##},
),
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-O><A-O><A-O><A-i><A-i><A-i>",
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
),
];
for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}
Ok(())
}

View File

@ -1320,15 +1320,27 @@ impl Document {
Ok(())
}
/// Select text within the [`Document`].
pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
/// Select text within the [`Document`], optionally clearing the previous selection state.
pub fn set_selection_clear(&mut self, view_id: ViewId, selection: Selection, clear_prev: bool) {
// TODO: use a transaction?
self.selections
.insert(view_id, selection.ensure_invariants(self.text().slice(..)));
helix_event::dispatch(SelectionDidChange {
doc: self,
view: view_id,
})
});
if clear_prev {
self.view_data
.entry(view_id)
.and_modify(|view_data| view_data.object_selections.clear());
}
}
/// Select text within the [`Document`].
pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
self.set_selection_clear(view_id, selection, true);
}
/// Find the origin selection of the text in a document, i.e. where
@ -1520,6 +1532,12 @@ impl Document {
apply_inlay_hint_changes(padding_after_inlay_hints);
}
// clear out all associated view object selections, as they are no
// longer valid
self.view_data
.values_mut()
.for_each(|view_data| view_data.object_selections.clear());
helix_event::dispatch(DocumentDidChange {
doc: self,
view: view_id,
@ -1962,13 +1980,13 @@ impl Document {
&self.selections
}
fn view_data(&self, view_id: ViewId) -> &ViewData {
pub fn view_data(&self, view_id: ViewId) -> &ViewData {
self.view_data
.get(&view_id)
.expect("This should only be called after ensure_view_init")
}
fn view_data_mut(&mut self, view_id: ViewId) -> &mut ViewData {
pub fn view_data_mut(&mut self, view_id: ViewId) -> &mut ViewData {
self.view_data.entry(view_id).or_default()
}
@ -2286,9 +2304,13 @@ impl Document {
}
}
/// Stores data needed for views that are tied to this specific Document.
#[derive(Debug, Default)]
pub struct ViewData {
view_position: ViewPosition,
/// used to store previous selections of tree-sitter objects
pub object_selections: HashMap<&'static str, Vec<Selection>>,
}
#[derive(Clone, Debug)]
@ -2339,6 +2361,7 @@ mod test {
Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
);
let view = ViewId::default();
doc.ensure_view_init(view);
doc.set_selection(view, Selection::single(0, 0));
let transaction =
@ -2377,7 +2400,9 @@ mod test {
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
);
let view = ViewId::default();
doc.ensure_view_init(view);
doc.set_selection(view, Selection::single(5, 5));
// insert

View File

@ -137,8 +137,6 @@ 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 objects
pub object_selections: Vec<Selection>,
/// all gutter-related configuration settings, used primarily for gutter rendering
pub gutters: GutterConfig,
/// A mapping between documents and the last history revision the view was updated at.
@ -175,7 +173,6 @@ impl View {
jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
docs_access_history: Vec::new(),
last_modified_docs: [None, None],
object_selections: Vec::new(),
gutters,
doc_revisions: HashMap::new(),
diagnostics_handler: DiagnosticsHandler::new(),

0
runtime/queries/snakemake/LICENSE 100755 → 100644
View File

View File

View File

View File

View File

View File