knave-expand 0.13.2

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

#[derive(Clone, Debug)]
struct Line {
    text: String,
    comment: Option<String>,
}

#[derive(Clone, Debug)]
pub struct Block {
    column: usize,
    first: Line,
    later: Vec<Line>,
}

impl LaidOut for Block {
    fn text(text: &str, column: usize) -> Self {
        let mut lines = text.split('\n');
        Self {
            column,
            first: Line::plain(lines.next().unwrap_or_default()),
            later: lines.map(Line::plain).collect(),
        }
    }

    fn shifted_string(text: &str, shift: isize, column: usize) -> Self {
        let mut block = Self::text(text, column);
        for line in &mut block.later {
            line.shift(shift);
        }
        block
    }

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

    fn line_count(&self) -> usize {
        1 + self.later.len()
    }

    fn excuse_overflow(&mut self, _own_width: usize, _trailing: usize) {}

    fn end_column(&self) -> usize {
        match self.later.last() {
            Some(line) => line.width(),
            None => self.column + self.first.width(),
        }
    }

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

    fn append(&mut self, text: &str) {
        self.last_mut().text.push_str(text);
    }

    fn join(&mut self, separator: &str, other: Block) {
        let last = self.last_mut();
        last.text.push_str(separator);
        last.text.push_str(&other.first.text);
        last.comment = other.first.comment;
        self.later.extend(other.later);
    }

    fn push_indented(&mut self, indent: usize, other: Block) {
        self.later.push(Line::plain(&Self::spaces(indent)));
        self.join("", other);
    }

    fn push_blank(&mut self) {
        self.later.push(Line::plain(""));
    }

    fn add_trailing_comment(&mut self, comment: &str) {
        self.last_mut().comment = Some(comment.to_string());
    }
}

impl Block {
    #[cfg(test)]
    pub fn overflow(&self) -> usize {
        let first = self.column + self.first.width();
        self.later
            .iter()
            .map(Line::width)
            .chain([first])
            .map(Self::overflow_of)
            .sum()
    }

    pub const WIDTH: usize = 80;
    pub const FILL_WIDTH: usize = 60;

    pub fn overflow_of(width: usize) -> usize {
        width.saturating_sub(Self::WIDTH)
    }

    pub fn render_into(&self, output: &mut String) {
        self.first.render_into(output);
        for line in &self.later {
            output.push('\n');
            line.render_into(output);
        }
    }

    fn last(&self) -> &Line {
        self.later.last().unwrap_or(&self.first)
    }

    fn last_mut(&mut self) -> &mut Line {
        self.later.last_mut().unwrap_or(&mut self.first)
    }

    fn spaces(count: usize) -> String {
        " ".repeat(count)
    }
}

impl Line {
    fn plain(text: &str) -> Self {
        Self {
            text: text.to_string(),
            comment: None,
        }
    }

    fn width(&self) -> usize {
        self.text.chars().count()
    }

    fn shift(&mut self, shift: isize) {
        if self.text.is_empty() {
            return;
        }
        let indentation = self.text.len() - self.text.trim_start_matches(' ').len();
        let shifted = indentation.saturating_add_signed(shift);
        self.text = format!("{}{}", Block::spaces(shifted), &self.text[indentation..]);
    }

    fn render_into(&self, output: &mut String) {
        output.push_str(&self.text);
        if let Some(comment) = &self.comment {
            output.push(' ');
            output.push_str(comment);
        }
    }
}

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

    struct BlockFixture;

    impl BlockFixture {
        fn rendered(block: &Block) -> String {
            let mut output = String::new();
            block.render_into(&mut output);
            output
        }
    }

    #[test]
    fn a_closing_parenthesis_goes_before_a_trailing_comment() {
        let mut block = Block::text("b", 0);
        block.add_trailing_comment("; why");
        block.append(")");
        assert_eq!(BlockFixture::rendered(&block), "b) ; why");
    }

    #[test]
    fn a_shift_moves_each_later_line_and_leaves_blank_lines_empty() {
        let block = Block::shifted_string("\"a\n\n  b\"", 2, 0);
        assert_eq!(BlockFixture::rendered(&block), "\"a\n\n    b\"");
    }

    #[test]
    fn a_negative_shift_removes_at_most_the_indentation() {
        let block = Block::shifted_string("\"a\n b\"", -3, 0);
        assert_eq!(BlockFixture::rendered(&block), "\"a\nb\"");
    }

    #[test]
    fn overflow_counts_the_columns_past_the_width() {
        let block = Block::text(&"x".repeat(Block::WIDTH), 2);
        assert_eq!(block.overflow(), 2);
    }
}