Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

Commit

Permalink
feature(formatter): Add BestFitting IR element
Browse files Browse the repository at this point in the history
This PR introduces the new IR element `BestFitting`. The IR matches Prettier's `ConditionalGroupContent`. This IR can be useful if the best representation depends on the available space. `BestFitting` defers the choice of how to print the element to the printer by providing multiple variants. The printer picks the element that best fits given the circumstances (that's where its name is coming from but I'm open for better names).

The printer picks the first variant that fits on the current line and falls back to print the last variant in expanded mode if no variant fits.
  • Loading branch information
MichaReiser authored and Micha Reiser committed May 17, 2022
1 parent 18b7dc1 commit 6b85f2f
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 5 deletions.
155 changes: 155 additions & 0 deletions crates/rome_formatter/src/format_element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,108 @@ pub fn hard_group_elements<T: Into<FormatElement>>(content: T) -> FormatElement
}
}

/// Provides multiple different alternatives and the printer picks the first one that fits.
/// Use this as last resort because it requires that the printer must try all variants in the worst case.
/// The passed variants must be in the following order:
/// * First: The variant that takes up most space horizontally
/// * Last: The variant that takes up the least space horizontally by splitting the content over multiple lines.
///
/// ## Examples
///
/// ```
/// use rome_formatter::{Formatted, LineWidth};
/// use rome_formatter::prelude::*;
///
/// let elements =
/// format_elements![token("aVeryLongIdentifier"),
/// best_fitting(vec![
/// // Everything fits on a single line
/// format_elements![
/// token("("),
/// group_elements(format_elements![
/// token("["),
/// soft_block_indent(format_elements![
/// token("1,"),
/// soft_line_break_or_space(),
/// token("2,"),
/// soft_line_break_or_space(),
/// token("3"),
/// ]),
/// token("]")
/// ]),
/// token(")")
/// ],
///
/// // Breaks after `[`, but prints all elements on a single line
/// format_elements![
/// token("("),
/// token("["),
/// block_indent(token("1, 2, 3")),
/// token("]"),
/// token(")"),
/// ],
///
/// // Breaks after `[` and prints each element on a single line
/// format_elements![
/// token("("),
/// block_indent(format_elements![
/// token("["),
/// block_indent(format_elements![
/// token("1,"),
/// hard_line_break(),
/// token("2,"),
/// hard_line_break(),
/// token("3"),
/// ]),
/// token("]"),
/// ]),
/// token(")")
/// ]
/// ])
/// ];
///
/// // Takes the first variant if everything fits on a single line
/// assert_eq!(
/// "aVeryLongIdentifier([1, 2, 3])",
/// Formatted::new(elements.clone(), PrinterOptions::default())
/// .print()
/// .as_code()
/// );
///
/// // It takes the second if the first variant doesn't fit on a single line. The second variant
/// // has some additional line breaks to make sure inner groups don't break
/// assert_eq!(
/// "aVeryLongIdentifier([\n\t1, 2, 3\n])",
/// Formatted::new(elements.clone(), PrinterOptions::default().with_print_width(21.try_into().unwrap()))
/// .print()
/// .as_code()
/// );
///
/// // Prints the last option as last resort
/// assert_eq!(
/// "aVeryLongIdentifier(\n\t[\n\t\t1,\n\t\t2,\n\t\t3\n\t]\n)",
/// Formatted::new(elements.clone(), PrinterOptions::default().with_print_width(20.try_into().unwrap()))
/// .print()
/// .as_code()
/// );
/// ```
///
/// ## Complexity
/// Be mindful of using this IR element as it has a considerable performance penalty:
/// * There are multiple representation for the same content. This results in increased memory usage
/// and traversal time in the printer.
/// * The worst case complexity is that the printer tires each variant. This can result in quadratic
/// complexity if used in nested structures.
///
/// ## Prettier
/// This IR is similar to Prettier's `ConditionalGroupContent` IR. It provides the same functionality but
/// differs in that Prettier automatically wraps each variant in a `Group`. Rome doesn't do so.
/// You can wrap the variant content in a group if you want to use soft line breaks.
#[inline]
pub fn best_fitting(variants: Vec<FormatElement>) -> FormatElement {
FormatElement::BestFitting(BestFitting::new(variants))
}

/// Adds a conditional content that is emitted only if it isn't inside an enclosing `Group` that
/// is printed on a single line. The element allows, for example, to insert a trailing comma after the last
/// array element only if the array doesn't fit on a single line.
Expand Down Expand Up @@ -1087,6 +1189,10 @@ pub enum FormatElement {

/// A token that tracks tokens/nodes that are printed using [`format_verbatim`](crate::Formatter::format_verbatim) API
Verbatim(Verbatim),

/// A list of different variants representing the same content. The printer picks the best fitting content.
/// Line breaks inside of a best fitting don't propagate to parent groups.
BestFitting(BestFitting),
}

