From 89c5ea65552fe38750dd9e59cb850e7f899d75ba Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 19 Jun 2023 17:50:38 +0200 Subject: [PATCH] Add BeestFittingMode --- 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::*;