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

feature(formatter): Add BestFitting IR element #2591

Merged
merged 3 commits into from
May 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions crates/rome_formatter/src/format_element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,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 @@ -1234,6 +1238,10 @@ impl Debug for FormatElement {
.debug_tuple("Verbatim")
.field(&verbatim.element)
.finish(),
FormatElement::BestFitting(best_fitting) => {
write!(fmt, "BestFitting")?;
best_fitting.fmt(fmt)
}
FormatElement::ExpandParent => write!(fmt, "ExpandParent"),
}
}
Expand Down Expand Up @@ -1374,6 +1382,64 @@ 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 {
/// Creates a new best fitting IR with the given variants. The method itself isn't unsafe
/// but it is to discourage people from using it because the printer will panic if
/// the slice doesn't contain at least the least and most expanded variants.
///
/// You're looking for a way to create a `BestFitting` object, use the `best_fitting![least_expanded, most_expanded]` macro.
///
/// ## Safety
/// The slice must contain at least two variants.
#[doc(hidden)]
pub unsafe fn from_slice_unchecked(variants: &[FormatElement]) -> Self {
debug_assert!(
variants.len() >= 2,
"Requires at least the least expanded and most expanded variants"
);

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

/// Returns the most expanded variant
pub fn most_expanded(&self) -> &FormatElement {
self.variants.last().expect(
"Most contain at least two elements, as guaranteed by the best fitting builder.",
)
}

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

/// Returns the least expanded variant
pub fn most_flat(&self) -> &FormatElement {
self.variants.first().expect(
"Most contain at least two elements, as guaranteed by the best fitting builder.",
)
}
}

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 @@ -1623,6 +1689,7 @@ impl FormatElement {
FormatElement::LineSuffix(_) => false,
FormatElement::Comment(content) => content.will_break(),
FormatElement::Verbatim(verbatim) => verbatim.element.will_break(),
FormatElement::BestFitting(_) => false,
FormatElement::LineSuffixBoundary => false,
FormatElement::ExpandParent => true,
}
Expand Down
107 changes: 107 additions & 0 deletions crates/rome_formatter/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,113 @@ macro_rules! __count_elements {
($_head:expr, $($tail:expr),* $(,)?) => {1usize + $crate::__count_elements!($($tail),*)};
}

/// 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!(
/// // 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.
#[macro_export]
macro_rules! best_fitting {
($least_expanded:expr, $($tail:expr),+ $(,)?) => {{
let inner = unsafe {
$crate::format_element::BestFitting::from_slice_unchecked(&[$least_expanded, $($tail),+])
};
FormatElement::BestFitting(inner)
}}
}

#[doc(hidden)]
pub struct FormatBuilder<O> {
builder: ConcatBuilder,
Expand Down
4 changes: 2 additions & 2 deletions crates/rome_formatter/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ pub use crate::formatter::Formatter;
pub use crate::printer::PrinterOptions;

pub use crate::{
format_elements, formatted, Format, Format as _, FormatError, FormatResult, FormatRule,
FormatWithRule as _, IntoFormatElement as _,
best_fitting, format_elements, formatted, Format, Format as _, FormatError, FormatResult,
FormatRule, FormatWithRule as _, IntoFormatElement as _,
};
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 @@ -247,6 +247,45 @@ impl<'a> Printer<'a> {
"Fits should always return false for `ExpandParent`"
);
}
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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to have the first variant printed in flat mode ? Or maybe it wouldn't make any difference

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's where I intentionally diverged from prettier as I outlined in the PR.

Prettier wraps each variant in an implicit Group which is why they try to fit each element with Flat first. We can do the same. The downside of this is that you often want to remove the implicit group from the second variant because you want to make that one break. Prettier does that by commonly wrapping all of the 1... variants in a group with shouldBreak: true.

We can also go a middle way and add an implicit group for the first variant but not for the other variants. But I'm not sure if that will be confusing.

));
return;
}
}
}
}
}
}
}
}

Expand Down Expand Up @@ -765,6 +804,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))
}
FormatElement::ExpandParent => {
if args.mode.is_flat() || args.hard_group {
return Fits::No;
Expand Down Expand Up @@ -1124,8 +1171,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