diff --git a/crates/rome_formatter/src/format_element.rs b/crates/rome_formatter/src/format_element.rs index c5dbbd25261..766e184b8c0 100644 --- a/crates/rome_formatter/src/format_element.rs +++ b/crates/rome_formatter/src/format_element.rs @@ -1,12 +1,16 @@ use crate::builders::ConcatBuilder; use crate::intersperse::{Intersperse, IntersperseFn}; use crate::{format_elements, TextRange, TextSize}; +#[cfg(target_pointer_width = "64")] +use rome_rowan::static_assert; use rome_rowan::{ Language, SyntaxNode, SyntaxToken, SyntaxTokenText, SyntaxTriviaPieceComments, TextLen, }; use std::borrow::Cow; use std::fmt::{self, Debug, Formatter}; +use std::num::NonZeroU32; use std::ops::Deref; +use std::sync::atomic::{AtomicU32, Ordering}; type Content = Box; @@ -326,7 +330,8 @@ where } /// Concatenates a list of [FormatElement]s with spaces and line breaks to fit -/// them on as few lines as possible +/// them on as few lines as possible. Each element introduces a conceptual group. The printer +/// first tries to print the item in flat mode but then prints it in expanded mode if it doesn't fit. /// /// ## Examples /// @@ -688,9 +693,18 @@ pub fn soft_line_indent_or_space>(content: T) -> FormatEl /// ``` #[inline] pub fn group_elements>(content: T) -> FormatElement { - let content: FormatElement = content.into(); + group_elements_impl(content.into(), None) +} + +/// Creates a group with a specific id. Useful for cases where `if_group_breaks` and `if_group_fits_on_line` +/// shouldn't refer to the direct parent group. +pub fn group_elements_with_id(content: FormatElement, id: GroupId) -> FormatElement { + group_elements_impl(content, Some(id)) +} + +fn group_elements_impl(content: FormatElement, id: Option) -> FormatElement { let (leading, content, trailing) = content.split_trivia(); - format_elements![leading, Group::new(content), trailing] + format_elements![leading, Group::new(content).with_id(id), trailing] } /// Creates a group that forces all elements inside it to be printed on a @@ -803,15 +817,60 @@ pub fn hard_group_elements>(content: T) -> FormatElement /// ``` #[inline] pub fn if_group_breaks>(content: T) -> FormatElement { - let content = content.into(); + if_group_breaks_impl(content.into(), None) +} + +/// Inserts some content that the printer only prints if the group with the specified `group_id` +/// is printed in multiline mode. +/// +/// ## Examples +/// +/// Prints the trailing comma if the array group doesn't fit. The `group_id` is necessary +/// because `fill` creates an implicit group around each item and tries to print the item in flat mode. +/// The item `[4]` in this example fits on a single line but the trailing comma should still be printed +/// +/// ``` +/// use rome_formatter::{Formatted, LineWidth}; +/// use rome_formatter::prelude::*; +/// +/// let group_id = GroupId::new("array"); +/// +/// let elements = group_elements_with_id(format_elements![ +/// token("["), +/// soft_block_indent(fill_elements(vec![ +/// format_elements![token("1,")], +/// format_elements![token("234568789,")], +/// format_elements![token("3456789,")], +/// format_elements![ +/// token("["), +/// soft_block_indent(token("4")), +/// token("]"), +/// if_group_with_id_breaks(token(","), group_id) +/// ], +/// ])), +/// token("]"), +/// ], group_id); +/// +/// let options = PrinterOptions { +/// print_width: LineWidth::try_from(20).unwrap(), +/// ..PrinterOptions::default() +/// }; +/// assert_eq!( +/// "[\n\t1, 234568789,\n\t3456789, [4],\n]", +/// Formatted::new(elements, options).print().as_code() +/// ); +/// ``` +pub fn if_group_with_id_breaks(content: FormatElement, group_id: GroupId) -> FormatElement { + if_group_breaks_impl(content, Some(group_id)) +} +fn if_group_breaks_impl(content: FormatElement, group_id: Option) -> FormatElement { if content.is_empty() { content } else { - FormatElement::from(ConditionalGroupContent::new( - content, - GroupPrintMode::Multiline, - )) + FormatElement::from( + ConditionalGroupContent::new(content, PrintMode::Expanded).with_group_id(group_id), + ) } } @@ -879,15 +938,27 @@ pub fn if_group_fits_on_single_line(flat_content: TFlat) -> FormatElement where TFlat: Into, { - let flat_content = flat_content.into(); + if_group_fits_on_line_impl(flat_content.into(), None) +} +/// Inserts some content that the printer only prints if the group with the specified `group_id` +/// is printed in flat mode. +/// +#[inline] +pub fn if_group_with_id_fits_on_line(flat_content: FormatElement, id: GroupId) -> FormatElement { + if_group_fits_on_line_impl(flat_content, Some(id)) +} + +fn if_group_fits_on_line_impl( + flat_content: FormatElement, + group_id: Option, +) -> FormatElement { if flat_content.is_empty() { flat_content } else { - FormatElement::from(ConditionalGroupContent::new( - flat_content, - GroupPrintMode::Flat, - )) + FormatElement::from( + ConditionalGroupContent::new(flat_content, PrintMode::Flat).with_group_id(group_id), + ) } } @@ -1181,11 +1252,19 @@ impl Deref for List { #[derive(Clone, PartialEq, Eq)] pub struct Group { pub(crate) content: Content, + pub(crate) id: Option, } impl Debug for Group { fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { - fmt.debug_tuple("").field(&self.content).finish() + if let Some(id) = &self.id { + fmt.debug_struct("") + .field("content", &self.content) + .field("id", id) + .finish() + } else { + fmt.debug_tuple("").field(&self.content).finish() + } } } @@ -1193,14 +1272,83 @@ impl Group { pub fn new(content: FormatElement) -> Self { Self { content: Box::new(content), + id: None, } } + + pub fn with_id(mut self, id: Option) -> Self { + self.id = id; + self + } } -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum GroupPrintMode { +#[derive(Clone, Copy, Eq, PartialEq, Hash)] +pub struct DebugGroupId { + id: NonZeroU32, + name: &'static str, +} + +impl DebugGroupId { + pub fn new(debug_name: &'static str) -> Self { + static ID: AtomicU32 = AtomicU32::new(1); + let id = NonZeroU32::new(ID.fetch_add(1, Ordering::Relaxed)).unwrap(); + + Self { + id, + name: debug_name, + } + } +} + +impl Debug for DebugGroupId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "#{}-{}", self.name, self.id) + } +} + +/// Unique ID identifying a group +#[repr(transparent)] +#[derive(Clone, Copy, Eq, PartialEq, Hash)] +pub struct ReleaseGroupId(NonZeroU32); + +impl ReleaseGroupId { + /// Creates a new unique group id with the given debug name (only stored in debug builds) + pub fn new(_: &'static str) -> Self { + static ID: AtomicU32 = AtomicU32::new(1); + + let id = NonZeroU32::new(ID.fetch_add(1, Ordering::Relaxed)).unwrap(); + + Self(id) + } +} + +impl Debug for ReleaseGroupId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "#{}", self.0) + } +} + +#[cfg(not(debug_assertions))] +pub type GroupId = ReleaseGroupId; +#[cfg(debug_assertions)] +pub type GroupId = DebugGroupId; + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum PrintMode { + /// Omits any soft line breaks Flat, - Multiline, + /// Prints soft line breaks as line breaks + Expanded, +} + +impl PrintMode { + pub const fn is_flat(&self) -> bool { + matches!(self, PrintMode::Flat) + } + + pub const fn is_expanded(&self) -> bool { + matches!(self, PrintMode::Expanded) + } } #[derive(Debug, Clone, Eq, PartialEq)] @@ -1210,16 +1358,25 @@ pub struct ConditionalGroupContent { /// In what mode the content should be printed. /// * 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. - pub(crate) mode: GroupPrintMode, + pub(crate) mode: PrintMode, + + /// The id of the group for which it should check if it breaks or not + pub(crate) group_id: Option, } impl ConditionalGroupContent { - pub fn new(content: FormatElement, mode: GroupPrintMode) -> Self { + pub fn new(content: FormatElement, mode: PrintMode) -> Self { Self { content: Box::new(content), mode, + group_id: None, } } + + pub fn with_group_id(mut self, id: Option) -> Self { + self.group_id = id; + self + } } /// See [token] for documentation @@ -1492,12 +1649,6 @@ impl FormatElement { // re-create the grouping around the content only (leading, hard_group_elements(content), trailing) } - - FormatElement::Group(group) => { - let (leading, content, trailing) = group.content.split_trivia(); - // re-create the grouping around the content only - (leading, group_elements(content), trailing) - } // Non-list elements are returned directly _ => (empty_element(), self, empty_element()), } @@ -1570,10 +1721,9 @@ impl From> for FormatElement { mod tests { use crate::format_element::{ - empty_element, join_elements, normalize_newlines, ConditionalGroupContent, List, - VerbatimKind, LINE_TERMINATORS, + empty_element, join_elements, normalize_newlines, List, LINE_TERMINATORS, }; - use crate::{concat_elements, space_token, token, FormatElement, TextRange, Token, Verbatim}; + use crate::{concat_elements, space_token, token, FormatElement}; #[test] fn concat_elements_returns_a_list_token_containing_the_passed_in_elements() { @@ -1689,19 +1839,32 @@ mod tests { assert_eq!(normalize_newlines("a\u{2028}b", LINE_TERMINATORS), "a\nb"); assert_eq!(normalize_newlines("a\u{2029}b", LINE_TERMINATORS), "a\nb"); } - - #[test] - fn sizes() { - assert_eq!(8, std::mem::size_of::()); - assert_eq!(8, std::mem::size_of::()); - assert_eq!(16, std::mem::size_of::()); - assert_eq!(24, std::mem::size_of::()); - assert_eq!(16, std::mem::size_of::()); - assert_eq!(24, std::mem::size_of::()); - // Increasing the size of FormatElement has serious consequences on runtime performance and memory footprint. - // Is there a more efficient way to encode the data to avoid increasing its size? Can the information - // be recomputed at a later point in time? - // You reduced the size of a format element? Excellent work! - assert_eq!(32, std::mem::size_of::()); - } } + +#[cfg(target_pointer_width = "64")] +static_assert!(std::mem::size_of::() == 8usize); + +#[cfg(target_pointer_width = "64")] +static_assert!(std::mem::size_of::() == 8usize); + +#[cfg(target_pointer_width = "64")] +static_assert!(std::mem::size_of::() == 16usize); + +#[cfg(target_pointer_width = "64")] +static_assert!(std::mem::size_of::() == 24usize); + +#[cfg(not(debug_assertions))] +#[cfg(target_pointer_width = "64")] +static_assert!(std::mem::size_of::() == 16usize); + +#[cfg(target_pointer_width = "64")] +static_assert!(std::mem::size_of::() == 24usize); + +// Increasing the size of FormatElement has serious consequences on runtime performance and memory footprint. +// Is there a more efficient way to encode the data to avoid increasing its size? Can the information +// be recomputed at a later point in time? +// You reduced the size of a format element? Excellent work! + +#[cfg(not(debug_assertions))] +#[cfg(target_pointer_width = "64")] +static_assert!(std::mem::size_of::() == 32usize); diff --git a/crates/rome_formatter/src/printer/mod.rs b/crates/rome_formatter/src/printer/mod.rs index d07fc4e54f0..c93e2c734e8 100644 --- a/crates/rome_formatter/src/printer/mod.rs +++ b/crates/rome_formatter/src/printer/mod.rs @@ -1,5 +1,1177 @@ -mod printer; mod printer_options; -pub use printer::Printer; pub use printer_options::*; + +use crate::format_element::{ + ConditionalGroupContent, Group, GroupId, LineMode, List, PrintMode, VerbatimKind, +}; +use crate::intersperse::Intersperse; +use crate::{FormatElement, Printed, SourceMarker, TextRange}; +use std::collections::HashMap; + +use crate::prelude::Line; +use rome_rowan::TextSize; +use std::iter::{once, Rev}; + +/// Prints the format elements into a string +#[derive(Debug, Default)] +pub struct Printer<'a> { + options: PrinterOptions, + state: PrinterState<'a>, +} + +impl<'a> Printer<'a> { + pub fn new(options: PrinterOptions) -> Self { + Self { + options, + state: PrinterState::default(), + } + } + + /// Prints the passed in element as well as all its content + pub fn print(self, element: &'a FormatElement) -> Printed { + self.print_with_indent(element, 0) + } + + /// Prints the passed in element as well as all its content, + /// starting at the specified indentation level + pub fn print_with_indent(mut self, element: &'a FormatElement, indent: u16) -> Printed { + tracing::debug_span!("Printer::print").in_scope(move || { + let mut queue = ElementCallQueue::default(); + + queue.enqueue(PrintElementCall::new( + element, + PrintElementArgs::new(indent), + )); + + while let Some(print_element_call) = queue.dequeue() { + self.print_element( + &mut queue, + print_element_call.element, + print_element_call.args, + ); + + if queue.is_empty() && !self.state.line_suffixes.is_empty() { + queue.extend(self.state.line_suffixes.drain(..)); + } + } + + Printed::new( + self.state.buffer, + None, + self.state.source_markers, + self.state.verbatim_markers, + ) + }) + } + + /// Prints a single element and push the following elements to queue + fn print_element( + &mut self, + queue: &mut ElementCallQueue<'a>, + element: &'a FormatElement, + args: PrintElementArgs, + ) { + match element { + FormatElement::Empty => {} + FormatElement::Space => { + if self.state.line_width > 0 { + self.state.pending_space = true; + } + } + FormatElement::Token(token) => { + // Print pending indention + if self.state.pending_indent > 0 { + self.print_str( + self.options + .indent_string + .repeat(self.state.pending_indent as usize) + .as_str(), + ); + self.state.pending_indent = 0; + } + + // Print pending spaces + if self.state.pending_space { + self.print_str(" "); + self.state.pending_space = false; + } + + if let Some(source) = token.source_position() { + self.state.source_markers.push(SourceMarker { + source: *source, + dest: TextSize::from(self.state.buffer.len() as u32), + }); + } + + self.print_str(token); + } + + FormatElement::HardGroup(group) => queue.enqueue(PrintElementCall::new( + &group.content, + args.with_hard_group(true), + )), + + FormatElement::Group(Group { content, id }) => { + let args = args.with_hard_group(false); + + let group_mode = 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 + queue.enqueue(PrintElementCall::new(content.as_ref(), args)); + 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 + _ => { + // 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 + + let flat_args = args.with_print_mode(PrintMode::Flat); + // TODO replace `ElementCallQueue::default` with the real queue to also measure the rest of the document + if fits_on_line(&[content], flat_args, &ElementCallQueue::default(), self) { + queue.enqueue(PrintElementCall::new(content, flat_args)); + self.state.measured_group_fits = true; + PrintMode::Flat + } else { + queue.enqueue(PrintElementCall::new( + content, + args.with_print_mode(PrintMode::Expanded), + )); + PrintMode::Expanded + } + } + }; + + if let Some(id) = id { + self.state.group_modes.insert(*id, group_mode); + } + } + + FormatElement::Fill(list) => { + self.print_fill(queue, list, args); + } + + FormatElement::List(list) => { + queue.extend(list.iter().map(|t| PrintElementCall::new(t, args))); + } + + FormatElement::Indent(indent) => { + queue.enqueue(PrintElementCall::new( + &indent.content, + args.with_incremented_indent(), + )); + } + + FormatElement::ConditionalGroupContent(ConditionalGroupContent { + mode, + content, + group_id, + }) => { + let group_mode = match group_id { + None => &args.mode, + Some(id) => self + .state + .group_modes + .get(id) + .unwrap_or_else(|| panic!("Expected group with id '{id:?}' to exist.")), + }; + + if args.hard_group { + if mode == &PrintMode::Flat { + queue.enqueue(PrintElementCall::new(content, args)); + } + } else if group_mode == mode { + queue.enqueue(PrintElementCall::new(content, args)); + } + } + + FormatElement::Line(line) => { + if (args.mode.is_flat() || args.hard_group) + && matches!(line.mode, LineMode::Soft | LineMode::SoftOrSpace) + { + if line.mode == LineMode::SoftOrSpace && self.state.line_width > 0 { + self.state.pending_space = true; + } + } else if !self.state.line_suffixes.is_empty() { + self.queue_line_suffixes(element, args, queue); + } else { + // Only print a newline if the current line isn't already empty + if self.state.line_width > 0 { + self.print_str("\n"); + } + + // Print a second line break if this is an empty line + if line.mode == LineMode::Empty && !self.state.has_empty_line { + self.print_str("\n"); + self.state.has_empty_line = true; + } + + self.state.pending_space = false; + self.state.pending_indent = args.indent; + + // Fit's only tests if groups up to the first line break fit. + // The next group must re-measure if it still fits. + self.state.measured_group_fits = false; + } + } + + FormatElement::LineSuffix(suffix) => { + self.state + .line_suffixes + .push(PrintElementCall::new(&**suffix, args)); + } + FormatElement::Comment(content) => { + queue.enqueue(PrintElementCall::new(content.as_ref(), args)); + } + + FormatElement::Verbatim(verbatim) => { + if let VerbatimKind::Verbatim { length } = &verbatim.kind { + self.state.verbatim_markers.push(TextRange::at( + TextSize::from(self.state.buffer.len() as u32), + *length, + )); + } + + queue.enqueue(PrintElementCall::new(&verbatim.element, args)); + } + } + } + + fn queue_line_suffixes( + &mut self, + line_break: &'a FormatElement, + args: PrintElementArgs, + queue: &mut ElementCallQueue<'a>, + ) { + if self.state.line_suffixes.is_empty() { + return; + } + + // If the indentation level has changed since these line suffixes were queued, + // insert a line break before to push the comments into the new indent block + // SAFETY: Indexing into line_suffixes is guarded by the above call to is_empty + let has_line_break = self.state.line_suffixes[0].args.indent < args.indent; + + // Print this line break element again once all the line suffixes have been flushed + let call_self = PrintElementCall::new(line_break, args); + + let line_break = if has_line_break { + // Duplicate this line break element before the line + // suffixes if a line break is required + Some(call_self.clone()) + } else { + None + }; + + queue.extend( + line_break + .into_iter() + .chain(self.state.line_suffixes.drain(..).map(move |mut call| { + // Overwrite the arguments for the PrintElementCalls in the queue with the current arguments + call.args = args; + call + })) + .chain(once(call_self)), + ); + } + + /// Tries to fit as much content as possible on a single line. + /// Each item forms a virtual group that is either printed in flat or expanded mode. + /// It handles three different cases: + /// + /// * The first and second content fit on a single line. It prints the content and separator in flat mode. + /// * The first content fits on a single line, but the second doesn't. It prints the content in flat and the separator in expanded mode. + /// * Neither the first nor the second content fit on the line. It brings the first content and the separator in expanded mode. + fn print_fill( + &mut self, + queue: &mut ElementCallQueue<'a>, + content: &'a List, + args: PrintElementArgs, + ) { + const SPACE: &FormatElement = &FormatElement::Space; + const HARD_LINE_BREAK: &FormatElement = &FormatElement::Line(Line::new(LineMode::Hard)); + let empty_rest = ElementCallQueue::default(); + + let mut items = content.iter(); + + let current_content = match items.next() { + None => { + // Empty list + return; + } + Some(item) => item, + }; + + let mut current_fits = fits_on_line( + &[current_content], + args.with_print_mode(PrintMode::Flat), + &empty_rest, + self, + ); + + self.print_all( + queue, + &[current_content], + args.with_print_mode(if current_fits { + PrintMode::Flat + } else { + PrintMode::Expanded + }), + ); + + // Process remaining items + loop { + let next_item = match items.next() { + None => { + // End of list + break; + } + Some(item) => item, + }; + + // A line break in expanded mode is always necessary if the current item didn't fit. + // otherwise see if both contents fit on the line. + let current_and_next_fit = current_fits + && fits_on_line( + &[SPACE, next_item], + args.with_print_mode(PrintMode::Flat), + &empty_rest, + self, + ); + + if current_and_next_fit { + // Print Space and next item on the same line + self.print_all( + queue, + &[SPACE, next_item], + args.with_print_mode(PrintMode::Flat), + ); + } else { + // Print the separator and then check again if the next item fits on the line now + self.print_all( + queue, + &[HARD_LINE_BREAK], + args.with_print_mode(PrintMode::Expanded), + ); + + let next_fits = fits_on_line( + &[next_item], + args.with_print_mode(PrintMode::Flat), + &empty_rest, + self, + ); + + if next_fits { + self.print_all(queue, &[next_item], args.with_print_mode(PrintMode::Flat)); + } else { + self.print_all( + queue, + &[next_item], + args.with_print_mode(PrintMode::Expanded), + ); + } + + current_fits = next_fits; + } + } + } + + /// Fully print an element (print the element itself and all its descendants) + /// + /// Unlike [print_element], this function ensures the entire element has + /// been printed when it returns and the queue is back to its original state + fn print_all( + &mut self, + queue: &mut ElementCallQueue<'a>, + elements: &[&'a FormatElement], + args: PrintElementArgs, + ) { + let min_queue_length = queue.0.len(); + + queue.extend( + elements + .iter() + .map(|element| PrintElementCall::new(element, args)), + ); + + while let Some(call) = queue.dequeue() { + self.print_element(queue, call.element, call.args); + + if queue.0.len() == min_queue_length { + return; + } + + debug_assert!(queue.0.len() > min_queue_length); + } + } + + fn print_str(&mut self, content: &str) { + for char in content.chars() { + if char == '\n' { + self.state + .buffer + .push_str(self.options.line_ending.as_str()); + self.state.generated_line += 1; + self.state.generated_column = 0; + self.state.line_width = 0; + } else { + self.state.buffer.push(char); + self.state.generated_column += 1; + + let char_width = if char == '\t' { + self.options.tab_width as usize + } else { + 1 + }; + + self.state.line_width += char_width; + } + + self.state.has_empty_line = false; + } + } +} + +/// Printer state that is global to all elements. +/// Stores the result of the print operation (buffer and mappings) and at what +/// position the printer currently is. +#[derive(Default, Debug)] +struct PrinterState<'a> { + buffer: String, + source_markers: Vec, + pending_indent: u16, + pending_space: bool, + measured_group_fits: bool, + generated_line: usize, + generated_column: usize, + line_width: usize, + has_empty_line: bool, + line_suffixes: Vec>, + verbatim_markers: Vec, + /// Tracks the mode in which groups with ids are printed. + group_modes: HashMap, + // Re-used queue to measure if a group fits. Optimisation to avoid re-allocating a new + // vec everytime a group gets measured + measure_queue: Vec>, +} + +/// Stores arguments passed to `print_element` call, holding the state specific to printing an element. +/// E.g. the `indent` depends on the token the Printer's currently processing. That's why +/// it must be stored outside of the [PrinterState] that stores the state common to all elements. +/// +/// The state is passed by value, which is why it's important that it isn't storing any heavy +/// data structures. Such structures should be stored on the [PrinterState] instead. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +struct PrintElementArgs { + indent: u16, + hard_group: bool, + mode: PrintMode, +} + +impl PrintElementArgs { + pub fn new(indent: u16) -> Self { + Self { + indent, + ..Self::default() + } + } + + pub fn with_incremented_indent(mut self) -> Self { + self.indent += 1; + self + } + + pub fn with_hard_group(mut self, hard_group: bool) -> Self { + self.hard_group = hard_group; + self + } + + pub fn with_print_mode(mut self, mode: PrintMode) -> Self { + self.mode = mode; + self + } +} + +impl Default for PrintElementArgs { + fn default() -> Self { + Self { + indent: 0, + hard_group: false, + mode: PrintMode::Expanded, + } + } +} + +/// The Printer uses a stack that emulates recursion. E.g. recursively processing the elements: +/// `indent(concat(string, string))` would result in the following call stack: +/// +/// ```plain +/// print_element(indent, indent = 0); +/// print_element(concat, indent = 1); +/// print_element(string, indent = 1); +/// print_element(string, indent = 1); +/// ``` +/// The `PrintElementCall` stores the data for a single `print_element` call consisting of the element +/// and the `args` that's passed to `print_element`. +#[derive(Debug, Eq, PartialEq, Clone)] +struct PrintElementCall<'element> { + element: &'element FormatElement, + args: PrintElementArgs, +} + +impl<'element> PrintElementCall<'element> { + pub fn new(element: &'element FormatElement, args: PrintElementArgs) -> Self { + Self { element, args } + } +} + +/// Small helper that manages the order in which the elements should be visited. +#[derive(Debug, Default)] +struct ElementCallQueue<'a>(Vec>); + +impl<'a> ElementCallQueue<'a> { + pub fn new(elements: Vec>) -> Self { + Self(elements) + } + + fn extend(&mut self, calls: T) + where + T: IntoIterator>, + T::IntoIter: DoubleEndedIterator, + { + // Reverse the calls because elements are removed from the back of the vec + // in reversed insertion order + self.0.extend(calls.into_iter().rev()); + } + + pub fn enqueue(&mut self, call: PrintElementCall<'a>) { + self.0.push(call); + } + + pub fn dequeue(&mut self) -> Option> { + self.0.pop() + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn into_vec(self) -> Vec> { + self.0 + } +} + +/// Tests if it's possible to print the content of the queue up to the first hard line break +/// or the end of the document on a single line without exceeding the line width. +#[must_use = "Only determines if content fits on a single line but doesn't print it"] +fn fits_on_line<'a>( + elements: &[&'a FormatElement], + args: PrintElementArgs, + queue: &ElementCallQueue<'a>, + printer: &mut Printer<'a>, +) -> bool { + let shared_buffer = std::mem::take(&mut printer.state.measure_queue); + debug_assert!(shared_buffer.is_empty()); + + let mut measure_queue = MeasureQueue::new() + .with_rest(queue) + .with_queue(ElementCallQueue::new(shared_buffer)); + + measure_queue.extend( + elements + .iter() + .map(|element| PrintElementCall::new(element, args)), + ); + + let mut measure_state = MeasureState { + pending_indent: printer.state.pending_indent, + pending_space: printer.state.pending_space, + line_width: printer.state.line_width, + }; + + while let Some((element, args)) = measure_queue.dequeue() { + match fits_element_on_line( + element, + args, + &mut measure_state, + &mut measure_queue, + &printer.options, + ) { + Fits::Yes => { + return true; + } + Fits::No => { + return false; + } + Fits::Maybe => { + // Continue checking next item + } + } + } + + let mut shared_buffer = measure_queue.into_vec(); + // Clear out remaining items + shared_buffer.clear(); + printer.state.measure_queue = shared_buffer; + + true +} + +/// Tests if the passed element fits on the current line or not. +fn fits_element_on_line<'a, 'rest>( + element: &'a FormatElement, + args: PrintElementArgs, + state: &mut MeasureState, + queue: &mut MeasureQueue<'a, 'rest>, + options: &PrinterOptions, +) -> Fits { + match element { + FormatElement::Empty => {} + + FormatElement::Space => { + if state.line_width > 0 { + state.pending_space = true; + } + } + + FormatElement::Line(line) => { + if args.mode.is_flat() || args.hard_group { + match line.mode { + LineMode::SoftOrSpace => { + state.pending_space = true; + } + LineMode::Soft => {} + LineMode::Hard | LineMode::Empty => { + // Propagate the line break to make it break the parent too, except if this is + // a hard group that ALWAYS fits on a single line. + return Fits::from(args.hard_group); + } + } + } 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 Fits::Yes; + } + } + + FormatElement::Indent(indent) => queue.enqueue(PrintElementCall::new( + &indent.content, + args.with_incremented_indent(), + )), + + FormatElement::HardGroup(group) => queue.enqueue(PrintElementCall::new( + &group.content, + args.with_hard_group(true), + )), + FormatElement::Group(group) => queue.enqueue(PrintElementCall::new( + &group.content, + args.with_hard_group(false).with_print_mode(PrintMode::Flat), + )), + + FormatElement::ConditionalGroupContent(conditional) => { + if args.hard_group { + if conditional.mode == PrintMode::Flat { + queue.enqueue(PrintElementCall::new(&conditional.content, args)) + } + } else if args.mode == conditional.mode { + queue.enqueue(PrintElementCall::new(&conditional.content, args)) + } + } + + FormatElement::List(list) => { + queue.extend(list.iter().map(|t| PrintElementCall::new(t, args))) + } + + FormatElement::Fill(content) => { + const SPACE: &FormatElement = &FormatElement::Space; + queue.queue.0.extend( + Intersperse::new(content.iter().rev(), SPACE) + .map(|t| PrintElementCall::new(t, args)), + ) + } + + FormatElement::Token(token) => { + state.line_width += state.pending_indent as usize * options.indent_string.len(); + + if state.pending_space { + state.line_width += 1; + } + + for c in token.chars() { + let char_width = match c { + '\t' => options.tab_width, + '\n' => { + return match args.mode { + PrintMode::Flat => Fits::No, + PrintMode::Expanded => Fits::Yes, + } + } + _ => 1, + }; + state.line_width += char_width as usize; + } + + if state.line_width > options.print_width.value().into() { + return Fits::No; + } + + state.pending_space = false; + state.pending_indent = 0; + } + + FormatElement::LineSuffix(_) => { + // The current behavior is to return `false` for all line suffixes if trying to print + // something in "flat" mode. + if args.mode.is_flat() { + return Fits::No; + } + } + + FormatElement::Comment(content) => queue.enqueue(PrintElementCall::new(content, args)), + + FormatElement::Verbatim(verbatim) => { + queue.enqueue(PrintElementCall::new(&verbatim.element, args)) + } + } + + Fits::Maybe +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum Fits { + // Element fits + Yes, + // Element doesn't fit + No, + // Element may fit, depends on the elements following it + Maybe, +} + +impl From for Fits { + fn from(value: bool) -> Self { + match value { + true => Fits::Yes, + false => Fits::No, + } + } +} + +/// State used when measuring if a group fits on a single line +#[derive(Debug)] +struct MeasureState { + pending_indent: u16, + pending_space: bool, + line_width: usize, +} + +#[derive(Debug)] +struct MeasureQueue<'a, 'rest> { + /// Queue that holds the elements that the `fits` operation inspects. + /// Normally, these all the elements belonging to the group that is tested if it fits + queue: ElementCallQueue<'a>, + /// Queue that contains the remaining elements in the documents. + rest_queue: Rev>>, +} + +impl<'a, 'rest> MeasureQueue<'a, 'rest> { + fn new() -> Self { + Self { + rest_queue: [].iter().rev(), + queue: ElementCallQueue::default(), + } + } + + fn with_rest(mut self, rest_queue: &'rest ElementCallQueue<'a>) -> Self { + // Last element in the vector is the first element in the queue + self.rest_queue = rest_queue.0.as_slice().iter().rev(); + self + } + + fn with_queue(mut self, queue: ElementCallQueue<'a>) -> Self { + debug_assert!(queue.is_empty()); + self.queue = queue; + self + } + + fn enqueue(&mut self, call: PrintElementCall<'a>) { + self.queue.enqueue(call); + } + + fn extend(&mut self, calls: T) + where + T: IntoIterator>, + T::IntoIter: DoubleEndedIterator, + { + // Reverse the calls because elements are removed from the back of the vec + // in reversed insertion order + self.queue.0.extend(calls.into_iter().rev()); + } + + fn dequeue(&mut self) -> Option<(&'a FormatElement, PrintElementArgs)> { + let next = match self.queue.dequeue() { + Some(call) => (call.element, call.args), + None => { + let rest_item = self.rest_queue.next()?; + + (rest_item.element, rest_item.args) + } + }; + + Some(next) + } + + fn into_vec(self) -> Vec> { + self.queue.into_vec() + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + use crate::printer::{LineEnding, Printer, PrinterOptions}; + use crate::FormatElement::LineSuffix; + use crate::{LineWidth, Printed}; + + /// Prints the given element with the default printer options + fn print_element>(element: T) -> Printed { + let options = PrinterOptions { + indent_string: String::from(" "), + ..PrinterOptions::default() + }; + + Printer::new(options).print(&element.into()) + } + + #[test] + fn it_prints_a_group_on_a_single_line_if_it_fits() { + let result = print_element(create_array_element(vec![ + token("\"a\""), + token("\"b\""), + token("\"c\""), + token("\"d\""), + ])); + + assert_eq!(r#"["a", "b", "c", "d"]"#, result.as_code()) + } + + #[test] + fn it_tracks_the_indent_for_each_token() { + let element = format_elements![ + token("a"), + soft_block_indent(format_elements![ + token("b"), + soft_block_indent(format_elements![ + token("c"), + soft_block_indent(format_elements![token("d"), soft_line_break(), token("d"),],), + token("c"), + ],), + token("b"), + ],), + token("a"), + ]; + + assert_eq!( + r#"a + b + c + d + d + c + b +a"#, + print_element(element).as_code() + ) + } + + #[test] + fn it_converts_line_endings() { + let options = PrinterOptions { + line_ending: LineEnding::CarriageReturnLineFeed, + ..PrinterOptions::default() + }; + + let program = format_elements![ + token("function main() {"), + block_indent(token("let x = `This is a multiline\nstring`;"),), + token("}"), + hard_line_break(), + ]; + + let result = Printer::new(options).print(&program); + + assert_eq!( + "function main() {\r\n\tlet x = `This is a multiline\r\nstring`;\r\n}\r\n", + result.as_code() + ); + } + + #[test] + fn it_breaks_a_group_if_a_string_contains_a_newline() { + let result = print_element(create_array_element(vec![ + token("`This is a string spanning\ntwo lines`"), + token("\"b\""), + ])); + + assert_eq!( + r#"[ + `This is a string spanning +two lines`, + "b", +]"#, + result.as_code() + ) + } + + #[test] + fn it_breaks_a_group_if_it_contains_a_hard_line_break() { + let result = print_element(group_elements(format_elements![ + token("a"), + block_indent(token("b")) + ])); + + assert_eq!("a\n b\n", result.as_code()) + } + + #[test] + fn it_breaks_parent_groups_if_they_dont_fit_on_a_single_line() { + let result = print_element(create_array_element(vec![ + token("\"a\""), + token("\"b\""), + token("\"c\""), + token("\"d\""), + create_array_element(vec![ + token("\"0123456789\""), + token("\"0123456789\""), + token("\"0123456789\""), + token("\"0123456789\""), + token("\"0123456789\""), + ]), + ])); + + assert_eq!( + r#"[ + "a", + "b", + "c", + "d", + ["0123456789", "0123456789", "0123456789", "0123456789", "0123456789"], +]"#, + result.as_code() + ); + } + + #[test] + fn it_use_the_indent_character_specified_in_the_options() { + let printer = Printer::new(PrinterOptions { + indent_string: String::from("\t"), + tab_width: 4, + print_width: LineWidth::try_from(19).unwrap(), + ..PrinterOptions::default() + }); + + let element = + create_array_element(vec![token("'a'"), token("'b'"), token("'c'"), token("'d'")]); + + let result = printer.print(&element); + + assert_eq!("[\n\t'a',\n\t\'b',\n\t\'c',\n\t'd',\n]", result.as_code()); + } + + #[test] + fn it_prints_consecutive_hard_lines_as_one() { + let result = print_element(format_elements![ + token("a"), + hard_line_break(), + hard_line_break(), + hard_line_break(), + token("b"), + ]); + + assert_eq!("a\nb", result.as_code()) + } + + #[test] + fn it_prints_consecutive_empty_lines_as_one() { + let result = print_element(format_elements![ + token("a"), + empty_line(), + empty_line(), + empty_line(), + token("b"), + ]); + + assert_eq!("a\n\nb", result.as_code()) + } + + #[test] + fn it_prints_consecutive_mixed_lines_as_one() { + let result = print_element(format_elements![ + token("a"), + empty_line(), + hard_line_break(), + empty_line(), + hard_line_break(), + token("b"), + ]); + + assert_eq!("a\n\nb", result.as_code()) + } + + #[test] + fn test_sequence() { + let document = format_elements![ + hard_group_elements(format_elements![ + token("something"), + space_token(), + token("&&") + ]), + group_elements(soft_line_indent_or_space(format_elements![ + token("elsewhere"), + space_token(), + token("&&"), + soft_line_break_or_space(), + token("happy"), + space_token(), + token("&&"), + soft_line_break_or_space(), + token("thoughts"), + space_token(), + token("&&"), + soft_line_break_or_space(), + token("dldldlldldldldldldldl"), + space_token(), + token("&&"), + soft_line_break_or_space(), + token("dldldlldldldldldldldl") + ])), + token(";"), + hard_line_break() + ]; + + let printed = print_element(document); + + assert_eq!( + r#"something && + elsewhere && + happy && + thoughts && + dldldlldldldldldldldl && + dldldlldldldldldldldl; +"#, + printed.as_code() + ); + } + + #[test] + fn test_fill_breaks() { + let document = fill_elements(vec![ + // These all fit on the same line together + format_elements![token("1"), token(",")], + format_elements![token("2"), token(",")], + format_elements![token("3"), token(",")], + // This one fits on a line by itself, + format_elements![token("723493294"), token(",")], + // fits without breaking + format_elements![group_elements(format_elements![ + token("["), + soft_block_indent(token("5")), + token("],") + ])], + // this one must be printed in expanded mode to fit + group_elements(format_elements![ + token("["), + soft_block_indent(token("123456789")), + token("]"), + ]), + ]); + + let printed = + Printer::new(PrinterOptions::default().with_print_with(LineWidth(10))).print(&document); + + assert_eq!( + printed.as_code(), + "1, 2, 3,\n723493294,\n[5],\n[\n\t123456789\n]" + ) + } + + #[test] + fn line_suffix() { + let document = format_elements![ + group_elements(format_elements![ + token("["), + soft_block_indent(format_elements![fill_elements(vec![ + format_elements![token("1"), token(",")], + format_elements![token("2"), token(",")], + format_elements![token("3"), if_group_breaks(token(","))] + ])]), + token("]") + ]), + token(";"), + comment(LineSuffix(Box::new(format_elements![ + space_token(), + token("// trailing"), + space_token() + ]))) + ]; + + let printed = print_element(document); + + assert_eq!(printed.as_code(), "[1, 2, 3]; // trailing") + } + + #[test] + fn hard_group_inner_group_breaks() { + let document = format_elements![hard_group_elements(format_elements![ + token("do"), + space_token(), + group_elements(format_elements![token("{"), token("}")]), + space_token(), + token("while"), + space_token(), + group_elements(format_elements![ + token("("), + soft_block_indent(format_elements![ + hard_group_elements(token("testsomelongerid")), + space_token(), + token("&&"), + soft_line_break_or_space(), + token("thoughts"), + space_token(), + token("&&"), + soft_line_break_or_space(), + token("somethingsomethingsomethingsomethin") + ]), + token(")") + ]), + token(";") + ])]; + + let printed = print_element(document); + + assert_eq!( + printed.as_code(), + r#"do {} while ( + testsomelongerid && + thoughts && + somethingsomethingsomethingsomethin +);"# + ); + } + + fn create_array_element(items: Vec) -> FormatElement { + let separator = format_elements![token(","), soft_line_break_or_space(),]; + + let elements = + format_elements![join_elements(separator, items), if_group_breaks(token(","))]; + + group_elements(format_elements![ + token("["), + soft_block_indent(elements), + token("]"), + ]) + } +} diff --git a/crates/rome_formatter/src/printer/printer.rs b/crates/rome_formatter/src/printer/printer.rs deleted file mode 100644 index afbf5bc403d..00000000000 --- a/crates/rome_formatter/src/printer/printer.rs +++ /dev/null @@ -1,763 +0,0 @@ -use super::printer_options::PrinterOptions; -use crate::format_element::{ - ConditionalGroupContent, Group, GroupPrintMode, LineMode, List, VerbatimKind, -}; -use crate::intersperse::Intersperse; -use crate::{hard_line_break, space_token, FormatElement, Printed, SourceMarker, TextRange}; -use rome_rowan::TextSize; -use std::iter::once; - -/// Error returned if printing an item as a flat string fails because it either contains -/// explicit line breaks or would otherwise exceed the specified line width. -struct LineBreakRequiredError; - -/// Prints the format elements into a string -#[derive(Debug, Clone, Default)] -pub struct Printer<'a> { - options: PrinterOptions, - state: PrinterState<'a>, -} - -impl<'a> Printer<'a> { - pub fn new>(options: T) -> Self { - Self { - options: options.into(), - state: PrinterState::default(), - } - } - - /// Prints the passed in element as well as all its content - pub fn print(self, element: &'a FormatElement) -> Printed { - tracing::debug_span!("Printer::print").in_scope(move || self.print_with_indent(element, 0)) - } - - /// Prints the passed in element as well as all its content, - /// starting at the specified indentation level - pub fn print_with_indent(mut self, element: &'a FormatElement, indent: u16) -> Printed { - let mut queue = ElementCallQueue::new(); - - queue.enqueue(PrintElementCall::new( - element, - PrintElementArgs { - indent, - hard_group: false, - }, - )); - - while let Some(print_element_call) = queue.dequeue() { - self.print_element( - &mut queue, - print_element_call.element, - print_element_call.args, - ); - - if queue.is_empty() && !self.state.line_suffixes.is_empty() { - queue.extend(self.state.line_suffixes.drain(..)); - } - } - - Printed::new( - self.state.buffer, - None, - self.state.source_markers, - self.state.verbatim_markers, - ) - } - - /// Prints a single element and push the following elements to queue - fn print_element( - &mut self, - queue: &mut ElementCallQueue<'a>, - element: &'a FormatElement, - args: PrintElementArgs, - ) { - match element { - FormatElement::Space => { - if self.state.line_width > 0 { - self.state.pending_space = true; - } - } - FormatElement::Empty => {} - FormatElement::Token(token) => { - // Print pending indention - if self.state.pending_indent > 0 { - self.print_str( - self.options - .indent_string - .repeat(self.state.pending_indent as usize) - .as_str(), - ); - self.state.pending_indent = 0; - } - - // Print pending spaces - if self.state.pending_space { - self.print_str(" "); - self.state.pending_space = false; - } - - if let Some(source) = token.source_position() { - self.state.source_markers.push(SourceMarker { - source: *source, - dest: TextSize::from(self.state.buffer.len() as u32), - }); - } - - self.print_str(token); - } - - FormatElement::HardGroup(group) => queue.enqueue(PrintElementCall::new( - group.content.as_ref(), - args.with_hard_group(true), - )), - FormatElement::Group(Group { content }) => { - let args = args.with_hard_group(false); - if self.try_print_flat(queue, element, args.clone()).is_err() { - // Flat printing didn't work, print with line breaks - queue.enqueue(PrintElementCall::new(content.as_ref(), args)); - } - } - - FormatElement::Fill(list) => { - self.print_fill(queue, list, args); - } - - FormatElement::List(list) => { - queue.extend(list.iter().map(|t| PrintElementCall::new(t, args.clone()))); - } - - FormatElement::Indent(indent) => { - queue.enqueue(PrintElementCall::new( - &indent.content, - args.with_incremented_indent(), - )); - } - - FormatElement::ConditionalGroupContent(ConditionalGroupContent { mode, content }) => { - if args.hard_group == matches!(mode, GroupPrintMode::Flat) { - queue.enqueue(PrintElementCall::new(content, args)); - } - } - - FormatElement::Line(line) => { - if args.hard_group && matches!(line.mode, LineMode::Soft | LineMode::SoftOrSpace) { - self.state.pending_space |= line.mode == LineMode::SoftOrSpace; - } else if !self.state.line_suffixes.is_empty() { - // If the indentation level has changed since these line suffixes were queued, - // insert a line break before to push the comments into the new indent block - // SAFETY: Indexing into line_suffixes is guarded by the above call to is_empty - let has_line_break = self.state.line_suffixes[0].args.indent < args.indent; - - // Print this line break element again once all the line suffixes have been flushed - let call_self = PrintElementCall::new(element, args.clone()); - - let line_break = if has_line_break { - // Duplicate this line break element before the line - // suffixes if a line break is required - Some(call_self.clone()) - } else { - None - }; - - queue.extend( - line_break - .into_iter() - .chain(self.state.line_suffixes.drain(..).map(move |mut call| { - // Overwrite the arguments for the PrintElementCalls in the queue with the current arguments - call.args = args.clone(); - call - })) - .chain(once(call_self)), - ); - } else { - // Only print a newline if the current line isn't already empty - if self.state.line_width > 0 { - self.print_str("\n"); - } - - // Print a second line break if this is an empty line - if line.mode == LineMode::Empty && !self.state.has_empty_line { - self.print_str("\n"); - self.state.has_empty_line = true; - } - - self.state.pending_space = false; - self.state.pending_indent = args.indent; - } - } - - FormatElement::LineSuffix(suffix) => { - self.state - .line_suffixes - .push(PrintElementCall::new(&**suffix, args)); - } - FormatElement::Comment(content) => { - queue.enqueue(PrintElementCall::new(content.as_ref(), args)); - } - - FormatElement::Verbatim(verbatim) => { - if let VerbatimKind::Verbatim { length } = &verbatim.kind { - self.state.verbatim_markers.push(TextRange::at( - TextSize::from(self.state.buffer.len() as u32), - *length, - )); - } - - queue.enqueue(PrintElementCall::new(&verbatim.element, args)); - } - } - } - - /// Tries to print an element without any line breaks. Reverts any made `state` changes (by this function) - /// and returns with a [LineBreakRequiredError] if the `element` contains any hard line breaks - /// or printing the group exceeds the configured maximal print width. - fn try_print_flat( - &mut self, - queue: &mut ElementCallQueue<'a>, - element: &'a FormatElement, - args: PrintElementArgs, - ) -> Result<(), LineBreakRequiredError> { - let snapshot = self.state.snapshot(); - let min_queue_length = queue.0.len(); - - queue.enqueue(PrintElementCall::new(element, args)); - - while let Some(call) = queue.dequeue() { - if let Err(err) = self.try_print_flat_element(queue, call.element, call.args) { - self.state.restore(snapshot); - queue.0.truncate(min_queue_length); - return Err(err); - } - - if queue.0.len() == min_queue_length { - break; - } - - debug_assert!(queue.0.len() > min_queue_length); - } - - Ok(()) - } - - fn try_print_flat_element( - &mut self, - queue: &mut ElementCallQueue<'a>, - element: &'a FormatElement, - args: PrintElementArgs, - ) -> Result<(), LineBreakRequiredError> { - match element { - FormatElement::Token(_) => { - let current_line = self.state.generated_line; - - // Delegate to generic string printing - self.print_element(queue, element, args); - - // If the line is too long, break the group - if self.state.line_width > self.options.print_width.value().into() { - return Err(LineBreakRequiredError); - } - - // If a new line was printed, break the group - if current_line != self.state.generated_line { - return Err(LineBreakRequiredError); - } - } - FormatElement::Line(line) => { - match line.mode { - LineMode::SoftOrSpace => { - if self.state.line_width > 0 { - self.state.pending_space = true; - } - } - // We want a flat structure, so omit soft line wraps - LineMode::Soft => {} - LineMode::Hard | LineMode::Empty => return Err(LineBreakRequiredError), - } - } - - FormatElement::HardGroup(group) => queue.enqueue(PrintElementCall::new( - group.content.as_ref(), - args.with_hard_group(true), - )), - FormatElement::Group(group) => queue.enqueue(PrintElementCall::new( - group.content.as_ref(), - args.with_hard_group(false), - )), - - // Fill elements are printed as space-separated lists in flat mode - FormatElement::Fill(list) => { - // Intersperse the list of elements with spaces before pushing - // them to the queue, however elements in the queue are stored - // as references so the space element must be allocated in a - // static so its reference is bound to the static lifetime - static SPACE: FormatElement = space_token(); - queue.0.extend( - Intersperse::new(list.iter().rev(), &SPACE) - .map(|t| PrintElementCall::new(t, args.clone())), - ); - } - - FormatElement::ConditionalGroupContent(ConditionalGroupContent { - mode: GroupPrintMode::Flat, - content, - }) => queue.enqueue(PrintElementCall::new(content, args)), - - // Omit if there's no flat_contents - FormatElement::ConditionalGroupContent(ConditionalGroupContent { - mode: GroupPrintMode::Multiline, - .. - }) => {} - - FormatElement::Comment(content) => { - queue.enqueue(PrintElementCall::new(content.as_ref(), args)); - } - - FormatElement::LineSuffix { .. } => return Err(LineBreakRequiredError), - - FormatElement::Empty - | FormatElement::Space - | FormatElement::Indent { .. } - | FormatElement::Verbatim { .. } - | FormatElement::List { .. } => self.print_element(queue, element, args), - } - - Ok(()) - } - - /// Print a list in fill mode. - /// - /// Prints the elements of the list separated by spaces, but backtrack if - /// they go over the print width and insert a line break before resuming - /// printing - fn print_fill( - &mut self, - queue: &mut ElementCallQueue<'a>, - content: &'a List, - args: PrintElementArgs, - ) { - let mut snapshot = None; - - for item in content.iter() { - if snapshot.is_some() { - self.state.pending_space = true; - } - - self.print_all(queue, item, args.clone()); - - if self.state.line_width > self.options.print_width.value().into() { - if let Some(snapshot) = snapshot.take() { - self.state.restore(snapshot); - - static LINE: FormatElement = hard_line_break(); - self.print_all(queue, &LINE, args.clone()); - - self.print_all(queue, item, args.clone()); - } - } - - snapshot = Some(self.state.snapshot()); - } - } - - /// Fully print an element (print the element itself and all its descendants) - /// - /// Unlike [print_element], this function ensures the entire element has - /// been printed when it returns and the queue is back to its original state - fn print_all( - &mut self, - queue: &mut ElementCallQueue<'a>, - element: &'a FormatElement, - args: PrintElementArgs, - ) { - let min_queue_length = queue.0.len(); - - queue.enqueue(PrintElementCall::new(element, args)); - - while let Some(call) = queue.dequeue() { - self.print_element(queue, call.element, call.args); - - if queue.0.len() == min_queue_length { - return; - } - - debug_assert!(queue.0.len() > min_queue_length); - } - } - - fn print_str(&mut self, content: &str) { - self.state.buffer.reserve(content.len()); - - for char in content.chars() { - if char == '\n' { - for char in self.options.line_ending.as_str().chars() { - self.state.buffer.push(char); - } - - self.state.generated_line += 1; - self.state.generated_column = 0; - self.state.line_width = 0; - } else { - self.state.buffer.push(char); - self.state.generated_column += 1; - - let char_width = if char == '\t' { - self.options.tab_width as usize - } else { - 1 - }; - - self.state.line_width += char_width; - } - - self.state.has_empty_line = false; - } - } -} - -/// Printer state that is global to all elements. -/// Stores the result of the print operation (buffer and mappings) and at what -/// position the printer currently is. -#[derive(Default, Debug, Clone)] -struct PrinterState<'a> { - buffer: String, - source_markers: Vec, - pending_indent: u16, - pending_space: bool, - generated_line: usize, - generated_column: usize, - line_width: usize, - has_empty_line: bool, - line_suffixes: Vec>, - verbatim_markers: Vec, -} - -impl<'a> PrinterState<'a> { - /// Allows creating a snapshot of the state that can be restored using [restore] - pub fn snapshot(&self) -> PrinterStateSnapshot { - PrinterStateSnapshot { - pending_space: self.pending_space, - pending_indents: self.pending_indent, - generated_line: self.generated_line, - generated_column: self.generated_column, - line_width: self.line_width, - has_empty_line: self.has_empty_line, - buffer_position: self.buffer.len(), - tokens_position: self.source_markers.len(), - verbatim_markers: self.verbatim_markers.len(), - } - } - - /// Restores the printer state to the state stored in the snapshot. - pub fn restore(&mut self, snapshot: PrinterStateSnapshot) { - self.pending_space = snapshot.pending_space; - self.pending_indent = snapshot.pending_indents; - self.generated_column = snapshot.generated_column; - self.generated_line = snapshot.generated_line; - self.line_width = snapshot.line_width; - self.has_empty_line = snapshot.has_empty_line; - self.buffer.truncate(snapshot.buffer_position); - self.source_markers.truncate(snapshot.tokens_position); - self.verbatim_markers.truncate(snapshot.verbatim_markers); - } -} - -/// Snapshot of a printer state. -struct PrinterStateSnapshot { - pending_indents: u16, - pending_space: bool, - generated_column: usize, - generated_line: usize, - line_width: usize, - has_empty_line: bool, - buffer_position: usize, - tokens_position: usize, - verbatim_markers: usize, -} - -/// Stores arguments passed to `print_element` call, holding the state specific to printing an element. -/// E.g. the `indent` depends on the token the Printer's currently processing. That's why -/// it must be stored outside of the [PrinterState] that stores the state common to all elements. -/// -/// The state is passed by value, which is why it's important that it isn't storing any heavy -/// data structures. Such structures should be stored on the [PrinterState] instead. -#[derive(Debug, Default, Clone, Eq, PartialEq)] -struct PrintElementArgs { - indent: u16, - hard_group: bool, -} - -impl PrintElementArgs { - pub fn new(indent: u16) -> Self { - Self { - indent, - hard_group: false, - } - } - - pub fn with_incremented_indent(self) -> Self { - Self::new(self.indent + 1) - } - - pub fn with_hard_group(self, hard_group: bool) -> Self { - Self { hard_group, ..self } - } -} - -/// The Printer uses a stack that emulates recursion. E.g. recursively processing the elements: -/// `indent(concat(string, string))` would result in the following call stack: -/// -/// ```plain -/// print_element(indent, indent = 0); -/// print_element(concat, indent = 1); -/// print_element(string, indent = 1); -/// print_element(string, indent = 1); -/// ``` -/// The `PrintElementCall` stores the data for a single `print_element` call consisting of the element -/// and the `args` that's passed to `print_element`. -#[derive(Debug, Eq, PartialEq, Clone)] -struct PrintElementCall<'element> { - element: &'element FormatElement, - args: PrintElementArgs, -} - -impl<'element> PrintElementCall<'element> { - pub fn new(element: &'element FormatElement, args: PrintElementArgs) -> Self { - Self { element, args } - } -} - -/// Small helper that manages the order in which the elements should be visited. -#[derive(Debug, Default)] -struct ElementCallQueue<'a>(Vec>); - -impl<'a> ElementCallQueue<'a> { - #[inline] - pub fn new() -> Self { - Self(Vec::new()) - } - - #[inline] - fn extend(&mut self, calls: T) - where - T: IntoIterator>, - T::IntoIter: DoubleEndedIterator, - { - // Reverse the calls because elements are removed from the back of the vec - // in reversed insertion order - self.0.extend(calls.into_iter().rev()); - } - - #[inline] - pub fn enqueue(&mut self, call: PrintElementCall<'a>) { - self.0.push(call); - } - - #[inline] - pub fn dequeue(&mut self) -> Option> { - self.0.pop() - } - - #[inline] - fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -#[cfg(test)] -mod tests { - use crate::format_element::join_elements; - use crate::printer::{LineEnding, Printer, PrinterOptions}; - use crate::{ - block_indent, empty_line, format_elements, group_elements, hard_line_break, - if_group_breaks, soft_block_indent, soft_line_break, soft_line_break_or_space, token, - FormatElement, LineWidth, Printed, - }; - - /// Prints the given element with the default printer options - fn print_element>(element: T) -> Printed { - let options = PrinterOptions { - indent_string: String::from(" "), - ..PrinterOptions::default() - }; - - Printer::new(options).print(&element.into()) - } - - #[test] - fn it_prints_a_group_on_a_single_line_if_it_fits() { - let result = print_element(create_array_element(vec![ - token("\"a\""), - token("\"b\""), - token("\"c\""), - token("\"d\""), - ])); - - assert_eq!(r#"["a", "b", "c", "d"]"#, result.as_code()) - } - - #[test] - fn it_tracks_the_indent_for_each_token() { - let element = format_elements![ - token("a"), - soft_block_indent(format_elements![ - token("b"), - soft_block_indent(format_elements![ - token("c"), - soft_block_indent(format_elements![token("d"), soft_line_break(), token("d"),],), - token("c"), - ],), - token("b"), - ],), - token("a"), - ]; - - assert_eq!( - r#"a - b - c - d - d - c - b -a"#, - print_element(element).as_code() - ) - } - - #[test] - fn it_breaks_a_group_if_a_string_contains_a_newline() { - let result = print_element(create_array_element(vec![ - token("`This is a string spanning\ntwo lines`"), - token("\"b\""), - ])); - - assert_eq!( - r#"[ - `This is a string spanning -two lines`, - "b", -]"#, - result.as_code() - ) - } - - #[test] - fn it_converts_line_endings_in_strings() { - let options = PrinterOptions { - line_ending: LineEnding::CarriageReturnLineFeed, - ..PrinterOptions::default() - }; - - let program = format_elements![ - token("function main() {"), - block_indent(token("let x = `This is a multiline\nstring`;"),), - token("}"), - hard_line_break(), - ]; - - let result = Printer::new(options).print(&program); - - assert_eq!( - "function main() {\r\n\tlet x = `This is a multiline\r\nstring`;\r\n}\r\n", - result.as_code() - ); - } - - #[test] - fn it_breaks_parent_groups_if_they_dont_fit_on_a_single_line() { - let result = print_element(create_array_element(vec![ - token("\"a\""), - token("\"b\""), - token("\"c\""), - token("\"d\""), - create_array_element(vec![ - token("\"0123456789\""), - token("\"0123456789\""), - token("\"0123456789\""), - token("\"0123456789\""), - token("\"0123456789\""), - ]), - ])); - - assert_eq!( - r#"[ - "a", - "b", - "c", - "d", - ["0123456789", "0123456789", "0123456789", "0123456789", "0123456789"], -]"#, - result.as_code() - ); - } - - #[test] - fn it_use_the_indent_character_specified_in_the_options() { - let printer = Printer::new(PrinterOptions { - indent_string: String::from("\t"), - tab_width: 4, - print_width: LineWidth::try_from(19).unwrap(), - ..PrinterOptions::default() - }); - - let element = - create_array_element(vec![token("'a'"), token("'b'"), token("'c'"), token("'d'")]); - - let result = printer.print(&element); - - assert_eq!("[\n\t'a',\n\t\'b',\n\t\'c',\n\t'd',\n]", result.as_code()); - } - - fn create_array_element(items: Vec) -> FormatElement { - let separator = format_elements![token(","), soft_line_break_or_space(),]; - - let elements = - format_elements![join_elements(separator, items), if_group_breaks(token(","))]; - - group_elements(format_elements![ - token("["), - soft_block_indent(elements), - token("]"), - ]) - } - - #[test] - fn it_prints_consecutive_hard_lines_as_one() { - let result = print_element(format_elements![ - token("a"), - hard_line_break(), - hard_line_break(), - hard_line_break(), - token("b"), - ]); - - assert_eq!("a\nb", result.as_code()) - } - - #[test] - fn it_prints_consecutive_empty_lines_as_one() { - let result = print_element(format_elements![ - token("a"), - empty_line(), - empty_line(), - empty_line(), - token("b"), - ]); - - assert_eq!("a\n\nb", result.as_code()) - } - - #[test] - fn it_prints_consecutive_mixed_lines_as_one() { - let result = print_element(format_elements![ - token("a"), - empty_line(), - hard_line_break(), - empty_line(), - hard_line_break(), - token("b"), - ]); - - assert_eq!("a\n\nb", result.as_code()) - } -} diff --git a/crates/rome_js_formatter/src/formatter.rs b/crates/rome_js_formatter/src/formatter.rs index 40caa52b446..f66403222d2 100644 --- a/crates/rome_js_formatter/src/formatter.rs +++ b/crates/rome_js_formatter/src/formatter.rs @@ -8,7 +8,7 @@ use rome_rowan::{AstNode, AstNodeList, AstSeparatedList, Language, SyntaxTriviaP use std::iter::once; -#[derive(Debug)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum TrailingSeparator { Allowed, Disallowed, @@ -30,6 +30,24 @@ impl Default for TrailingSeparator { } } +#[derive(Debug, Default, Copy, Clone)] +pub struct FormatSeparatedOptions { + trailing_separator: TrailingSeparator, + group_id: Option, +} + +impl FormatSeparatedOptions { + pub fn with_trailing_separator(mut self, separator: TrailingSeparator) -> Self { + self.trailing_separator = separator; + self + } + + pub fn with_group_id(mut self, group_id: Option) -> Self { + self.group_id = group_id; + self + } +} + /// Determines if the whitespace separating comment trivias /// from their associated tokens should be printed or trimmed #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -426,63 +444,18 @@ where Ok(concat_elements(elements.into_iter().rev())) } -enum DelimitedContent { - BlockIndent(FormatElement), - SoftBlockIndent(FormatElement), - SoftBlockSpaces(FormatElement), -} - /// JS specific formatter extensions pub(crate) trait JsFormatter { fn as_formatter(&self) -> &Formatter; - /// Formats a group delimited by an opening and closing token, placing the - /// content in a [block_indent] group - fn format_delimited_block_indent( - &self, - open_token: &JsSyntaxToken, + #[must_use] + fn delimited<'a, 'fmt>( + &'fmt self, + open_token: &'a JsSyntaxToken, content: FormatElement, - close_token: &JsSyntaxToken, - ) -> FormatResult { - format_delimited( - self.as_formatter(), - open_token, - DelimitedContent::BlockIndent(content), - close_token, - ) - } - - /// Formats a group delimited by an opening and closing token, placing the - /// content in a [soft_block_indent] group - fn format_delimited_soft_block_indent( - &self, - open_token: &JsSyntaxToken, - content: FormatElement, - close_token: &JsSyntaxToken, - ) -> FormatResult { - format_delimited( - self.as_formatter(), - open_token, - DelimitedContent::SoftBlockIndent(content), - close_token, - ) - } - - /// Formats a group delimited by an opening and closing token, placing the - /// content in an [indent] group with [soft_line_break_or_space] tokens at the - /// start and end - fn format_delimited_soft_block_spaces( - &self, - open_token: &JsSyntaxToken, - content: FormatElement, - close_token: &JsSyntaxToken, - ) -> FormatResult { - format_delimited( - self.as_formatter(), - open_token, - DelimitedContent::SoftBlockSpaces(content), - close_token, - ) + close_token: &'a JsSyntaxToken, + ) -> FormatDelimited<'a, 'fmt> { + FormatDelimited::new(open_token, content, close_token, self.as_formatter()) } /// Print out a `token` from the original source with a different `content`. @@ -515,7 +488,30 @@ pub(crate) trait JsFormatter { &self, list: &L, separator_factory: F, - trailing_separator: TrailingSeparator, + ) -> FormatResult> + where + L: AstSeparatedList, + for<'a> L::Node: AstNode + AsFormat<'a>, + F: Fn() -> FormatElement, + { + self.format_separated_with_options( + list, + separator_factory, + FormatSeparatedOptions::default(), + ) + } + + /// Prints a separated list of nodes + /// + /// Trailing separators will be reused from the original list or + /// created by calling the `separator_factory` function. + /// The last trailing separator in the list will only be printed + /// if the outer group breaks. + fn format_separated_with_options( + &self, + list: &L, + separator_factory: F, + options: FormatSeparatedOptions, ) -> FormatResult> where L: AstSeparatedList, @@ -526,6 +522,16 @@ pub(crate) trait JsFormatter { let last_index = list.len().saturating_sub(1); let formatter = self.as_formatter(); + let trailing_separator_factory = || { + if let Some(group_id) = options.group_id { + if_group_with_id_breaks(separator_factory(), group_id) + } else { + if_group_breaks(separator_factory()) + } + }; + + let trailing_separator = options.trailing_separator; + for (index, element) in list.elements().enumerate() { let node = formatted![formatter, [element.node()?.format()]]?; @@ -537,7 +543,7 @@ pub(crate) trait JsFormatter { // Use format_replaced instead of wrapping the result of format_token // in order to remove only the token itself when the group doesn't break // but still print its associated trivias unconditionally - self.format_replaced(separator, if_group_breaks(Token::from(separator))) + self.format_replaced(separator, trailing_separator_factory()) } else if trailing_separator.is_mandatory() { formatted![formatter, [separator.format()]]? } else { @@ -548,7 +554,7 @@ pub(crate) trait JsFormatter { } } else if index == last_index { if trailing_separator.is_allowed() { - if_group_breaks(separator_factory()) + trailing_separator_factory() } else if trailing_separator.is_mandatory() { separator_factory() } else { @@ -612,88 +618,169 @@ impl JsFormatter for Formatter { /// /// Calling this method is required to correctly handle the comments attached /// to the opening and closing tokens and insert them inside the group block -fn format_delimited( - formatter: &Formatter, - open_token: &JsSyntaxToken, - content: DelimitedContent, - close_token: &JsSyntaxToken, -) -> FormatResult { - formatter.track_token(open_token); - formatter.track_token(close_token); - - let open_token_trailing_trivia = format_trailing_trivia(open_token); - let close_token_leading_trivia = format_leading_trivia(close_token, TriviaPrintMode::Trim); +pub struct FormatDelimited<'a, 'fmt> { + open_token: &'a JsSyntaxToken, + content: FormatElement, + close_token: &'a JsSyntaxToken, + mode: DelimitedMode, + formatter: &'fmt Formatter, +} - let open_token_trailing_trivia = if !open_token_trailing_trivia.is_empty() { - formatted![ - formatter, - [open_token_trailing_trivia, soft_line_break_or_space()] - ]? - } else { - empty_element() - }; - let close_token_leading_trivia = if !close_token_leading_trivia.is_empty() { - formatted![ +impl<'a, 'fmt> FormatDelimited<'a, 'fmt> { + fn new( + open_token: &'a JsSyntaxToken, + content: FormatElement, + close_token: &'a JsSyntaxToken, + formatter: &'fmt Formatter, + ) -> Self { + Self { + open_token, + content, + close_token, + mode: DelimitedMode::SoftBlockIndent(None), formatter, - [soft_line_break_or_space(), close_token_leading_trivia] - ]? - } else { - empty_element() - }; + } + } - let formatted_content = match content { - DelimitedContent::BlockIndent(content) => block_indent(formatted![ + fn with_mode(mut self, mode: DelimitedMode) -> Self { + self.mode = mode; + self + } + + /// Formats a group delimited by an opening and closing token, placing the + /// content in a [block_indent] group + pub fn block_indent(self) -> Self { + self.with_mode(DelimitedMode::BlockIndent) + } + + /// Formats a group delimited by an opening and closing token, placing the + /// content in a [soft_block_indent] group + pub fn soft_block_indent(self) -> Self { + self.with_mode(DelimitedMode::SoftBlockIndent(None)) + } + + /// Formats a group delimited by an opening and closing token, placing the + /// content in an [indent] group with [soft_line_break_or_space] tokens at the + /// start and end + pub fn soft_block_spaces(self) -> Self { + self.with_mode(DelimitedMode::SoftBlockSpaces(None)) + } + + pub fn soft_block_indent_with_group_id(self, group_id: Option) -> Self { + self.with_mode(DelimitedMode::SoftBlockIndent(group_id)) + } + + pub fn soft_block_spaces_with_group_id(self, group_id: Option) -> Self { + self.with_mode(DelimitedMode::SoftBlockSpaces(group_id)) + } + + pub fn finish(self) -> FormatResult { + let FormatDelimited { formatter, - [ - open_token_trailing_trivia, - content, - close_token_leading_trivia - ] - ]?), - DelimitedContent::SoftBlockIndent(content) => soft_block_indent(formatted![ + open_token, + close_token, + content, + mode, + } = self; + + formatter.track_token(open_token); + formatter.track_token(close_token); + + let open_token_trailing_trivia = format_trailing_trivia(open_token); + let close_token_leading_trivia = format_leading_trivia(close_token, TriviaPrintMode::Trim); + + let open_token_trailing_trivia = if !open_token_trailing_trivia.is_empty() { + formatted![ + formatter, + [open_token_trailing_trivia, soft_line_break_or_space()] + ]? + } else { + empty_element() + }; + let close_token_leading_trivia = if !close_token_leading_trivia.is_empty() { + formatted![ + formatter, + [soft_line_break_or_space(), close_token_leading_trivia] + ]? + } else { + empty_element() + }; + + let formatted_content = match mode { + DelimitedMode::BlockIndent => block_indent(formatted![ + formatter, + [ + open_token_trailing_trivia, + content, + close_token_leading_trivia + ] + ]?), + DelimitedMode::SoftBlockIndent(_) => soft_block_indent(formatted![ + formatter, + [ + open_token_trailing_trivia, + content, + close_token_leading_trivia + ] + ]?), + DelimitedMode::SoftBlockSpaces(_) => { + if open_token_trailing_trivia.is_empty() + && content.is_empty() + && close_token_leading_trivia.is_empty() + { + empty_element() + } else { + formatted![ + formatter, + [ + indent(formatted![ + formatter, + [ + soft_line_break_or_space(), + open_token_trailing_trivia, + content, + close_token_leading_trivia, + ] + ]?), + soft_line_break_or_space(), + ] + ]? + } + } + }; + + let delimited = format_elements![ + Token::from(open_token), + formatted_content, + Token::from(close_token), + ]; + + let grouped = match mode { + // Group is useless, the block indent would expand it right anyway but there are some uses + // that are nested inside of a `HardGroup` that depend on the expanded state. Leave it for now + DelimitedMode::BlockIndent => group_elements(delimited), + DelimitedMode::SoftBlockIndent(group_id) | DelimitedMode::SoftBlockSpaces(group_id) => { + match group_id { + None => group_elements(delimited), + Some(group_id) => group_elements_with_id(delimited, group_id), + } + } + }; + + formatted![ formatter, [ - open_token_trailing_trivia, - content, - close_token_leading_trivia + format_leading_trivia(open_token, TriviaPrintMode::Full), + grouped, + format_trailing_trivia(close_token), ] - ]?), - DelimitedContent::SoftBlockSpaces(content) => { - if open_token_trailing_trivia.is_empty() - && content.is_empty() - && close_token_leading_trivia.is_empty() - { - empty_element() - } else { - formatted![ - formatter, - [ - indent(formatted![ - formatter, - [ - soft_line_break_or_space(), - open_token_trailing_trivia, - content, - close_token_leading_trivia, - ] - ]?), - soft_line_break_or_space(), - ] - ]? - } - } - }; - - formatted![ - formatter, - [ - format_leading_trivia(open_token, TriviaPrintMode::Full), - group_elements(format_elements![ - Token::from(open_token), - formatted_content, - Token::from(close_token), - ]), - format_trailing_trivia(close_token), ] - ] + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum DelimitedMode { + BlockIndent, + SoftBlockIndent(Option), + SoftBlockSpaces(Option), } diff --git a/crates/rome_js_formatter/src/js/any/class.rs b/crates/rome_js_formatter/src/js/any/class.rs index 58ad7f4bd11..71359c6d57a 100644 --- a/crates/rome_js_formatter/src/js/any/class.rs +++ b/crates/rome_js_formatter/src/js/any/class.rs @@ -37,16 +37,19 @@ impl FormatRule for FormatJsAnyClass { ]), implements_clause, space_token(), - formatter.format_delimited_block_indent( - &node.l_curly_token()?, - join_elements_hard_line( - node.members() - .into_iter() - .map(|node| node.syntax().clone()) - .zip(formatter.format_all(node.members().iter().formatted())?) - ), - &node.r_curly_token()? - )? + formatter + .delimited( + &node.l_curly_token()?, + join_elements_hard_line( + node.members() + .into_iter() + .map(|node| node.syntax().clone()) + .zip(formatter.format_all(node.members().iter().formatted())?) + ), + &node.r_curly_token()? + ) + .block_indent() + .finish() ] ]?)) } diff --git a/crates/rome_js_formatter/src/js/assignments/array_assignment_pattern.rs b/crates/rome_js_formatter/src/js/assignments/array_assignment_pattern.rs index 1d29d7c6091..a532e55e55b 100644 --- a/crates/rome_js_formatter/src/js/assignments/array_assignment_pattern.rs +++ b/crates/rome_js_formatter/src/js/assignments/array_assignment_pattern.rs @@ -15,10 +15,13 @@ impl FormatNodeFields for FormatNodeRule for FormatNodeRule for FormatNodeRule { r_curly_token, } = node.as_fields(); - formatter.format_delimited_block_indent( - &l_curly_token?, - formatted![ - formatter, - [directives.format(), formatter.format_list(&statements),] - ]?, - &r_curly_token?, - ) + formatter + .delimited( + &l_curly_token?, + formatted![ + formatter, + [directives.format(), formatter.format_list(&statements),] + ]?, + &r_curly_token?, + ) + .block_indent() + .finish() } } diff --git a/crates/rome_js_formatter/src/js/bindings/array_binding_pattern.rs b/crates/rome_js_formatter/src/js/bindings/array_binding_pattern.rs index 0be53d1b827..673ef4af7cb 100644 --- a/crates/rome_js_formatter/src/js/bindings/array_binding_pattern.rs +++ b/crates/rome_js_formatter/src/js/bindings/array_binding_pattern.rs @@ -15,10 +15,13 @@ impl FormatNodeFields for FormatNodeRule for FormatNodeRule for FormatNodeRule for FormatNodeRule { r_paren_token, } = node.as_fields(); - formatter.format_delimited_soft_block_indent( - &l_paren_token?, - formatted![formatter, [items.format()]]?, - &r_paren_token?, - ) + formatter + .delimited( + &l_paren_token?, + formatted![formatter, [items.format()]]?, + &r_paren_token?, + ) + .soft_block_indent() + .finish() } } diff --git a/crates/rome_js_formatter/src/js/classes/static_initialization_block_class_member.rs b/crates/rome_js_formatter/src/js/classes/static_initialization_block_class_member.rs index ddfcc8899f6..b670ac09004 100644 --- a/crates/rome_js_formatter/src/js/classes/static_initialization_block_class_member.rs +++ b/crates/rome_js_formatter/src/js/classes/static_initialization_block_class_member.rs @@ -19,11 +19,14 @@ impl FormatNodeFields } = node.as_fields(); let static_token = static_token.format(); - let separated = formatter.format_delimited_block_indent( - &l_curly_token?, - formatter.format_list(&statements), - &r_curly_token?, - )?; + let separated = formatter + .delimited( + &l_curly_token?, + formatter.format_list(&statements), + &r_curly_token?, + ) + .block_indent() + .finish()?; formatted![formatter, [static_token, space_token(), separated]] } } diff --git a/crates/rome_js_formatter/src/js/declarations/catch_declaration.rs b/crates/rome_js_formatter/src/js/declarations/catch_declaration.rs index 5e0588178df..2b63c5ce9e4 100644 --- a/crates/rome_js_formatter/src/js/declarations/catch_declaration.rs +++ b/crates/rome_js_formatter/src/js/declarations/catch_declaration.rs @@ -16,10 +16,13 @@ impl FormatNodeFields for FormatNodeRule type_annotation, } = node.as_fields(); - formatter.format_delimited_soft_block_indent( - &l_paren_token?, - formatted![formatter, [binding.format(), type_annotation.format()]]?, - &r_paren_token?, - ) + formatter + .delimited( + &l_paren_token?, + formatted![formatter, [binding.format(), type_annotation.format()]]?, + &r_paren_token?, + ) + .soft_block_indent() + .finish() } } diff --git a/crates/rome_js_formatter/src/js/expressions/array_expression.rs b/crates/rome_js_formatter/src/js/expressions/array_expression.rs index 91430f53eab..22abdfa246a 100644 --- a/crates/rome_js_formatter/src/js/expressions/array_expression.rs +++ b/crates/rome_js_formatter/src/js/expressions/array_expression.rs @@ -1,5 +1,6 @@ use crate::prelude::*; +use crate::generated::FormatJsArrayElementList; use crate::FormatNodeFields; use rome_js_syntax::JsArrayExpression; use rome_js_syntax::JsArrayExpressionFields; @@ -15,10 +16,14 @@ impl FormatNodeFields for FormatNodeRule { r_brack_token, } = node.as_fields(); - formatter.format_delimited_soft_block_indent( - &l_brack_token?, - formatted![formatter, [elements.format()]]?, - &r_brack_token?, - ) + let group_id = GroupId::new("array"); + + let elements = + FormatJsArrayElementList::format_with_group_id(&elements, formatter, Some(group_id))?; + + formatter + .delimited(&l_brack_token?, elements, &r_brack_token?) + .soft_block_indent_with_group_id(Some(group_id)) + .finish() } } diff --git a/crates/rome_js_formatter/src/js/expressions/call_arguments.rs b/crates/rome_js_formatter/src/js/expressions/call_arguments.rs index d7ed13e91bd..192e86e8fb0 100644 --- a/crates/rome_js_formatter/src/js/expressions/call_arguments.rs +++ b/crates/rome_js_formatter/src/js/expressions/call_arguments.rs @@ -28,11 +28,14 @@ impl FormatNodeFields for FormatNodeRule { ]?)); } - formatter.format_delimited_soft_block_indent( - &l_paren_token?, - formatted![formatter, [args.format()]]?, - &r_paren_token?, - ) + formatter + .delimited( + &l_paren_token?, + formatted![formatter, [args.format()]]?, + &r_paren_token?, + ) + .soft_block_indent() + .finish() } } diff --git a/crates/rome_js_formatter/src/js/expressions/object_expression.rs b/crates/rome_js_formatter/src/js/expressions/object_expression.rs index 2bc848357df..fee1f998c98 100644 --- a/crates/rome_js_formatter/src/js/expressions/object_expression.rs +++ b/crates/rome_js_formatter/src/js/expressions/object_expression.rs @@ -18,17 +18,15 @@ impl FormatNodeFields for FormatNodeRule let members_content = formatted![formatter, [members.format()]]?; if members.is_empty() { - formatter.format_delimited_soft_block_indent( - &l_curly_token?, - members_content, - &r_curly_token?, - ) + formatter + .delimited(&l_curly_token?, members_content, &r_curly_token?) + .soft_block_indent() + .finish() } else { - formatter.format_delimited_soft_block_spaces( - &l_curly_token?, - members_content, - &r_curly_token?, - ) + formatter + .delimited(&l_curly_token?, members_content, &r_curly_token?) + .soft_block_spaces() + .finish() } } } diff --git a/crates/rome_js_formatter/src/js/expressions/parenthesized_expression.rs b/crates/rome_js_formatter/src/js/expressions/parenthesized_expression.rs index 17b40e1d56a..a705a43762a 100644 --- a/crates/rome_js_formatter/src/js/expressions/parenthesized_expression.rs +++ b/crates/rome_js_formatter/src/js/expressions/parenthesized_expression.rs @@ -80,11 +80,14 @@ impl FormatNodeFields for FormatNodeRule for FormatJsArrayElementList { node: &JsArrayElementList, formatter: &Formatter, ) -> FormatResult { + Self::format_with_group_id(node, formatter, None) + } +} + +impl FormatJsArrayElementList { + /// Formats the array list with + pub fn format_with_group_id( + node: &JsArrayElementList, + formatter: &Formatter, + group_id: Option, + ) -> FormatResult { + dbg!(group_id); if !has_formatter_trivia(node.syntax()) && can_print_fill(node) { return Ok(fill_elements( // Using format_separated is valid in this case as can_print_fill does not allow holes - formatter.format_separated(node, || token(","), TrailingSeparator::default())?, + formatter.format_separated_with_options( + node, + || token(","), + FormatSeparatedOptions::default().with_group_id(group_id), + )?, )); } diff --git a/crates/rome_js_formatter/src/js/lists/call_argument_list.rs b/crates/rome_js_formatter/src/js/lists/call_argument_list.rs index f7f89c40719..bb846ab4adf 100644 --- a/crates/rome_js_formatter/src/js/lists/call_argument_list.rs +++ b/crates/rome_js_formatter/src/js/lists/call_argument_list.rs @@ -1,4 +1,3 @@ -use crate::formatter::TrailingSeparator; use crate::generated::FormatJsCallArgumentList; use crate::prelude::*; use rome_js_syntax::JsCallArgumentList; @@ -12,7 +11,7 @@ impl FormatRule for FormatJsCallArgumentList { ) -> FormatResult { Ok(join_elements( soft_line_break_or_space(), - formatter.format_separated(node, || token(","), TrailingSeparator::default())?, + formatter.format_separated(node, || token(","))?, )) } } diff --git a/crates/rome_js_formatter/src/js/lists/constructor_parameter_list.rs b/crates/rome_js_formatter/src/js/lists/constructor_parameter_list.rs index 1836ff12614..76725dadb15 100644 --- a/crates/rome_js_formatter/src/js/lists/constructor_parameter_list.rs +++ b/crates/rome_js_formatter/src/js/lists/constructor_parameter_list.rs @@ -1,4 +1,3 @@ -use crate::formatter::TrailingSeparator; use crate::generated::FormatJsConstructorParameterList; use crate::prelude::*; use rome_js_syntax::JsConstructorParameterList; @@ -12,7 +11,7 @@ impl FormatRule for FormatJsConstructorParameterList ) -> FormatResult { Ok(join_elements( soft_line_break_or_space(), - formatter.format_separated(node, || token(","), TrailingSeparator::default())?, + formatter.format_separated(node, || token(","))?, )) } } diff --git a/crates/rome_js_formatter/src/js/lists/export_named_from_specifier_list.rs b/crates/rome_js_formatter/src/js/lists/export_named_from_specifier_list.rs index 787e902ff69..8433e3cc375 100644 --- a/crates/rome_js_formatter/src/js/lists/export_named_from_specifier_list.rs +++ b/crates/rome_js_formatter/src/js/lists/export_named_from_specifier_list.rs @@ -1,4 +1,3 @@ -use crate::formatter::TrailingSeparator; use crate::generated::FormatJsExportNamedFromSpecifierList; use crate::prelude::*; use rome_js_syntax::JsExportNamedFromSpecifierList; @@ -12,7 +11,7 @@ impl FormatRule for FormatJsExportNamedFromSpeci ) -> FormatResult { Ok(join_elements( soft_line_break_or_space(), - formatter.format_separated(node, || token(","), TrailingSeparator::default())?, + formatter.format_separated(node, || token(","))?, )) } } diff --git a/crates/rome_js_formatter/src/js/lists/export_named_specifier_list.rs b/crates/rome_js_formatter/src/js/lists/export_named_specifier_list.rs index 898f2d3e631..66f9e065d10 100644 --- a/crates/rome_js_formatter/src/js/lists/export_named_specifier_list.rs +++ b/crates/rome_js_formatter/src/js/lists/export_named_specifier_list.rs @@ -1,4 +1,3 @@ -use crate::formatter::TrailingSeparator; use crate::generated::FormatJsExportNamedSpecifierList; use crate::prelude::*; use rome_js_syntax::JsExportNamedSpecifierList; @@ -12,7 +11,7 @@ impl FormatRule for FormatJsExportNamedSpecifierList ) -> FormatResult { Ok(join_elements( soft_line_break_or_space(), - formatter.format_separated(node, || token(","), TrailingSeparator::default())?, + formatter.format_separated(node, || token(","))?, )) } } diff --git a/crates/rome_js_formatter/src/js/lists/import_assertion_entry_list.rs b/crates/rome_js_formatter/src/js/lists/import_assertion_entry_list.rs index dabd6bec3aa..8e893006be5 100644 --- a/crates/rome_js_formatter/src/js/lists/import_assertion_entry_list.rs +++ b/crates/rome_js_formatter/src/js/lists/import_assertion_entry_list.rs @@ -1,4 +1,3 @@ -use crate::formatter::TrailingSeparator; use crate::generated::FormatJsImportAssertionEntryList; use crate::prelude::*; use rome_js_syntax::JsImportAssertionEntryList; @@ -12,7 +11,7 @@ impl FormatRule for FormatJsImportAssertionEntryList ) -> FormatResult { Ok(join_elements( soft_line_break_or_space(), - formatter.format_separated(node, || token(","), TrailingSeparator::default())?, + formatter.format_separated(node, || token(","))?, )) } } diff --git a/crates/rome_js_formatter/src/js/lists/named_import_specifier_list.rs b/crates/rome_js_formatter/src/js/lists/named_import_specifier_list.rs index 11e921dc57c..76a9d0caf3d 100644 --- a/crates/rome_js_formatter/src/js/lists/named_import_specifier_list.rs +++ b/crates/rome_js_formatter/src/js/lists/named_import_specifier_list.rs @@ -1,4 +1,3 @@ -use crate::formatter::TrailingSeparator; use crate::generated::FormatJsNamedImportSpecifierList; use crate::prelude::*; use rome_js_syntax::JsNamedImportSpecifierList; @@ -12,7 +11,7 @@ impl FormatRule for FormatJsNamedImportSpecifierList ) -> FormatResult { Ok(join_elements( soft_line_break_or_space(), - formatter.format_separated(node, || token(","), TrailingSeparator::default())?, + formatter.format_separated(node, || token(","))?, )) } } diff --git a/crates/rome_js_formatter/src/js/lists/object_assignment_pattern_property_list.rs b/crates/rome_js_formatter/src/js/lists/object_assignment_pattern_property_list.rs index f69d50cf2d9..a588e2e9ba5 100644 --- a/crates/rome_js_formatter/src/js/lists/object_assignment_pattern_property_list.rs +++ b/crates/rome_js_formatter/src/js/lists/object_assignment_pattern_property_list.rs @@ -1,4 +1,4 @@ -use crate::formatter::TrailingSeparator; +use crate::formatter::{FormatSeparatedOptions, TrailingSeparator}; use crate::generated::FormatJsObjectAssignmentPatternPropertyList; use crate::prelude::*; use rome_js_syntax::{JsAnyObjectAssignmentPatternMember, JsObjectAssignmentPatternPropertyList}; @@ -29,7 +29,11 @@ impl FormatRule Ok(join_elements( soft_line_break_or_space(), - formatter.format_separated(node, || token(","), trailing_separator)?, + formatter.format_separated_with_options( + node, + || token(","), + FormatSeparatedOptions::default().with_trailing_separator(trailing_separator), + )?, )) } } diff --git a/crates/rome_js_formatter/src/js/lists/object_binding_pattern_property_list.rs b/crates/rome_js_formatter/src/js/lists/object_binding_pattern_property_list.rs index 4a1444e097a..f2377366939 100644 --- a/crates/rome_js_formatter/src/js/lists/object_binding_pattern_property_list.rs +++ b/crates/rome_js_formatter/src/js/lists/object_binding_pattern_property_list.rs @@ -1,4 +1,4 @@ -use crate::formatter::TrailingSeparator; +use crate::formatter::{FormatSeparatedOptions, TrailingSeparator}; use crate::generated::FormatJsObjectBindingPatternPropertyList; use crate::prelude::*; use rome_js_syntax::{JsAnyObjectBindingPatternMember, JsObjectBindingPatternPropertyList}; @@ -27,7 +27,11 @@ impl FormatRule for FormatJsObjectBindingPat Ok(join_elements( soft_line_break_or_space(), - formatter.format_separated(node, || token(","), trailing_separator)?, + formatter.format_separated_with_options( + node, + || token(","), + FormatSeparatedOptions::default().with_trailing_separator(trailing_separator), + )?, )) } } diff --git a/crates/rome_js_formatter/src/js/lists/object_member_list.rs b/crates/rome_js_formatter/src/js/lists/object_member_list.rs index 630db381769..d5139e9220e 100644 --- a/crates/rome_js_formatter/src/js/lists/object_member_list.rs +++ b/crates/rome_js_formatter/src/js/lists/object_member_list.rs @@ -1,4 +1,3 @@ -use crate::formatter::TrailingSeparator; use crate::generated::FormatJsObjectMemberList; use crate::prelude::*; use rome_js_syntax::JsObjectMemberList; @@ -11,8 +10,7 @@ impl FormatRule for FormatJsObjectMemberList { node: &JsObjectMemberList, formatter: &Formatter, ) -> FormatResult { - let members = - formatter.format_separated(node, || token(","), TrailingSeparator::default())?; + let members = formatter.format_separated(node, || token(","))?; Ok(join_elements_soft_line( node.elements() diff --git a/crates/rome_js_formatter/src/js/lists/parameter_list.rs b/crates/rome_js_formatter/src/js/lists/parameter_list.rs index d216c75d027..c9a1ff77429 100644 --- a/crates/rome_js_formatter/src/js/lists/parameter_list.rs +++ b/crates/rome_js_formatter/src/js/lists/parameter_list.rs @@ -1,4 +1,4 @@ -use crate::formatter::TrailingSeparator; +use crate::formatter::{FormatSeparatedOptions, TrailingSeparator}; use crate::generated::FormatJsParameterList; use crate::prelude::*; use rome_js_syntax::{JsAnyParameter, JsParameterList}; @@ -24,7 +24,11 @@ impl FormatRule for FormatJsParameterList { Ok(join_elements( soft_line_break_or_space(), - formatter.format_separated(node, || token(","), trailing_separator)?, + formatter.format_separated_with_options( + node, + || token(","), + FormatSeparatedOptions::default().with_trailing_separator(trailing_separator), + )?, )) } } diff --git a/crates/rome_js_formatter/src/js/module/export_named_clause.rs b/crates/rome_js_formatter/src/js/module/export_named_clause.rs index 2cae85bdf06..9e52f93b38c 100644 --- a/crates/rome_js_formatter/src/js/module/export_named_clause.rs +++ b/crates/rome_js_formatter/src/js/module/export_named_clause.rs @@ -21,11 +21,14 @@ impl FormatNodeFields for FormatNodeRule for FormatNodeRule for FormatNodeRule { r_curly_token, } = node.as_fields(); - let result = formatter.format_delimited_soft_block_spaces( - &l_curly_token?, - formatted![formatter, [assertions.format()]]?, - &r_curly_token?, - )?; + let result = formatter + .delimited( + &l_curly_token?, + formatted![formatter, [assertions.format()]]?, + &r_curly_token?, + ) + .soft_block_spaces() + .finish()?; formatted![formatter, [assert_token.format(), space_token(), result]] } diff --git a/crates/rome_js_formatter/src/js/module/named_import_specifiers.rs b/crates/rome_js_formatter/src/js/module/named_import_specifiers.rs index f93140d2066..320876cf7fd 100644 --- a/crates/rome_js_formatter/src/js/module/named_import_specifiers.rs +++ b/crates/rome_js_formatter/src/js/module/named_import_specifiers.rs @@ -15,10 +15,13 @@ impl FormatNodeFields for FormatNodeRule for FormatNodeRule { ] ] } else { - formatter.format_delimited_block_indent(&l_curly_token?, stmts, &r_curly_token?) + formatter + .delimited(&l_curly_token?, stmts, &r_curly_token?) + .block_indent() + .finish() } } } diff --git a/crates/rome_js_formatter/src/js/statements/do_while_statement.rs b/crates/rome_js_formatter/src/js/statements/do_while_statement.rs index e4cfe808343..4dfe6b37a9c 100644 --- a/crates/rome_js_formatter/src/js/statements/do_while_statement.rs +++ b/crates/rome_js_formatter/src/js/statements/do_while_statement.rs @@ -27,11 +27,14 @@ impl FormatNodeFields for FormatNodeRule space_token(), while_token.format(), space_token(), - formatter.format_delimited_soft_block_indent( - &l_paren_token?, - formatted![formatter, [test.format()]]?, - &r_paren_token?, - )?, + formatter + .delimited( + &l_paren_token?, + formatted![formatter, [test.format()]]?, + &r_paren_token?, + ) + .soft_block_indent() + .finish()?, semicolon_token.format().or_format(|| token(";")) ] ]?; diff --git a/crates/rome_js_formatter/src/js/statements/for_in_statement.rs b/crates/rome_js_formatter/src/js/statements/for_in_statement.rs index d4eb4359c60..2773be5a528 100644 --- a/crates/rome_js_formatter/src/js/statements/for_in_statement.rs +++ b/crates/rome_js_formatter/src/js/statements/for_in_statement.rs @@ -32,20 +32,23 @@ impl FormatNodeFields for FormatNodeRule { [ for_token, space_token(), - formatter.format_delimited_soft_block_indent( - &l_paren_token?, - formatted![ - formatter, - [ - initializer, - soft_line_break_or_space(), - in_token, - soft_line_break_or_space(), - expression, - ] - ]?, - &r_paren_token? - )?, + formatter + .delimited( + &l_paren_token?, + formatted![ + formatter, + [ + initializer, + soft_line_break_or_space(), + in_token, + soft_line_break_or_space(), + expression, + ] + ]?, + &r_paren_token? + ) + .soft_block_indent() + .finish()?, ] ]?, body?, diff --git a/crates/rome_js_formatter/src/js/statements/for_of_statement.rs b/crates/rome_js_formatter/src/js/statements/for_of_statement.rs index 0b08b797f7c..ae126463f5d 100644 --- a/crates/rome_js_formatter/src/js/statements/for_of_statement.rs +++ b/crates/rome_js_formatter/src/js/statements/for_of_statement.rs @@ -32,20 +32,23 @@ impl FormatNodeFields for FormatNodeRule { await_token .format() .with_or_empty(|token| formatted![formatter, [token, space_token()]]), - formatter.format_delimited_soft_block_indent( - &l_paren_token?, - formatted![ - formatter, - [ - initializer.format(), - soft_line_break_or_space(), - of_token.format(), - soft_line_break_or_space(), - expression.format(), - ] - ]?, - &r_paren_token? - )?, + formatter + .delimited( + &l_paren_token?, + formatted![ + formatter, + [ + initializer.format(), + soft_line_break_or_space(), + of_token.format(), + soft_line_break_or_space(), + expression.format(), + ] + ]?, + &r_paren_token? + ) + .soft_block_indent() + .finish()?, ] ]?, body?, diff --git a/crates/rome_js_formatter/src/js/statements/for_statement.rs b/crates/rome_js_formatter/src/js/statements/for_statement.rs index bf367bf320a..da06ebf90af 100644 --- a/crates/rome_js_formatter/src/js/statements/for_statement.rs +++ b/crates/rome_js_formatter/src/js/statements/for_statement.rs @@ -55,11 +55,10 @@ impl FormatNodeFields for FormatNodeRule { [ for_token.format(), space_token(), - formatter.format_delimited_soft_block_indent( - &l_paren_token?, - inner, - &r_paren_token?, - )?, + formatter + .delimited(&l_paren_token?, inner, &r_paren_token?,) + .soft_block_indent() + .finish()?, body ] ]?)) diff --git a/crates/rome_js_formatter/src/js/statements/if_statement.rs b/crates/rome_js_formatter/src/js/statements/if_statement.rs index b97bfe99506..e225c1c9330 100644 --- a/crates/rome_js_formatter/src/js/statements/if_statement.rs +++ b/crates/rome_js_formatter/src/js/statements/if_statement.rs @@ -68,11 +68,14 @@ fn format_if_element( ]), if_token.format(), space_token(), - formatter.format_delimited_soft_block_indent( - &l_paren_token?, - formatted![formatter, [test.format()]]?, - &r_paren_token?, - )?, + formatter + .delimited( + &l_paren_token?, + formatted![formatter, [test.format()]]?, + &r_paren_token?, + ) + .soft_block_indent() + .finish()?, space_token(), into_block(formatter, consequent?)?, ] diff --git a/crates/rome_js_formatter/src/js/statements/switch_statement.rs b/crates/rome_js_formatter/src/js/statements/switch_statement.rs index b2162c39eaf..13eded9b6b1 100644 --- a/crates/rome_js_formatter/src/js/statements/switch_statement.rs +++ b/crates/rome_js_formatter/src/js/statements/switch_statement.rs @@ -23,26 +23,32 @@ impl FormatNodeFields for FormatNodeRule { [ switch_token.format(), space_token(), - formatter.format_delimited_soft_block_indent( - &l_paren_token?, - formatted![formatter, [discriminant.format()]]?, - &r_paren_token?, - )?, + formatter + .delimited( + &l_paren_token?, + formatted![formatter, [discriminant.format()]]?, + &r_paren_token?, + ) + .soft_block_indent() + .finish()?, space_token(), - formatter.format_delimited_block_indent( - &l_curly_token?, - if cases.is_empty() { - hard_line_break() - } else { - join_elements_hard_line( - cases - .iter() - .map(|node| node.syntax().clone()) - .zip(formatter.format_all(cases.iter().formatted())?), - ) - }, - &r_curly_token? - )? + formatter + .delimited( + &l_curly_token?, + if cases.is_empty() { + hard_line_break() + } else { + join_elements_hard_line( + cases + .iter() + .map(|node| node.syntax().clone()) + .zip(formatter.format_all(cases.iter().formatted())?), + ) + }, + &r_curly_token? + ) + .block_indent() + .finish()? ] ]?)) } diff --git a/crates/rome_js_formatter/src/js/statements/while_statement.rs b/crates/rome_js_formatter/src/js/statements/while_statement.rs index f7b577b0d8a..4fa7682bdd2 100644 --- a/crates/rome_js_formatter/src/js/statements/while_statement.rs +++ b/crates/rome_js_formatter/src/js/statements/while_statement.rs @@ -25,11 +25,14 @@ impl FormatNodeFields for FormatNodeRule { [ while_token.format(), space_token(), - formatter.format_delimited_soft_block_indent( - &l_paren_token?, - formatted![formatter, [test.format()]]?, - &r_paren_token?, - )?, + formatter + .delimited( + &l_paren_token?, + formatted![formatter, [test.format()]]?, + &r_paren_token?, + ) + .soft_block_indent() + .finish()?, ] ]?, body?, diff --git a/crates/rome_js_formatter/src/js/statements/with_statement.rs b/crates/rome_js_formatter/src/js/statements/with_statement.rs index 647118633f8..39060dfb17a 100644 --- a/crates/rome_js_formatter/src/js/statements/with_statement.rs +++ b/crates/rome_js_formatter/src/js/statements/with_statement.rs @@ -25,11 +25,14 @@ impl FormatNodeFields for FormatNodeRule { [ with_token.format(), space_token(), - formatter.format_delimited_soft_block_indent( - &l_paren_token?, - formatted![formatter, [object.format()]]?, - &r_paren_token?, - )?, + formatter + .delimited( + &l_paren_token?, + formatted![formatter, [object.format()]]?, + &r_paren_token?, + ) + .soft_block_indent() + .finish()?, ] ]?, body?, diff --git a/crates/rome_js_formatter/src/ts/assignments/type_assertion_assignment.rs b/crates/rome_js_formatter/src/ts/assignments/type_assertion_assignment.rs index a0407ebf4c7..95024c819bf 100644 --- a/crates/rome_js_formatter/src/ts/assignments/type_assertion_assignment.rs +++ b/crates/rome_js_formatter/src/ts/assignments/type_assertion_assignment.rs @@ -19,11 +19,14 @@ impl FormatNodeFields for FormatNodeRule for FormatNodeRule { r_curly_token, } = node.as_fields(); - formatter.format_delimited_block_indent( - &l_curly_token?, - formatted![formatter, [items.format()]]?, - &r_curly_token?, - ) + formatter + .delimited( + &l_curly_token?, + formatted![formatter, [items.format()]]?, + &r_curly_token?, + ) + .block_indent() + .finish() } } diff --git a/crates/rome_js_formatter/src/ts/bindings/type_parameters.rs b/crates/rome_js_formatter/src/ts/bindings/type_parameters.rs index 1b927689dbb..5b222ea1fce 100644 --- a/crates/rome_js_formatter/src/ts/bindings/type_parameters.rs +++ b/crates/rome_js_formatter/src/ts/bindings/type_parameters.rs @@ -13,10 +13,13 @@ impl FormatNodeFields for FormatNodeRule { l_angle_token, } = node.as_fields(); - formatter.format_delimited_soft_block_indent( - &l_angle_token?, - formatted![formatter, [items.format()]]?, - &r_angle_token?, - ) + formatter + .delimited( + &l_angle_token?, + formatted![formatter, [items.format()]]?, + &r_angle_token?, + ) + .soft_block_indent() + .finish() } } diff --git a/crates/rome_js_formatter/src/ts/declarations/enum_declaration.rs b/crates/rome_js_formatter/src/ts/declarations/enum_declaration.rs index e0606ee0f32..dcf7edab4a9 100644 --- a/crates/rome_js_formatter/src/ts/declarations/enum_declaration.rs +++ b/crates/rome_js_formatter/src/ts/declarations/enum_declaration.rs @@ -1,4 +1,3 @@ -use crate::formatter::TrailingSeparator; use crate::prelude::*; use crate::FormatNodeFields; use rome_js_syntax::{TsEnumDeclaration, TsEnumDeclarationFields}; @@ -17,18 +16,17 @@ impl FormatNodeFields for FormatNodeRule { r_curly_token, } = node.as_fields(); - let list = formatter.format_delimited_soft_block_spaces( - &l_curly_token?, - join_elements( - soft_line_break_or_space(), - formatter.format_separated( - &members, - || token(","), - TrailingSeparator::default(), - )?, - ), - &r_curly_token?, - )?; + let list = formatter + .delimited( + &l_curly_token?, + join_elements( + soft_line_break_or_space(), + formatter.format_separated(&members, || token(","))?, + ), + &r_curly_token?, + ) + .soft_block_spaces() + .finish()?; formatted![ formatter, diff --git a/crates/rome_js_formatter/src/ts/declarations/interface_declaration.rs b/crates/rome_js_formatter/src/ts/declarations/interface_declaration.rs index d5b1cfc3ca0..3cbc24ec26c 100644 --- a/crates/rome_js_formatter/src/ts/declarations/interface_declaration.rs +++ b/crates/rome_js_formatter/src/ts/declarations/interface_declaration.rs @@ -16,11 +16,15 @@ impl FormatNodeFields for FormatNodeRule for FormatNodeRule { r_angle_token, } = node.as_fields(); - formatter.format_delimited_soft_block_indent( - &l_angle_token?, - formatted![formatter, [ts_type_argument_list.format()]]?, - &r_angle_token?, - ) + formatter + .delimited( + &l_angle_token?, + formatted![formatter, [ts_type_argument_list.format()]]?, + &r_angle_token?, + ) + .soft_block_indent() + .finish() } } diff --git a/crates/rome_js_formatter/src/ts/expressions/type_assertion_expression.rs b/crates/rome_js_formatter/src/ts/expressions/type_assertion_expression.rs index c9dc4d1f930..d8c01ba3bfd 100644 --- a/crates/rome_js_formatter/src/ts/expressions/type_assertion_expression.rs +++ b/crates/rome_js_formatter/src/ts/expressions/type_assertion_expression.rs @@ -18,11 +18,14 @@ impl FormatNodeFields for FormatNodeRule for FormatTsEnumMemberList { ) -> FormatResult { Ok(join_elements( soft_line_break_or_space(), - formatter.format_separated(node, || token(","), TrailingSeparator::default())?, + formatter.format_separated(node, || token(","))?, )) } } diff --git a/crates/rome_js_formatter/src/ts/lists/tuple_type_element_list.rs b/crates/rome_js_formatter/src/ts/lists/tuple_type_element_list.rs index b68692c3147..6e0aea2d678 100644 --- a/crates/rome_js_formatter/src/ts/lists/tuple_type_element_list.rs +++ b/crates/rome_js_formatter/src/ts/lists/tuple_type_element_list.rs @@ -1,4 +1,3 @@ -use crate::formatter::TrailingSeparator; use crate::generated::FormatTsTupleTypeElementList; use crate::prelude::*; use rome_js_syntax::TsTupleTypeElementList; @@ -12,7 +11,7 @@ impl FormatRule for FormatTsTupleTypeElementList { ) -> FormatResult { Ok(join_elements( soft_line_break_or_space(), - formatter.format_separated(node, || token(","), TrailingSeparator::default())?, + formatter.format_separated(node, || token(","))?, )) } } diff --git a/crates/rome_js_formatter/src/ts/lists/type_argument_list.rs b/crates/rome_js_formatter/src/ts/lists/type_argument_list.rs index 435fbc542ff..f1e37e82ea5 100644 --- a/crates/rome_js_formatter/src/ts/lists/type_argument_list.rs +++ b/crates/rome_js_formatter/src/ts/lists/type_argument_list.rs @@ -1,4 +1,4 @@ -use crate::formatter::TrailingSeparator; +use crate::formatter::{FormatSeparatedOptions, TrailingSeparator}; use crate::generated::FormatTsTypeArgumentList; use crate::prelude::*; use rome_js_syntax::TsTypeArgumentList; @@ -12,7 +12,12 @@ impl FormatRule for FormatTsTypeArgumentList { ) -> FormatResult { Ok(join_elements( soft_line_break_or_space(), - formatter.format_separated(node, || token(","), TrailingSeparator::Disallowed)?, + formatter.format_separated_with_options( + node, + || token(","), + FormatSeparatedOptions::default() + .with_trailing_separator(TrailingSeparator::Disallowed), + )?, )) } } diff --git a/crates/rome_js_formatter/src/ts/lists/type_list.rs b/crates/rome_js_formatter/src/ts/lists/type_list.rs index c7f0e622370..b70dddd5c40 100644 --- a/crates/rome_js_formatter/src/ts/lists/type_list.rs +++ b/crates/rome_js_formatter/src/ts/lists/type_list.rs @@ -1,4 +1,4 @@ -use crate::formatter::TrailingSeparator; +use crate::formatter::{FormatSeparatedOptions, TrailingSeparator}; use crate::generated::FormatTsTypeList; use crate::prelude::*; use rome_js_syntax::TsTypeList; @@ -13,7 +13,12 @@ impl FormatRule for FormatTsTypeList { // the grouping will be applied by the parent Ok(join_elements( soft_line_break_or_space(), - formatter.format_separated(node, || token(","), TrailingSeparator::Disallowed)?, + formatter.format_separated_with_options( + node, + || token(","), + FormatSeparatedOptions::default() + .with_trailing_separator(TrailingSeparator::Disallowed), + )?, )) } } diff --git a/crates/rome_js_formatter/src/ts/lists/type_parameter_list.rs b/crates/rome_js_formatter/src/ts/lists/type_parameter_list.rs index 312157dda8f..348348e9b05 100644 --- a/crates/rome_js_formatter/src/ts/lists/type_parameter_list.rs +++ b/crates/rome_js_formatter/src/ts/lists/type_parameter_list.rs @@ -1,4 +1,4 @@ -use crate::formatter::TrailingSeparator; +use crate::formatter::{FormatSeparatedOptions, TrailingSeparator}; use crate::generated::FormatTsTypeParameterList; use crate::prelude::*; use rome_js_syntax::TsTypeParameterList; @@ -24,7 +24,11 @@ impl FormatRule for FormatTsTypeParameterList { }; Ok(join_elements( soft_line_break_or_space(), - formatter.format_separated(node, || token(","), trailing_separator)?, + formatter.format_separated_with_options( + node, + || token(","), + FormatSeparatedOptions::default().with_trailing_separator(trailing_separator), + )?, )) } } diff --git a/crates/rome_js_formatter/src/ts/types/mapped_type.rs b/crates/rome_js_formatter/src/ts/types/mapped_type.rs index 3608c94b8d6..3f8e8c2d0b8 100644 --- a/crates/rome_js_formatter/src/ts/types/mapped_type.rs +++ b/crates/rome_js_formatter/src/ts/types/mapped_type.rs @@ -23,36 +23,40 @@ impl FormatNodeFields for FormatNodeRule { r_curly_token, } = node.as_fields(); - formatter.format_delimited_block_indent( - &l_curly_token?, - format_with_semicolon( - formatter, - formatted![ + formatter + .delimited( + &l_curly_token?, + format_with_semicolon( formatter, - [ - readonly_modifier - .format() - .with_or_empty(|readonly| formatted![ + formatted![ + formatter, + [ + readonly_modifier + .format() + .with_or_empty(|readonly| formatted![ + formatter, + [readonly, space_token()] + ]), + l_brack_token.format(), + property_name.format(), + space_token(), + in_token.format(), + space_token(), + keys_type.format(), + as_clause.format().with_or_empty(|clause| formatted![ formatter, - [readonly, space_token()] + [space_token(), clause] ]), - l_brack_token.format(), - property_name.format(), - space_token(), - in_token.format(), - space_token(), - keys_type.format(), - as_clause - .format() - .with_or_empty(|clause| formatted![formatter, [space_token(), clause]]), - r_brack_token.format(), - optional_modifier.format(), - mapped_type.format(), - ] - ]?, - semicolon_token, - )?, - &r_curly_token?, - ) + r_brack_token.format(), + optional_modifier.format(), + mapped_type.format(), + ] + ]?, + semicolon_token, + )?, + &r_curly_token?, + ) + .block_indent() + .finish() } } diff --git a/crates/rome_js_formatter/src/ts/types/object_type.rs b/crates/rome_js_formatter/src/ts/types/object_type.rs index e0ef59fb371..0b0c62f504d 100644 --- a/crates/rome_js_formatter/src/ts/types/object_type.rs +++ b/crates/rome_js_formatter/src/ts/types/object_type.rs @@ -13,10 +13,13 @@ impl FormatNodeFields for FormatNodeRule { r_curly_token, } = node.as_fields(); - formatter.format_delimited_soft_block_spaces( - &l_curly_token?, - formatted![formatter, [members.format()]]?, - &r_curly_token?, - ) + formatter + .delimited( + &l_curly_token?, + formatted![formatter, [members.format()]]?, + &r_curly_token?, + ) + .soft_block_spaces() + .finish() } } diff --git a/crates/rome_js_formatter/src/ts/types/parenthesized_type.rs b/crates/rome_js_formatter/src/ts/types/parenthesized_type.rs index 7beb84200e7..f1d6a3c218a 100644 --- a/crates/rome_js_formatter/src/ts/types/parenthesized_type.rs +++ b/crates/rome_js_formatter/src/ts/types/parenthesized_type.rs @@ -14,10 +14,13 @@ impl FormatNodeFields for FormatNodeRule for FormatNodeRule { r_brack_token, } = node.as_fields(); - formatter.format_delimited_soft_block_indent( - &l_brack_token?, - formatted![formatter, [elements.format()]]?, - &r_brack_token?, - ) + formatter + .delimited( + &l_brack_token?, + formatted![formatter, [elements.format()]]?, + &r_brack_token?, + ) + .soft_block_indent() + .finish() } } diff --git a/crates/rome_js_formatter/src/utils/mod.rs b/crates/rome_js_formatter/src/utils/mod.rs index 785c9cb5cc0..cec7ea82908 100644 --- a/crates/rome_js_formatter/src/utils/mod.rs +++ b/crates/rome_js_formatter/src/utils/mod.rs @@ -226,11 +226,10 @@ impl TemplateElement { [dollar_curly_token.format(), middle, r_curly_token.format()] ]?)) } else { - formatter.format_delimited_soft_block_indent( - &dollar_curly_token, - middle, - &r_curly_token, - ) + formatter + .delimited(&dollar_curly_token, middle, &r_curly_token) + .soft_block_indent() + .finish() } } diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/comments-closure-typecast/closure-compiler-type-cast.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/comments-closure-typecast/closure-compiler-type-cast.js.snap index b2e90ad793c..526f5c70f3f 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/comments-closure-typecast/closure-compiler-type-cast.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/comments-closure-typecast/closure-compiler-type-cast.js.snap @@ -1,8 +1,6 @@ --- source: crates/rome_js_formatter/tests/prettier_tests.rs -assertion_line: 144 expression: closure-compiler-type-cast.js - --- # Input ```js @@ -84,11 +82,8 @@ let object = { let assignment = /** @type {string} */ (getValue()); let value = /** @type {string} */ (this.members[0]).functionCall(); -functionCall( - 1 + /** @type {string} */ - (value), /** @type {!Foo} */ - ({}), -); +functionCall(1 + /** @type {string} */ + (value), /** @type {!Foo} */ ({})); function returnValue() { return (["hello", "you"]); /** @type {!Array.} */ diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/comments/binary-expressions-block-comments.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/comments/binary-expressions-block-comments.js.snap index 0dd862c4386..a3b98cfa9f9 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/comments/binary-expressions-block-comments.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/comments/binary-expressions-block-comments.js.snap @@ -49,8 +49,7 @@ a = b + /** TODO this is a very very very very long comment that makes it go > 8 # Output ```js -a = - b || /** Comment */ +a = b || /** Comment */ c; a = b /** Comment */ || c; @@ -66,8 +65,7 @@ a = b || /** TODO this is a very very very very long comment that makes it go > 80 columns */ c; -a = - b && /** Comment */ +a = b && /** Comment */ c; a = b /** Comment */ && c; @@ -83,8 +81,7 @@ a = b && /** TODO this is a very very very very long comment that makes it go > 80 columns */ c; -a = - b + /** Comment */ +a = b + /** Comment */ c; a = b /** Comment */ + c; @@ -104,14 +101,14 @@ a = # Lines exceeding max width of 80 characters ``` - 8: b || /** TODO this is a very very very very long comment that makes it go > 80 columns */ - 12: b /** TODO this is a very very very very long comment that makes it go > 80 columns */ || c; - 15: b || /** TODO this is a very very very very long comment that makes it go > 80 columns */ - 25: b && /** TODO this is a very very very very long comment that makes it go > 80 columns */ - 29: b /** TODO this is a very very very very long comment that makes it go > 80 columns */ && c; - 32: b && /** TODO this is a very very very very long comment that makes it go > 80 columns */ - 42: b + /** TODO this is a very very very very long comment that makes it go > 80 columns */ - 46: b /** TODO this is a very very very very long comment that makes it go > 80 columns */ + c; - 49: b + /** TODO this is a very very very very long comment that makes it go > 80 columns */ + 7: b || /** TODO this is a very very very very long comment that makes it go > 80 columns */ + 11: b /** TODO this is a very very very very long comment that makes it go > 80 columns */ || c; + 14: b || /** TODO this is a very very very very long comment that makes it go > 80 columns */ + 23: b && /** TODO this is a very very very very long comment that makes it go > 80 columns */ + 27: b /** TODO this is a very very very very long comment that makes it go > 80 columns */ && c; + 30: b && /** TODO this is a very very very very long comment that makes it go > 80 columns */ + 39: b + /** TODO this is a very very very very long comment that makes it go > 80 columns */ + 43: b /** TODO this is a very very very very long comment that makes it go > 80 columns */ + c; + 46: b + /** TODO this is a very very very very long comment that makes it go > 80 columns */ ``` diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/trailing-comma/trailing_whitespace.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/trailing-comma/trailing_whitespace.js.snap index 96cdaf3f1f8..7ce42437546 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/trailing-comma/trailing_whitespace.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/trailing-comma/trailing_whitespace.js.snap @@ -1,8 +1,6 @@ --- source: crates/rome_js_formatter/tests/prettier_tests.rs -assertion_line: 144 expression: trailing_whitespace.js - --- # Input ```js @@ -91,12 +89,10 @@ function supersupersupersuperLongF( a; } -this.getAttribute( - function (s) +this.getAttribute(function (s) /*string*/ { console.log(); - }, -); + }); this.getAttribute( function (s) /*string*/ { console.log(); diff --git a/crates/rome_js_formatter/tests/specs/prettier/typescript/conditional-types/comments.ts.snap b/crates/rome_js_formatter/tests/specs/prettier/typescript/conditional-types/comments.ts.snap index 1d1f493cbd9..1ee00cfbba3 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/typescript/conditional-types/comments.ts.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/typescript/conditional-types/comments.ts.snap @@ -1,8 +1,6 @@ --- source: crates/rome_js_formatter/tests/prettier_tests.rs -assertion_line: 144 expression: comments.ts - --- # Input ```js @@ -96,15 +94,15 @@ type T = test extends B ? foo : bar; /* comment */ type T = test extends B - ? foo /* comment + ? /* comment comment comment comment - */ + */ foo : test extends B - ? foo /* comment + ? /* comment comment - comment */ + comment */ foo : bar; type T = test extends B ? /* comment */ foo : bar; @@ -117,23 +115,21 @@ type T = test extends B ? foo : bar; /* comment type T = test extends B ? foo - : test extends B - /* comment + : /* comment comment comment comment - */ + */ test extends B ? foo - : bar; /* comment + : /* comment comment comment - */ + */ bar; type T = test extends B ? foo : /* comment */ bar; type T = test extends B - ? test extends B - /* c + ? test extends B /* c c */ ? foo : bar diff --git a/crates/rome_rowan/src/utility_types.rs b/crates/rome_rowan/src/utility_types.rs index bed80393fca..5eb8c96a626 100644 --- a/crates/rome_rowan/src/utility_types.rs +++ b/crates/rome_rowan/src/utility_types.rs @@ -146,11 +146,12 @@ impl Iterator for TokenAtOffset { impl ExactSizeIterator for TokenAtOffset {} #[cfg(target_pointer_width = "64")] -macro_rules! _static_assert { +#[macro_export] +macro_rules! static_assert { ($expr:expr) => { const _: i32 = 0 / $expr as i32; }; } #[cfg(target_pointer_width = "64")] -pub(crate) use _static_assert as static_assert; +pub use static_assert; diff --git a/website/playground/package.json b/website/playground/package.json index b71e49df3d4..637e84c89bb 100644 --- a/website/playground/package.json +++ b/website/playground/package.json @@ -8,7 +8,7 @@ "build": "pnpm build:wasm && pnpm build:js", "build:js": "tsc && vite build", "build:wasm": "wasm-pack build --target web --release", - "build:wasm-dev": "wasm-pack build --target web", + "build:wasm-dev": "wasm-pack build --target web --dev", "format": "cargo run -p rome_cli --release -- format --write ./src" }, "dependencies": {