diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index d7314609..61981d6e 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -39,13 +39,101 @@ pub fn find_nth_pairs_pos( n: usize, ) -> Option<(usize, usize)> { let (open, close) = get_pair(ch); - // find_nth* do not consider current character; +1/-1 to include them - let open_pos = search::find_nth_prev(text, open, pos + 1, n, true)?; - let close_pos = search::find_nth_next(text, close, pos - 1, n, true)?; + + let (open_pos, close_pos) = if open == close { + // find_nth* do not consider current character; +1/-1 to include them + ( + search::find_nth_prev(text, open, pos + 1, n, true)?, + search::find_nth_next(text, close, pos - 1, n, true)?, + ) + } else { + ( + find_nth_open_pair(text, open, close, pos, n)?, + find_nth_close_pair(text, open, close, pos, n)?, + ) + }; Some((open_pos, close_pos)) } +fn find_nth_open_pair( + text: RopeSlice, + open: char, + close: char, + mut pos: usize, + n: usize, +) -> Option { + let mut chars = text.chars_at(pos + 1); + + // Adjusts pos for the first iteration, and handles the case of the + // cursor being *on* the close character which will get falsely stepped over + // if not skipped here + if chars.prev()? == open { + return Some(pos); + } + + for _ in 0..n { + let mut step_over: usize = 0; + + loop { + let c = chars.prev()?; + pos = pos.saturating_sub(1); + + // ignore other surround pairs that are enclosed *within* our search scope + if c == close { + step_over += 1; + } else if c == open { + if step_over == 0 { + break; + } + + step_over = step_over.saturating_sub(1); + } + } + } + + Some(pos) +} + +fn find_nth_close_pair( + text: RopeSlice, + open: char, + close: char, + mut pos: usize, + n: usize, +) -> Option { + if pos >= text.len_chars() { + return None; + } + + let mut chars = text.chars_at(pos); + + if chars.next()? == close { + return Some(pos); + } + + for _ in 0..n { + let mut step_over: usize = 0; + + loop { + let c = chars.next()?; + pos += 1; + + if c == open { + step_over += 1; + } else if c == close { + if step_over == 0 { + break; + } + + step_over = step_over.saturating_sub(1); + } + } + } + + Some(pos) +} + /// 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. @@ -101,6 +189,27 @@ mod test { assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27))); } + #[test] + 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))); + } + + #[test] + fn test_find_nth_pairs_pos_step() { + 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, '(', 15, 1), Some((5, 24))); + assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31))); + } + #[test] fn test_find_nth_pairs_pos_mixed() { let doc = Rope::from("(so [many {good} text] here)");