#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct AttachedLines {
buf: String,
offsets: Vec<u32>,
}
impl AttachedLines {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.offsets.len()
}
pub fn is_empty(&self) -> bool {
self.offsets.is_empty()
}
pub fn push(&mut self, line: &str) {
let start = u32::try_from(self.buf.len()).expect("attached buffer exceeded 4 GiB");
self.offsets.push(start);
self.buf.push_str(line);
self.buf.push('\n');
}
pub fn get(&self, i: usize) -> Option<&str> {
let start = *self.offsets.get(i)? as usize;
let end = self
.offsets
.get(i + 1)
.map(|&o| o as usize - 1)
.unwrap_or_else(|| self.buf.len() - 1);
Some(&self.buf[start..end])
}
pub fn iter(&self) -> AttachedLinesIter<'_> {
AttachedLinesIter {
lines: self,
pos: 0,
}
}
}
impl<'a> IntoIterator for &'a AttachedLines {
type Item = &'a str;
type IntoIter = AttachedLinesIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
#[derive(Debug, Clone)]
pub struct AttachedLinesIter<'a> {
lines: &'a AttachedLines,
pos: usize,
}
impl<'a> Iterator for AttachedLinesIter<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
let line = self.lines.get(self.pos)?;
self.pos += 1;
Some(line)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.lines.len() - self.pos;
(remaining, Some(remaining))
}
}
impl ExactSizeIterator for AttachedLinesIter<'_> {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_default() {
let a = AttachedLines::new();
assert_eq!(a.len(), 0);
assert!(a.is_empty());
assert!(a.get(0).is_none());
assert_eq!(a.iter().count(), 0);
}
#[test]
fn push_and_iterate_preserves_order_and_content() {
let mut a = AttachedLines::new();
a.push("first");
a.push("");
a.push("third line");
assert_eq!(a.len(), 3);
assert!(!a.is_empty());
assert_eq!(a.get(0), Some("first"));
assert_eq!(a.get(1), Some(""));
assert_eq!(a.get(2), Some("third line"));
assert!(a.get(3).is_none());
let collected: Vec<&str> = a.iter().collect();
assert_eq!(collected, vec!["first", "", "third line"]);
}
#[test]
fn intoiter_for_ref_works_in_for_loop() {
let mut a = AttachedLines::new();
a.push("a");
a.push("b");
let mut out = Vec::new();
for line in &a {
out.push(line.to_string());
}
assert_eq!(out, vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn lines_with_embedded_separators_round_trip() {
let mut a = AttachedLines::new();
a.push("has\nnewline");
a.push("plain");
assert_eq!(a.get(0), Some("has\nnewline"));
assert_eq!(a.get(1), Some("plain"));
}
#[test]
fn allocation_pattern_is_logarithmic_not_per_line() {
let mut a = AttachedLines::new();
for i in 0..200 {
a.push(&format!(
"variable_some_long_name_{i}: [a typical value here]"
));
}
assert_eq!(a.len(), 200);
for (i, line) in a.iter().enumerate() {
assert!(line.starts_with(&format!("variable_some_long_name_{i}")));
}
}
}