use std::ops::Range;
#[derive(Default)]
pub(crate) struct RunBuilder {
runs: Vec<Range<usize>>,
}
impl RunBuilder {
pub(crate) fn push_visible(&mut self, start: usize, end: usize) {
if start >= end {
return;
}
if let Some(last) = self.runs.last_mut()
&& last.end == start
{
last.end = end;
return;
}
self.runs.push(start..end);
}
pub(crate) fn finish(self) -> Vec<Range<usize>> {
self.runs
}
}
pub(crate) fn find_matching(
chars: &[(usize, char)],
open_i: usize,
open_ch: char,
close_ch: char,
respect_escapes: bool,
) -> Option<usize> {
if chars.get(open_i).map(|&(_, c)| c) != Some(open_ch) {
return None;
}
let mut depth = 0i32;
let mut i = open_i;
while let Some(&(_, ch)) = chars.get(i) {
if respect_escapes && ch == '\\' {
i += 2;
continue;
}
if ch == open_ch {
depth += 1;
} else if ch == close_ch {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
i += 1;
}
None
}
pub(crate) fn skip_balanced(
chars: &[(usize, char)],
i: usize,
open_ch: char,
close_ch: char,
respect_escapes: bool,
) -> usize {
if chars.get(i).map(|&(_, c)| c) != Some(open_ch) {
return i;
}
find_matching(chars, i, open_ch, close_ch, respect_escapes).map_or(chars.len(), |end| end + 1)
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
reason = "Panicking is acceptable and often desired in tests."
)]
mod tests {
use super::*;
fn chars_of(s: &str) -> Vec<(usize, char)> {
s.char_indices().collect()
}
#[test]
fn run_builder_merges_adjacent_pushes() {
let mut builder = RunBuilder::default();
builder.push_visible(0, 3);
builder.push_visible(3, 5);
builder.push_visible(7, 9);
assert_eq!(builder.finish(), vec![0..5, 7..9]);
}
#[test]
fn run_builder_ignores_empty_pushes() {
let mut builder = RunBuilder::default();
builder.push_visible(2, 2);
assert_eq!(builder.finish(), Vec::<Range<usize>>::new());
}
#[test]
fn find_matching_tracks_nesting_depth() {
let chars = chars_of("[a[b]c]d");
assert_eq!(find_matching(&chars, 0, '[', ']', false), Some(6));
}
#[test]
fn find_matching_respects_escapes() {
let chars = chars_of(r"[a\]b]c");
assert_eq!(find_matching(&chars, 0, '[', ']', true), Some(5));
}
#[test]
fn find_matching_returns_none_when_unterminated() {
let chars = chars_of("[abc");
assert_eq!(find_matching(&chars, 0, '[', ']', false), None);
}
#[test]
fn skip_balanced_returns_index_past_close() {
let chars = chars_of("{abc}def");
assert_eq!(skip_balanced(&chars, 0, '{', '}', false), 5);
}
#[test]
fn skip_balanced_is_noop_when_not_at_open() {
let chars = chars_of("abc");
assert_eq!(skip_balanced(&chars, 0, '{', '}', false), 0);
}
}