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.rs269
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(),