use crate::FormatOptions;
use crate::ir::Doc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mode {
Flat,
Break,
}
struct Work<'a> {
indent: usize,
mode: Mode,
doc: &'a Doc,
}
#[must_use]
pub fn format(doc: &Doc, options: &FormatOptions) -> String {
let mut output = String::new();
let mut col: usize = 0;
let mut stack: Vec<Work<'_>> = vec![Work {
indent: 0,
mode: Mode::Break,
doc,
}];
while let Some(Work { indent, mode, doc }) = stack.pop() {
match doc {
Doc::Text(s) => {
output.push_str(s);
col += s.len();
}
Doc::HardLine => {
output.push('\n');
push_indent(&mut output, indent, options);
col = indent_width(indent, options);
}
Doc::Line => match mode {
Mode::Flat => {
output.push(' ');
col += 1;
}
Mode::Break => {
output.push('\n');
push_indent(&mut output, indent, options);
col = indent_width(indent, options);
}
},
Doc::Indent(inner) => {
stack.push(Work {
indent: indent + 1,
mode,
doc: inner,
});
}
Doc::Concat(docs) => {
for child in docs.iter().rev() {
stack.push(Work {
indent,
mode,
doc: child,
});
}
}
Doc::Group(inner) => {
let effective_mode =
if mode == Mode::Flat || fits(inner, options.print_width.saturating_sub(col)) {
Mode::Flat
} else {
Mode::Break
};
stack.push(Work {
indent,
mode: effective_mode,
doc: inner,
});
}
Doc::FlatAlt { flat, break_ } => {
let chosen = match mode {
Mode::Flat => flat.as_ref(),
Mode::Break => break_.as_ref(),
};
stack.push(Work {
indent,
mode,
doc: chosen,
});
}
}
}
output
}
fn fits(doc: &Doc, remaining: usize) -> bool {
let mut rem: Option<usize> = Some(remaining);
let mut stack: Vec<&Doc> = vec![doc];
while let Some(doc) = stack.pop() {
if rem.is_none() {
return false;
}
match doc {
Doc::Text(s) => {
rem = rem.and_then(|r| r.checked_sub(s.len()));
}
Doc::HardLine => {
return false;
}
Doc::Line => {
rem = rem.and_then(|r| r.checked_sub(1));
}
Doc::Indent(inner) | Doc::Group(inner) => {
stack.push(inner);
}
Doc::Concat(docs) => {
for child in docs.iter().rev() {
stack.push(child);
}
}
Doc::FlatAlt { flat, .. } => {
stack.push(flat.as_ref());
}
}
}
rem.is_some()
}
fn push_indent(out: &mut String, indent: usize, options: &FormatOptions) {
if options.use_tabs {
for _ in 0..indent {
out.push('\t');
}
} else {
let spaces = indent * options.tab_width;
for _ in 0..spaces {
out.push(' ');
}
}
}
const fn indent_width(indent: usize, options: &FormatOptions) -> usize {
if options.use_tabs {
indent
} else {
indent * options.tab_width
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FormatOptions;
use crate::ir::{concat, flat_alt, group, hard_line, indent, join, line, text};
fn opts(print_width: usize) -> FormatOptions {
FormatOptions {
print_width,
..Default::default()
}
}
#[test]
fn simple_text() {
let doc = text("hello");
assert_eq!(format(&doc, &opts(80)), "hello");
}
#[test]
fn group_fits_flat() {
let doc = group(concat(vec![
text("["),
indent(concat(vec![
line(),
text("a"),
text(","),
line(),
text("b"),
])),
line(),
text("]"),
]));
let result = format(&doc, &opts(80));
assert_eq!(result, "[ a, b ]");
}
#[test]
fn group_breaks_when_too_wide() {
let doc = group(concat(vec![
text("["),
indent(concat(vec![
line(),
text("long_item_a"),
text(","),
line(),
text("long_item_b"),
])),
line(),
text("]"),
]));
let result = format(&doc, &opts(10));
assert!(
result.contains('\n'),
"expected newline in result: {result:?}"
);
}
#[test]
fn nested_groups_independent() {
let inner = group(concat(vec![text("a"), line(), text("b")]));
let outer = group(concat(vec![
text("prefix:"),
indent(concat(vec![line(), inner])),
]));
assert_eq!(format(&outer, &opts(80)), "prefix: a b");
let result = format(&outer, &opts(5));
assert!(result.contains('\n'), "expected outer to break: {result:?}");
assert!(
result.contains("a b"),
"expected inner to stay flat: {result:?}"
);
}
#[test]
fn indent_increases_indentation() {
let doc = group(concat(vec![
text("key:"),
indent(concat(vec![line(), text("value")])),
]));
let result = format(&doc, &opts(5));
assert_eq!(result, "key:\n value");
}
#[test]
fn hard_line_forces_break_in_flat() {
let doc = group(concat(vec![text("a"), hard_line(), text("b")]));
let result = format(&doc, &opts(80));
assert_eq!(result, "a\nb");
}
#[test]
fn flat_alt_modes() {
let doc_flat = group(flat_alt(text("flat"), text("break")));
assert_eq!(format(&doc_flat, &opts(80)), "flat");
let doc_break = group(concat(vec![
text("very_long_prefix_that_will_not_fit_on_one_line"),
flat_alt(text("_flat"), text("_break")),
]));
let result = format(&doc_break, &opts(10));
assert!(result.ends_with("_break"), "result: {result:?}");
}
#[test]
fn join_intersperse() {
let sep = text(", ");
let items = vec![text("a"), text("b"), text("c")];
let doc = join(&sep, items);
assert_eq!(format(&doc, &opts(80)), "a, b, c");
}
#[test]
fn join_empty() {
let sep = text(", ");
let doc = join(&sep, vec![]);
assert_eq!(format(&doc, &opts(80)), "");
}
#[test]
fn use_tabs() {
let doc = group(concat(vec![
text("key:"),
indent(concat(vec![line(), text("value")])),
]));
let options = FormatOptions {
print_width: 5,
use_tabs: true,
tab_width: 4,
};
let result = format(&doc, &options);
assert_eq!(result, "key:\n\tvalue");
}
#[test]
fn print_width_controls_breaks() {
let items = vec![text("item_one"), text("item_two"), text("item_three")];
let sep = concat(vec![text(","), line()]);
let doc = group(concat(vec![
text("["),
indent(concat(vec![line(), join(&sep, items)])),
line(),
text("]"),
]));
let wide = format(&doc, &opts(80));
assert!(!wide.contains('\n'));
let narrow = format(&doc, &opts(10));
assert!(narrow.contains('\n'));
}
#[test]
fn group_flat_alt_short_fits_uses_flat_variant() {
let short_doc = text("ok");
let long_doc = text("this_is_a_very_long_expanded_form_that_would_not_fit");
let doc = Doc::Group(Box::new(Doc::FlatAlt {
flat: Box::new(short_doc),
break_: Box::new(long_doc),
}));
let result = format(&doc, &opts(80));
assert_eq!(result, "ok");
}
#[test]
fn indent_width_use_tabs_counts_tabs_as_one_column_each() {
let options = FormatOptions {
print_width: 80,
tab_width: 4, use_tabs: true,
};
assert_eq!(indent_width(3, &options), 3);
}
#[test]
fn concat_empty_vec_renders_empty_string() {
let doc = Doc::Concat(vec![]);
let result = format(&doc, &opts(80));
assert_eq!(result, "");
}
}