#[derive(Debug, Clone, Copy)]
pub struct Line<'a> {
line: &'a str,
end: usize,
}
impl<'a> Line<'a> {
pub fn as_str(&self) -> &'a str {
self.line
}
}
#[derive(Debug)]
pub struct ChunkCursor<'a> {
text: &'a str,
current_end: usize,
start_line_no: i64,
next_line_no: i64,
}
impl<'a> ChunkCursor<'a> {
pub fn new(text: &'a str, start_line_no: i64) -> Option<Self> {
if text.is_empty() {
return None;
}
Some(ChunkCursor {
text,
current_end: 0,
start_line_no,
next_line_no: start_line_no,
})
}
pub fn start_line_no(&self) -> i64 {
self.start_line_no
}
pub fn peek_line(&self) -> Option<Line<'a>> {
if self.current_end >= self.text.len() {
return None;
}
let rest = &self.text[self.current_end..];
match rest.find('\n') {
Some(nl) => {
let line = rest[..nl].strip_suffix('\r').unwrap_or(&rest[..nl]);
Some(Line {
line,
end: self.current_end + nl + 1,
})
}
None => Some(Line {
line: rest,
end: self.text.len(),
}),
}
}
pub fn merge(&mut self, line: Line<'a>) {
debug_assert!(
line.end > self.current_end && line.end <= self.text.len(),
"merge() given a Line whose end {} is outside the unread region [{}, {}]",
line.end,
self.current_end,
self.text.len(),
);
debug_assert_eq!(
line.line.as_ptr(),
self.text[self.current_end..].as_ptr(),
"merge() given a Line that did not come from this cursor's peek_line()",
);
self.current_end = line.end;
self.next_line_no += 1;
}
pub fn next(self) -> (&'a str, Option<ChunkCursor<'a>>) {
let chunk = &self.text[..self.current_end];
let rest = &self.text[self.current_end..];
let next = if rest.is_empty() {
None
} else {
Some(ChunkCursor {
text: rest,
current_end: 0,
start_line_no: self.next_line_no,
next_line_no: self.next_line_no,
})
};
(chunk, next)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn collect(
text: &str,
mut merge: impl FnMut(&ChunkCursor, &Line) -> bool,
) -> Vec<(String, i64, i64)> {
let mut out = Vec::new();
let mut cursor = ChunkCursor::new(text, 1);
while let Some(mut c) = cursor {
let first = c.peek_line().expect("a fresh cursor has at least one line");
c.merge(first);
while let Some(line) = c.peek_line() {
if merge(&c, &line) {
c.merge(line);
} else {
break;
}
}
let (start, end) = (c.start_line_no(), c.next_line_no - 1);
let (chunk, rest) = c.next();
out.push((chunk.to_string(), start, end));
cursor = rest;
}
out
}
fn one_per_line(text: &str) -> Vec<(String, i64, i64)> {
collect(text, |_, _| false)
}
#[test]
fn empty_input_has_no_chunks() {
assert!(ChunkCursor::new("", 1).is_none());
}
#[test]
fn single_line_no_trailing_newline() {
assert_eq!(one_per_line("abc"), vec![("abc".to_string(), 1, 1)]);
}
#[test]
fn single_line_with_trailing_newline() {
assert_eq!(one_per_line("abc\n"), vec![("abc\n".to_string(), 1, 1)]);
}
#[test]
fn multiple_lines_one_per_chunk() {
assert_eq!(
one_per_line("a\nb\nc"),
vec![
("a\n".to_string(), 1, 1),
("b\n".to_string(), 2, 2),
("c".to_string(), 3, 3),
],
);
}
#[test]
fn blank_lines_are_their_own_chunks() {
assert_eq!(
one_per_line("a\n\nb"),
vec![
("a\n".to_string(), 1, 1),
("\n".to_string(), 2, 2),
("b".to_string(), 3, 3),
],
);
}
#[test]
fn peek_does_not_consume() {
let cursor = ChunkCursor::new("a\nb", 1).unwrap();
let first = cursor.peek_line().unwrap();
let again = cursor.peek_line().unwrap();
assert_eq!(first.as_str(), "a");
assert_eq!(again.as_str(), "a");
assert_eq!(cursor.current_end, 0); }
#[test]
fn merge_builds_a_contiguous_multiline_slice() {
let text = "Read:Virtual[\n - (1, 'alice')\n - => id:i64]\nNext";
let chunks = collect(text, |_, line| line.as_str().trim_start().starts_with("- "));
assert_eq!(chunks.len(), 2);
assert_eq!(
chunks[0],
(
"Read:Virtual[\n - (1, 'alice')\n - => id:i64]\n".to_string(),
1,
3,
),
);
assert_eq!(chunks[1], ("Next".to_string(), 4, 4));
}
#[test]
fn carriage_returns_are_stripped_from_line_content_but_kept_in_chunk() {
let cursor = ChunkCursor::new("a\r\nb", 1).unwrap();
let line = cursor.peek_line().unwrap();
assert_eq!(line.as_str(), "a");
let mut c = cursor;
c.merge(line);
let (chunk, _) = c.next();
assert_eq!(chunk, "a\r\n");
}
#[test]
fn line_numbers_account_for_a_custom_start() {
let chunks = collect("x\ny", |_, _| false)
.into_iter()
.map(|(_, s, e)| (s, e))
.collect::<Vec<_>>();
assert_eq!(chunks, vec![(1, 1), (2, 2)]);
let mut cursor = ChunkCursor::new("x\ny", 10);
let mut starts = Vec::new();
while let Some(mut c) = cursor {
c.merge(c.peek_line().unwrap());
starts.push(c.start_line_no());
cursor = c.next().1;
}
assert_eq!(starts, vec![10, 11]);
}
#[test]
fn end_line_no_tracks_merged_lines() {
let text = "a\nb\nc\nd";
let chunks = collect(text, |_, _| true);
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].1, 1); assert_eq!(chunks[0].2, 4); assert_eq!(chunks[0].0, "a\nb\nc\nd");
}
}