Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BestFittingMode #5184

Merged
merged 1 commit into from
Jun 20, 2023
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
126 changes: 119 additions & 7 deletions crates/ruff_formatter/src/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2131,11 +2131,12 @@ impl<'a, 'buf, Context> FillBuilder<'a, 'buf, Context> {
/// The first variant is the most flat, and the last is the most expanded variant.
/// See [`best_fitting!`] macro for a more in-detail documentation
#[derive(Copy, Clone)]
pub struct FormatBestFitting<'a, Context> {
pub struct BestFitting<'a, Context> {
variants: Arguments<'a, Context>,
mode: BestFittingMode,
}

impl<'a, Context> FormatBestFitting<'a, Context> {
impl<'a, Context> BestFitting<'a, Context> {
/// 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.
Expand All @@ -2150,11 +2151,119 @@ impl<'a, Context> FormatBestFitting<'a, Context> {
"Requires at least the least expanded and most expanded variants"
);

Self { variants }
Self {
variants,
mode: BestFittingMode::default(),
}
}

/// Changes the mode used by this best fitting element to determine whether a variant fits.
///
/// ## Examples
///
/// ### All Lines
///
/// ```
/// use ruff_formatter::{Formatted, LineWidth, format, format_args, SimpleFormatOptions};
/// use ruff_formatter::prelude::*;
///
/// # fn main() -> FormatResult<()> {
/// let formatted = format!(
/// SimpleFormatContext::default(),
/// [
/// best_fitting!(
/// // Everything fits on a single line
/// format_args!(
/// group(&format_args![
/// text("["),
/// soft_block_indent(&format_args![
/// text("1,"),
/// soft_line_break_or_space(),
/// text("2,"),
/// soft_line_break_or_space(),
/// text("3"),
/// ]),
/// text("]")
/// ]),
/// space(),
/// text("+"),
/// space(),
/// text("aVeryLongIdentifier")
/// ),
///
/// // Breaks after `[` and prints each elements on a single line
/// // The group is necessary because the variant, by default is printed in flat mode and a
/// // hard line break indicates that the content doesn't fit.
/// format_args!(
/// text("["),
/// group(&block_indent(&format_args![text("1,"), hard_line_break(), text("2,"), hard_line_break(), text("3")])).should_expand(true),
/// text("]"),
/// space(),
/// text("+"),
/// space(),
/// text("aVeryLongIdentifier")
/// ),
///
/// // Adds parentheses and indents the body, breaks after the operator
/// format_args!(
/// text("("),
/// block_indent(&format_args![
/// text("["),
/// block_indent(&format_args![
/// text("1,"),
/// hard_line_break(),
/// text("2,"),
/// hard_line_break(),
/// text("3"),
/// ]),
/// text("]"),
/// hard_line_break(),
/// text("+"),
/// space(),
/// text("aVeryLongIdentifier")
/// ]),
/// text(")")
/// )
/// ).with_mode(BestFittingMode::AllLines)
/// ]
/// )?;
///
/// let document = formatted.into_document();
///
/// // Takes the first variant if everything fits on a single line
/// assert_eq!(
/// "[1, 2, 3] + aVeryLongIdentifier",
/// Formatted::new(document.clone(), SimpleFormatContext::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!(
/// "[\n\t1,\n\t2,\n\t3\n] + aVeryLongIdentifier",
/// Formatted::new(document.clone(), SimpleFormatContext::new(SimpleFormatOptions { line_width: 23.try_into().unwrap(), ..SimpleFormatOptions::default() }))
/// .print()?
/// .as_code()
/// );
///
/// // Prints the last option as last resort
/// assert_eq!(
/// "(\n\t[\n\t\t1,\n\t\t2,\n\t\t3\n\t]\n\t+ aVeryLongIdentifier\n)",
/// Formatted::new(document.clone(), SimpleFormatContext::new(SimpleFormatOptions { line_width: 22.try_into().unwrap(), ..SimpleFormatOptions::default() }))
/// .print()?
/// .as_code()
/// );
/// # Ok(())
/// # }
/// ```
pub fn with_mode(mut self, mode: BestFittingMode) -> Self {
self.mode = mode;
self
}
}