#[derive(Clone, Eq, PartialEq)]
Expand Down Expand Up @@ -1168,6 +1274,10 @@ impl Debug for FormatElement {
.debug_tuple("Verbatim")
.field(&verbatim.element)
.finish(),
FormatElement::BestFitting(best_fitting) => {
write!(fmt, "BestFitting")?;
best_fitting.fmt(fmt)
}
}
}
}
Expand Down Expand Up @@ -1307,6 +1417,50 @@ impl PrintMode {
}
}

/// Provides the printer with different representations for the same element so that the printer
/// can pick the best fitting variant.
///
/// Best fitting is defined as the variant that takes the most horizontal space but fits on the line.
#[derive(Clone, Eq, PartialEq)]
pub struct BestFitting {
/// The different variants for this element.
/// The first element is the one that takes up the most space horizontally (the most flat),
/// The last element takes up the least space horizontally (but most horizontal space).
variants: Box<[FormatElement]>,
}

impl BestFitting {
pub fn new(variants: Vec<FormatElement>) -> Self {
assert!(variants.len() > 1, "The variants collection must contain at least two variants where the first is the least expanded state and the last element the most expanded option.");

Self {
variants: variants.into_boxed_slice(),
}
}

pub fn most_expanded(&self) -> &FormatElement {
self.variants.last().expect(
"Most contain at least two elements, as guaranteed by the constructor invariant.",
)
}

pub fn variants(&self) -> &[FormatElement] {
&self.variants
}

pub fn most_flat(&self) -> &FormatElement {
self.variants.first().expect(
"Most contain at least two elements, as guaranteed by the constructor invariant.",
)
}
}

impl Debug for BestFitting {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_list().entries(&*self.variants).finish()
}
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ConditionalGroupContent {
pub(crate) content: Content,
Expand Down Expand Up @@ -1556,6 +1710,7 @@ impl FormatElement {
FormatElement::LineSuffix(_) => false,
FormatElement::Comment(content) => content.will_break(),
FormatElement::Verbatim(verbatim) => verbatim.element.will_break(),
FormatElement::BestFitting(_) => false,
}
}

Expand Down
51 changes: 49 additions & 2 deletions crates/rome_formatter/src/printer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,45 @@ impl<'a> Printer<'a> {

queue.enqueue(PrintElementCall::new(&verbatim.element, args));
}
FormatElement::BestFitting(best_fitting) => {
match args.mode {
PrintMode::Flat if self.state.measured_group_fits => {
queue.enqueue(PrintElementCall::new(best_fitting.most_flat(), args))
}
_ => {
let last_index = best_fitting.variants().len() - 1;
for (index, variant) in best_fitting.variants().iter().enumerate() {
if index == last_index {
// No variant fits, take the last (most expanded) as fallback
queue.enqueue(PrintElementCall::new(
variant,
args.with_print_mode(PrintMode::Expanded),
));
break;
} else {
// Test if this variant fits and if so, use it. Otherwise try the next
// variant.

// TODO pass in proper queue to respect remaining content in document.
if fits_on_line(
&[variant],
args.with_print_mode(PrintMode::Expanded),
&ElementCallQueue::default(),
self,
) {
self.state.measured_group_fits = true;

queue.enqueue(PrintElementCall::new(
variant,
args.with_print_mode(PrintMode::Expanded),
));
return;
}
}
}
}
}
}
}
}

Expand Down Expand Up @@ -750,6 +789,14 @@ fn fits_element_on_line<'a, 'rest>(
FormatElement::Verbatim(verbatim) => {
queue.enqueue(PrintElementCall::new(&verbatim.element, args))
}
FormatElement::BestFitting(best_fitting) => {
let content = match args.mode {
PrintMode::Flat => best_fitting.most_flat(),
PrintMode::Expanded => best_fitting.most_expanded(),
};

queue.enqueue(PrintElementCall::new(content, args))
}
}

Fits::Maybe
Expand Down Expand Up @@ -1103,8 +1150,8 @@ two lines`,
]),
]);

let printed =
Printer::new(PrinterOptions::default().with_print_with(LineWidth(10))).print(&document);
let printed = Printer::new(PrinterOptions::default().with_print_width(LineWidth(10)))
.print(&document);

assert_eq!(
printed.as_code(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ pub struct PrinterOptions {
}

impl PrinterOptions {
pub fn with_print_with(mut self, with: LineWidth) -> Self {
self.print_width = with;
pub fn with_print_width(mut self, width: LineWidth) -> Self {
self.print_width = width;
self
}

Expand Down
2 changes: 1 addition & 1 deletion crates/rome_js_formatter/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl FormatOptions for JsFormatOptions {
fn as_print_options(&self) -> PrinterOptions {
PrinterOptions::default()
.with_indent(self.indent_style)
.with_print_with(self.line_width)
.with_print_width(self.line_width)
}
}

Expand Down

0 comments on commit 6b85f2f

Please sign in to comment.