From d9e59b21cd850819134e07458bb9846f80c86707 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 20 Jun 2023 18:16:01 +0200 Subject: [PATCH] Add BestFittingMode (#5184) ## Summary Black supports for layouts when it comes to breaking binary expressions: ```rust #[derive(Copy, Clone, Debug, Eq, PartialEq)] enum BinaryLayout { /// Put each operand on their own line if either side expands Default, /// Try to expand the left to make it fit. Add parentheses if the left or right don't fit. /// ///```python /// [ /// a, /// b /// ] & c ///``` ExpandLeft, /// Try to expand the right to make it fix. Add parentheses if the left or right don't fit. /// /// ```python /// a & [ /// b, /// c /// ] /// ``` ExpandRight, /// Both the left and right side can be expanded. Try in the following order: /// * expand the right side /// * expand the left side /// * expand both sides /// /// to make the expression fit /// /// ```python /// [ /// a, /// b /// ] & [ /// c, /// d /// ] /// ``` ExpandRightThenLeft, } ``` Our current implementation only handles `ExpandRight` and `Default` correctly. `ExpandLeft` turns out to be surprisingly hard. This PR adds a new `BestFittingMode` parameter to `BestFitting` to support `ExpandLeft`. There are 3 variants that `ExpandLeft` must support: **Variant 1**: Everything fits on the line (easy) ```python [a, b] + c ``` **Variant 2**: Left breaks, but right fits on the line. Doesn't need parentheses ```python [ a, b ] + c ``` **Variant 3**: The left breaks, but there's still not enough space for the right hand side. Parenthesize the whole expression: ```python ( [ a, b ] + ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc ) ``` Solving Variant 1 and 2 on their own is straightforward The printer gives us this behavior by nesting right inside of the group of left: ``` group(&format_args![ if_group_breaks(&text("(")), soft_block_indent(&group(&format_args![ left, soft_line_break_or_space(), op, space(), group(&right) ])), if_group_breaks(&text(")")) ]) ``` The fundamental problem is that the outer group, which adds the parentheses, always breaks if the left side breaks. That means, we end up with ```python ( [ a, b ] + c ) ``` which is not what we want (we only want parentheses if the right side doesn't fit). Okay, so nesting groups don't work because of the outer parentheses. Sequencing groups doesn't work because it results in a right-to-left breaking which is the opposite of what we want. Could we use best fitting? Almost! ``` best_fitting![ // All flat format_args![left, space(), op, space(), right], // Break left format_args!(group(&left).should_expand(true), space(), op, space(), right], // Break all format_args![ text("("), block_indent!(&format_args![ left, hard_line_break(), op, space() right ]) ] ] ``` I hope I managed to write this up correctly. The problem is that the printer never reaches the 3rd variant because the second variant always fits: * The `group(&left).should_expand(true)` changes the group so that all `soft_line_breaks` are turned into hard line breaks. This is necessary because we want to test if the content fits if we break after the `[`. * Now, the whole idea of `best_fitting` is that you can pretend that some content fits on the line when it actually does not. The way this works is that the printer **only** tests if all the content of the variant **up to** the first line break fits on the line (we insert that line break by using `should_expand(true))`. The printer doesn't care whether the rest `a\n, b\n ] + c` all fits on (multiple?) lines. Why does breaking right work but not breaking the left? The difference is that we can make the decision whether to parenthesis the expression based on the left expression. We can't do this for breaking left because the decision whether to insert parentheses or not would depend on a lookahead: will the right side break. We simply don't know this yet when printing the parentheses (it would work for the right parentheses but not for the left and indent). What we kind of want here is to tell the printer: Look, what comes here may or may not fit on a single line but we don't care. Simply test that what comes **after** fits on a line. This PR adds a new `BestFittingMode` that has a new `AllLines` option that gives us the desired behavior of testing all content and not just up to the first line break. ## Test Plan I added a new example to `BestFitting::with_mode` --- crates/ruff_formatter/src/builders.rs | 126 ++++++++++++- crates/ruff_formatter/src/format_element.rs | 99 +++++++--- .../src/format_element/document.rs | 18 +- .../ruff_formatter/src/format_element/tag.rs | 10 +- crates/ruff_formatter/src/lib.rs | 2 +- crates/ruff_formatter/src/macros.rs | 6 +- .../ruff_formatter/src/printer/call_stack.rs | 13 +- crates/ruff_formatter/src/printer/mod.rs | 175 +++++++++++------- 8 files changed, 329 insertions(+), 120 deletions(-) diff --git a/crates/ruff_formatter/src/builders.rs b/crates/ruff_formatter/src/builders.rs index 24879eb868bf1..1f3ef8fff1846 100644 --- a/crates/ruff_formatter/src/builders.rs +++ b/crates/ruff_formatter/src/builders.rs @@ -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. @@ -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 Format for FormatBestFitting<'_, Context> { +impl Format for BestFitting<'_, Context> { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { let mut buffer = VecBuffer::new(f.state_mut()); let variants = self.variants.items(); @@ -2172,9 +2281,12 @@ impl Format 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) diff --git a/crates/ruff_formatter/src/format_element.rs b/crates/ruff_formatter/src/format_element.rs index a7dd7dedae9ba..5c5b3ff3e91a0 100644 --- a/crates/ruff_formatter/src/format_element.rs +++ b/crates/ruff_formatter/src/format_element.rs @@ -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; @@ -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), @@ -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() } @@ -134,6 +139,15 @@ impl PrintMode { } } +impl From 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]>); @@ -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(_) @@ -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. @@ -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() } } @@ -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]); diff --git a/crates/ruff_formatter/src/format_element/document.rs b/crates/ruff_formatter/src/format_element/document.rs index 2b83cb9907028..9799511fc587c 100644 --- a/crates/ruff_formatter/src/format_element/document.rs +++ b/crates/ruff_formatter/src/format_element/document.rs @@ -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); } @@ -280,14 +280,14 @@ impl Format> 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()])?; } @@ -296,6 +296,16 @@ impl Format> for &[FormatElement] { FormatElement::Line(LineMode::Hard), ])?; + if *mode != BestFittingMode::AllLines { + write!( + f, + [ + dynamic_text(&std::format!("mode: {mode:?},"), None), + space() + ] + )?; + } + write!(f, [text("])")])?; } diff --git a/crates/ruff_formatter/src/format_element/tag.rs b/crates/ruff_formatter/src/format_element/tag.rs index 38ebbaf1c48f8..6c8d03c20be81 100644 --- a/crates/ruff_formatter/src/format_element/tag.rs +++ b/crates/ruff_formatter/src/format_element/tag.rs @@ -203,8 +203,8 @@ pub enum DedentMode { #[derive(Debug, Clone, Eq, PartialEq)] pub struct Condition { - /// - Flat -> Omitted if the enclosing group is a multiline group, printed for groups fitting on a single line - /// - Multiline -> Omitted if the enclosing group fits on a single line, printed if the group breaks over multiple lines. + /// - `Flat` -> Omitted if the enclosing group is a multiline group, printed for groups fitting on a single line + /// - `Expanded` -> Omitted if the enclosing group fits on a single line, printed if the group breaks over multiple lines. pub(crate) mode: PrintMode, /// The id of the group for which it should check if it breaks or not. The group must appear in the document @@ -213,7 +213,7 @@ pub struct Condition { } impl Condition { - pub fn new(mode: PrintMode) -> Self { + pub(crate) fn new(mode: PrintMode) -> Self { Self { mode, group_id: None, @@ -224,10 +224,6 @@ impl Condition { self.group_id = id; self } - - pub fn mode(&self) -> PrintMode { - self.mode - } } #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/crates/ruff_formatter/src/lib.rs b/crates/ruff_formatter/src/lib.rs index d322b77a7ab48..4e40deb77dd31 100644 --- a/crates/ruff_formatter/src/lib.rs +++ b/crates/ruff_formatter/src/lib.rs @@ -48,7 +48,7 @@ pub use buffer::{ Buffer, BufferExtensions, BufferSnapshot, Inspect, PreambleBuffer, RemoveSoftLinesBuffer, VecBuffer, }; -pub use builders::FormatBestFitting; +pub use builders::BestFitting; pub use source_code::{SourceCode, SourceCodeSlice}; pub use crate::diagnostics::{ActualStart, FormatError, InvalidDocumentError, PrintError}; diff --git a/crates/ruff_formatter/src/macros.rs b/crates/ruff_formatter/src/macros.rs index fb6c66e6fa9a0..4f5bcdf0508d6 100644 --- a/crates/ruff_formatter/src/macros.rs +++ b/crates/ruff_formatter/src/macros.rs @@ -320,17 +320,17 @@ macro_rules! format { /// the content up to the first non-soft line break without exceeding the configured print width. /// This definition differs from groups as that non-soft line breaks make group expand. /// -/// [crate::FormatBestFitting] acts as a "break" boundary, meaning that it is considered to fit +/// [crate::BestFitting] acts as a "break" boundary, meaning that it is considered to fit /// /// /// [`Flat`]: crate::format_element::PrintMode::Flat /// [`Expanded`]: crate::format_element::PrintMode::Expanded -/// [`MostExpanded`]: crate::format_element::BestFitting::most_expanded +/// [`MostExpanded`]: crate::format_element::BestFittingVariants::most_expanded #[macro_export] macro_rules! best_fitting { ($least_expanded:expr, $($tail:expr),+ $(,)?) => {{ unsafe { - $crate::FormatBestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+)) + $crate::BestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+)) } }} } diff --git a/crates/ruff_formatter/src/printer/call_stack.rs b/crates/ruff_formatter/src/printer/call_stack.rs index 8bedc9f783fcc..a262f210a7ca9 100644 --- a/crates/ruff_formatter/src/printer/call_stack.rs +++ b/crates/ruff_formatter/src/printer/call_stack.rs @@ -1,7 +1,7 @@ use crate::format_element::tag::TagKind; use crate::format_element::PrintMode; use crate::printer::stack::{Stack, StackedStack}; -use crate::printer::Indention; +use crate::printer::{Indention, MeasureMode}; use crate::{IndentStyle, InvalidDocumentError, PrintError, PrintResult}; use std::fmt::Debug; use std::num::NonZeroU8; @@ -28,6 +28,7 @@ pub(super) struct StackFrame { pub(super) struct PrintElementArgs { indent: Indention, mode: PrintMode, + measure_mode: MeasureMode, } impl PrintElementArgs { @@ -42,6 +43,10 @@ impl PrintElementArgs { self.mode } + pub(super) fn measure_mode(&self) -> MeasureMode { + self.measure_mode + } + pub(super) fn indention(&self) -> Indention { self.indent } @@ -70,6 +75,11 @@ impl PrintElementArgs { self.mode = mode; self } + + pub(crate) fn with_measure_mode(mut self, mode: MeasureMode) -> Self { + self.measure_mode = mode; + self + } } impl Default for PrintElementArgs { @@ -77,6 +87,7 @@ impl Default for PrintElementArgs { Self { indent: Indention::Level(0), mode: PrintMode::Expanded, + measure_mode: MeasureMode::FirstLine, } } } diff --git a/crates/ruff_formatter/src/printer/mod.rs b/crates/ruff_formatter/src/printer/mod.rs index 223a42005db24..df4608f733a20 100644 --- a/crates/ruff_formatter/src/printer/mod.rs +++ b/crates/ruff_formatter/src/printer/mod.rs @@ -4,18 +4,10 @@ mod printer_options; mod queue; mod stack; -pub use printer_options::*; - -use crate::format_element::{BestFitting, LineMode, PrintMode}; -use crate::{ - ActualStart, FormatElement, GroupId, IndentStyle, InvalidDocumentError, PrintError, - PrintResult, Printed, SourceMarker, TextRange, -}; - use crate::format_element::document::Document; -use crate::format_element::tag::Condition; +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::EndFill; use crate::printer::call_stack::{ CallStack, FitsCallStack, PrintCallStack, PrintElementArgs, StackFrame, }; @@ -24,7 +16,12 @@ 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, +}; use drop_bomb::DebugDropBomb; +pub use printer_options::*; use ruff_text_size::{TextLen, TextSize}; use std::num::NonZeroU8; use unicode_width::UnicodeWidthChar; @@ -137,8 +134,8 @@ impl<'a> Printer<'a> { self.flush_line_suffixes(queue, stack, Some(HARD_BREAK)); } - FormatElement::BestFitting(best_fitting) => { - self.print_best_fitting(best_fitting, queue, stack)?; + FormatElement::BestFitting { variants, mode } => { + self.print_best_fitting(variants, *mode, queue, stack)?; } FormatElement::Interned(content) => { @@ -146,30 +143,31 @@ impl<'a> Printer<'a> { } FormatElement::Tag(StartGroup(group)) => { - let group_mode = if !group.mode().is_flat() { - PrintMode::Expanded - } else { - 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; - - // 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(TagKind::Group, args.with_print_mode(PrintMode::Flat)); - let fits = self.fits(queue, stack)?; - stack.pop(TagKind::Group)?; - - if fits { + let group_mode = match group.mode() { + GroupMode::Expand | GroupMode::Propagated => PrintMode::Expanded, + GroupMode::Flat => { + 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 - } else { - PrintMode::Expanded + } + // 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; + + // 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(TagKind::Group, args.with_print_mode(PrintMode::Flat)); + let fits = self.fits(queue, stack)?; + stack.pop(TagKind::Group)?; + + if fits { + PrintMode::Flat + } else { + PrintMode::Expanded + } } } } @@ -211,10 +209,10 @@ impl<'a> Printer<'a> { Some(id) => self.state.group_modes.unwrap_print_mode(*id, element), }; - if group_mode != *mode { - queue.skip_content(TagKind::ConditionalContent); - } else { + if *mode == group_mode { stack.push(TagKind::ConditionalContent, args); + } else { + queue.skip_content(TagKind::ConditionalContent); } } @@ -249,6 +247,7 @@ impl<'a> Printer<'a> { FormatElement::Tag(tag @ (StartLabelled(_) | StartEntry)) => { stack.push(tag.kind(), args); } + FormatElement::Tag( tag @ (EndLabelled | EndEntry @@ -371,19 +370,19 @@ impl<'a> Printer<'a> { fn print_best_fitting( &mut self, - best_fitting: &'a BestFitting, + 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(best_fitting.most_flat()); + queue.extend_back(variants.most_flat()); self.print_entry(queue, stack, args) } else { self.state.measured_group_fits = true; - - let normal_variants = &best_fitting.variants()[..best_fitting.variants().len() - 1]; + let normal_variants = &variants[..variants.len() - 1]; for variant in normal_variants.iter() { // Test if this variant fits and if so, use it. Otherwise try the next @@ -394,12 +393,14 @@ impl<'a> Printer<'a> { return invalid_start_tag(TagKind::Entry, variant.first()); } - let entry_args = args.with_print_mode(PrintMode::Flat); - // 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 = &variant[1..]; + let entry_args = args + .with_print_mode(PrintMode::Flat) + .with_measure_mode(MeasureMode::from(mode)); + queue.extend_back(content); stack.push(TagKind::Entry, entry_args); let variant_fits = self.fits(queue, stack)?; @@ -411,12 +412,12 @@ impl<'a> Printer<'a> { if variant_fits { queue.extend_back(variant); - return self.print_entry(queue, stack, entry_args); + return self.print_entry(queue, stack, args.with_print_mode(PrintMode::Flat)); } } // No variant fits, take the last (most expanded) as fallback - let most_expanded = best_fitting.most_expanded(); + let most_expanded = variants.most_expanded(); queue.extend_back(most_expanded); self.print_entry(queue, stack, args.with_print_mode(PrintMode::Expanded)) } @@ -555,7 +556,7 @@ impl<'a> Printer<'a> { } } - if queue.top() == Some(&FormatElement::Tag(EndFill)) { + if queue.top() == Some(&FormatElement::Tag(Tag::EndFill)) { Ok(()) } else { invalid_end_tag(TagKind::Fill, stack.top_kind()) @@ -959,8 +960,8 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { FormatElement::Space => return Ok(self.fits_text(" ")), FormatElement::Line(line_mode) => { - if args.mode().is_flat() { - match line_mode { + match args.mode() { + PrintMode::Flat => match line_mode { LineMode::SoftOrSpace => return Ok(self.fits_text(" ")), LineMode::Soft => {} LineMode::Hard | LineMode::Empty => { @@ -970,13 +971,22 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { 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 => { + // Continue measuring on the next line + self.state.line_width = 0; + } + } } - } else { - // 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 -> Fits - return Ok(Fits::Yes); } } @@ -1000,17 +1010,21 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { FormatElement::SourcePosition(_) => {} - FormatElement::BestFitting(best_fitting) => { - let slice = match args.mode() { - PrintMode::Flat => best_fitting.most_flat(), - PrintMode::Expanded => best_fitting.most_expanded(), + 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::StartEntry))) { return invalid_start_tag(TagKind::Entry, slice.first()); } - self.queue.extend_back(slice); + self.stack.push(TagKind::Entry, args); + self.queue.extend_back(&slice[1..]); } FormatElement::Interned(content) => self.queue.extend_back(content), @@ -1040,22 +1054,23 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { return Ok(Fits::No); } - let group_mode = if !group.mode().is_flat() { + // Continue printing groups in expanded mode if measuring a `fits_expanded` element + let print_mode = if !group.mode().is_flat() { PrintMode::Expanded } else { args.mode() }; self.stack - .push(TagKind::Group, args.with_print_mode(group_mode)); + .push(TagKind::Group, args.with_print_mode(print_mode)); if let Some(id) = group.id() { - self.group_modes_mut().insert_print_mode(id, group_mode); + self.group_modes_mut().insert_print_mode(id, print_mode); } } FormatElement::Tag(StartConditionalContent(condition)) => { - let group_mode = match condition.group_id { + let print_mode = match condition.group_id { None => args.mode(), Some(group_id) => self .group_modes() @@ -1063,20 +1078,20 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { .unwrap_or_else(|| args.mode()), }; - if group_mode != condition.mode { - self.queue.skip_content(TagKind::ConditionalContent); - } else { + if condition.mode == print_mode { self.stack.push(TagKind::ConditionalContent, args); + } else { + self.queue.skip_content(TagKind::ConditionalContent); } } FormatElement::Tag(StartIndentIfGroupBreaks(id)) => { - let group_mode = self + let print_mode = self .group_modes() .get_print_mode(*id) .unwrap_or_else(|| args.mode()); - match group_mode { + match print_mode { PrintMode::Flat => { self.stack.push(TagKind::IndentIfGroupBreaks, args); } @@ -1103,6 +1118,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { ) => { self.stack.push(tag.kind(), args); } + FormatElement::Tag( tag @ (EndFill | EndVerbatim @@ -1234,6 +1250,27 @@ struct FitsState { line_width: usize, } +#[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 non 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, +} + +impl From for MeasureMode { + fn from(value: BestFittingMode) -> Self { + match value { + BestFittingMode::FirstLine => Self::FirstLine, + BestFittingMode::AllLines => Self::AllLines, + } + } +} + #[cfg(test)] mod tests { use crate::prelude::*;