use crate::formatter::ir::Ir;
use crate::formatter::style::FormatStyle;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mode {
Flat,
Break,
}
pub fn print(doc: &Ir, style: FormatStyle) -> String {
print_at(doc, style, 0)
}
pub fn print_at(doc: &Ir, style: FormatStyle, indent: usize) -> String {
let indent_step = style.indent_width as usize;
let width = style.line_width as usize;
let mut out = String::new();
let mut col = indent;
let mut stack: Vec<(usize, Mode, &Ir)> = vec![(indent, Mode::Break, doc)];
while let Some((indent, mode, ir)) = stack.pop() {
match ir {
Ir::Text(s) => {
out.push_str(s);
match s.rfind('\n') {
Some(i) => col = s[i + 1..].chars().count(),
None => col += s.chars().count(),
}
}
Ir::Concat(items) => {
for item in items.iter().rev() {
stack.push((indent, mode, item));
}
}
Ir::Indent(inner) => stack.push((indent + indent_step, mode, inner)),
Ir::Line => match mode {
Mode::Flat => {
out.push(' ');
col += 1;
}
Mode::Break => col = newline(&mut out, indent),
},
Ir::SoftLine => {
if mode == Mode::Break {
col = newline(&mut out, indent);
}
}
Ir::HardLine => col = newline(&mut out, indent),
Ir::BlankLine => {
out.push('\n');
col = 0;
}
Ir::Group(inner) => {
let mode = if fits(width.saturating_sub(col), inner, &stack) {
Mode::Flat
} else {
Mode::Break
};
stack.push((indent, mode, inner));
}
Ir::IfBreak(broken, flat) => {
let s = if mode == Mode::Break { broken } else { flat };
out.push_str(s);
col += s.chars().count();
}
Ir::HugGroup {
prefix,
body,
close,
explode,
} => {
if hug_fits(width.saturating_sub(col), prefix, body, close, &stack) {
stack.push((indent, mode, close));
stack.push((indent, mode, body));
stack.push((indent, mode, prefix));
} else {
stack.push((indent, mode, explode));
}
}
}
}
out
}
fn newline(out: &mut String, indent: usize) -> usize {
out.push('\n');
for _ in 0..indent {
out.push(' ');
}
indent
}
fn fits(remaining: usize, inner: &Ir, rest: &[(usize, Mode, &Ir)]) -> bool {
let mut stack: Vec<(bool, Mode, &Ir)> = Vec::with_capacity(rest.len() + 1);
for (_, mode, ir) in rest {
stack.push((false, *mode, ir));
}
stack.push((true, Mode::Flat, inner));
fits_stack(remaining as isize, stack)
}
fn hug_fits(
remaining: usize,
prefix: &Ir,
body: &Ir,
close: &Ir,
rest: &[(usize, Mode, &Ir)],
) -> bool {
let mut stack: Vec<(bool, Mode, &Ir)> = Vec::with_capacity(rest.len() + 3);
for (_, mode, ir) in rest {
stack.push((false, *mode, ir));
}
stack.push((false, Mode::Flat, close));
stack.push((false, Mode::Break, body));
stack.push((true, Mode::Flat, prefix));
fits_stack(remaining as isize, stack)
}
fn fits_stack(mut remaining: isize, mut stack: Vec<(bool, Mode, &Ir)>) -> bool {
while let Some((in_group, mode, ir)) = stack.pop() {
if remaining < 0 {
return false;
}
match ir {
Ir::Text(s) => match s.find('\n') {
Some(i) => {
remaining -= s[..i].chars().count() as isize;
return !in_group && remaining >= 0;
}
None => remaining -= s.chars().count() as isize,
},
Ir::Concat(items) => {
for item in items.iter().rev() {
stack.push((in_group, mode, item));
}
}
Ir::Indent(child) => stack.push((in_group, mode, child)),
Ir::Group(child) => stack.push((in_group, mode, child)),
Ir::Line => match mode {
Mode::Flat => remaining -= 1,
Mode::Break => return true,
},
Ir::SoftLine => {
if mode == Mode::Break {
return true;
}
}
Ir::HardLine | Ir::BlankLine => return !in_group,
Ir::IfBreak(broken, flat) => {
let s = if mode == Mode::Break { broken } else { flat };
remaining -= s.chars().count() as isize;
}
Ir::HugGroup {
prefix,
body,
close,
..
} => {
stack.push((in_group, mode, close));
stack.push((in_group, mode, body));
stack.push((in_group, mode, prefix));
}
}
}
remaining >= 0
}
#[cfg(test)]
mod tests {
use super::*;
fn list_doc() -> Ir {
Ir::group(Ir::concat([
Ir::text("["),
Ir::indent(Ir::concat([
Ir::SoftLine,
Ir::text("a,"),
Ir::Line,
Ir::text("b,"),
Ir::Line,
Ir::text("c"),
])),
Ir::SoftLine,
Ir::text("]"),
]))
}
#[test]
fn group_stays_flat_when_it_fits() {
let style = FormatStyle {
line_width: 80,
indent_width: 4,
};
assert_eq!(print(&list_doc(), style), "[a, b, c]");
}
#[test]
fn group_breaks_when_too_wide() {
let style = FormatStyle {
line_width: 5,
indent_width: 4,
};
assert_eq!(print(&list_doc(), style), "[\n a,\n b,\n c\n]");
}
#[test]
fn print_at_starts_from_the_given_column() {
let style = FormatStyle {
line_width: 9,
indent_width: 4,
};
assert_eq!(print_at(&list_doc(), style, 0), "[a, b, c]");
assert_eq!(
print_at(&list_doc(), style, 4),
"[\n a,\n b,\n c\n ]"
);
}
fn trailing_comma_doc() -> Ir {
Ir::group(Ir::concat([
Ir::text("("),
Ir::indent(Ir::concat([
Ir::SoftLine,
Ir::text("a,"),
Ir::Line,
Ir::text("b"),
Ir::if_break(",", ""),
])),
Ir::SoftLine,
Ir::text(")"),
]))
}
#[test]
fn if_break_is_empty_when_flat() {
let style = FormatStyle {
line_width: 80,
indent_width: 4,
};
assert_eq!(print(&trailing_comma_doc(), style), "(a, b)");
}
#[test]
fn if_break_emits_when_broken() {
let style = FormatStyle {
line_width: 4,
indent_width: 4,
};
assert_eq!(print(&trailing_comma_doc(), style), "(\n a,\n b,\n)");
}
fn hug_doc() -> Ir {
let body = || {
Ir::group(Ir::concat([
Ir::text("["),
Ir::indent(Ir::concat([
Ir::SoftLine,
Ir::text("x,"),
Ir::Line,
Ir::text("y"),
])),
Ir::SoftLine,
Ir::text("]"),
]))
};
let explode = Ir::group(Ir::concat([
Ir::text("("),
Ir::indent(Ir::concat([
Ir::SoftLine,
Ir::text("aa"),
Ir::text(","),
Ir::Line,
body(),
Ir::if_break(",", ""),
])),
Ir::SoftLine,
Ir::text(")"),
]));
Ir::concat([
Ir::text("f"),
Ir::hug_group(
Ir::concat([Ir::text("("), Ir::text("aa"), Ir::text(", ")]),
body(),
Ir::text(")"),
explode,
),
])
}
#[test]
fn hug_group_stays_flat_when_it_fits() {
let style = FormatStyle {
line_width: 80,
indent_width: 4,
};
assert_eq!(print(&hug_doc(), style), "f(aa, [x, y])");
}
#[test]
fn hug_group_hugs_when_first_line_fits() {
let style = FormatStyle {
line_width: 8,
indent_width: 4,
};
assert_eq!(print(&hug_doc(), style), "f(aa, [\n x,\n y\n])");
}
#[test]
fn hug_group_explodes_when_first_line_overflows() {
let style = FormatStyle {
line_width: 6,
indent_width: 4,
};
assert_eq!(
print(&hug_doc(), style),
"f(\n aa,\n [\n x,\n y\n ],\n)"
);
}
}