diff --git a/crates/oxc_formatter/src/formatter/builders.rs b/crates/oxc_formatter/src/formatter/builders.rs index af190d37c21c7..75f85dcdf4c9f 100644 --- a/crates/oxc_formatter/src/formatter/builders.rs +++ b/crates/oxc_formatter/src/formatter/builders.rs @@ -1,12 +1,11 @@ use std::{cell::Cell, num::NonZeroU8}; use Tag::{ - EndAlign, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, EndIndent, - EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, StartAlign, StartConditionalContent, - StartDedent, StartEntry, StartFill, StartGroup, StartIndent, StartIndentIfGroupBreaks, - StartLabelled, StartLineSuffix, + EndAlign, EndBestFittingEntry, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, + EndIndent, EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, StartAlign, + StartBestFittingEntry, StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup, + StartIndent, StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, }; -use oxc_allocator::Vec as ArenaVec; use oxc_span::{GetSpan, Span}; use super::{ @@ -2535,18 +2534,13 @@ impl<'ast> Format<'ast> for BestFitting<'_, 'ast> { let mut buffer = VecBuffer::new(f.state_mut()); let variants = self.variants.items(); - let mut formatted_variants = Vec::with_capacity(variants.len()); - for variant in variants { - buffer.write_element(FormatElement::Tag(StartEntry)); + buffer.write_element(FormatElement::Tag(StartBestFittingEntry)); buffer.write_fmt(Arguments::from(variant)); - buffer.write_element(FormatElement::Tag(EndEntry)); - - formatted_variants.push(buffer.take_vec().into_bump_slice()); + buffer.write_element(FormatElement::Tag(EndBestFittingEntry)); } - let formatted_variants = - ArenaVec::from_iter_in(formatted_variants, f.context().allocator()); + let formatted_variants = buffer.take_vec().into_bump_slice(); // 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 diff --git a/crates/oxc_formatter/src/formatter/format_element/document.rs b/crates/oxc_formatter/src/formatter/format_element/document.rs index 939d06ae80897..dbd2a42cf5336 100644 --- a/crates/oxc_formatter/src/formatter/format_element/document.rs +++ b/crates/oxc_formatter/src/formatter/format_element/document.rs @@ -181,10 +181,10 @@ impl std::fmt::Display for Document<'_> { impl<'a> Format<'a> for &[FormatElement<'a>] { fn fmt(&self, f: &mut Formatter<'_, 'a>) { use Tag::{ - EndAlign, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, EndIndent, - EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, StartAlign, - StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup, StartIndent, - StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, + EndAlign, EndBestFittingEntry, EndConditionalContent, EndDedent, EndEntry, EndFill, + EndGroup, EndIndent, EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, StartAlign, + StartBestFittingEntry, StartConditionalContent, StartDedent, StartEntry, StartFill, + StartGroup, StartIndent, StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, }; write!(f, [ContentArrayStart]); @@ -278,7 +278,7 @@ impl<'a> Format<'a> for &[FormatElement<'a>] { ]); for variant in best_fitting.variants() { - write!(f, [&**variant, hard_line_break()]); + write!(f, [variant, hard_line_break()]); } f.write_elements([ @@ -499,10 +499,10 @@ impl<'a> Format<'a> for &[FormatElement<'a>] { write!(f, [token("fill(")]); } - StartEntry => { + StartEntry | StartBestFittingEntry => { // handled after the match for all start tags } - EndEntry => write!(f, [ContentArrayEnd]), + EndEntry | EndBestFittingEntry => write!(f, [ContentArrayEnd]), EndFill | EndLabelled diff --git a/crates/oxc_formatter/src/formatter/format_element/mod.rs b/crates/oxc_formatter/src/formatter/format_element/mod.rs index 8037c4fc06ad1..e3f03a5fecf2b 100644 --- a/crates/oxc_formatter/src/formatter/format_element/mod.rs +++ b/crates/oxc_formatter/src/formatter/format_element/mod.rs @@ -319,12 +319,16 @@ impl FormatElements for FormatElement<'_> { /// can pick the best fitting variant. /// /// Best fitting is defined as the variant that takes the most horizontal space but fits on the line. +/// +/// Variants are stored in a flat slice delimited by `StartBestFittingEntry` and `EndBestFittingEntry` +/// tags. This avoids per-variant allocations compared to storing `&[&[FormatElement]]`. #[derive(Clone, Eq, PartialEq)] pub struct BestFittingElement<'a> { - /// 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: &'a [&'a [FormatElement<'a>]], + /// The different variants for this element, stored as a flat slice. + /// Each variant is delimited by `StartBestFittingEntry` and `EndBestFittingEntry` tags. + /// The first variant is the one that takes up the most space horizontally (the most flat), + /// The last variant takes up the least space horizontally (but most vertical space). + variants: &'a [FormatElement<'a>], } impl<'a> BestFittingElement<'a> { @@ -335,47 +339,97 @@ impl<'a> BestFittingElement<'a> { /// 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. + /// The slice must contain at least two variants delimited by `StartBestFittingEntry` and + /// `EndBestFittingEntry` tags. #[doc(hidden)] - pub unsafe fn from_vec_unchecked(variants: ArenaVec<'a, &'a [FormatElement<'a>]>) -> Self { + pub unsafe fn from_vec_unchecked(variants: &'a [FormatElement<'a>]) -> Self { debug_assert!( - variants.len() >= 2, + { + let count = variants + .iter() + .filter(|e| matches!(e, FormatElement::Tag(Tag::StartBestFittingEntry))) + .count(); + count >= 2 + }, "Requires at least the least expanded and most expanded variants" ); - Self { variants: variants.into_bump_slice() } - } - - /// Returns the most expanded variant - pub fn most_expanded(&self) -> &[FormatElement<'a>] { - self.variants.last().expect( - "Most contain at least two elements, as guaranteed by the best fitting builder.", - ) + Self { variants } } - /// Splits the variants into the most expanded and the remaining flat variants - pub fn split_to_most_expanded_and_flat_variants( - &self, - ) -> (&&[FormatElement<'a>], &[&[FormatElement<'a>]]) { - // SAFETY: We have already asserted that there are at least two variants for creating this struct. - unsafe { self.variants.split_last().unwrap_unchecked() } + /// Returns an iterator over the variants. + pub fn variants(&self) -> BestFittingVariantsIter<'a> { + BestFittingVariantsIter { elements: self.variants } } - pub fn variants(&self) -> &[&'a [FormatElement<'a>]] { - self.variants + /// Returns the most expanded variant (the last one). + pub fn most_expanded(&self) -> &'a [FormatElement<'a>] { + self.variants().last().expect( + "Must contain at least two elements, as guaranteed by the best fitting builder.", + ) } - /// Returns the least expanded variant - pub fn most_flat(&self) -> &[FormatElement<'a>] { - self.variants.first().expect( - "Most contain at least two elements, as guaranteed by the best fitting builder.", + /// Returns the least expanded variant (the first one, most flat). + pub fn most_flat(&self) -> &'a [FormatElement<'a>] { + self.variants().next().expect( + "Must contain at least two elements, as guaranteed by the best fitting builder.", ) } } impl std::fmt::Debug for BestFittingElement<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_list().entries(self.variants).finish() + f.debug_list().entries(self.variants()).finish() + } +} + +/// Iterator over the variants of a `BestFittingElement`. +/// +/// Each variant is delimited by `StartBestFittingEntry` and `EndBestFittingEntry` tags. +pub struct BestFittingVariantsIter<'a> { + elements: &'a [FormatElement<'a>], +} + +impl<'a> Iterator for BestFittingVariantsIter<'a> { + type Item = &'a [FormatElement<'a>]; + + fn next(&mut self) -> Option { + if self.elements.is_empty() { + return None; + } + + // Find the StartBestFittingEntry tag + let start_index = self.elements.iter().position(|element| { + matches!(element, FormatElement::Tag(Tag::StartBestFittingEntry)) + })?; + + // Find the matching EndBestFittingEntry tag + let mut depth = 0usize; + let end_index = self.elements[start_index..].iter().position(|element| { + match element { + FormatElement::Tag(Tag::StartBestFittingEntry) => { + depth += 1; + } + FormatElement::Tag(Tag::EndBestFittingEntry) => { + depth -= 1; + if depth == 0 { + return true; + } + } + _ => {} + } + false + })?; + + let end_index = start_index + end_index; + + // The variant includes the start and end tags + let variant = &self.elements[start_index..=end_index]; + + // Move past the end tag for the next iteration + self.elements = &self.elements[end_index + 1..]; + + Some(variant) } } diff --git a/crates/oxc_formatter/src/formatter/format_element/tag.rs b/crates/oxc_formatter/src/formatter/format_element/tag.rs index ab1466c34f398..4570b42323c9e 100644 --- a/crates/oxc_formatter/src/formatter/format_element/tag.rs +++ b/crates/oxc_formatter/src/formatter/format_element/tag.rs @@ -62,6 +62,11 @@ pub enum Tag { /// See [crate::builders::labelled] for documentation. StartLabelled(LabelId), EndLabelled, + + /// Marks the start of a best fitting variant entry. + StartBestFittingEntry, + /// Marks the end of a best fitting variant entry. + EndBestFittingEntry, } impl Tag { @@ -79,6 +84,7 @@ impl Tag { | Tag::StartEntry | Tag::StartLineSuffix | Tag::StartLabelled(_) + | Tag::StartBestFittingEntry ) } @@ -89,10 +95,10 @@ impl Tag { pub const fn kind(&self) -> TagKind { use Tag::{ - EndAlign, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, EndIndent, - EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, StartAlign, - StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup, StartIndent, - StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, + EndAlign, EndBestFittingEntry, EndConditionalContent, EndDedent, EndEntry, EndFill, + EndGroup, EndIndent, EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, StartAlign, + StartBestFittingEntry, StartConditionalContent, StartDedent, StartEntry, StartFill, + StartGroup, StartIndent, StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, }; match self { @@ -106,6 +112,7 @@ impl Tag { StartEntry | EndEntry => TagKind::Entry, StartLineSuffix | EndLineSuffix => TagKind::LineSuffix, StartLabelled(_) | EndLabelled => TagKind::Labelled, + StartBestFittingEntry | EndBestFittingEntry => TagKind::BestFittingEntry, } } } @@ -126,6 +133,7 @@ pub enum TagKind { LineSuffix, Labelled, TailwindClass, + BestFittingEntry, } #[derive(Debug, Copy, Default, Clone, Eq, PartialEq)] diff --git a/crates/oxc_formatter/src/formatter/printer/mod.rs b/crates/oxc_formatter/src/formatter/printer/mod.rs index a5dee9acad8d8..e9ee1c7fd2ad9 100644 --- a/crates/oxc_formatter/src/formatter/printer/mod.rs +++ b/crates/oxc_formatter/src/formatter/printer/mod.rs @@ -87,10 +87,10 @@ impl<'a> Printer<'a> { element: &'a FormatElement, ) -> PrintResult<()> { use Tag::{ - EndAlign, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, EndIndent, - EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, StartAlign, - StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup, StartIndent, - StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, + EndAlign, EndBestFittingEntry, EndConditionalContent, EndDedent, EndEntry, EndFill, + EndGroup, EndIndent, EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, StartAlign, + StartBestFittingEntry, StartConditionalContent, StartDedent, StartEntry, StartFill, + StartGroup, StartIndent, StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, }; let args = stack.top(); @@ -244,11 +244,16 @@ impl<'a> Printer<'a> { indent_stack.push_suffix(indent_stack.indention()); self.state.line_suffixes.extend(args, queue.iter_content(TagKind::LineSuffix)); } - FormatElement::Tag(tag @ (StartLabelled(_) | StartEntry)) => { + FormatElement::Tag(tag @ (StartLabelled(_) | StartEntry | StartBestFittingEntry)) => { stack.push(tag.kind(), args); } FormatElement::Tag( - tag @ (EndLabelled | EndEntry | EndGroup | EndConditionalContent | EndFill), + tag @ (EndLabelled + | EndEntry + | EndBestFittingEntry + | EndGroup + | EndConditionalContent + | EndFill), ) => { stack.pop(tag.kind())?; } @@ -336,20 +341,19 @@ impl<'a> Printer<'a> { if args.mode().is_flat() && self.state.measured_group_fits { queue.extend_back(best_fitting.most_flat()); - self.print_entry(queue, stack, indent_stack, args) + self.print_best_fitting_entry(queue, stack, indent_stack, args) } else { self.state.measured_group_fits = true; - let (most_expanded, remaining_variants) = - best_fitting.split_to_most_expanded_and_flat_variants(); - - for variant in remaining_variants { - // Test if this variant fits and if so, use it. Otherwise try the next - // variant. + // Collect all variants from the iterator + let variants: Vec<_> = best_fitting.variants().collect(); + // Try all variants except the last (most expanded) one + for variant in &variants[..variants.len() - 1] { // Try to fit only the first variant on a single line - if !matches!(variant.first(), Some(&FormatElement::Tag(Tag::StartEntry))) { - return invalid_start_tag(TagKind::Entry, variant.first()); + if !matches!(variant.first(), Some(&FormatElement::Tag(Tag::StartBestFittingEntry))) + { + return invalid_start_tag(TagKind::BestFittingEntry, variant.first()); } let entry_args = args.with_print_mode(PrintMode::Flat); @@ -359,9 +363,9 @@ impl<'a> Printer<'a> { let content = &variant[1..]; queue.extend_back(content); - stack.push(TagKind::Entry, entry_args); + stack.push(TagKind::BestFittingEntry, entry_args); let variant_fits = self.fits(queue, stack, indent_stack)?; - stack.pop(TagKind::Entry)?; + stack.pop(TagKind::BestFittingEntry)?; // Remove the content slice because printing needs the variant WITH the start entry let popped_slice = queue.pop_slice(); @@ -369,14 +373,65 @@ impl<'a> Printer<'a> { if variant_fits { queue.extend_back(variant); - return self.print_entry(queue, stack, indent_stack, entry_args); + return self.print_best_fitting_entry(queue, stack, indent_stack, entry_args); } } // No variant fits, take the last (most expanded) as fallback - queue.extend_back(most_expanded); - self.print_entry(queue, stack, indent_stack, args.with_print_mode(PrintMode::Expanded)) + if let Some(most_expanded) = variants.last() { + queue.extend_back(most_expanded); + } + self.print_best_fitting_entry( + queue, + stack, + indent_stack, + args.with_print_mode(PrintMode::Expanded), + ) + } + } + + /// Prints a single best fitting entry delimited by `StartBestFittingEntry` and `EndBestFittingEntry` tags. + fn print_best_fitting_entry( + &mut self, + queue: &mut PrintQueue<'a>, + stack: &mut PrintCallStack, + indent_stack: &mut PrintIndentStack, + args: PrintElementArgs, + ) -> PrintResult<()> { + let start_entry = queue.top(); + + if !matches!(start_entry, Some(&FormatElement::Tag(Tag::StartBestFittingEntry))) { + return invalid_start_tag(TagKind::BestFittingEntry, start_entry); + } + + let mut depth = 0usize; + + while let Some(element) = queue.pop() { + match element { + FormatElement::Tag(Tag::StartBestFittingEntry) => { + // Handle the start of the first element by pushing the args on the stack. + if depth == 0 { + depth = 1; + stack.push(TagKind::BestFittingEntry, args); + } else { + depth += 1; + } + } + FormatElement::Tag(Tag::EndBestFittingEntry) => { + depth -= 1; + // Reached the end entry, pop the entry from the stack and return. + if depth == 0 { + stack.pop(TagKind::BestFittingEntry)?; + return Ok(()); + } + } + _ => { + self.print_element(stack, indent_stack, queue, element)?; + } + } } + + invalid_end_tag(TagKind::BestFittingEntry, stack.top_kind()) } /// Tries to fit as much content as possible on a single line. @@ -959,10 +1014,10 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { /// Tests if the passed element fits on the current line or not. fn fits_element(&mut self, element: &'a FormatElement) -> PrintResult { use Tag::{ - EndAlign, EndConditionalContent, EndDedent, EndEntry, EndFill, EndGroup, EndIndent, - EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, StartAlign, - StartConditionalContent, StartDedent, StartEntry, StartFill, StartGroup, StartIndent, - StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, + EndAlign, EndBestFittingEntry, EndConditionalContent, EndDedent, EndEntry, EndFill, + EndGroup, EndIndent, EndIndentIfGroupBreaks, EndLabelled, EndLineSuffix, StartAlign, + StartBestFittingEntry, StartConditionalContent, StartDedent, StartEntry, StartFill, + StartGroup, StartIndent, StartIndentIfGroupBreaks, StartLabelled, StartLineSuffix, }; let args = self.stack.top(); @@ -1065,10 +1120,11 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { PrintMode::Expanded => best_fitting.most_expanded(), }; - if !matches!(slice.first(), Some(FormatElement::Tag(Tag::StartEntry))) { - return invalid_start_tag(TagKind::Entry, slice.first()); + if !matches!(slice.first(), Some(FormatElement::Tag(Tag::StartBestFittingEntry))) { + return invalid_start_tag(TagKind::BestFittingEntry, slice.first()); } + // Queue the full slice including StartBestFittingEntry/EndBestFittingEntry tags self.queue.extend_back(slice); } @@ -1146,11 +1202,18 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> { return invalid_end_tag(TagKind::LineSuffix, self.stack.top_kind()); } - FormatElement::Tag(tag @ (StartFill | StartLabelled(_) | StartEntry)) => { + FormatElement::Tag( + tag @ (StartFill | StartLabelled(_) | StartEntry | StartBestFittingEntry), + ) => { self.stack.push(tag.kind(), args); } FormatElement::Tag( - tag @ (EndLabelled | EndEntry | EndGroup | EndConditionalContent | EndFill), + tag @ (EndLabelled + | EndEntry + | EndBestFittingEntry + | EndGroup + | EndConditionalContent + | EndFill), ) => { self.stack.pop(tag.kind())?; } diff --git a/crates/oxc_formatter/src/print/call_like_expression/arguments.rs b/crates/oxc_formatter/src/print/call_like_expression/arguments.rs index 06a02831cc242..6f6fd2bac621d 100644 --- a/crates/oxc_formatter/src/print/call_like_expression/arguments.rs +++ b/crates/oxc_formatter/src/print/call_like_expression/arguments.rs @@ -717,11 +717,11 @@ fn write_grouped_arguments<'a>( // First write the most expanded variant because it needs `arguments`. let most_expanded = { let mut buffer = VecBuffer::new(f.state_mut()); - buffer.write_element(FormatElement::Tag(Tag::StartEntry)); + buffer.write_element(FormatElement::Tag(Tag::StartBestFittingEntry)); format_all_elements_broken_out(node, elements.iter().cloned(), true, &mut buffer); - buffer.write_element(FormatElement::Tag(Tag::EndEntry)); + buffer.write_element(FormatElement::Tag(Tag::EndBestFittingEntry)); buffer.into_vec().into_bump_slice() }; @@ -796,7 +796,7 @@ fn write_grouped_arguments<'a>( let middle_variant = { let mut buffer = VecBuffer::new(f.state_mut()); - buffer.write_element(FormatElement::Tag(Tag::StartEntry)); + buffer.write_element(FormatElement::Tag(Tag::StartBestFittingEntry)); write!( buffer, @@ -830,21 +830,32 @@ fn write_grouped_arguments<'a>( ] ); - buffer.write_element(FormatElement::Tag(Tag::EndEntry)); + buffer.write_element(FormatElement::Tag(Tag::EndBestFittingEntry)); buffer.into_vec().into_bump_slice() }; // If the grouped content breaks, then we can skip the most_flat variant, // since we already know that it won't be fitting on a single line. + // + // Combine all variants into a single flat buffer with StartBestFittingEntry/EndBestFittingEntry markers. let variants = if grouped_breaks { write!(f, [expand_parent()]); - ArenaVec::from_array_in([middle_variant, most_expanded], f.context().allocator()) + + // Create a flat buffer with [middle_variant, most_expanded] + let mut buffer = VecBuffer::new(f.state_mut()); + for element in middle_variant { + buffer.write_element(element.clone()); + } + for element in most_expanded { + buffer.write_element(element.clone()); + } + buffer.into_vec().into_bump_slice() } else { // Write the most flat variant with the first or last argument grouped. let most_flat = { let mut buffer = VecBuffer::new(f.state_mut()); - buffer.write_element(FormatElement::Tag(Tag::StartEntry)); + buffer.write_element(FormatElement::Tag(Tag::StartBestFittingEntry)); write!( buffer, @@ -865,19 +876,28 @@ fn write_grouped_arguments<'a>( ] ); - buffer.write_element(FormatElement::Tag(Tag::EndEntry)); + buffer.write_element(FormatElement::Tag(Tag::EndBestFittingEntry)); buffer.into_vec().into_bump_slice() }; - ArenaVec::from_array_in([most_flat, middle_variant, most_expanded], f.context().allocator()) + // Create a flat buffer with [most_flat, middle_variant, most_expanded] + let mut buffer = VecBuffer::new(f.state_mut()); + for element in most_flat { + buffer.write_element(element.clone()); + } + for element in middle_variant { + buffer.write_element(element.clone()); + } + for element in most_expanded { + buffer.write_element(element.clone()); + } + buffer.into_vec().into_bump_slice() }; - // SAFETY: Safe because variants is guaranteed to contain exactly 3 entries: - // * most flat - // * middle - // * most expanded - // ... and best fitting only requires the most flat/and expanded. + // SAFETY: Safe because variants is guaranteed to contain at least 2 entries + // (either [middle, most_expanded] or [most_flat, middle, most_expanded]), + // each delimited by StartBestFittingEntry/EndBestFittingEntry tags. unsafe { f.write_element(FormatElement::BestFitting( format_element::BestFittingElement::from_vec_unchecked(variants),