Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-core/src/selection.rs')
| -rw-r--r-- | helix-core/src/selection.rs | 269 |
1 files changed, 80 insertions, 189 deletions
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 5bde08e3..c44685ee 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -7,15 +7,11 @@ use crate::{ ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary, prev_grapheme_boundary, }, - line_ending::get_line_ending, movement::Direction, - tree_sitter::Node, - Assoc, ChangeSet, RopeSlice, + Assoc, ChangeSet, RopeGraphemes, RopeSlice, }; -use helix_stdx::range::is_subset; -use helix_stdx::rope::{self, RopeSliceExt}; use smallvec::{smallvec, SmallVec}; -use std::{borrow::Cow, iter, slice}; +use std::borrow::Cow; /// A single selection range. /// @@ -75,12 +71,6 @@ impl Range { Self::new(head, head) } - pub fn from_node(node: Node, text: RopeSlice, direction: Direction) -> Self { - 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) - } - /// Start of the range. #[inline] #[must_use] @@ -123,7 +113,7 @@ impl Range { } /// `Direction::Backward` when head < anchor. - /// `Direction::Forward` otherwise. + /// `Direction::Backward` otherwise. #[inline] #[must_use] pub fn direction(&self) -> Direction { @@ -176,7 +166,7 @@ impl Range { /// function runs in O(N) (N is number of changes) and can therefore /// cause performance problems if run for a large number of ranges as the /// complexity is then O(MN) (for multicuror M=N usually). Instead use - /// [Selection::map] or [ChangeSet::update_positions]. + /// [Selection::map] or [ChangeSet::update_positions] instead pub fn map(mut self, changes: &ChangeSet) -> Self { use std::cmp::Ordering; if changes.is_empty() { @@ -185,16 +175,16 @@ impl Range { let positions_to_map = match self.anchor.cmp(&self.head) { Ordering::Equal => [ - (&mut self.anchor, Assoc::AfterSticky), - (&mut self.head, Assoc::AfterSticky), + (&mut self.anchor, Assoc::After), + (&mut self.head, Assoc::After), ], Ordering::Less => [ - (&mut self.anchor, Assoc::AfterSticky), - (&mut self.head, Assoc::BeforeSticky), + (&mut self.anchor, Assoc::After), + (&mut self.head, Assoc::Before), ], Ordering::Greater => [ - (&mut self.head, Assoc::AfterSticky), - (&mut self.anchor, Assoc::BeforeSticky), + (&mut self.head, Assoc::After), + (&mut self.anchor, Assoc::Before), ], }; changes.update_positions(positions_to_map.into_iter()); @@ -379,17 +369,11 @@ impl Range { /// Returns true if this Range covers a single grapheme in the given text pub fn is_single_grapheme(&self, doc: RopeSlice) -> bool { - let mut graphemes = doc.slice(self.from()..self.to()).graphemes(); + let mut graphemes = RopeGraphemes::new(doc.slice(self.from()..self.to())); let first = graphemes.next(); let second = graphemes.next(); first.is_some() && second.is_none() } - - /// 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())) - } } impl From<(usize, usize)> for Range { @@ -402,15 +386,6 @@ impl From<(usize, usize)> for Range { } } -impl From<Range> for helix_stdx::Range { - fn from(range: Range) -> Self { - Self { - start: range.from(), - end: range.to(), - } - } -} - /// A selection consists of one or more selection ranges. /// invariant: A selection can never be empty (always contains at least primary range). #[derive(Debug, Clone, PartialEq, Eq)] @@ -492,16 +467,16 @@ impl Selection { range.old_visual_position = None; match range.anchor.cmp(&range.head) { Ordering::Equal => [ - (&mut range.anchor, Assoc::AfterSticky), - (&mut range.head, Assoc::AfterSticky), + (&mut range.anchor, Assoc::After), + (&mut range.head, Assoc::After), ], Ordering::Less => [ - (&mut range.anchor, Assoc::AfterSticky), - (&mut range.head, Assoc::BeforeSticky), + (&mut range.anchor, Assoc::After), + (&mut range.head, Assoc::Before), ], Ordering::Greater => [ - (&mut range.head, Assoc::AfterSticky), - (&mut range.anchor, Assoc::BeforeSticky), + (&mut range.head, Assoc::After), + (&mut range.anchor, Assoc::Before), ], } }); @@ -513,20 +488,6 @@ impl Selection { &self.ranges } - /// Returns an iterator over the line ranges of each range in the selection. - /// - /// Adjacent and overlapping line ranges of the [Range]s in the selection are merged. - pub fn line_ranges<'a>(&'a self, text: RopeSlice<'a>) -> LineRangeIter<'a> { - LineRangeIter { - ranges: self.ranges.iter().peekable(), - text, - } - } - - pub fn range_bounds(&self) -> impl Iterator<Item = helix_stdx::Range> + '_ { - self.ranges.iter().map(|&range| range.into()) - } - pub fn primary_index(&self) -> usize { self.primary_index } @@ -555,8 +516,6 @@ impl Selection { } /// Normalizes a `Selection`. - /// - /// Ranges are sorted by [Range::from], with overlapping ranges merged. fn normalize(mut self) -> Self { if self.len() < 2 { return self; @@ -619,6 +578,7 @@ impl Selection { self } + // TODO: consume an iterator or a vec to reduce allocations? #[must_use] pub fn new(ranges: SmallVec<[Range; 1]>, primary_index: usize) -> Self { assert!(!ranges.is_empty()); @@ -673,7 +633,7 @@ impl Selection { pub fn fragments<'a>( &'a self, text: RopeSlice<'a>, - ) -> impl DoubleEndedIterator<Item = Cow<'a, str>> + ExactSizeIterator<Item = Cow<'a, str>> + ) -> impl DoubleEndedIterator<Item = Cow<'a, str>> + ExactSizeIterator<Item = Cow<str>> + 'a { self.ranges.iter().map(move |range| range.fragment(text)) } @@ -696,9 +656,32 @@ impl Selection { self.ranges.len() } - /// returns true if self ⊇ other + // returns true if self ⊇ other pub fn contains(&self, other: &Selection) -> bool { - is_subset::<true>(self.range_bounds(), other.range_bounds()) + 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; + } + } + } } } @@ -720,59 +703,17 @@ impl IntoIterator for Selection { } } -impl FromIterator<Range> for Selection { - fn from_iter<T: IntoIterator<Item = Range>>(ranges: T) -> Self { - Self::new(ranges.into_iter().collect(), 0) - } -} - -impl From<Range> for Selection { - fn from(range: Range) -> Self { - Self { - ranges: smallvec![range], - primary_index: 0, - } - } -} - -pub struct LineRangeIter<'a> { - ranges: iter::Peekable<slice::Iter<'a, Range>>, - text: RopeSlice<'a>, -} - -impl Iterator for LineRangeIter<'_> { - type Item = (usize, usize); - - fn next(&mut self) -> Option<Self::Item> { - let (start, mut end) = self.ranges.next()?.line_range(self.text); - while let Some((next_start, next_end)) = - self.ranges.peek().map(|range| range.line_range(self.text)) - { - // Merge overlapping and adjacent ranges. - // This subtraction cannot underflow because the ranges are sorted. - if next_start - end <= 1 { - end = next_end; - self.ranges.next(); - } else { - break; - } - } - - Some((start, end)) - } -} - // TODO: checkSelection -> check if valid for doc length && sorted pub fn keep_or_remove_matches( text: RopeSlice, selection: &Selection, - regex: &rope::Regex, + regex: &crate::regex::Regex, remove: bool, ) -> Option<Selection> { let result: SmallVec<_> = selection .iter() - .filter(|range| regex.is_match(text.regex_input_at(range.from()..range.to())) ^ remove) + .filter(|range| regex.is_match(&range.fragment(text)) ^ remove) .copied() .collect(); @@ -783,20 +724,25 @@ pub fn keep_or_remove_matches( None } -// TODO: support to split on capture #N instead of whole match pub fn select_on_matches( text: RopeSlice, selection: &Selection, - regex: &rope::Regex, + regex: &crate::regex::Regex, ) -> Option<Selection> { let mut result = SmallVec::with_capacity(selection.len()); for sel in selection { - for mat in regex.find_iter(text.regex_input_at(sel.from()..sel.to())) { + // TODO: can't avoid occasional allocations since Regex can't operate on chunks yet + let fragment = sel.fragment(text); + + let sel_start = sel.from(); + let start_byte = text.char_to_byte(sel_start); + + for mat in regex.find_iter(&fragment) { // TODO: retain range direction - let start = text.byte_to_char(mat.start()); - let end = text.byte_to_char(mat.end()); + let start = text.byte_to_char(start_byte + mat.start()); + let end = text.byte_to_char(start_byte + mat.end()); let range = Range::new(start, end); // Make sure the match is not right outside of the selection. @@ -815,7 +761,12 @@ pub fn select_on_matches( None } -pub fn split_on_newline(text: RopeSlice, selection: &Selection) -> Selection { +// TODO: support to split on capture #N instead of whole match +pub fn split_on_matches( + text: RopeSlice, + selection: &Selection, + regex: &crate::regex::Regex, +) -> Selection { let mut result = SmallVec::with_capacity(selection.len()); for sel in selection { @@ -825,49 +776,21 @@ pub fn split_on_newline(text: RopeSlice, selection: &Selection) -> Selection { continue; } + // TODO: can't avoid occasional allocations since Regex can't operate on chunks yet + let fragment = sel.fragment(text); + let sel_start = sel.from(); let sel_end = sel.to(); - let mut start = sel_start; - - for line in sel.slice(text).lines() { - let Some(line_ending) = get_line_ending(&line) else { - break; - }; - let line_end = start + line.len_chars(); - // TODO: retain range direction - result.push(Range::new(start, line_end - line_ending.len_chars())); - start = line_end; - } - - if start < sel_end { - result.push(Range::new(start, sel_end)); - } - } - - // TODO: figure out a new primary index - Selection::new(result, 0) -} - -pub fn split_on_matches(text: RopeSlice, selection: &Selection, regex: &rope::Regex) -> Selection { - let mut result = SmallVec::with_capacity(selection.len()); + let start_byte = text.char_to_byte(sel_start); - for sel in selection { - // Special case: zero-width selection. - if sel.from() == sel.to() { - result.push(*sel); - continue; - } - - let sel_start = sel.from(); - let sel_end = sel.to(); let mut start = sel_start; - for mat in regex.find_iter(text.regex_input_at(sel_start..sel_end)) { + for mat in regex.find_iter(&fragment) { // TODO: retain range direction - let end = text.byte_to_char(mat.start()); + let end = text.byte_to_char(start_byte + mat.start()); result.push(Range::new(start, end)); - start = text.byte_to_char(mat.end()); + start = text.byte_to_char(start_byte + mat.end()); } if start < sel_end { @@ -1098,12 +1021,14 @@ mod test { #[test] fn test_select_on_matches() { + use crate::regex::{Regex, RegexBuilder}; + let r = Rope::from_str("Nobody expects the Spanish inquisition"); let s = r.slice(..); let selection = Selection::single(0, r.len_chars()); assert_eq!( - select_on_matches(s, &selection, &rope::Regex::new(r"[A-Z][a-z]*").unwrap()), + select_on_matches(s, &selection, &Regex::new(r"[A-Z][a-z]*").unwrap()), Some(Selection::new( smallvec![Range::new(0, 6), Range::new(19, 26)], 0 @@ -1113,14 +1038,8 @@ mod test { let r = Rope::from_str("This\nString\n\ncontains multiple\nlines"); let s = r.slice(..); - let start_of_line = rope::RegexBuilder::new() - .syntax(rope::Config::new().multi_line(true)) - .build(r"^") - .unwrap(); - let end_of_line = rope::RegexBuilder::new() - .syntax(rope::Config::new().multi_line(true)) - .build(r"$") - .unwrap(); + let start_of_line = RegexBuilder::new(r"^").multi_line(true).build().unwrap(); + let end_of_line = RegexBuilder::new(r"$").multi_line(true).build().unwrap(); // line without ending assert_eq!( @@ -1158,9 +1077,9 @@ mod test { select_on_matches( s, &Selection::single(0, s.len_chars()), - &rope::RegexBuilder::new() - .syntax(rope::Config::new().multi_line(true)) - .build(r"^[a-z ]*$") + &RegexBuilder::new(r"^[a-z ]*$") + .multi_line(true) + .build() .unwrap() ), Some(Selection::new( @@ -1201,32 +1120,6 @@ mod test { } #[test] - fn selection_line_ranges() { - let (text, selection) = crate::test::print( - r#" L0 - #[|these]# line #(|ranges)# are #(|merged)# L1 - L2 - single one-line #(|range)# L3 - L4 - single #(|multiline L5 - range)# L6 - L7 - these #(|multiline L8 - ranges)# are #(|also L9 - merged)# L10 - L11 - adjacent #(|ranges)# L12 - are merged #(|the same way)# L13 - "#, - ); - let rope = Rope::from_str(&text); - assert_eq!( - vec![(1, 1), (3, 3), (5, 6), (8, 10), (12, 13)], - selection.line_ranges(rope.slice(..)).collect::<Vec<_>>(), - ); - } - - #[test] fn test_cursor() { let r = Rope::from_str("\r\nHi\r\nthere!"); let s = r.slice(..); @@ -1278,15 +1171,13 @@ mod test { #[test] fn test_split_on_matches() { + use crate::regex::Regex; + let text = Rope::from(" abcd efg wrs xyz 123 456"); let selection = Selection::new(smallvec![Range::new(0, 9), Range::new(11, 20),], 0); - let result = split_on_matches( - text.slice(..), - &selection, - &rope::Regex::new(r"\s+").unwrap(), - ); + let result = split_on_matches(text.slice(..), &selection, &Regex::new(r"\s+").unwrap()); assert_eq!( result.ranges(), |