#[derive(Debug, Default, Clone)]
pub struct LineBuffer {
pending: String,
}
impl LineBuffer {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, delta: &str) {
if delta.is_empty() {
return;
}
self.pending.push_str(delta);
}
pub fn take_committable(&mut self) -> String {
let Some(last_nl) = self.pending.rfind('\n') else {
return String::new();
};
self.pending.drain(..=last_nl).collect()
}
pub fn flush(&mut self) -> String {
std::mem::take(&mut self.pending)
}
pub fn is_empty(&self) -> bool {
self.pending.is_empty()
}
pub fn pending_len(&self) -> usize {
self.pending.len()
}
pub fn reset(&mut self) {
self.pending.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn push_without_newline_holds_everything() {
let mut buf = LineBuffer::new();
buf.push("hello");
assert_eq!(buf.take_committable(), "");
assert_eq!(buf.pending_len(), 5);
assert!(!buf.is_empty());
}
#[test]
fn push_with_trailing_partial_returns_only_prefix() {
let mut buf = LineBuffer::new();
buf.push("hello\nwo");
assert_eq!(buf.take_committable(), "hello\n");
assert_eq!(buf.pending_len(), 2);
assert!(!buf.is_empty());
}
#[test]
fn next_push_is_concatenated_with_held_tail() {
let mut buf = LineBuffer::new();
buf.push("hello\nwo");
assert_eq!(buf.take_committable(), "hello\n");
buf.push("rld\n");
assert_eq!(buf.take_committable(), "world\n");
assert!(buf.is_empty());
}
#[test]
fn flush_returns_unterminated_tail() {
let mut buf = LineBuffer::new();
buf.push("trailing without newline");
assert_eq!(buf.take_committable(), "");
assert_eq!(buf.flush(), "trailing without newline");
assert!(buf.is_empty());
}
#[test]
fn flush_is_empty_when_buffer_drained() {
let mut buf = LineBuffer::new();
buf.push("a\n");
assert_eq!(buf.take_committable(), "a\n");
assert_eq!(buf.flush(), "");
}
#[test]
fn multi_line_burst_returns_prefix_through_last_newline() {
let mut buf = LineBuffer::new();
buf.push("a\nb\nc\nd");
assert_eq!(buf.take_committable(), "a\nb\nc\n");
assert_eq!(buf.pending_len(), 1);
buf.push("\n");
assert_eq!(buf.take_committable(), "d\n");
}
#[test]
fn partial_code_fence_never_escapes_the_gate() {
let mut buf = LineBuffer::new();
buf.push("foo```");
let c1 = buf.take_committable();
assert!(
c1.is_empty() || c1.ends_with('\n'),
"partial fence leaked: {c1:?}"
);
assert!(
!c1.contains("foo```"),
"fence opener escaped without newline: {c1:?}"
);
buf.push("rust\nlet x");
let c2 = buf.take_committable();
assert!(
c2.ends_with('\n'),
"expected newline-terminated commit: {c2:?}"
);
assert_eq!(c2, "foo```rust\n");
buf.push("= 1;\n```\n");
let c3 = buf.take_committable();
assert_eq!(c3, "let x= 1;\n```\n");
assert!(buf.is_empty());
}
#[test]
fn empty_push_is_a_noop() {
let mut buf = LineBuffer::new();
buf.push("");
assert!(buf.is_empty());
assert_eq!(buf.take_committable(), "");
}
#[test]
fn reset_clears_pending_tail() {
let mut buf = LineBuffer::new();
buf.push("partial");
assert_eq!(buf.pending_len(), 7);
buf.reset();
assert!(buf.is_empty());
assert_eq!(buf.flush(), "");
}
}