ruff/crates/ruff_formatter/src/printer/mod.rs

2099 lines
73 KiB
Rust

use std::num::NonZeroU8;
use drop_bomb::DebugDropBomb;
use unicode_width::UnicodeWidthChar;
pub use printer_options::*;
use ruff_text_size::{Ranged, TextLen, TextSize};
use crate::format_element::document::Document;
use crate::format_element::tag::{Condition, GroupMode};
use crate::format_element::{BestFittingMode, BestFittingVariants, LineMode, PrintMode};
use crate::prelude::tag::{DedentMode, Tag, TagKind, VerbatimKind};
use crate::prelude::{tag, TextWidth};
use crate::printer::call_stack::{
CallStack, FitsCallStack, PrintCallStack, PrintElementArgs, StackFrame,
};
use crate::printer::line_suffixes::{LineSuffixEntry, LineSuffixes};
use crate::printer::queue::{
AllPredicate, FitsEndPredicate, FitsQueue, PrintQueue, Queue, SingleEntryPredicate,
};
use crate::source_code::SourceCode;
use crate::{
ActualStart, FormatElement, GroupId, IndentStyle, InvalidDocumentError, PrintError,
PrintResult, Printed, SourceMarker, TextRange,
};
mod call_stack;
mod line_suffixes;
mod printer_options;
mod queue;
mod stack;
/// Prints the format elements into a string
#[derive(Debug, Default)]
pub struct Printer<'a> {
options: PrinterOptions,
source_code: SourceCode<'a>,
state: PrinterState<'a>,
}
impl<'a> Printer<'a> {
pub fn new(source_code: SourceCode<'a>, options: PrinterOptions) -> Self {
Self {
source_code,
options,
state: PrinterState::with_capacity(source_code.as_str().len()),
}
}
/// Prints the passed in element as well as all its content
pub fn print(self, document: &'a Document) -> PrintResult<Printed> {
self.print_with_indent(document, 0)
}
/// Prints the passed in element as well as all its content,
/// starting at the specified indentation level
#[tracing::instrument(level = "debug", name = "Printer::print", skip_all)]
pub fn print_with_indent(
mut self,
document: &'a Document,
indent: u16,
) -> PrintResult<Printed> {
let indentation = Indention::Level(indent);
self.state.pending_indent = indentation;
let mut stack = PrintCallStack::new(PrintElementArgs::new(indentation));
let mut queue: PrintQueue<'a> = PrintQueue::new(document.as_ref());
loop {
if let Some(element) = queue.pop() {
self.print_element(&mut stack, &mut queue, element)?;
} else {
if !self.flush_line_suffixes(&mut queue, &mut stack, None) {
break;
}
}
}
Ok(Printed::new(
self.state.buffer,
None,
self.state.source_markers,
self.state.verbatim_markers,
))
}
/// Prints a single element and push the following elements to queue
fn print_element(
&mut self,
stack: &mut PrintCallStack,
queue: &mut PrintQueue<'a>,
element: &'a FormatElement,
) -> PrintResult<()> {
#[allow(clippy::enum_glob_use)]
use Tag::*;
let args = stack.top();
match element {
FormatElement::Space => self.print_text(Text::Token(" "), None),
FormatElement::Token { text } => self.print_text(Text::Token(text), None),
FormatElement::Text { text, text_width } => self.print_text(
Text::Text {
text,
text_width: *text_width,
},
None,
),
FormatElement::SourceCodeSlice { slice, text_width } => {
let text = slice.text(self.source_code);
self.print_text(
Text::Text {
text,
text_width: *text_width,
},
Some(slice.range()),
);
}
FormatElement::Line(line_mode) => {
if args.mode().is_flat()
&& matches!(line_mode, LineMode::Soft | LineMode::SoftOrSpace)
{
if line_mode == &LineMode::SoftOrSpace {
self.print_text(Text::Token(" "), None);
}
} else if self.state.line_suffixes.has_pending() {
self.flush_line_suffixes(queue, stack, Some(element));
} else {
// Only print a newline if the current line isn't already empty
if self.state.line_width > 0 {
self.print_char('\n');
}
// Print a second line break if this is an empty line
if line_mode == &LineMode::Empty {
self.print_char('\n');
}
self.state.pending_indent = args.indention();
}
}
FormatElement::ExpandParent => {
// Handled in `Document::propagate_expands()
}
FormatElement::SourcePosition(position) => {
self.state.source_position = *position;
// The printer defers printing indents until the next text
// is printed. Pushing the marker now would mean that the
// mapped range includes the indent range, which we don't want.
// Only add a marker if we're not in an indented context, e.g. at the end of the file.
if self.state.pending_indent.is_empty() {
self.push_marker();
}
}
FormatElement::LineSuffixBoundary => {
const HARD_BREAK: &FormatElement = &FormatElement::Line(LineMode::Hard);
self.flush_line_suffixes(queue, stack, Some(HARD_BREAK));
}
FormatElement::BestFitting { variants, mode } => {
self.print_best_fitting(variants, *mode, queue, stack)?;
}
FormatElement::Interned(content) => {
queue.extend_back(content);
}
FormatElement::Tag(StartGroup(group)) => {
let print_mode = match group.mode() {
GroupMode::Expand | GroupMode::Propagated => PrintMode::Expanded,
GroupMode::Flat => {
self.flat_group_print_mode(TagKind::Group, group.id(), args, queue, stack)?
}
};
if let Some(id) = group.id() {
self.state.group_modes.insert_print_mode(id, print_mode);
}
stack.push(TagKind::Group, args.with_print_mode(print_mode));
}
FormatElement::Tag(StartBestFitParenthesize { id }) => {
const OPEN_PAREN: FormatElement = FormatElement::Token { text: "(" };
const INDENT: FormatElement = FormatElement::Tag(Tag::StartIndent);
const HARD_LINE_BREAK: FormatElement = FormatElement::Line(LineMode::Hard);
let fits_flat = self.flat_group_print_mode(
TagKind::BestFitParenthesize,
*id,
args,
queue,
stack,
)? == PrintMode::Flat;
let print_mode = if fits_flat {
PrintMode::Flat
} else {
// Test if the content fits in expanded mode. If not, prefer avoiding the parentheses
// over parenthesizing the expression.
if let Some(id) = id {
self.state
.group_modes
.insert_print_mode(*id, PrintMode::Expanded);
}
stack.push(
TagKind::BestFitParenthesize,
args.with_measure_mode(MeasureMode::AllLines),
);
queue.extend_back(&[OPEN_PAREN, INDENT, HARD_LINE_BREAK]);
let fits_expanded = self.fits(queue, stack)?;
queue.pop_slice();
stack.pop(TagKind::BestFitParenthesize)?;
if fits_expanded {
PrintMode::Expanded
} else {
PrintMode::Flat
}
};
if let Some(id) = id {
self.state.group_modes.insert_print_mode(*id, print_mode);
}
if print_mode.is_expanded() {
// Parenthesize the content. The `EndIndent` is handled inside of the `EndBestFitParenthesize`
queue.extend_back(&[OPEN_PAREN, INDENT, HARD_LINE_BREAK]);
}
stack.push(
TagKind::BestFitParenthesize,
args.with_print_mode(print_mode),
);
}
FormatElement::Tag(EndBestFitParenthesize) => {
if args.mode().is_expanded() {
const HARD_LINE_BREAK: FormatElement = FormatElement::Line(LineMode::Hard);
const CLOSE_PAREN: FormatElement = FormatElement::Token { text: ")" };
// Finish the indent and print the hardline break and closing parentheses.
stack.pop(TagKind::Indent)?;
queue.extend_back(&[HARD_LINE_BREAK, CLOSE_PAREN]);
}
stack.pop(TagKind::BestFitParenthesize)?;
}
FormatElement::Tag(StartConditionalGroup(group)) => {
let condition = group.condition();
let expected_mode = match condition.group_id {
None => args.mode(),
Some(id) => self.state.group_modes.get_print_mode(id)?,
};
if expected_mode == condition.mode {
let print_mode = match group.mode() {
GroupMode::Expand | GroupMode::Propagated => PrintMode::Expanded,
GroupMode::Flat => self.flat_group_print_mode(
TagKind::ConditionalGroup,
None,
args,
queue,
stack,
)?,
};
stack.push(TagKind::ConditionalGroup, args.with_print_mode(print_mode));
} else {
// Condition isn't met, render as normal content
stack.push(TagKind::ConditionalGroup, args);
}
}
FormatElement::Tag(StartFill) => {
self.print_fill_entries(queue, stack)?;
}
FormatElement::Tag(StartIndent) => {
stack.push(
TagKind::Indent,
args.increment_indent_level(self.options.indent_style()),
);
}
FormatElement::Tag(StartDedent(mode)) => {
let args = match mode {
DedentMode::Level => args.decrement_indent(),
DedentMode::Root => args.reset_indent(),
};
stack.push(TagKind::Dedent, args);
}
FormatElement::Tag(StartAlign(align)) => {
stack.push(TagKind::Align, args.set_indent_align(align.count()));
}
FormatElement::Tag(StartConditionalContent(Condition { mode, group_id })) => {
let group_mode = match group_id {
None => args.mode(),
Some(id) => self.state.group_modes.get_print_mode(*id)?,
};
if *mode == group_mode {
stack.push(TagKind::ConditionalContent, args);
} else {
queue.skip_content(TagKind::ConditionalContent);
}
}
FormatElement::Tag(StartIndentIfGroupBreaks(group_id)) => {
let group_mode = self.state.group_modes.get_print_mode(*group_id)?;
let args = match group_mode {
PrintMode::Flat => args,
PrintMode::Expanded => args.increment_indent_level(self.options.indent_style),
};
stack.push(TagKind::IndentIfGroupBreaks, args);
}
FormatElement::Tag(StartLineSuffix { reserved_width }) => {
self.state.line_width += reserved_width;
self.state
.line_suffixes
.extend(args, queue.iter_content(TagKind::LineSuffix));
}
FormatElement::Tag(StartVerbatim(kind)) => {
if let VerbatimKind::Verbatim { length } = kind {
// SAFETY: Ruff only supports formatting files <= 4GB
#[allow(clippy::cast_possible_truncation)]
self.state.verbatim_markers.push(TextRange::at(
TextSize::from(self.state.buffer.len() as u32),
*length,
));
}
stack.push(TagKind::Verbatim, args);
}
FormatElement::Tag(StartFitsExpanded(tag::FitsExpanded { condition, .. })) => {
let condition_met = match condition {
Some(condition) => {
let group_mode = match condition.group_id {
Some(group_id) => self.state.group_modes.get_print_mode(group_id)?,
None => args.mode(),
};
condition.mode == group_mode
}
None => true,
};
if condition_met {
// We measured the inner groups all in expanded. It now is necessary to measure if the inner groups fit as well.
self.state.measured_group_fits = false;
}
stack.push(TagKind::FitsExpanded, args);
}
FormatElement::Tag(
tag @ (StartLabelled(_) | StartEntry | StartBestFittingEntry { .. }),
) => {
stack.push(tag.kind(), args);
}
FormatElement::Tag(
tag @ (EndLabelled
| EndEntry
| EndGroup
| EndConditionalGroup
| EndIndent
| EndDedent
| EndAlign
| EndConditionalContent
| EndIndentIfGroupBreaks
| EndFitsExpanded
| EndVerbatim
| EndLineSuffix
| EndBestFittingEntry
| EndFill),
) => {
stack.pop(tag.kind())?;
}
};
Ok(())
}
fn fits(&mut self, queue: &PrintQueue<'a>, stack: &PrintCallStack) -> PrintResult<bool> {
let mut measure = FitsMeasurer::new(queue, stack, self);
let result = measure.fits(&mut AllPredicate);
measure.finish();
result
}
fn flat_group_print_mode(
&mut self,
kind: TagKind,
id: Option<GroupId>,
args: PrintElementArgs,
queue: &PrintQueue<'a>,
stack: &mut PrintCallStack,
) -> PrintResult<PrintMode> {
let print_mode = match args.mode() {
PrintMode::Flat if self.state.measured_group_fits => {
// A parent group has already verified that this group fits on a single line
// Thus, just continue in flat mode
PrintMode::Flat
}
// The printer is either in expanded mode or it's necessary to re-measure if the group fits
// because the printer printed a line break
_ => {
self.state.measured_group_fits = true;
if let Some(id) = id {
self.state
.group_modes
.insert_print_mode(id, PrintMode::Flat);
}
// Measure to see if the group fits up on a single line. If that's the case,
// print the group in "flat" mode, otherwise continue in expanded mode
stack.push(kind, args.with_print_mode(PrintMode::Flat));
let fits = self.fits(queue, stack)?;
stack.pop(kind)?;
if fits {
PrintMode::Flat
} else {
PrintMode::Expanded
}
}
};
Ok(print_mode)
}
fn print_text(&mut self, text: Text, source_range: Option<TextRange>) {
if !self.state.pending_indent.is_empty() {
let (indent_char, repeat_count) = match self.options.indent_style() {
IndentStyle::Tab => ('\t', 1),
IndentStyle::Space => (' ', self.options.indent_width()),
};
let indent = std::mem::take(&mut self.state.pending_indent);
let total_indent_char_count = indent.level() as usize * repeat_count as usize;
self.state
.buffer
.reserve(total_indent_char_count + indent.align() as usize);
for _ in 0..total_indent_char_count {
self.print_char(indent_char);
}
for _ in 0..indent.align() {
self.print_char(' ');
}
}
// Insert source map markers before and after the token
//
// If the token has source position information the start marker
// will use the start position of the original token, and the end
// marker will use that position + the text length of the token
//
// If the token has no source position (was created by the formatter)
// both the start and end marker will use the last known position
// in the input source (from state.source_position)
if let Some(range) = source_range {
self.state.source_position = range.start();
}
self.push_marker();
match text {
#[allow(clippy::cast_possible_truncation)]
Text::Token(token) => {
self.state.buffer.push_str(token);
self.state.line_width += token.len() as u32;
}
Text::Text {
text,
text_width: width,
} => {
if let Some(width) = width.width() {
self.state.buffer.push_str(text);
self.state.line_width += width.value();
} else {
for char in text.chars() {
self.print_char(char);
}
}
}
}
if let Some(range) = source_range {
self.state.source_position = range.end();
}
self.push_marker();
}
fn push_marker(&mut self) {
if self.options.source_map_generation.is_disabled() {
return;
}
let marker = SourceMarker {
source: self.state.source_position,
dest: self.state.buffer.text_len(),
};
if self
.state
.source_markers
.last()
.map_or(true, |last| last != &marker)
{
self.state.source_markers.push(marker);
}
}
fn flush_line_suffixes(
&mut self,
queue: &mut PrintQueue<'a>,
stack: &mut PrintCallStack,
line_break: Option<&'a FormatElement>,
) -> bool {
let suffixes = self.state.line_suffixes.take_pending();
if suffixes.len() > 0 {
// Print this line break element again once all the line suffixes have been flushed
if let Some(line_break) = line_break {
queue.push(line_break);
}
for entry in suffixes.rev() {
match entry {
LineSuffixEntry::Suffix(suffix) => {
queue.push(suffix);
}
LineSuffixEntry::Args(args) => {
const LINE_SUFFIX_END: &FormatElement =
&FormatElement::Tag(Tag::EndLineSuffix);
stack.push(TagKind::LineSuffix, args);
queue.push(LINE_SUFFIX_END);
}
}
}
true
} else {
false
}
}
fn print_best_fitting(
&mut self,
variants: &'a BestFittingVariants,
mode: BestFittingMode,
queue: &mut PrintQueue<'a>,
stack: &mut PrintCallStack,
) -> PrintResult<()> {
let args = stack.top();
if args.mode().is_flat() && self.state.measured_group_fits {
queue.extend_back(variants.most_flat());
self.print_entry(queue, stack, args, TagKind::BestFittingEntry)
} else {
self.state.measured_group_fits = true;
let mut variants_iter = variants.into_iter();
let mut current = variants_iter.next().unwrap();
for next in variants_iter {
// Test if this variant fits and if so, use it. Otherwise try the next
// variant.
// Try to fit only the first variant on a single line
if !matches!(
current.first(),
Some(&FormatElement::Tag(Tag::StartBestFittingEntry))
) {
return invalid_start_tag(TagKind::BestFittingEntry, current.first());
}
// Skip the first element because we want to override the args for the entry and the
// args must be popped from the stack as soon as it sees the matching end entry.
let content = &current[1..];
let entry_args = args
.with_print_mode(PrintMode::Flat)
.with_measure_mode(MeasureMode::from(mode));
queue.extend_back(content);
stack.push(TagKind::BestFittingEntry, entry_args);
let variant_fits = self.fits(queue, stack)?;
stack.pop(TagKind::BestFittingEntry)?;
// Remove the content slice because printing needs the variant WITH the start entry
let popped_slice = queue.pop_slice();
debug_assert_eq!(popped_slice, Some(content));
if variant_fits {
queue.extend_back(current);
return self.print_entry(
queue,
stack,
args.with_print_mode(PrintMode::Flat),
TagKind::BestFittingEntry,
);
}
current = next;
}
// At this stage current is the most expanded.
// No variant fits, take the last (most expanded) as fallback
queue.extend_back(current);
self.print_entry(
queue,
stack,
args.with_print_mode(PrintMode::Expanded),
TagKind::BestFittingEntry,
)
}
}
/// Tries to fit as much content as possible on a single line.
///
/// `Fill` is a sequence of *item*, *separator*, *item*, *separator*, *item*, ... entries.
/// The goal is to fit as many items (with their separators) on a single line as possible and
/// first expand the *separator* if the content exceeds the print width and only fallback to expanding
/// the *item*s if the *item* or the *item* and the expanded *separator* don't fit on the line.
///
/// The implementation handles the following 5 cases:
///
/// - The *item*, *separator*, and the *next item* fit on the same line.
/// Print the *item* and *separator* in flat mode.
/// - The *item* and *separator* fit on the line but there's not enough space for the *next item*.
/// Print the *item* in flat mode and the *separator* in expanded mode.
/// - The *item* fits on the line but the *separator* does not in flat mode.
/// Print the *item* in flat mode and the *separator* in expanded mode.
/// - The *item* fits on the line but the *separator* does not in flat **NOR** expanded mode.
/// Print the *item* and *separator* in expanded mode.
/// - The *item* does not fit on the line.
/// Print the *item* and *separator* in expanded mode.
fn print_fill_entries(
&mut self,
queue: &mut PrintQueue<'a>,
stack: &mut PrintCallStack,
) -> PrintResult<()> {
let args = stack.top();
// It's already known that the content fit, print all items in flat mode.
if self.state.measured_group_fits && args.mode().is_flat() {
stack.push(TagKind::Fill, args.with_print_mode(PrintMode::Flat));
return Ok(());
}
stack.push(TagKind::Fill, args);
while matches!(queue.top(), Some(FormatElement::Tag(Tag::StartEntry))) {
let mut measurer = FitsMeasurer::new_flat(queue, stack, self);
// The number of item/separator pairs that fit on the same line.
let mut flat_pairs = 0usize;
let mut item_fits = measurer.fill_item_fits()?;
let last_pair_layout = if item_fits {
// Measure the remaining pairs until the first item or separator that does not fit (or the end of the fill element).
// Optimisation to avoid re-measuring the next-item twice:
// * Once when measuring if the *item*, *separator*, *next-item* fit
// * A second time when measuring if *next-item*, *separator*, *next-next-item* fit.
loop {
// Item that fits without a following separator.
if !matches!(
measurer.queue.top(),
Some(FormatElement::Tag(Tag::StartEntry))
) {
break FillPairLayout::Flat;
}
let separator_fits = measurer.fill_separator_fits(PrintMode::Flat)?;
// Item fits but the flat separator does not.
if !separator_fits {
break FillPairLayout::ItemMaybeFlat;
}
// Last item/separator pair that both fit
if !matches!(
measurer.queue.top(),
Some(FormatElement::Tag(Tag::StartEntry))
) {
break FillPairLayout::Flat;
}
item_fits = measurer.fill_item_fits()?;
if item_fits {
flat_pairs += 1;
} else {
// Item and separator both fit, but the next element doesn't.
// Print the separator in expanded mode and then re-measure if the item now
// fits in the next iteration of the outer loop.
break FillPairLayout::ItemFlatSeparatorExpanded;
}
}
} else {
// Neither item nor separator fit, print both in expanded mode.
FillPairLayout::Expanded
};
measurer.finish();
self.state.measured_group_fits = true;
// Print all pairs that fit in flat mode.
for _ in 0..flat_pairs {
self.print_fill_item(queue, stack, args.with_print_mode(PrintMode::Flat))?;
self.print_fill_separator(queue, stack, args.with_print_mode(PrintMode::Flat))?;
}
let item_mode = match last_pair_layout {
FillPairLayout::Flat | FillPairLayout::ItemFlatSeparatorExpanded => PrintMode::Flat,
FillPairLayout::Expanded => PrintMode::Expanded,
FillPairLayout::ItemMaybeFlat => {
let mut measurer = FitsMeasurer::new_flat(queue, stack, self);
// SAFETY: That the item fits is guaranteed by `ItemMaybeFlat`.
// Re-measuring is required to get the measurer in the correct state for measuring the separator.
assert!(measurer.fill_item_fits()?);
let separator_fits = measurer.fill_separator_fits(PrintMode::Expanded)?;
measurer.finish();
if separator_fits {
PrintMode::Flat
} else {
PrintMode::Expanded
}
}
};
self.print_fill_item(queue, stack, args.with_print_mode(item_mode))?;
if matches!(queue.top(), Some(FormatElement::Tag(Tag::StartEntry))) {
let separator_mode = match last_pair_layout {
FillPairLayout::Flat => PrintMode::Flat,
FillPairLayout::ItemFlatSeparatorExpanded
| FillPairLayout::Expanded
| FillPairLayout::ItemMaybeFlat => PrintMode::Expanded,
};
// Push a new stack frame with print mode `Flat` for the case where the separator gets printed in expanded mode
// but does contain a group to ensure that the group will measure "fits" with the "flat" versions of the next item/separator.
stack.push(TagKind::Fill, args.with_print_mode(PrintMode::Flat));
self.print_fill_separator(queue, stack, args.with_print_mode(separator_mode))?;
stack.pop(TagKind::Fill)?;
}
}
if queue.top() == Some(&FormatElement::Tag(Tag::EndFill)) {
Ok(())
} else {
invalid_end_tag(TagKind::Fill, stack.top_kind())
}
}
/// Semantic alias for [`Self::print_entry`] for fill items.
fn print_fill_item(
&mut self,
queue: &mut PrintQueue<'a>,
stack: &mut PrintCallStack,
args: PrintElementArgs,
) -> PrintResult<()> {
self.print_entry(queue, stack, args, TagKind::Entry)
}
/// Semantic alias for [`Self::print_entry`] for fill separators.
fn print_fill_separator(
&mut self,
queue: &mut PrintQueue<'a>,
stack: &mut PrintCallStack,
args: PrintElementArgs,
) -> PrintResult<()> {
self.print_entry(queue, stack, args, TagKind::Entry)
}
/// Fully print an element (print the element itself and all its descendants)
///
/// Unlike [`print_element`], this function ensures the entire element has
/// been printed when it returns and the queue is back to its original state
fn print_entry(
&mut self,
queue: &mut PrintQueue<'a>,
stack: &mut PrintCallStack,
args: PrintElementArgs,
kind: TagKind,
) -> PrintResult<()> {
let start_entry = queue.top();
if queue
.pop()
.is_some_and(|start| start.tag_kind() == Some(kind))
{
stack.push(kind, args);
} else {
return invalid_start_tag(kind, start_entry);
}
let mut depth = 1u32;
while let Some(element) = queue.pop() {
match element {
FormatElement::Tag(Tag::StartEntry | Tag::StartBestFittingEntry) => {
depth += 1;
}
FormatElement::Tag(end_tag @ (Tag::EndEntry | Tag::EndBestFittingEntry)) => {
depth -= 1;
// Reached the end entry, pop the entry from the stack and return.
if depth == 0 {
stack.pop(end_tag.kind())?;
return Ok(());
}
}
_ => {
// Fall through
}
}
self.print_element(stack, queue, element)?;
}
invalid_end_tag(kind, stack.top_kind())
}
fn print_char(&mut self, char: char) {
if char == '\n' {
self.state
.buffer
.push_str(self.options.line_ending.as_str());
self.state.line_width = 0;
// Fit's only tests if groups up to the first line break fit.
// The next group must re-measure if it still fits.
self.state.measured_group_fits = false;
} else {
self.state.buffer.push(char);
#[allow(clippy::cast_possible_truncation)]
let char_width = if char == '\t' {
self.options.indent_width.value()
} else {
// SAFETY: A u32 is sufficient to represent the width of a file <= 4GB
char.width().unwrap_or(0) as u32
};
self.state.line_width += char_width;
}
}
}
#[derive(Copy, Clone, Debug)]
enum FillPairLayout {
/// The item, separator, and next item fit. Print the first item and the separator in flat mode.
Flat,
/// The item and separator fit but the next element does not. Print the item in flat mode and
/// the separator in expanded mode.
ItemFlatSeparatorExpanded,
/// The item does not fit. Print the item and any potential separator in expanded mode.
Expanded,
/// The item fits but the separator does not in flat mode. If the separator fits in expanded mode then
/// print the item in flat and the separator in expanded mode, otherwise print both in expanded mode.
ItemMaybeFlat,
}
/// Printer state that is global to all elements.
/// Stores the result of the print operation (buffer and mappings) and at what
/// position the printer currently is.
#[derive(Default, Debug)]
struct PrinterState<'a> {
buffer: String,
source_markers: Vec<SourceMarker>,
source_position: TextSize,
pending_indent: Indention,
measured_group_fits: bool,
line_width: u32,
line_suffixes: LineSuffixes<'a>,
verbatim_markers: Vec<TextRange>,
group_modes: GroupModes,
// Re-used queue to measure if a group fits. Optimisation to avoid re-allocating a new
// vec every time a group gets measured
fits_stack: Vec<StackFrame>,
fits_queue: Vec<std::slice::Iter<'a, FormatElement>>,
}
impl<'a> PrinterState<'a> {
fn with_capacity(capacity: usize) -> Self {
Self {
buffer: String::with_capacity(capacity),
..Self::default()
}
}
}
/// Tracks the mode in which groups with ids are printed. Stores the groups at `group.id()` index.
/// This is based on the assumption that the group ids for a single document are dense.
#[derive(Debug, Default)]
struct GroupModes(Vec<Option<PrintMode>>);
impl GroupModes {
fn insert_print_mode(&mut self, group_id: GroupId, mode: PrintMode) {
let index = u32::from(group_id) as usize;
if self.0.len() <= index {
self.0.resize(index + 1, None);
}
self.0[index] = Some(mode);
}
fn get_print_mode(&self, group_id: GroupId) -> PrintResult<PrintMode> {
let index = u32::from(group_id) as usize;
match self.0.get(index) {
Some(Some(print_mode)) => Ok(*print_mode),
None | Some(None) => Err(PrintError::InvalidDocument(
InvalidDocumentError::UnknownGroupId { group_id },
)),
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum Indention {
/// Indent the content by `count` levels by using the indention sequence specified by the printer options.
Level(u16),
/// Indent the content by n-`level`s using the indention sequence specified by the printer options and `align` spaces.
Align { level: u16, align: NonZeroU8 },
}
impl Indention {
const fn is_empty(self) -> bool {
matches!(self, Indention::Level(0))
}
/// Creates a new indention level with a zero-indent.
const fn new() -> Self {
Indention::Level(0)
}
/// Returns the indention level
fn level(self) -> u16 {
match self {
Indention::Level(count) => count,
Indention::Align { level: indent, .. } => indent,
}
}
/// Returns the number of trailing align spaces or 0 if none
fn align(self) -> u8 {
match self {
Indention::Level(_) => 0,
Indention::Align { align, .. } => align.into(),
}
}
/// Increments the level by one.
///
/// The behaviour depends on the [`indent_style`][IndentStyle] if this is an [`Indent::Align`]:
/// - **Tabs**: `align` is converted into an indent. This results in `level` increasing by two: once for the align, once for the level increment
/// - **Spaces**: Increments the `level` by one and keeps the `align` unchanged.
/// Keeps any the current value is [`Indent::Align`] and increments the level by one.
fn increment_level(self, indent_style: IndentStyle) -> Self {
match self {
Indention::Level(count) => Indention::Level(count + 1),
// Increase the indent AND convert the align to an indent
Indention::Align { level, .. } if indent_style.is_tab() => Indention::Level(level + 2),
Indention::Align {
level: indent,
align,
} => Indention::Align {
level: indent + 1,
align,
},
}
}
/// Decrements the indent by one by:
/// - Reducing the level by one if this is [`Indent::Level`]
/// - Removing the `align` if this is [`Indent::Align`]
///
/// No-op if the level is already zero.
fn decrement(self) -> Self {
match self {
Indention::Level(level) => Indention::Level(level.saturating_sub(1)),
Indention::Align { level, .. } => Indention::Level(level),
}
}
/// Adds an `align` of `count` spaces to the current indention.
///
/// It increments the `level` value if the current value is [`Indent::IndentAlign`].
fn set_align(self, count: NonZeroU8) -> Self {
match self {
Indention::Level(indent_count) => Indention::Align {
level: indent_count,
align: count,
},
// Convert the existing align to an indent
Indention::Align { level: indent, .. } => Indention::Align {
level: indent + 1,
align: count,
},
}
}
}
impl Default for Indention {
fn default() -> Self {
Indention::new()
}
}
#[must_use = "FitsMeasurer must be finished."]
struct FitsMeasurer<'a, 'print> {
state: FitsState,
queue: FitsQueue<'a, 'print>,
stack: FitsCallStack<'print>,
printer: &'print mut Printer<'a>,
must_be_flat: bool,
/// Bomb that enforces that finish is explicitly called to restore the `fits_stack` and `fits_queue` vectors.
bomb: DebugDropBomb,
}
impl<'a, 'print> FitsMeasurer<'a, 'print> {}
impl<'a, 'print> FitsMeasurer<'a, 'print> {
fn new_flat(
print_queue: &'print PrintQueue<'a>,
print_stack: &'print PrintCallStack,
printer: &'print mut Printer<'a>,
) -> Self {
let mut measurer = Self::new(print_queue, print_stack, printer);
measurer.must_be_flat = true;
measurer
}
fn new(
print_queue: &'print PrintQueue<'a>,
print_stack: &'print PrintCallStack,
printer: &'print mut Printer<'a>,
) -> Self {
let saved_stack = std::mem::take(&mut printer.state.fits_stack);
let saved_queue = std::mem::take(&mut printer.state.fits_queue);
debug_assert!(saved_stack.is_empty());
debug_assert!(saved_queue.is_empty());
let fits_queue = FitsQueue::new(print_queue, saved_queue);
let fits_stack = FitsCallStack::new(print_stack, saved_stack);
let fits_state = FitsState {
pending_indent: printer.state.pending_indent,
line_width: printer.state.line_width,
has_line_suffix: printer.state.line_suffixes.has_pending(),
};
Self {
state: fits_state,
queue: fits_queue,
stack: fits_stack,
must_be_flat: false,
printer,
bomb: DebugDropBomb::new(
"MeasurerFits must be `finished` to restore the `fits_queue` and `fits_stack`.",
),
}
}
/// Tests if it's possible to print the content of the queue up to the first hard line break
/// or the end of the document on a single line without exceeding the line width.
fn fits<P>(&mut self, predicate: &mut P) -> PrintResult<bool>
where
P: FitsEndPredicate,
{
while let Some(element) = self.queue.pop() {
match self.fits_element(element)? {
Fits::Yes => return Ok(true),
Fits::No => {
return Ok(false);
}
Fits::Maybe => {
if predicate.is_end(element)? {
break;
}
continue;
}
}
}
Ok(true)
}
/// Tests if the content of a `Fill` item fits in [`PrintMode::Flat`].
///
/// Returns `Err` if the top element of the queue is not a [`Tag::StartEntry`]
/// or if the document has any mismatching start/end tags.
fn fill_item_fits(&mut self) -> PrintResult<bool> {
self.fill_entry_fits(PrintMode::Flat)
}
/// Tests if the content of a `Fill` separator fits with `mode`.
///
/// Returns `Err` if the top element of the queue is not a [`Tag::StartEntry`]
/// or if the document has any mismatching start/end tags.
fn fill_separator_fits(&mut self, mode: PrintMode) -> PrintResult<bool> {
self.fill_entry_fits(mode)
}
/// Tests if the elements between the [`Tag::StartEntry`] and [`Tag::EndEntry`]
/// of a fill item or separator fits with `mode`.
///
/// Returns `Err` if the queue isn't positioned at a [`Tag::StartEntry`] or if
/// the matching [`Tag::EndEntry`] is missing.
fn fill_entry_fits(&mut self, mode: PrintMode) -> PrintResult<bool> {
let start_entry = self.queue.top();
if !matches!(start_entry, Some(&FormatElement::Tag(Tag::StartEntry))) {
return invalid_start_tag(TagKind::Entry, start_entry);
}
self.stack
.push(TagKind::Fill, self.stack.top().with_print_mode(mode));
let mut predicate = SingleEntryPredicate::default();
let fits = self.fits(&mut predicate)?;
if predicate.is_done() {
self.stack.pop(TagKind::Fill)?;
}
Ok(fits)
}
/// Tests if the passed element fits on the current line or not.
fn fits_element(&mut self, element: &'a FormatElement) -> PrintResult<Fits> {
#[allow(clippy::enum_glob_use)]
use Tag::*;
let args = self.stack.top();
match element {
FormatElement::Space => return Ok(self.fits_text(Text::Token(" "), args)),
FormatElement::Line(line_mode) => {
match args.mode() {
PrintMode::Flat => match line_mode {
LineMode::SoftOrSpace => return Ok(self.fits_text(Text::Token(" "), args)),
LineMode::Soft => {}
LineMode::Hard | LineMode::Empty => {
return Ok(if self.must_be_flat {
Fits::No
} else {
Fits::Yes
});
}
},
PrintMode::Expanded => {
match args.measure_mode() {
MeasureMode::FirstLine => {
// Reachable if the restQueue contains an element with mode expanded because Expanded
// is what the mode's initialized to by default
// This means, the printer is outside of the current element at this point and any
// line break should be printed as regular line break
return Ok(Fits::Yes);
}
MeasureMode::AllLines | MeasureMode::AllLinesAllowTextOverflow => {
// Continue measuring on the next line
self.state.line_width = 0;
self.state.pending_indent = args.indention();
}
}
}
}
}
FormatElement::Token { text } => return Ok(self.fits_text(Text::Token(text), args)),
FormatElement::Text { text, text_width } => {
return Ok(self.fits_text(
Text::Text {
text,
text_width: *text_width,
},
args,
))
}
FormatElement::SourceCodeSlice { slice, text_width } => {
let text = slice.text(self.printer.source_code);
return Ok(self.fits_text(
Text::Text {
text,
text_width: *text_width,
},
args,
));
}
FormatElement::LineSuffixBoundary => {
if self.state.has_line_suffix {
return Ok(Fits::No);
}
}
FormatElement::ExpandParent => {
if self.must_be_flat {
return Ok(Fits::No);
}
}
FormatElement::SourcePosition(_) => {}
FormatElement::BestFitting { variants, mode } => {
let (slice, args) = match args.mode() {
PrintMode::Flat => (
variants.most_flat(),
args.with_measure_mode(MeasureMode::from(*mode)),
),
PrintMode::Expanded => (variants.most_expanded(), args),
};
if !matches!(
slice.first(),
Some(FormatElement::Tag(Tag::StartBestFittingEntry))
) {
return invalid_start_tag(TagKind::BestFittingEntry, slice.first());
}
self.stack.push(TagKind::BestFittingEntry, args);
self.queue.extend_back(&slice[1..]);
}
FormatElement::Interned(content) => self.queue.extend_back(content),
FormatElement::Tag(StartIndent) => {
self.stack.push(
TagKind::Indent,
args.increment_indent_level(self.options().indent_style()),
);
}
FormatElement::Tag(StartDedent(mode)) => {
let args = match mode {
DedentMode::Level => args.decrement_indent(),
DedentMode::Root => args.reset_indent(),
};
self.stack.push(TagKind::Dedent, args);
}
FormatElement::Tag(StartAlign(align)) => {
self.stack
.push(TagKind::Align, args.set_indent_align(align.count()));
}
FormatElement::Tag(StartGroup(group)) => {
return Ok(self.fits_group(TagKind::Group, group.mode(), group.id(), args));
}
FormatElement::Tag(StartBestFitParenthesize { id }) => {
if let Some(id) = id {
self.printer
.state
.group_modes
.insert_print_mode(*id, args.mode());
}
// Don't use the parenthesized with indent layout even when measuring expanded mode similar to `BestFitting`.
// This is to expand the left and not right after the `(` parentheses (it is okay to expand after the content that it wraps).
self.stack.push(TagKind::BestFitParenthesize, args);
}
FormatElement::Tag(EndBestFitParenthesize) => {
// If this is the end tag of the outer most parentheses for which we measure if it fits,
// pop the indent.
if args.mode().is_expanded() && self.stack.top_kind() == Some(TagKind::Indent) {
self.stack.pop(TagKind::Indent).unwrap();
let unindented = self.stack.pop(TagKind::BestFitParenthesize)?;
// There's a hard line break after the indent but don't return `Fits::Yes` here
// to ensure any trailing comments (that, unfortunately, are attached to the statement and not the expression)
// fit too.
self.state.line_width = 0;
self.state.pending_indent = unindented.indention();
return Ok(self.fits_text(Text::Token(")"), unindented));
}
self.stack.pop(TagKind::BestFitParenthesize)?;
}
FormatElement::Tag(StartConditionalGroup(group)) => {
let condition = group.condition();
let print_mode = match condition.group_id {
None => args.mode(),
Some(group_id) => self.group_modes().get_print_mode(group_id)?,
};
if condition.mode == print_mode {
return Ok(self.fits_group(
TagKind::ConditionalGroup,
group.mode(),
None,
args,
));
}
self.stack.push(TagKind::ConditionalGroup, args);
}
FormatElement::Tag(StartConditionalContent(condition)) => {
let print_mode = match condition.group_id {
None => args.mode(),
Some(group_id) => self.group_modes().get_print_mode(group_id)?,
};
if condition.mode == print_mode {
self.stack.push(TagKind::ConditionalContent, args);
} else {
self.queue.skip_content(TagKind::ConditionalContent);
}
}
FormatElement::Tag(StartIndentIfGroupBreaks(id)) => {
let print_mode = self.group_modes().get_print_mode(*id)?;
match print_mode {
PrintMode::Flat => {
self.stack.push(TagKind::IndentIfGroupBreaks, args);
}
PrintMode::Expanded => {
self.stack.push(
TagKind::IndentIfGroupBreaks,
args.increment_indent_level(self.options().indent_style()),
);
}
}
}
FormatElement::Tag(StartLineSuffix { reserved_width }) => {
if *reserved_width > 0 {
self.state.line_width += reserved_width;
if self.state.line_width > self.options().line_width.into() {
return Ok(Fits::No);
}
}
self.queue.skip_content(TagKind::LineSuffix);
self.state.has_line_suffix = true;
}
FormatElement::Tag(EndLineSuffix) => {
return invalid_end_tag(TagKind::LineSuffix, self.stack.top_kind());
}
FormatElement::Tag(StartFitsExpanded(tag::FitsExpanded {
condition,
propagate_expand,
})) => {
match args.mode() {
PrintMode::Expanded => {
// As usual, nothing to measure
self.stack.push(TagKind::FitsExpanded, args);
}
PrintMode::Flat => {
let condition_met = match condition {
Some(condition) => {
let group_mode = match condition.group_id {
Some(group_id) => {
self.group_modes().get_print_mode(group_id)?
}
None => args.mode(),
};
condition.mode == group_mode
}
None => true,
};
if condition_met {
// Measure in fully expanded mode and allow overflows
self.stack.push(
TagKind::FitsExpanded,
args.with_measure_mode(MeasureMode::AllLinesAllowTextOverflow)
.with_print_mode(PrintMode::Expanded),
);
} else {
if propagate_expand.get() {
return Ok(Fits::No);
}
// As usual
self.stack.push(TagKind::FitsExpanded, args);
}
}
}
}
FormatElement::Tag(
tag @ (StartFill
| StartVerbatim(_)
| StartLabelled(_)
| StartEntry
| StartBestFittingEntry { .. }),
) => {
self.stack.push(tag.kind(), args);
}
FormatElement::Tag(
tag @ (EndFill
| EndVerbatim
| EndLabelled
| EndEntry
| EndGroup
| EndConditionalGroup
| EndIndentIfGroupBreaks
| EndConditionalContent
| EndAlign
| EndDedent
| EndIndent
| EndBestFittingEntry
| EndFitsExpanded),
) => {
self.stack.pop(tag.kind())?;
}
}
Ok(Fits::Maybe)
}
fn fits_group(
&mut self,
kind: TagKind,
group_mode: GroupMode,
id: Option<GroupId>,
args: PrintElementArgs,
) -> Fits {
if self.must_be_flat && !group_mode.is_flat() {
return Fits::No;
}
// Continue printing groups in expanded mode if measuring a `best_fitting` element where
// a group expands.
let print_mode = if group_mode.is_flat() {
args.mode()
} else {
PrintMode::Expanded
};
self.stack.push(kind, args.with_print_mode(print_mode));
if let Some(id) = id {
self.group_modes_mut().insert_print_mode(id, print_mode);
}
Fits::Maybe
}
fn fits_text(&mut self, text: Text, args: PrintElementArgs) -> Fits {
fn exceeds_width(fits: &FitsMeasurer, args: PrintElementArgs) -> bool {
fits.state.line_width > fits.options().line_width.into()
&& !args.measure_mode().allows_text_overflow()
}
let indent = std::mem::take(&mut self.state.pending_indent);
self.state.line_width +=
u32::from(indent.level()) * self.options().indent_width() + u32::from(indent.align());
match text {
#[allow(clippy::cast_possible_truncation)]
Text::Token(token) => {
self.state.line_width += token.len() as u32;
}
Text::Text { text, text_width } => {
if let Some(width) = text_width.width() {
self.state.line_width += width.value();
} else {
for c in text.chars() {
let char_width = match c {
'\t' => self.options().indent_width.value(),
'\n' => {
if self.must_be_flat {
return Fits::No;
}
match args.measure_mode() {
MeasureMode::FirstLine => {
return if exceeds_width(self, args) {
Fits::No
} else {
Fits::Yes
};
}
MeasureMode::AllLines
| MeasureMode::AllLinesAllowTextOverflow => {
self.state.line_width = 0;
continue;
}
}
}
// SAFETY: A u32 is sufficient to format files <= 4GB
#[allow(clippy::cast_possible_truncation)]
c => c.width().unwrap_or(0) as u32,
};
self.state.line_width += char_width;
}
}
}
}
if exceeds_width(self, args) {
return Fits::No;
}
Fits::Maybe
}
fn finish(mut self) {
self.bomb.defuse();
let mut queue = self.queue.finish();
queue.clear();
self.printer.state.fits_queue = queue;
let mut stack = self.stack.finish();
stack.clear();
self.printer.state.fits_stack = stack;
}
fn options(&self) -> &PrinterOptions {
&self.printer.options
}
fn group_modes(&self) -> &GroupModes {
&self.printer.state.group_modes
}
fn group_modes_mut(&mut self) -> &mut GroupModes {
&mut self.printer.state.group_modes
}
}
#[cold]
fn invalid_end_tag<R>(end_tag: TagKind, start_tag: Option<TagKind>) -> PrintResult<R> {
Err(PrintError::InvalidDocument(match start_tag {
None => InvalidDocumentError::StartTagMissing { kind: end_tag },
Some(kind) => InvalidDocumentError::StartEndTagMismatch {
start_kind: end_tag,
end_kind: kind,
},
}))
}
#[cold]
fn invalid_start_tag<R>(expected: TagKind, actual: Option<&FormatElement>) -> PrintResult<R> {
let start = match actual {
None => ActualStart::EndOfDocument,
Some(FormatElement::Tag(tag)) => {
if tag.is_start() {
ActualStart::Start(tag.kind())
} else {
ActualStart::End(tag.kind())
}
}
Some(_) => ActualStart::Content,
};
Err(PrintError::InvalidDocument(
InvalidDocumentError::ExpectedStart {
actual: start,
expected_start: expected,
},
))
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum Fits {
// Element fits
Yes,
// Element doesn't fit
No,
// Element may fit, depends on the elements following it
Maybe,
}
impl From<bool> for Fits {
fn from(value: bool) -> Self {
if value {
Fits::Yes
} else {
Fits::No
}
}
}
/// State used when measuring if a group fits on a single line
#[derive(Debug)]
struct FitsState {
pending_indent: Indention,
has_line_suffix: bool,
line_width: u32,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum MeasureMode {
/// The content fits if a hard line break or soft line break in [`PrintMode::Expanded`] is seen
/// before exceeding the configured print width.
/// Returns
FirstLine,
/// The content only fits if none of the lines exceed the print width. Lines are terminated by either
/// a hard line break or a soft line break in [`PrintMode::Expanded`].
AllLines,
/// Measures all lines and allows lines to exceed the configured line width. Useful when it only matters
/// whether the content *before* and *after* fits.
AllLinesAllowTextOverflow,
}
impl MeasureMode {
/// Returns `true` if this mode allows text exceeding the configured line width.
const fn allows_text_overflow(self) -> bool {
matches!(self, MeasureMode::AllLinesAllowTextOverflow)
}
}
impl From<BestFittingMode> for MeasureMode {
fn from(value: BestFittingMode) -> Self {
match value {
BestFittingMode::FirstLine => Self::FirstLine,
BestFittingMode::AllLines => Self::AllLines,
}
}
}
#[derive(Copy, Clone, Debug)]
enum Text<'a> {
/// ASCII only text that contains no line breaks or tab characters.
Token(&'a str),
/// Arbitrary text. May contain `\n` line breaks, tab characters, or unicode characters.
Text {
text: &'a str,
text_width: TextWidth,
},
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
use crate::printer::{LineEnding, Printer, PrinterOptions};
use crate::source_code::SourceCode;
use crate::{
format_args, write, Document, FormatState, IndentStyle, IndentWidth, LineWidth, Printed,
VecBuffer,
};
fn format(root: &dyn Format<SimpleFormatContext>) -> Printed {
format_with_options(
root,
PrinterOptions {
indent_style: IndentStyle::Space,
..PrinterOptions::default()
},
)
}
fn format_with_options(
root: &dyn Format<SimpleFormatContext>,
options: PrinterOptions,
) -> Printed {
let formatted = crate::format!(SimpleFormatContext::default(), [root]).unwrap();
Printer::new(SourceCode::default(), options)
.print(formatted.document())
.expect("Document to be valid")
}
#[test]
fn it_prints_a_group_on_a_single_line_if_it_fits() {
let result = format(&FormatArrayElements {
items: vec![
&token("\"a\""),
&token("\"b\""),
&token("\"c\""),
&token("\"d\""),
],
});
assert_eq!(r#"["a", "b", "c", "d"]"#, result.as_code());
}
#[test]
fn it_tracks_the_indent_for_each_token() {
let formatted = format(&format_args!(
token("a"),
soft_block_indent(&format_args!(
token("b"),
soft_block_indent(&format_args!(
token("c"),
soft_block_indent(&format_args!(token("d"), soft_line_break(), token("d"),)),
token("c"),
)),
token("b"),
)),
token("a")
));
assert_eq!(
"a
b
c
d
d
c
b
a",
formatted.as_code()
);
}
#[test]
fn it_converts_line_endings() {
let options = PrinterOptions {
line_ending: LineEnding::CarriageReturnLineFeed,
..PrinterOptions::default()
};
let result = format_with_options(
&format_args![
token("function main() {"),
block_indent(&text("let x = `This is a multiline\nstring`;", None)),
token("}"),
hard_line_break()
],
options,
);
assert_eq!(
"function main() {\r\n\tlet x = `This is a multiline\r\nstring`;\r\n}\r\n",
result.as_code()
);
}
#[test]
fn it_breaks_a_group_if_a_string_contains_a_newline() {
let result = format(&FormatArrayElements {
items: vec![
&text("`This is a string spanning\ntwo lines`", None),
&token("\"b\""),
],
});
assert_eq!(
r#"[
`This is a string spanning
two lines`,
"b",
]"#,
result.as_code()
);
}
#[test]
fn it_breaks_a_group_if_it_contains_a_hard_line_break() {
let result = format(&group(&format_args![token("a"), block_indent(&token("b"))]));
assert_eq!("a\n b\n", result.as_code());
}
#[test]
fn it_breaks_parent_groups_if_they_dont_fit_on_a_single_line() {
let result = format(&FormatArrayElements {
items: vec![
&token("\"a\""),
&token("\"b\""),
&token("\"c\""),
&token("\"d\""),
&FormatArrayElements {
items: vec![
&token("\"0123456789\""),
&token("\"0123456789\""),
&token("\"0123456789\""),
&token("\"0123456789\""),
&token("\"0123456789\""),
],
},
],
});
assert_eq!(
r#"[
"a",
"b",
"c",
"d",
["0123456789", "0123456789", "0123456789", "0123456789", "0123456789"],
]"#,
result.as_code()
);
}
#[test]
fn it_use_the_indent_character_specified_in_the_options() {
let options = PrinterOptions {
indent_style: IndentStyle::Tab,
indent_width: IndentWidth::try_from(4).unwrap(),
line_width: LineWidth::try_from(19).unwrap(),
..PrinterOptions::default()
};
let result = format_with_options(
&FormatArrayElements {
items: vec![&token("'a'"), &token("'b'"), &token("'c'"), &token("'d'")],
},
options,
);
assert_eq!("[\n\t'a',\n\t\'b',\n\t\'c',\n\t'd',\n]", result.as_code());
}
#[test]
fn it_prints_consecutive_hard_lines_as_one() {
let result = format(&format_args![
token("a"),
hard_line_break(),
hard_line_break(),
hard_line_break(),
token("b"),
]);
assert_eq!("a\nb", result.as_code());
}
#[test]
fn it_prints_consecutive_empty_lines_as_many() {
let result = format(&format_args![
token("a"),
empty_line(),
empty_line(),
empty_line(),
token("b"),
]);
assert_eq!("a\n\n\n\nb", result.as_code());
}
#[test]
fn it_prints_consecutive_mixed_lines_as_many() {
let result = format(&format_args![
token("a"),
empty_line(),
hard_line_break(),
empty_line(),
hard_line_break(),
token("b"),
]);
assert_eq!("a\n\n\nb", result.as_code());
}
#[test]
fn test_fill_breaks() {
let mut state = FormatState::new(SimpleFormatContext::default());
let mut buffer = VecBuffer::new(&mut state);
let mut formatter = Formatter::new(&mut buffer);
formatter
.fill()
// These all fit on the same line together
.entry(
&soft_line_break_or_space(),
&format_args!(token("1"), token(",")),
)
.entry(
&soft_line_break_or_space(),
&format_args!(token("2"), token(",")),
)
.entry(
&soft_line_break_or_space(),
&format_args!(token("3"), token(",")),
)
// This one fits on a line by itself,
.entry(
&soft_line_break_or_space(),
&format_args!(token("723493294"), token(",")),
)
// fits without breaking
.entry(
&soft_line_break_or_space(),
&group(&format_args!(
token("["),
soft_block_indent(&token("5")),
token("],")
)),
)
// this one must be printed in expanded mode to fit
.entry(
&soft_line_break_or_space(),
&group(&format_args!(
token("["),
soft_block_indent(&token("123456789")),
token("]"),
)),
)
.finish()
.unwrap();
let document = Document::from(buffer.into_vec());
let printed = Printer::new(
SourceCode::default(),
PrinterOptions::default().with_line_width(LineWidth::try_from(10).unwrap()),
)
.print(&document)
.unwrap();
assert_eq!(
printed.as_code(),
"1, 2, 3,\n723493294,\n[5],\n[\n\t123456789\n]"
);
}
#[test]
fn line_suffix_printed_at_end() {
let printed = format(&format_args![
group(&format_args![
token("["),
soft_block_indent(&format_with(|f| {
f.fill()
.entry(
&soft_line_break_or_space(),
&format_args!(token("1"), token(",")),
)
.entry(
&soft_line_break_or_space(),
&format_args!(token("2"), token(",")),
)
.entry(
&soft_line_break_or_space(),
&format_args!(token("3"), if_group_breaks(&token(","))),
)
.finish()
})),
token("]")
]),
token(";"),
line_suffix(&format_args![space(), token("// trailing")], 0)
]);
assert_eq!(printed.as_code(), "[1, 2, 3]; // trailing");
}
#[test]
fn line_suffix_with_reserved_width() {
let printed = format(&format_args![
group(&format_args![
token("["),
soft_block_indent(&format_with(|f| {
f.fill()
.entry(
&soft_line_break_or_space(),
&format_args!(token("1"), token(",")),
)
.entry(
&soft_line_break_or_space(),
&format_args!(token("2"), token(",")),
)
.entry(
&soft_line_break_or_space(),
&format_args!(token("3"), if_group_breaks(&token(","))),
)
.finish()
})),
token("]")
]),
token(";"),
line_suffix(&format_args![space(), token("// Using reserved width causes this content to not fit even though it's a line suffix element")], 93)
]);
assert_eq!(printed.as_code(), "[\n 1, 2, 3\n]; // Using reserved width causes this content to not fit even though it's a line suffix element");
}
#[test]
fn conditional_with_group_id_in_fits() {
let content = format_with(|f| {
let group_id = f.group_id("test");
write!(
f,
[
group(&format_args![
token("The referenced group breaks."),
hard_line_break()
])
.with_group_id(Some(group_id)),
group(&format_args![
token("This group breaks because:"),
soft_line_break_or_space(),
if_group_fits_on_line(&token("This content fits but should not be printed.")).with_group_id(Some(group_id)),
if_group_breaks(&token("It measures with the 'if_group_breaks' variant because the referenced group breaks and that's just way too much text.")).with_group_id(Some(group_id)),
])
]
)
});
let printed = format(&content);
assert_eq!(printed.as_code(), "The referenced group breaks.\nThis group breaks because:\nIt measures with the 'if_group_breaks' variant because the referenced group breaks and that's just way too much text.");
}
#[test]
fn out_of_order_group_ids() {
let content = format_with(|f| {
let id_1 = f.group_id("id-1");
let id_2 = f.group_id("id-2");
write!(
f,
[
group(&token("Group with id-2")).with_group_id(Some(id_2)),
hard_line_break()
]
)?;
write!(
f,
[
group(&token("Group with id-1 does not fit on the line because it exceeds the line width of 80 characters by")).with_group_id(Some(id_1)),
hard_line_break()
]
)?;
write!(
f,
[
if_group_fits_on_line(&token("Group 2 fits")).with_group_id(Some(id_2)),
hard_line_break(),
if_group_breaks(&token("Group 1 breaks")).with_group_id(Some(id_1))
]
)
});
let printed = format(&content);
assert_eq!(
printed.as_code(),
"Group with id-2
Group with id-1 does not fit on the line because it exceeds the line width of 80 characters by
Group 2 fits
Group 1 breaks"
);
}
struct FormatArrayElements<'a> {
items: Vec<&'a dyn Format<SimpleFormatContext>>,
}
impl Format<SimpleFormatContext> for FormatArrayElements<'_> {
fn fmt(&self, f: &mut Formatter<SimpleFormatContext>) -> FormatResult<()> {
write!(
f,
[group(&format_args!(
token("["),
soft_block_indent(&format_args!(
format_with(|f| f
.join_with(format_args!(token(","), soft_line_break_or_space()))
.entries(&self.items)
.finish()),
if_group_breaks(&token(",")),
)),
token("]")
))]
)
}
}
}