Skip to content

Commit

Permalink
Add BeestFittingMode
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaReiser committed Jun 19, 2023
1 parent 361d45f commit f672744
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 120 deletions.
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

0 comments on commit f672744

Please sign in to comment.