Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-core/src/surround.rs')
| -rw-r--r-- | helix-core/src/surround.rs | 460 |
1 files changed, 138 insertions, 322 deletions
diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index e45346c9..32161b70 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -1,157 +1,32 @@ -use std::fmt::Display; - -use crate::{ - graphemes::next_grapheme_boundary, - match_brackets::{ - find_matching_bracket, find_matching_bracket_fuzzy, get_pair, is_close_bracket, - is_open_bracket, - }, - movement::Direction, - search, Range, Selection, Syntax, -}; +use crate::{search, Selection}; use ropey::RopeSlice; -#[derive(Debug, PartialEq, Eq)] -pub enum Error { - PairNotFound, - CursorOverlap, - RangeExceedsText, - CursorOnAmbiguousPair, -} - -impl Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match *self { - Error::PairNotFound => "Surround pair not found around all cursors", - Error::CursorOverlap => "Cursors overlap for a single surround pair range", - Error::RangeExceedsText => "Cursor range exceeds text length", - Error::CursorOnAmbiguousPair => "Cursor on ambiguous surround pair", - }) - } -} - -type Result<T> = std::result::Result<T, Error>; - -/// Finds the position of surround pairs of any [`crate::match_brackets::PAIRS`] -/// using tree-sitter when possible. +pub const PAIRS: &[(char, char)] = &[ + ('(', ')'), + ('[', ']'), + ('{', '}'), + ('<', '>'), + ('«', '»'), + ('「', '」'), + ('(', ')'), +]; + +/// Given any char in [PAIRS], return the open and closing chars. If not found in +/// [PAIRS] return (ch, ch). /// -/// # Returns +/// ``` +/// use helix_core::surround::get_pair; /// -/// Tuple `(anchor, head)`, meaning it is not always ordered. -pub fn find_nth_closest_pairs_pos( - syntax: Option<&Syntax>, - text: RopeSlice, - range: Range, - skip: usize, -) -> Result<(usize, usize)> { - match syntax { - Some(syntax) => find_nth_closest_pairs_ts(syntax, text, range, skip), - None => find_nth_closest_pairs_plain(text, range, skip), - } -} - -fn find_nth_closest_pairs_ts( - syntax: &Syntax, - text: RopeSlice, - range: Range, - mut skip: usize, -) -> Result<(usize, usize)> { - let mut opening = range.from(); - // We want to expand the selection if we are already on the found pair, - // otherwise we would need to subtract "-1" from "range.to()". - let mut closing = range.to(); - - while skip > 0 { - closing = find_matching_bracket_fuzzy(syntax, text, closing).ok_or(Error::PairNotFound)?; - opening = find_matching_bracket(syntax, text, closing).ok_or(Error::PairNotFound)?; - // If we're already on a closing bracket "find_matching_bracket_fuzzy" will return - // the position of the opening bracket. - if closing < opening { - (opening, closing) = (closing, opening); - } - - // In case found brackets are partially inside current selection. - if range.from() < opening || closing < range.to() - 1 { - closing = next_grapheme_boundary(text, closing); - } else { - skip -= 1; - if skip != 0 { - closing = next_grapheme_boundary(text, closing); - } - } - } - - // Keep the original direction. - if let Direction::Forward = range.direction() { - Ok((opening, closing)) - } else { - Ok((closing, opening)) - } -} - -fn find_nth_closest_pairs_plain( - text: RopeSlice, - range: Range, - mut skip: usize, -) -> Result<(usize, usize)> { - let mut stack = Vec::with_capacity(2); - let pos = range.from(); - let mut close_pos = pos.saturating_sub(1); - - for ch in text.chars_at(pos) { - close_pos += 1; - - if is_open_bracket(ch) { - // Track open pairs encountered so that we can step over - // the corresponding close pairs that will come up further - // down the loop. We want to find a lone close pair whose - // open pair is before the cursor position. - stack.push(ch); - continue; - } - - if !is_close_bracket(ch) { - // We don't care if this character isn't a brace pair item, - // so short circuit here. - continue; - } - - let (open, close) = get_pair(ch); - - if stack.last() == Some(&open) { - // If we are encountering the closing pair for an opener - // we just found while traversing, then its inside the - // selection and should be skipped over. - stack.pop(); - continue; - } - - match find_nth_open_pair(text, open, close, close_pos, 1) { - // Before we accept this pair, we want to ensure that the - // pair encloses the range rather than just the cursor. - Some(open_pos) - if open_pos <= pos.saturating_add(1) - && close_pos >= range.to().saturating_sub(1) => - { - // Since we have special conditions for when to - // accept, we can't just pass the skip parameter on - // through to the find_nth_*_pair methods, so we - // track skips manually here. - if skip > 1 { - skip -= 1; - continue; - } - - return match range.direction() { - Direction::Forward => Ok((open_pos, close_pos)), - Direction::Backward => Ok((close_pos, open_pos)), - }; - } - _ => continue, - } - } - - Err(Error::PairNotFound) +/// assert_eq!(get_pair('['), ('[', ']')); +/// assert_eq!(get_pair('}'), ('{', '}')); +/// assert_eq!(get_pair('"'), ('"', '"')); +/// ``` +pub fn get_pair(ch: char) -> (char, char) { + PAIRS + .iter() + .find(|(open, close)| *open == ch || *close == ch) + .copied() + .unwrap_or((ch, ch)) } /// Find the position of surround pairs of `ch` which can be either a closing @@ -160,41 +35,38 @@ fn find_nth_closest_pairs_plain( pub fn find_nth_pairs_pos( text: RopeSlice, ch: char, - range: Range, + pos: usize, n: usize, -) -> Result<(usize, usize)> { - if text.len_chars() < 2 { - return Err(Error::PairNotFound); - } - if range.to() >= text.len_chars() { - return Err(Error::RangeExceedsText); - } - +) -> Option<(usize, usize)> { let (open, close) = get_pair(ch); - let pos = range.cursor(text); - let (open, close) = if open == close { + if text.len_chars() < 2 || pos >= text.len_chars() { + return None; + } + + if open == close { if Some(open) == text.get_char(pos) { - // Cursor is directly on match char. We return no match - // because there's no way to know which side of the char - // we should be searching on. - return Err(Error::CursorOnAmbiguousPair); + // Special case: cursor is directly on a matching char. + match pos { + 0 => Some((pos, search::find_nth_next(text, close, pos + 1, n)?)), + _ if (pos + 1) == text.len_chars() => { + Some((search::find_nth_prev(text, open, pos, n)?, pos)) + } + // We return no match because there's no way to know which + // side of the char we should be searching on. + _ => None, + } + } else { + Some(( + search::find_nth_prev(text, open, pos, n)?, + search::find_nth_next(text, close, pos, n)?, + )) } - ( - search::find_nth_prev(text, open, pos, n), - search::find_nth_next(text, close, pos, n), - ) } else { - ( - find_nth_open_pair(text, open, close, pos, n), - find_nth_close_pair(text, open, close, pos, n), - ) - }; - - // preserve original direction - match range.direction() { - Direction::Forward => Option::zip(open, close).ok_or(Error::PairNotFound), - Direction::Backward => Option::zip(close, open).ok_or(Error::PairNotFound), + Some(( + find_nth_open_pair(text, open, close, pos, n)?, + find_nth_close_pair(text, open, close, pos, n)?, + )) } } @@ -205,10 +77,6 @@ fn find_nth_open_pair( mut pos: usize, n: usize, ) -> Option<usize> { - if pos >= text.len_chars() { - return None; - } - let mut chars = text.chars_at(pos + 1); // Adjusts pos for the first iteration, and handles the case of the @@ -283,34 +151,23 @@ fn find_nth_close_pair( /// Find position of surround characters around every cursor. Returns None /// if any positions overlap. Note that the positions are in a flat Vec. /// Use get_surround_pos().chunks(2) to get matching pairs of surround positions. -/// `ch` can be either closing or opening pair. If `ch` is None, surround pairs -/// are automatically detected around each cursor (note that this may result -/// in them selecting different surround characters for each selection). +/// `ch` can be either closing or opening pair. pub fn get_surround_pos( - syntax: Option<&Syntax>, text: RopeSlice, selection: &Selection, - ch: Option<char>, + ch: char, skip: usize, -) -> Result<Vec<usize>> { +) -> Option<Vec<usize>> { let mut change_pos = Vec::new(); - for &range in selection { - let (open_pos, close_pos) = { - let range_raw = match ch { - Some(ch) => find_nth_pairs_pos(text, ch, range, skip)?, - None => find_nth_closest_pairs_pos(syntax, text, range, skip)?, - }; - let range = Range::new(range_raw.0, range_raw.1); - (range.from(), range.to()) - }; + for range in selection { + let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range.head, skip)?; if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) { - return Err(Error::CursorOverlap); + return None; } - // ensure the positions are always paired in the forward direction - change_pos.extend_from_slice(&[open_pos.min(close_pos), close_pos.max(open_pos)]); + change_pos.extend_from_slice(&[open_pos, close_pos]); } - Ok(change_pos) + Some(change_pos) } #[cfg(test)] @@ -322,149 +179,108 @@ mod test { use smallvec::SmallVec; #[test] - fn test_get_surround_pos() { - #[rustfmt::skip] - let (doc, selection, expectations) = - rope_with_selections_and_expectations( - "(some) (chars)\n(newline)", - "_ ^ _ _ ^ _\n_ ^ _" - ); - - assert_eq!( - get_surround_pos(None, doc.slice(..), &selection, Some('('), 1).unwrap(), - expectations - ); + fn test_find_nth_pairs_pos() { + let doc = Rope::from("some (text) here"); + let slice = doc.slice(..); + + // cursor on [t]ext + assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 10))); + assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 10))); + // cursor on so[m]e + assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None); + // cursor on bracket itself + assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10))); + assert_eq!(find_nth_pairs_pos(slice, '(', 10, 1), Some((5, 10))); } #[test] - fn test_get_surround_pos_bail_different_surround_chars() { - #[rustfmt::skip] - let (doc, selection, _) = - rope_with_selections_and_expectations( - "[some]\n(chars)xx\n(newline)", - " ^ \n ^ \n " - ); - - assert_eq!( - get_surround_pos(None, doc.slice(..), &selection, Some('('), 1), - Err(Error::PairNotFound) - ); + fn test_find_nth_pairs_pos_skip() { + let doc = Rope::from("(so (many (good) text) here)"); + let slice = doc.slice(..); + + // cursor on go[o]d + assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 15))); + assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 21))); + assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27))); } #[test] - fn test_get_surround_pos_bail_overlapping_surround_chars() { - #[rustfmt::skip] - let (doc, selection, _) = - rope_with_selections_and_expectations( - "[some]\n(chars)xx\n(newline)", - " \n ^ \n ^ " - ); - - assert_eq!( - get_surround_pos(None, doc.slice(..), &selection, Some('('), 1), - Err(Error::PairNotFound) // overlapping surround chars - ); + fn test_find_nth_pairs_pos_same() { + let doc = Rope::from("'so 'many 'good' text' here'"); + let slice = doc.slice(..); + + // cursor on go[o]d + assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15))); + assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21))); + assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27))); + // cursor on the quotes + assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), None); + // this is the best we can do since opening and closing pairs are same + assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 4))); + assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 27))); } #[test] - fn test_get_surround_pos_bail_cursor_overlap() { - #[rustfmt::skip] - let (doc, selection, _) = - rope_with_selections_and_expectations( - "[some]\n(chars)xx\n(newline)", - " ^^ \n \n " - ); + fn test_find_nth_pairs_pos_step() { + let doc = Rope::from("((so)((many) good (text))(here))"); + let slice = doc.slice(..); - assert_eq!( - get_surround_pos(None, doc.slice(..), &selection, Some('['), 1), - Err(Error::CursorOverlap) - ); - } - - #[test] - fn test_find_nth_pairs_pos_quote_success() { - #[rustfmt::skip] - let (doc, selection, expectations) = - rope_with_selections_and_expectations( - "some 'quoted text' on this 'line'\n'and this one'", - " _ ^ _ \n " - ); - - assert_eq!(2, expectations.len()); - assert_eq!( - find_nth_pairs_pos(doc.slice(..), '\'', selection.primary(), 1) - .expect("find should succeed"), - (expectations[0], expectations[1]) - ) + // cursor on go[o]d + assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24))); + assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31))); } #[test] - fn test_find_nth_pairs_pos_nested_quote_success() { - #[rustfmt::skip] - let (doc, selection, expectations) = - rope_with_selections_and_expectations( - "some 'nested 'quoted' text' on this 'line'\n'and this one'", - " _ ^ _ \n " - ); - - assert_eq!(2, expectations.len()); - assert_eq!( - find_nth_pairs_pos(doc.slice(..), '\'', selection.primary(), 2) - .expect("find should succeed"), - (expectations[0], expectations[1]) - ) + fn test_find_nth_pairs_pos_mixed() { + let doc = Rope::from("(so [many {good} text] here)"); + let slice = doc.slice(..); + + // cursor on go[o]d + assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 15))); + assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 21))); + assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 27))); } #[test] - fn test_find_nth_pairs_pos_inside_quote_ambiguous() { - #[rustfmt::skip] - let (doc, selection, _) = - rope_with_selections_and_expectations( - "some 'nested 'quoted' text' on this 'line'\n'and this one'", - " ^ \n " - ); + fn test_get_surround_pos() { + let doc = Rope::from("(some) (chars)\n(newline)"); + let slice = doc.slice(..); + let selection = Selection::new( + SmallVec::from_slice(&[Range::point(2), Range::point(9), Range::point(20)]), + 0, + ); + // cursor on s[o]me, c[h]ars, newl[i]ne assert_eq!( - find_nth_pairs_pos(doc.slice(..), '\'', selection.primary(), 1), - Err(Error::CursorOnAmbiguousPair) - ) + get_surround_pos(slice, &selection, '(', 1) + .unwrap() + .as_slice(), + &[0, 5, 7, 13, 15, 23] + ); } #[test] - fn test_find_nth_closest_pairs_pos_index_range_panic() { - #[rustfmt::skip] - let (doc, selection, _) = - rope_with_selections_and_expectations( - "(a)c)", - "^^^^^" - ); - - assert_eq!( - find_nth_closest_pairs_pos(None, doc.slice(..), selection.primary(), 1), - Err(Error::PairNotFound) - ) - } - - // Create a Rope and a matching Selection using a specification language. - // ^ is a single-point selection. - // _ is an expected index. These are returned as a Vec<usize> for use in assertions. - fn rope_with_selections_and_expectations( - text: &str, - spec: &str, - ) -> (Rope, Selection, Vec<usize>) { - if text.len() != spec.len() { - panic!("specification must match text length -- are newlines aligned?"); - } - - let rope = Rope::from(text); + fn test_get_surround_pos_bail() { + let doc = Rope::from("[some]\n(chars)xx\n(newline)"); + let slice = doc.slice(..); - let selections: SmallVec<[Range; 1]> = spec - .match_indices('^') - .map(|(i, _)| Range::point(i)) - .collect(); + let selection = + Selection::new(SmallVec::from_slice(&[Range::point(2), Range::point(9)]), 0); - let expectations: Vec<usize> = spec.match_indices('_').map(|(i, _)| i).collect(); + // cursor on s[o]me, c[h]ars + assert_eq!( + get_surround_pos(slice, &selection, '(', 1), + None // different surround chars + ); - (rope, Selection::new(selections, 0), expectations) + let selection = Selection::new( + SmallVec::from_slice(&[Range::point(14), Range::point(24)]), + 0, + ); + // cursor on [x]x, newli[n]e + assert_eq!( + get_surround_pos(slice, &selection, '(', 1), + None // overlapping surround chars + ); } } |