use super::{OutputBuffer, OutputPart};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
enum Found {
#[default]
None,
Tentative { blank: bool },
Final { blank: bool },
}
#[derive(Debug, Clone, Default)]
pub(crate) struct LineCompletion {
completed: bool,
after_glue: bool,
line_visible: bool,
found: Found,
run_newlines: usize,
}
impl LineCompletion {
pub(crate) fn feed(&mut self, part: &OutputPart) {
if self.completed {
return;
}
if part.is_content() {
match self.found {
Found::Tentative { blank } | Found::Final { blank } => {
if !blank || part.is_visible() {
self.completed = true;
}
self.found = Found::Final { blank };
}
Found::None => {
if part.is_visible() {
self.line_visible = true;
}
}
}
self.after_glue = false;
self.run_newlines = 0;
return;
}
match part {
OutputPart::Newline => {
self.run_newlines += 1;
if !self.after_glue && self.found == Found::None {
self.found = Found::Tentative {
blank: !self.line_visible,
};
}
}
OutputPart::Glue => {
self.after_glue = true;
if self.run_newlines > 0 {
self.run_newlines -= 1;
if self.run_newlines == 0 && matches!(self.found, Found::Tentative { .. }) {
self.found = Found::None;
}
}
}
_ => {}
}
}
pub(crate) fn is_completed(&self) -> bool {
self.completed
}
}
impl OutputBuffer {
pub(crate) fn rescan_completion(&mut self) {
let mut state = LineCompletion::default();
for part in &self.transcript[self.cursor..] {
state.feed(part);
}
self.completion = state;
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use alloc::sync::Arc;
use alloc::vec::Vec;
use brink_format::{LineFlags, ListValue, Value};
use proptest::prelude::*;
use super::super::{OutputBuffer, OutputPart};
fn arb_part() -> impl Strategy<Value = OutputPart> {
prop_oneof![
Just(OutputPart::Newline),
Just(OutputPart::Glue),
Just(OutputPart::Spring),
Just(OutputPart::Text("a".to_string())),
Just(OutputPart::Text(" ".to_string())),
Just(OutputPart::Tag("t".to_string())),
Just(OutputPart::ValueRef(Value::Int(1))),
Just(OutputPart::ValueRef(Value::String(Arc::from(" ")))),
Just(OutputPart::ValueRef(Value::OptionVal(None))),
Just(OutputPart::ValueRef(Value::List(Arc::new(ListValue {
items: Vec::new(),
origins: Vec::new(),
})))),
Just(OutputPart::LineRef {
container_idx: 0,
line_idx: 0,
slots: Vec::new(),
flags: LineFlags::empty(),
}),
Just(OutputPart::LineRef {
container_idx: 0,
line_idx: 0,
slots: Vec::new(),
flags: LineFlags::ALL_WS,
}),
Just(OutputPart::ElementAttach("k".to_string(), "v".to_string())),
Just(OutputPart::ElementAttachEnd),
]
}
#[derive(Debug, Clone)]
enum Op {
Push(OutputPart),
MoveCursor(usize),
Trim(usize),
}
fn arb_op() -> impl Strategy<Value = Op> {
prop_oneof![
8 => arb_part().prop_map(Op::Push),
1 => (0usize..64).prop_map(Op::MoveCursor),
1 => (0usize..64).prop_map(Op::Trim),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(2048))]
#[test]
fn incremental_matches_batch_scan(ops in proptest::collection::vec(arb_op(), 1..48)) {
let mut buf = OutputBuffer::new();
for (n, op) in ops.into_iter().enumerate() {
match op {
Op::Push(part) => buf.push_part(part),
Op::MoveCursor(k) => {
buf.cursor = k.min(buf.transcript.len());
buf.rescan_completion();
}
Op::Trim(start) => buf.trim_function_end(start.min(buf.transcript.len())),
}
let batch = buf.has_completed_line_scan();
prop_assert_eq!(
buf.has_completed_line(),
batch,
"after op {} the incremental state disagrees with the scan; \
cursor={} transcript={:?} state={:?}",
n,
buf.cursor,
&buf.transcript[buf.cursor..],
buf.completion
);
}
}
}
#[test]
fn glue_stack_shapes() {
let nl = OutputPart::Newline;
let glue = OutputPart::Glue;
let a = || OutputPart::Text("a".to_string());
let cases: [(&str, Vec<OutputPart>, bool); 6] = [
("plain line", alloc::vec![a(), nl.clone(), a()], true),
(
"glue eats the newline",
alloc::vec![a(), nl.clone(), glue.clone(), a()],
false,
),
(
"glue after the newline eats the next one too",
alloc::vec![a(), nl.clone(), glue.clone(), nl.clone(), a()],
false,
),
(
"first of two newlines survives one glue",
alloc::vec![a(), nl.clone(), nl.clone(), glue.clone(), a()],
true,
),
(
"a later newline shields the first",
alloc::vec![
a(),
nl.clone(),
nl.clone(),
glue.clone(),
nl.clone(),
glue.clone(),
a()
],
true,
),
(
"two glues pop both",
alloc::vec![a(), nl.clone(), nl.clone(), glue.clone(), glue.clone(), a()],
false,
),
];
for (name, parts, want) in cases {
let mut buf = OutputBuffer::new();
for p in parts {
buf.push_part(p);
}
assert_eq!(buf.has_completed_line(), want, "{name}");
assert_eq!(buf.has_completed_line_scan(), want, "{name} (batch)");
}
}
}