knave-expand 0.14.21

The reading, scoping and expansion stages of knave
Documentation
use super::block::Block;
use super::layout::{LaidOut, LayoutEngine};
use super::syntax_tree::ListNode;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Measure {
    line_count: usize,
    closed_overflow: usize,
    end_column: usize,
    ends_with_comment: bool,
    excused_overflow: usize,
}

impl LaidOut for Measure {
    fn text(text: &str, column: usize) -> Self {
        let widths: Vec<usize> = text.split('\n').map(|line| line.chars().count()).collect();
        let (last, earlier) = widths.split_last().expect("a split text has a line");
        let absolute_end =
            |index: usize, width: usize| if index == 0 { column + width } else { width };
        Self {
            line_count: widths.len(),
            closed_overflow: earlier
                .iter()
                .enumerate()
                .map(|(index, width)| Block::overflow_of(absolute_end(index, *width)))
                .sum(),
            end_column: absolute_end(earlier.len(), *last),
            ends_with_comment: false,
            excused_overflow: 0,
        }
    }

    fn shifted_string(text: &str, shift: isize, column: usize) -> Self {
        let mut rendered = String::new();
        Block::shifted_string(text, shift, column).render_into(&mut rendered);
        Self::text(&rendered, column)
    }

    fn list(
        engine: &mut LayoutEngine<'_>,
        list: &ListNode,
        column: usize,
        trailing: usize,
    ) -> Self {
        engine.measure_list(list, column, trailing)
    }

    fn line_count(&self) -> usize {
        self.line_count
    }

    fn excuse_overflow(&mut self, own_width: usize, trailing: usize) {
        let closed_end = self.end_column + trailing;
        self.excused_overflow = Block::overflow_of(closed_end).min(own_width + trailing);
    }

    fn end_column(&self) -> usize {
        self.end_column
    }

    fn ends_with_comment(&self) -> bool {
        self.ends_with_comment
    }

    fn append(&mut self, text: &str) {
        self.end_column += text.chars().count();
    }

    fn join(&mut self, _separator: &str, other: Self) {
        self.line_count += other.line_count - 1;
        self.closed_overflow += other.closed_overflow;
        self.excused_overflow += other.excused_overflow;
        self.end_column = other.end_column;
        self.ends_with_comment = other.ends_with_comment;
    }

    fn push_indented(&mut self, _indent: usize, other: Self) {
        self.close_line();
        self.line_count += other.line_count - 1;
        self.closed_overflow += other.closed_overflow;
        self.excused_overflow += other.excused_overflow;
        self.end_column = other.end_column;
        self.ends_with_comment = other.ends_with_comment;
    }

    fn push_blank(&mut self) {
        self.close_line();
        self.end_column = 0;
        self.ends_with_comment = false;
    }

    fn add_trailing_comment(&mut self, _comment: &str) {
        self.ends_with_comment = true;
    }
}

impl Measure {
    pub fn avoidable_overflow(&self, trailing: usize) -> usize {
        let total_overflow = self.closed_overflow + Block::overflow_of(self.end_column + trailing);
        total_overflow.saturating_sub(self.excused_overflow)
    }

    fn close_line(&mut self) {
        self.closed_overflow += Block::overflow_of(self.end_column);
        self.line_count += 1;
    }
}

#[cfg(test)]
mod tests {
    use super::Measure;
    use crate::format::block::Block;
    use crate::format::layout::LaidOut;

    struct Agreement {
        block: Block,
        measure: Measure,
    }

    impl Agreement {
        fn of_text(text: &str, column: usize) -> Self {
            Self {
                block: Block::text(text, column),
                measure: Measure::text(text, column),
            }
        }

        fn push_indented(&mut self, indent: usize, text: &str) {
            self.block.push_indented(indent, Block::text(text, indent));
            self.measure
                .push_indented(indent, Measure::text(text, indent));
        }

        fn join(&mut self, text: &str) {
            let column = self.block.end_column() + 1;
            self.block.join(" ", Block::text(text, column));
            self.measure.join(" ", Measure::text(text, column));
        }

        fn assert_agrees(&self) {
            assert_eq!(
                (
                    self.block.line_count(),
                    self.block.overflow(),
                    self.block.end_column()
                ),
                (
                    self.measure.line_count(),
                    self.measure.avoidable_overflow(0),
                    self.measure.end_column()
                )
            );
        }
    }

    #[test]
    fn a_measure_counts_what_its_block_lays_out() {
        let long = "y".repeat(Block::WIDTH);
        let mut agreement = Agreement::of_text("(head", 70);
        agreement.join(&format!("\"{long}\nend\""));
        agreement.push_indented(4, &long);
        agreement.join("tail");
        agreement.assert_agrees();
    }
}