impl<Context> Format<Context> for FormatBestFitting<'_, Context> {
impl<Context> Format<Context> for BestFitting<'_, Context> {
fn fmt(&self, f: &mut Formatter<Context>) -> FormatResult<()> {
let mut buffer = VecBuffer::new(f.state_mut());
let variants = self.variants.items();
Expand All @@ -2172,9 +2281,12 @@ impl<Context> Format<Context> for FormatBestFitting<'_, Context> {
// SAFETY: The constructor guarantees that there are always at least two variants. It's, therefore,
// safe to call into the unsafe `from_vec_unchecked` function
let element = unsafe {
FormatElement::BestFitting(format_element::BestFitting::from_vec_unchecked(
formatted_variants,
))
FormatElement::BestFitting {
variants: format_element::BestFittingVariants::from_vec_unchecked(
formatted_variants,
),
mode: self.mode,
}
};

f.write_element(element)
Expand Down
99 changes: 71 additions & 28 deletions crates/ruff_formatter/src/format_element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::rc::Rc;

use crate::format_element::tag::{LabelId, Tag};
use crate::format_element::tag::{GroupMode, LabelId, Tag};
use crate::source_code::SourceCodeSlice;
use crate::TagKind;
use ruff_text_size::TextSize;
Expand Down Expand Up @@ -57,7 +57,10 @@ pub enum FormatElement {

/// 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),
BestFitting {
variants: BestFittingVariants,
mode: BestFittingMode,
},

/// A [Tag] that marks the start/end of some content to which some special formatting is applied.
Tag(Tag),
Expand All @@ -84,9 +87,11 @@ impl std::fmt::Debug for FormatElement {
.field(contains_newlines)
.finish(),
FormatElement::LineSuffixBoundary => write!(fmt, "LineSuffixBoundary"),
FormatElement::BestFitting(best_fitting) => {
fmt.debug_tuple("BestFitting").field(&best_fitting).finish()
}
FormatElement::BestFitting { variants, mode } => fmt
.debug_struct("BestFitting")
.field("variants", variants)
.field("mode", &mode)
.finish(),
FormatElement::Interned(interned) => {
fmt.debug_list().entries(interned.deref()).finish()
}
Expand Down Expand Up @@ -134,6 +139,15 @@ impl PrintMode {
}
}

impl From<GroupMode> for PrintMode {
fn from(value: GroupMode) -> Self {
match value {
GroupMode::Flat => PrintMode::Flat,
GroupMode::Expand | GroupMode::Propagated => PrintMode::Expanded,
}
}
}

#[derive(Clone)]
pub struct Interned(Rc<[FormatElement]>);

Expand Down Expand Up @@ -256,7 +270,10 @@ impl FormatElements for FormatElement {
FormatElement::Interned(interned) => interned.will_break(),
// Traverse into the most flat version because the content is guaranteed to expand when even
// the most flat version contains some content that forces a break.
FormatElement::BestFitting(best_fitting) => best_fitting.most_flat().will_break(),
FormatElement::BestFitting {
variants: best_fitting,
..
} => best_fitting.most_flat().will_break(),
FormatElement::LineSuffixBoundary
| FormatElement::Space
| FormatElement::Tag(_)
Expand Down Expand Up @@ -284,19 +301,36 @@ impl FormatElements for FormatElement {
}
}

/// 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<[Box<[FormatElement]>]>,
/// Mode used to determine if any variant (except the most expanded) fits for [`BestFittingVariants`].
#[repr(u8)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
pub enum BestFittingMode {
/// The variant fits if the content up to the first hard or a soft line break inside a [`Group`] with
/// [`PrintMode::Expanded`] fits on the line. The default mode.
///
/// [`Group`]: tag::Group
#[default]
FirstLine,

/// A variant fits if all lines fit into the configured print width. A line ends if by any
/// hard or a soft line break inside a [`Group`] with [`PrintMode::Expanded`].
/// The content doesn't fit if there's any hard line break outside a [`Group`] with [`PrintMode::Expanded`]
/// (a hard line break in content that should be considered in [`PrintMode::Flat`].
///
/// Use this mode with caution as it requires measuring all content of the variant which is more
/// expensive than using [`BestFittingMode::FirstLine`].
///
/// [`Group`]: tag::Group
AllLines,
}

impl 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).
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct BestFittingVariants(Box<[Box<[FormatElement]>]>);

impl BestFittingVariants {
/// 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.
Expand All @@ -312,33 +346,42 @@ impl BestFitting {
"Requires at least the least expanded and most expanded variants"
);

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

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

pub fn variants(&self) -> &[Box<[FormatElement]>] {
&self.variants
pub fn as_slice(&self) -> &[Box<[FormatElement]>] {
&self.0
}

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

impl std::fmt::Debug for BestFitting {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list().entries(&*self.variants).finish()
impl Deref for BestFittingVariants {
type Target = [Box<[FormatElement]>];

fn deref(&self) -> &Self::Target {
self.as_slice()
}
}

impl<'a> IntoIterator for &'a BestFittingVariants {
type Item = &'a Box<[FormatElement]>;
type IntoIter = std::slice::Iter<'a, Box<[FormatElement]>>;

fn into_iter(self) -> Self::IntoIter {
self.as_slice().iter()
}
}

Expand Down Expand Up @@ -397,7 +440,7 @@ mod sizes {
assert_eq_size!(ruff_text_size::TextRange, [u8; 8]);
assert_eq_size!(crate::prelude::tag::VerbatimKind, [u8; 8]);
assert_eq_size!(crate::prelude::Interned, [u8; 16]);
assert_eq_size!(crate::format_element::BestFitting, [u8; 16]);
assert_eq_size!(crate::format_element::BestFittingVariants, [u8; 16]);

#[cfg(not(debug_assertions))]
assert_eq_size!(crate::SourceCodeSlice, [u8; 8]);
Expand Down
18 changes: 14 additions & 4 deletions crates/ruff_formatter/src/format_element/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ impl Document {
interned_expands
}
},
FormatElement::BestFitting(best_fitting) => {
FormatElement::BestFitting { variants, mode: _ } => {
enclosing.push(Enclosing::BestFitting);

for variant in best_fitting.variants() {
for variant in variants {
propagate_expands(variant, enclosing, checked_interned);
}

Expand Down Expand Up @@ -280,14 +280,14 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
write!(f, [text("line_suffix_boundary")])?;
}

FormatElement::BestFitting(best_fitting) => {
FormatElement::BestFitting { variants, mode } => {
write!(f, [text("best_fitting([")])?;
f.write_elements([
FormatElement::Tag(StartIndent),
FormatElement::Line(LineMode::Hard),
])?;

for variant in best_fitting.variants() {
for variant in variants {
write!(f, [variant.deref(), hard_line_break()])?;
}

Expand All @@ -296,6 +296,16 @@ impl Format<IrFormatContext<'_>> for &[FormatElement] {
FormatElement::Line(LineMode::Hard),
])?;

if *mode != BestFittingMode::AllLines {
write!(
f,
[
dynamic_text(&std::format!("mode: {mode:?},"), None),
space()
]
)?;
}

write!(f, [text("])")])?;
}

Expand Down
Loading