From 6de57e71755ca5188385fac53abdea297dde3e84 Mon Sep 17 00:00:00 2001 From: Ulyssa Date: Mon, 17 Apr 2023 16:55:37 -0700 Subject: [PATCH] Support splitting selections based on a regular expression (#62) --- crates/modalkit/src/editing/buffer/mod.rs | 69 ++-- .../modalkit/src/editing/buffer/selection.rs | 348 +++++++++++++++--- crates/modalkit/src/editing/rope/mod.rs | 127 ++++++- crates/modalkit/src/prelude.rs | 14 +- 4 files changed, 473 insertions(+), 85 deletions(-) diff --git a/crates/modalkit/src/editing/buffer/mod.rs b/crates/modalkit/src/editing/buffer/mod.rs index 13703e5..d57d56b 100644 --- a/crates/modalkit/src/editing/buffer/mod.rs +++ b/crates/modalkit/src/editing/buffer/mod.rs @@ -479,36 +479,60 @@ where } } + /// Determine the effective shape and character offsets for an operation. fn _effective( &self, range: &CursorRange, forced: Option, ) -> (TargetShape, Vec<(CharOff, CharOff, bool)>) { - match forced.unwrap_or(range.shape) { - TargetShape::CharWise => { - let start = self.text.cursor_to_offset(&range.start); - let end = self.text.cursor_to_offset(&range.end); - let ranges = vec![(start, end, range.inclusive)]; + let shape = forced.unwrap_or(range.shape); + let offsets = self._effective_offsets(&range.start, &range.end, range.inclusive, shape); + (shape, offsets) + } + + /// Determine the effective offsets between two cursors given a [TargetShape]. + fn _effective_offsets( + &self, + start: &Cursor, + end: &Cursor, + inclusive: bool, + shape: TargetShape, + ) -> Vec<(CharOff, CharOff, bool)> { + self._effective_cursors(start, end, inclusive, shape) + .into_iter() + .map(|(l, r, i)| { + let l = self.text.cursor_to_offset(&l); + let r = self.text.cursor_to_offset(&r); + (l, r, i) + }) + .collect() + } - (TargetShape::CharWise, ranges) + /// Convert a [TargetShape] selection to its effective [Cursor] ends. + fn _effective_cursors( + &self, + start: &Cursor, + end: &Cursor, + inclusive: bool, + shape: TargetShape, + ) -> Vec<(Cursor, Cursor, bool)> { + match shape { + TargetShape::CharWise => { + vec![(start.clone(), end.clone(), inclusive)] }, TargetShape::LineWise => { - let start = self.text.offset_of_line(range.start.y); - let end = self.text.offset_of_line(range.end.y); - let ranges = match self.text.newlines(end).next() { - Some(end) => { - vec![(start, end, true)] - }, - None => { - vec![(start, self.text.len_offset(), false)] - }, - }; + let start = Cursor::new(start.y, 0); + let end = self.text.offset_of_line(end.y); - (TargetShape::LineWise, ranges) + if let Some(end) = self.text.newlines(end).next() { + vec![(start, self.text.offset_to_cursor(end), true)] + } else { + vec![(start, self.text.last(), false)] + } }, TargetShape::BlockWise => { // Determine the left and right borders of the block. - let (mut lc, mut rc) = block_cursors(&range.start, &range.end); + let (mut lc, mut rc) = block_cursors(start, end); let mut ranges = vec![]; let min = lc.x; @@ -516,7 +540,7 @@ where let lctx = &(&self.text, 0, true); let rctx = &(&self.text, 0, false); - for line in range.start.y..=range.end.y { + for line in start.y..=end.y { lc.set_line(line, lctx); rc.set_line(line, rctx); @@ -525,13 +549,10 @@ where continue; } - let left = self.text.cursor_to_offset(&lc); - let right = self.text.cursor_to_offset(&rc); - - ranges.push((left, right, true)); + ranges.push((lc.clone(), rc.clone(), true)); } - (TargetShape::BlockWise, ranges) + ranges }, } } diff --git a/crates/modalkit/src/editing/buffer/selection.rs b/crates/modalkit/src/editing/buffer/selection.rs index 73409af..9808380 100644 --- a/crates/modalkit/src/editing/buffer/selection.rs +++ b/crates/modalkit/src/editing/buffer/selection.rs @@ -520,71 +520,72 @@ where style: &SelectionSplitStyle, filter: TargetShapeFilter, ctx: &CursorGroupIdContext<'a>, - _: &mut Store, + store: &mut Store, ) -> EditResult { let gid = ctx.0; let mut group = self.get_group(gid); - let mut created = vec![]; + let members = std::mem::take(&mut group.members); - for state in group.iter_mut() { + let split = |mut state: CursorState| -> EditResult, I> { let (cursor, anchor, shape) = state.to_triple(); if !filter.matches(&shape) { - continue; + return Ok(vec![state]); } - match (style, shape) { + let split = match (style, shape) { (SelectionSplitStyle::Anchor, _) => { if anchor.y == cursor.y && anchor.x == cursor.x { // Anchor and cursor are already the same. - continue; + state.set_shape(TargetShape::CharWise); + return Ok(vec![state]); } // Create new selection from old anchor. - created.push(CursorState::Selection(anchor.clone(), anchor.clone(), shape)); + let anchor = CursorState::Selection(anchor.clone(), anchor.clone(), shape); - // Update this selection's anchor to be at the cursor position. + // Update the selection's anchor to be at the cursor position. state.set_anchor(cursor.clone()); + + vec![anchor, state] }, (SelectionSplitStyle::Lines, TargetShape::CharWise) => { let (start, end) = state.sorted(); - - for line in start.y..=end.y { - let lc = if line == start.y { - Cursor::new(line, start.x) - } else { - Cursor::new(line, 0) - }; - - let rc = if line == end.y { - Cursor::new(line, end.x) - } else { - Cursor::new(line, self.text.get_columns(line).saturating_sub(1)) - }; - - if line == start.y { - state.set_cursor(lc.clone()); - state.set_anchor(rc.clone()); - } else { - created.push(CursorState::Selection(lc.clone(), rc.clone(), shape)); - } - } + let range = start.y..=end.y; + + range + .into_iter() + .map(|line| { + let lc = if line == start.y { + Cursor::new(line, start.x) + } else { + Cursor::new(line, 0) + }; + + let rc = if line == end.y { + Cursor::new(line, end.x) + } else { + Cursor::new(line, self.text.get_columns(line).saturating_sub(1)) + }; + + CursorState::Selection(lc.clone(), rc.clone(), shape) + }) + .collect() }, (SelectionSplitStyle::Lines, TargetShape::LineWise) => { let (start, end) = state.sorted(); - - for line in start.y..=end.y { - let maxidx = self.text.get_columns(line).saturating_sub(1); - let lc = Cursor::new(line, 0); - let rc = Cursor::new(line, maxidx); - - if line == start.y { - state.set_cursor(lc.clone()); - state.set_anchor(rc.clone()); - } else { - created.push(CursorState::Selection(lc.clone(), rc.clone(), shape)); - } - } + let range = start.y..=end.y; + + range + .into_iter() + .map(|line| { + let maxidx = self.text.get_columns(line).saturating_sub(1); + let lc = Cursor::new(line, 0); + let rc = Cursor::new(line, maxidx); + + CursorState::Selection(lc.clone(), rc.clone(), shape) + }) + .collect() }, (SelectionSplitStyle::Lines, TargetShape::BlockWise) => { // Determine the left and right borders of the block. @@ -592,27 +593,92 @@ where // Sort the cursors. let (start, end) = state.sorted(); + let range = start.y..=end.y; - for line in start.y..=end.y { - let lctx = &(&self.text, 0, true); - let rctx = &(&self.text, 0, false); + range + .into_iter() + .map(|line| { + let lctx = &(&self.text, 0, true); + let rctx = &(&self.text, 0, false); - lc.set_line(line, lctx); - rc.set_line(line, rctx); + lc.set_line(line, lctx); + rc.set_line(line, rctx); - if line == start.y { - state.set_cursor(lc.clone()); - state.set_anchor(rc.clone()); - } else { - created.push(CursorState::Selection(lc.clone(), rc.clone(), shape)); + CursorState::Selection(lc.clone(), rc.clone(), shape) + }) + .collect() + }, + (SelectionSplitStyle::Regex(false), shape) => { + let needle = self._get_regex(store)?; + let ctx = &(&self.text, 0, true); + + self._effective_cursors(state.start(), state.end(), true, shape) + .into_iter() + .flat_map(|(s, mut e, inclusive)| { + if !inclusive && s < e { + e.column(MoveDir1D::Previous, true, 1, ctx); + } + self.text.find_matches(&s, &e, &needle) + }) + .map(|m| self.text.select(m)) + .collect() + }, + (SelectionSplitStyle::Regex(true), shape) => { + let needle = self._get_regex(store)?; + let ctx = &(&self.text, 0, true); + + let mut split = vec![]; + let cursors = self._effective_cursors(state.start(), state.end(), true, shape); + + for (mut lc, mut rc, inclusive) in cursors { + if !inclusive && lc < rc { + rc.column(MoveDir1D::Previous, true, 1, ctx); + } + + for m in self.text.find_matches(&lc, &rc, &needle) { + if lc < m.start { + let mut end = m.start; + end.column(MoveDir1D::Previous, true, 1, ctx); + split.push(CursorState::Selection(lc, end, TargetShape::CharWise)); + } + + lc = m.end; + + if m.inclusive { + lc.column(MoveDir1D::Next, true, 1, ctx); + } + } + + if lc <= rc { + split.push(CursorState::Selection(lc, rc, TargetShape::CharWise)); } } + + split }, - } + }; + + return Ok(split); + }; + + for member in members { + group.members.extend(split(member)?); + } + + let mut leader_splits = split(group.leader.clone())?.into_iter(); + + if let Some(leader) = leader_splits.next() { + group.leader = leader; + group.members.extend(leader_splits); + } else if let Some(leader) = group.members.pop() { + group.leader = leader; + } else { + let msg = "No selections remaining".to_string(); + let err = EditError::Failure(msg); + + return Err(err); } - group.members.append(&mut created); - group.merge(); self.set_group(gid, group); Ok(None) @@ -687,12 +753,24 @@ mod tests { }; } + macro_rules! selection_split_anchor { + ($ebuf: expr, $filter: expr, $ctx: expr, $store: expr) => { + selection_split!($ebuf, SelectionSplitStyle::Anchor, $filter, $ctx, $store) + }; + } + macro_rules! selection_split_lines { ($ebuf: expr, $filter: expr, $ctx: expr, $store: expr) => { selection_split!($ebuf, SelectionSplitStyle::Lines, $filter, $ctx, $store) }; } + macro_rules! selection_split_regex { + ($ebuf: expr, $filter: expr, $keep: expr, $ctx: expr, $store: expr) => { + selection_split!($ebuf, SelectionSplitStyle::Regex($keep), $filter, $ctx, $store) + }; + } + macro_rules! selection_extend { ($ebuf: expr, $et: expr, $ctx: expr, $store: expr) => { $ebuf @@ -791,6 +869,42 @@ mod tests { assert_eq!(ebuf.get_follower_selections(curid), None); } + #[test] + fn test_selection_split_anchor() { + let (mut ebuf, curid, vwctx, mut vctx, mut store) = + mkfivestr("a b c d\ne f g\nh i j k l\nm n o p\nq r\n"); + + // Start out at (2, 6). + ebuf.set_leader(curid, Cursor::new(2, 6)); + + vctx.target_shape = Some(TargetShape::CharWise); + + // Create a charwise selection across the three lines. + let mov = MoveType::FirstWord(MoveDir1D::Next); + edit!(ebuf, EditAction::Motion, mv!(mov, 2), ctx!(curid, vwctx, vctx), store); + + let selection = (Cursor::new(2, 6), Cursor::new(4, 0), TargetShape::CharWise); + assert_eq!(ebuf.get_leader_selection(curid), Some(selection.clone())); + assert_eq!(ebuf.get_follower_selections(curid), None); + assert_eq!(ebuf.get_leader(curid), Cursor::new(4, 0)); + + // Filter doesn't match, nothing happens. + selection_split_anchor!(ebuf, TargetShapeFilter::LINE, ctx!(curid, vwctx, vctx), store); + assert_eq!(ebuf.get_leader_selection(curid), Some(selection.clone())); + assert_eq!(ebuf.get_leader(curid), Cursor::new(4, 0)); + assert_eq!(ebuf.get_follower_selections(curid), None); + + // Filter matches, splits into two CharWise selections. + selection_split_anchor!(ebuf, TargetShapeFilter::CHAR, ctx!(curid, vwctx, vctx), store); + let selection1 = (Cursor::new(2, 6), Cursor::new(2, 6), TargetShape::CharWise); + let selection2 = (Cursor::new(4, 0), Cursor::new(4, 0), TargetShape::CharWise); + let selections = vec![selection2]; + assert_eq!(ebuf.get_leader_selection(curid), Some(selection1.clone())); + assert_eq!(ebuf.get_leader(curid), Cursor::new(2, 6)); + assert_eq!(ebuf.get_follower_selections(curid), Some(selections)); + assert_eq!(ebuf.get_followers(curid), vec![Cursor::new(4, 0)]); + } + #[test] fn test_selection_split_lines_blockwise() { let (mut ebuf, curid, vwctx, mut vctx, mut store) = @@ -801,7 +915,7 @@ mod tests { vctx.target_shape = Some(TargetShape::BlockWise); - // Create a charwise selection across the three lines. + // Create a blockwise selection across the three lines. let mov = MoveType::FirstWord(MoveDir1D::Next); edit!(ebuf, EditAction::Motion, mv!(mov, 2), ctx!(curid, vwctx, vctx), store); @@ -908,6 +1022,128 @@ mod tests { assert_eq!(ebuf.get_followers(curid), vec![Cursor::new(2, 0), Cursor::new(3, 0)]); } + #[test] + fn test_selection_split_regex_keep() { + let (mut ebuf, curid, vwctx, mut vctx, mut store) = + mkfivestr("foo,bar,baz\na,b,c\nd,,e,\nm n o p\nq r s t\n"); + + // Start out at (1, 0). + ebuf.set_leader(curid, Cursor::new(1, 0)); + + vctx.target_shape = Some(TargetShape::LineWise); + + store.registers.set_last_search(","); + + // Create a linewise selection across three lines. + let mov = MoveType::FirstWord(MoveDir1D::Next); + edit!(ebuf, EditAction::Motion, mv!(mov, 2), ctx!(curid, vwctx, vctx), store); + + let selection = (Cursor::new(1, 0), Cursor::new(3, 0), TargetShape::LineWise); + assert_eq!(ebuf.get_leader_selection(curid), Some(selection.clone())); + assert_eq!(ebuf.get_follower_selections(curid), None); + assert_eq!(ebuf.get_leader(curid), Cursor::new(3, 0)); + + // Filter doesn't match, nothing happens. + selection_split_regex!( + ebuf, + TargetShapeFilter::CHAR, + false, + ctx!(curid, vwctx, vctx), + store + ); + assert_eq!(ebuf.get_leader_selection(curid), Some(selection.clone())); + assert_eq!(ebuf.get_follower_selections(curid), None); + assert_eq!(ebuf.get_leader(curid), Cursor::new(3, 0)); + + // Filter matches, splits into multiple LineWise selections. + selection_split_regex!( + ebuf, + TargetShapeFilter::LINE, + false, + ctx!(curid, vwctx, vctx), + store + ); + + let selection1 = (Cursor::new(1, 1), Cursor::new(1, 1), TargetShape::CharWise); + let selection2 = (Cursor::new(1, 3), Cursor::new(1, 3), TargetShape::CharWise); + let selection3 = (Cursor::new(2, 1), Cursor::new(2, 1), TargetShape::CharWise); + let selection4 = (Cursor::new(2, 2), Cursor::new(2, 2), TargetShape::CharWise); + let selection5 = (Cursor::new(2, 4), Cursor::new(2, 4), TargetShape::CharWise); + let selections = vec![selection2, selection3, selection4, selection5]; + let followers = vec![ + Cursor::new(1, 3), + Cursor::new(2, 1), + Cursor::new(2, 2), + Cursor::new(2, 4), + ]; + + assert_eq!(ebuf.get_leader_selection(curid), Some(selection1.clone())); + assert_eq!(ebuf.get_leader(curid), Cursor::new(1, 1)); + assert_eq!(ebuf.get_follower_selections(curid), Some(selections)); + assert_eq!(ebuf.get_followers(curid), followers); + } + + #[test] + fn test_selection_split_regex_drop() { + let (mut ebuf, curid, vwctx, mut vctx, mut store) = + mkfivestr("foo,bar,baz\na,b,c\nd,,e,\nm n o p\nq r s t\n"); + + // Start out at (1, 0). + ebuf.set_leader(curid, Cursor::new(1, 0)); + + vctx.target_shape = Some(TargetShape::LineWise); + + store.registers.set_last_search(","); + + // Create a linewise selection across three lines. + let mov = MoveType::FirstWord(MoveDir1D::Next); + edit!(ebuf, EditAction::Motion, mv!(mov, 2), ctx!(curid, vwctx, vctx), store); + + let selection = (Cursor::new(1, 0), Cursor::new(3, 0), TargetShape::LineWise); + assert_eq!(ebuf.get_leader_selection(curid), Some(selection.clone())); + assert_eq!(ebuf.get_follower_selections(curid), None); + assert_eq!(ebuf.get_leader(curid), Cursor::new(3, 0)); + + // Filter doesn't match, nothing happens. + selection_split_regex!( + ebuf, + TargetShapeFilter::CHAR, + true, + ctx!(curid, vwctx, vctx), + store + ); + assert_eq!(ebuf.get_leader_selection(curid), Some(selection.clone())); + assert_eq!(ebuf.get_follower_selections(curid), None); + assert_eq!(ebuf.get_leader(curid), Cursor::new(3, 0)); + + // Filter matches, splits into multiple LineWise selections. + selection_split_regex!( + ebuf, + TargetShapeFilter::LINE, + true, + ctx!(curid, vwctx, vctx), + store + ); + + let selection1 = (Cursor::new(1, 0), Cursor::new(1, 0), TargetShape::CharWise); + let selection2 = (Cursor::new(1, 2), Cursor::new(1, 2), TargetShape::CharWise); + let selection3 = (Cursor::new(1, 4), Cursor::new(2, 0), TargetShape::CharWise); + let selection4 = (Cursor::new(2, 3), Cursor::new(2, 3), TargetShape::CharWise); + let selection5 = (Cursor::new(2, 5), Cursor::new(3, 7), TargetShape::CharWise); + let selections = vec![selection2, selection3, selection4, selection5]; + let followers = vec![ + Cursor::new(1, 2), + Cursor::new(1, 4), + Cursor::new(2, 3), + Cursor::new(2, 5), + ]; + + assert_eq!(ebuf.get_leader_selection(curid), Some(selection1.clone())); + assert_eq!(ebuf.get_leader(curid), Cursor::new(1, 0)); + assert_eq!(ebuf.get_follower_selections(curid), Some(selections)); + assert_eq!(ebuf.get_followers(curid), followers); + } + #[test] fn test_selection_cursor_set_charwise() { let (mut ebuf, curid, vwctx, mut vctx, mut store) = mkfivestr("hello world\na b c d\n"); diff --git a/crates/modalkit/src/editing/rope/mod.rs b/crates/modalkit/src/editing/rope/mod.rs index 699fc27..69bdd16 100644 --- a/crates/modalkit/src/editing/rope/mod.rs +++ b/crates/modalkit/src/editing/rope/mod.rs @@ -19,7 +19,7 @@ use ropey::{Rope, RopeSlice}; use crate::actions::EditAction; use crate::editing::{ context::Resolve, - cursor::{Cursor, CursorAdjustment, CursorChoice}, + cursor::{Cursor, CursorAdjustment, CursorChoice, CursorState}, }; use crate::prelude::*; @@ -1554,6 +1554,19 @@ impl EditRope { self.offset_to_cursor(self.last_offset()) } + /// Convert an [EditRange] into a [CursorState::Selection]. + pub fn select(&self, range: EditRange) -> CursorState { + if range.start >= range.end { + CursorState::Selection(range.start.clone(), range.start, range.shape) + } else if range.inclusive { + CursorState::Selection(range.start, range.end, range.shape) + } else { + let off = self.cursor_to_offset(&range.end).0.saturating_sub(1); + let end = self.offset_to_cursor(off.into()); + CursorState::Selection(range.start, end, range.shape) + } + } + /// Compare this rope with a new version, and return a vector of adjustments needed to fix /// cursors and marks when moving to the new version. pub fn diff(&self, other: &EditRope) -> Vec { @@ -2715,9 +2728,9 @@ impl CursorSearch for EditRope { let mso = rope.byte_to_char(m.start()); let meo = rope.byte_to_char(m.end()); let sc = self.offset_to_cursor(so + CharOff(mso)); - let ec = self.offset_to_cursor(eo + CharOff(meo)); + let ec = self.offset_to_cursor(so + CharOff(meo)); - EditRange::inclusive(sc, ec, TargetShape::CharWise) + EditRange::exclusive(sc, ec, TargetShape::CharWise) }) .collect() } @@ -2772,6 +2785,114 @@ mod tests { assert_eq!(r.max_line_idx(), 4); } + #[test] + fn test_select() { + let r = EditRope::from("hello world\nhelp\nkelp\n"); + + // Select inclusive charwise range. + let c = r.select(EditRange::inclusive( + Cursor::new(0, 2), + Cursor::new(1, 4), + TargetShape::CharWise, + )); + assert_eq!( + c, + CursorState::Selection(Cursor::new(0, 2), Cursor::new(1, 4), TargetShape::CharWise) + ); + + // Select exclusive charwise range. + let c = r.select(EditRange::exclusive( + Cursor::new(0, 2), + Cursor::new(1, 4), + TargetShape::CharWise, + )); + assert_eq!( + c, + CursorState::Selection(Cursor::new(0, 2), Cursor::new(1, 3), TargetShape::CharWise) + ); + + // Select inclusive linewise range. + let c = r.select(EditRange::inclusive( + Cursor::new(0, 2), + Cursor::new(1, 4), + TargetShape::LineWise, + )); + assert_eq!( + c, + CursorState::Selection(Cursor::new(0, 2), Cursor::new(1, 4), TargetShape::LineWise) + ); + + // Select exclusive linewise range. + let c = r.select(EditRange::exclusive( + Cursor::new(0, 2), + Cursor::new(1, 4), + TargetShape::LineWise, + )); + assert_eq!( + c, + CursorState::Selection(Cursor::new(0, 2), Cursor::new(1, 3), TargetShape::LineWise) + ); + } + + #[test] + fn test_find_matches() { + let r = EditRope::from("hello world\nhelp\nkelp\n"); + let needle1 = Regex::new("el").unwrap(); + let needle2 = Regex::new("ell").unwrap(); + + // Search all text for /el/. + let ms = r.find_matches(&Cursor::new(0, 0), &Cursor::new(2, 4), &needle1); + assert_eq!(ms.len(), 3); + assert_eq!( + ms[0], + EditRange::exclusive(Cursor::new(0, 1), Cursor::new(0, 3), TargetShape::CharWise) + ); + assert_eq!( + ms[1], + EditRange::exclusive(Cursor::new(1, 1), Cursor::new(1, 3), TargetShape::CharWise) + ); + assert_eq!( + ms[2], + EditRange::exclusive(Cursor::new(2, 1), Cursor::new(2, 3), TargetShape::CharWise) + ); + + // Search all text for /ell/. + let ms = r.find_matches(&Cursor::new(0, 0), &Cursor::new(2, 4), &needle2); + assert_eq!(ms.len(), 1); + assert_eq!( + ms[0], + EditRange::exclusive(Cursor::new(0, 1), Cursor::new(0, 4), TargetShape::CharWise) + ); + + // Search the second line for /el/. + let ms = r.find_matches(&Cursor::new(1, 0), &Cursor::new(1, 4), &needle1); + assert_eq!(ms.len(), 1); + assert_eq!( + ms[0], + EditRange::exclusive(Cursor::new(1, 1), Cursor::new(1, 3), TargetShape::CharWise) + ); + + // Search the second line for /ell/. + let ms = r.find_matches(&Cursor::new(1, 0), &Cursor::new(1, 4), &needle2); + assert_eq!(ms.len(), 0); + + // Searching the first two characters of the second line for /el/ should find nothing. + let ms = r.find_matches(&Cursor::new(1, 0), &Cursor::new(1, 1), &needle1); + assert_eq!(ms.len(), 0); + + // Searching the last two characters of the second line for /el/ should find nothing. + let ms = r.find_matches(&Cursor::new(1, 2), &Cursor::new(1, 3), &needle1); + assert_eq!(ms.len(), 0); + + // Searching the middle two characters of the second line for /el/ should find a match. + let ms = r.find_matches(&Cursor::new(1, 1), &Cursor::new(1, 2), &needle1); + assert_eq!(ms.len(), 1); + assert_eq!( + ms[0], + EditRange::exclusive(Cursor::new(1, 1), Cursor::new(1, 3), TargetShape::CharWise) + ); + } + #[test] fn test_get_line_columns() { let r = EditRope::from("a\nbc\n\ndefg\nhijkl\n"); diff --git a/crates/modalkit/src/prelude.rs b/crates/modalkit/src/prelude.rs index 1e6cd25..a3734d9 100644 --- a/crates/modalkit/src/prelude.rs +++ b/crates/modalkit/src/prelude.rs @@ -1266,12 +1266,22 @@ impl BoundaryTest for SelectionBoundary { #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum SelectionSplitStyle { - /// Split a selection into two selections, one at the current cursor position, and the other at - /// the anchor. + /// Split a selection into two [TargetShape::CharWise] selections, one at the current cursor + /// position, and the other at the anchor. Anchor, /// Split a selection at each line boundary it contains. Lines, + + /// Split a selection into [TargetShape::CharWise] parts based on the regular expression in + /// [Register::LastSearch]. + /// + /// When the [bool] argument is `false`, then text matching the regular expression will be + /// selected. + /// + /// When the [bool] argument is `true`, then text matching the regular expression will be + /// removed from the selections. + Regex(bool), } /// Different ways to change the boundaries of a visual selection.