From 2a40ff32b82dddd3777ccf6807d309bc1e48b997 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 16 Aug 2022 14:19:13 +0100 Subject: [PATCH] feat(rome_js_formatter): Template formatting (#3063) --- crates/rome_formatter/src/builders.rs | 5 +- crates/rome_formatter/src/format_element.rs | 2 +- crates/rome_formatter/src/lib.rs | 2 +- crates/rome_formatter/src/printer/mod.rs | 10 +- .../src/printer/printer_options/mod.rs | 40 ++- crates/rome_js_formatter/src/context.rs | 7 +- .../expressions/arrow_function_expression.rs | 63 +++- .../src/js/expressions/template.rs | 81 ++++-- .../js/expressions/template_chunk_element.rs | 43 ++- .../src/js/expressions/template_element.rs | 233 ++++++++++++++- .../src/js/lists/template_element_list.rs | 269 +++++++++++++++++- .../ts/expressions/template_chunk_element.rs | 11 +- .../src/ts/expressions/template_element.rs | 21 +- .../src/ts/lists/template_element_list.rs | 9 +- crates/rome_js_formatter/src/utils/mod.rs | 154 +--------- .../expression/logical_expression.js.snap | 12 +- .../specs/js/module/template/template.js | 10 +- .../specs/js/module/template/template.js.snap | 45 +-- .../js/comments/template-literal.js.snap | 27 +- .../js/line-suffix-boundary/boundary.js.snap | 43 ++- .../comment-inside.js.snap | 130 ++++----- .../js/multiparser-css/issue-5697.js.snap | 85 ------ .../multiparser-css/styled-components.js.snap | 100 ++----- .../multiparser-graphql/graphql-tag.js.snap | 38 ++- .../js/multiparser-html/lit-html.js.snap | 16 +- .../js/strings/template-literals.js.snap | 46 +-- .../prettier/js/template-align/indent.js.snap | 134 --------- .../js/template-literals/expressions.js.snap | 220 -------------- .../specs/prettier/js/template/arrow.js.snap | 52 ---- .../prettier/js/template/comment.js.snap | 18 +- .../js/template/faulty-locations.js.snap | 63 ---- .../prettier/js/template/graphql.js.snap | 78 ----- .../specs/prettier/js/template/indent.js.snap | 78 ----- .../specs/prettier/js/template/inline.js.snap | 11 +- .../multiparser-css/issue-6259.ts.snap | 15 +- .../template-literals/expressions.ts.snap | 36 --- .../tests/specs/ts/type/template_type.ts.snap | 19 +- website/playground/Cargo.toml | 2 +- 38 files changed, 956 insertions(+), 1272 deletions(-) delete mode 100644 crates/rome_js_formatter/tests/specs/prettier/js/multiparser-css/issue-5697.js.snap delete mode 100644 crates/rome_js_formatter/tests/specs/prettier/js/template-align/indent.js.snap delete mode 100644 crates/rome_js_formatter/tests/specs/prettier/js/template-literals/expressions.js.snap delete mode 100644 crates/rome_js_formatter/tests/specs/prettier/js/template/arrow.js.snap delete mode 100644 crates/rome_js_formatter/tests/specs/prettier/js/template/faulty-locations.js.snap delete mode 100644 crates/rome_js_formatter/tests/specs/prettier/js/template/graphql.js.snap delete mode 100644 crates/rome_js_formatter/tests/specs/prettier/js/template/indent.js.snap delete mode 100644 crates/rome_js_formatter/tests/specs/prettier/typescript/template-literals/expressions.ts.snap diff --git a/crates/rome_formatter/src/builders.rs b/crates/rome_formatter/src/builders.rs index 48bdb4ca5cb..c51789259e9 100644 --- a/crates/rome_formatter/src/builders.rs +++ b/crates/rome_formatter/src/builders.rs @@ -1504,6 +1504,7 @@ impl Format for ExpandParent { /// ``` /// use rome_formatter::{format_args, format, LineWidth}; /// use rome_formatter::prelude::*; +/// use rome_formatter::printer::PrintWidth; /// /// let context = SimpleFormatContext { /// line_width: LineWidth::try_from(20).unwrap(), @@ -1525,10 +1526,6 @@ impl Format for ExpandParent { /// ]) /// ]).unwrap(); /// -/// let options = PrinterOptions { -/// print_width: LineWidth::try_from(20).unwrap(), -/// ..PrinterOptions::default() -/// }; /// assert_eq!( /// "[\n\t'A somewhat longer string to force a line break',\n\t2,\n\t3,\n]", /// elements.print().as_code() diff --git a/crates/rome_formatter/src/format_element.rs b/crates/rome_formatter/src/format_element.rs index f6979f8d472..d0d9b611801 100644 --- a/crates/rome_formatter/src/format_element.rs +++ b/crates/rome_formatter/src/format_element.rs @@ -777,7 +777,7 @@ impl FormatContext for IrFormatContext { fn as_print_options(&self) -> PrinterOptions { PrinterOptions { tab_width: 2, - print_width: self.line_width(), + print_width: self.line_width().into(), line_ending: LineEnding::LineFeed, indent_style: IndentStyle::Space(2), } diff --git a/crates/rome_formatter/src/lib.rs b/crates/rome_formatter/src/lib.rs index 32b4669f65a..92dc5b415cf 100644 --- a/crates/rome_formatter/src/lib.rs +++ b/crates/rome_formatter/src/lib.rs @@ -270,7 +270,7 @@ impl FormatContext for SimpleFormatContext { fn as_print_options(&self) -> PrinterOptions { PrinterOptions::default() .with_indent(self.indent_style) - .with_print_width(self.line_width) + .with_print_width(self.line_width.into()) } } diff --git a/crates/rome_formatter/src/printer/mod.rs b/crates/rome_formatter/src/printer/mod.rs index 746145feeba..444b35ce847 100644 --- a/crates/rome_formatter/src/printer/mod.rs +++ b/crates/rome_formatter/src/printer/mod.rs @@ -942,7 +942,7 @@ fn fits_element_on_line<'a, 'rest>( state.line_width += char_width as usize; } - if state.line_width > options.print_width.value().into() { + if state.line_width > options.print_width.into() { return Fits::No; } @@ -1080,8 +1080,8 @@ impl<'a, 'rest> MeasureQueue<'a, 'rest> { #[cfg(test)] mod tests { use crate::prelude::*; - use crate::printer::{LineEnding, Printer, PrinterOptions}; - use crate::{format_args, write, FormatState, IndentStyle, LineWidth, Printed, VecBuffer}; + use crate::printer::{LineEnding, PrintWidth, Printer, PrinterOptions}; + use crate::{format_args, write, FormatState, IndentStyle, Printed, VecBuffer}; fn format(root: &dyn Format<()>) -> Printed { format_with_options( @@ -1230,7 +1230,7 @@ two lines`, let options = PrinterOptions { indent_style: IndentStyle::Tab, tab_width: 4, - print_width: LineWidth::try_from(19).unwrap(), + print_width: PrintWidth::new(19), ..PrinterOptions::default() }; @@ -1315,7 +1315,7 @@ two lines`, let document = buffer.into_element(); - let printed = Printer::new(PrinterOptions::default().with_print_width(LineWidth(10))) + let printed = Printer::new(PrinterOptions::default().with_print_width(PrintWidth::new(10))) .print(&document); assert_eq!( diff --git a/crates/rome_formatter/src/printer/printer_options/mod.rs b/crates/rome_formatter/src/printer/printer_options/mod.rs index 0f298b68bb6..d769046bdcd 100644 --- a/crates/rome_formatter/src/printer/printer_options/mod.rs +++ b/crates/rome_formatter/src/printer/printer_options/mod.rs @@ -7,7 +7,7 @@ pub struct PrinterOptions { pub tab_width: u8, /// What's the max width of a line. Defaults to 80 - pub print_width: LineWidth, + pub print_width: PrintWidth, /// The type of line ending to apply to the printed input pub line_ending: LineEnding, @@ -16,8 +16,42 @@ pub struct PrinterOptions { pub indent_style: IndentStyle, } +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct PrintWidth(u32); + +impl PrintWidth { + pub fn new(width: u32) -> Self { + Self(width) + } + + /// Returns a print width that guarantees that any content, regardless of its width, fits on the line. + /// + /// This has the effect that the printer never prints a line break for any soft line break. + pub fn infinite() -> Self { + Self(u32::MAX) + } +} + +impl Default for PrintWidth { + fn default() -> Self { + LineWidth::default().into() + } +} + +impl From for PrintWidth { + fn from(width: LineWidth) -> Self { + Self(u16::from(width) as u32) + } +} + +impl From for usize { + fn from(width: PrintWidth) -> Self { + width.0 as usize + } +} + impl PrinterOptions { - pub fn with_print_width(mut self, width: LineWidth) -> Self { + pub fn with_print_width(mut self, width: PrintWidth) -> Self { self.print_width = width; self } @@ -69,7 +103,7 @@ impl Default for PrinterOptions { fn default() -> Self { PrinterOptions { tab_width: 2, - print_width: LineWidth::default(), + print_width: PrintWidth::default(), indent_style: Default::default(), line_ending: LineEnding::LineFeed, } diff --git a/crates/rome_js_formatter/src/context.rs b/crates/rome_js_formatter/src/context.rs index e62503d0441..72d9acafd72 100644 --- a/crates/rome_js_formatter/src/context.rs +++ b/crates/rome_js_formatter/src/context.rs @@ -105,7 +105,7 @@ impl FormatContext for JsFormatContext { fn as_print_options(&self) -> PrinterOptions { PrinterOptions::default() .with_indent(self.indent_style) - .with_print_width(self.line_width) + .with_print_width(self.line_width.into()) } } @@ -160,7 +160,10 @@ impl CommentStyle for JsCommentStyle { fn is_group_start_token(&self, kind: JsSyntaxKind) -> bool { matches!( kind, - JsSyntaxKind::L_PAREN | JsSyntaxKind::L_BRACK | JsSyntaxKind::L_CURLY + JsSyntaxKind::L_PAREN + | JsSyntaxKind::L_BRACK + | JsSyntaxKind::L_CURLY + | JsSyntaxKind::DOLLAR_CURLY ) } diff --git a/crates/rome_js_formatter/src/js/expressions/arrow_function_expression.rs b/crates/rome_js_formatter/src/js/expressions/arrow_function_expression.rs index 814fc2b5c6f..8bc4c14ecb7 100644 --- a/crates/rome_js_formatter/src/js/expressions/arrow_function_expression.rs +++ b/crates/rome_js_formatter/src/js/expressions/arrow_function_expression.rs @@ -3,8 +3,8 @@ use rome_formatter::{format_args, write}; use crate::utils::{is_simple_expression, resolve_expression, starts_with_no_lookahead_token}; use rome_js_syntax::{ - JsAnyArrowFunctionParameters, JsAnyExpression, JsAnyFunctionBody, JsArrowFunctionExpression, - JsArrowFunctionExpressionFields, + JsAnyArrowFunctionParameters, JsAnyExpression, JsAnyFunctionBody, JsAnyTemplateElement, + JsArrowFunctionExpression, JsArrowFunctionExpressionFields, JsTemplate, }; #[derive(Debug, Clone, Default)] @@ -99,6 +99,9 @@ impl FormatNodeRule for FormatJsArrowFunctionExpressi false, !starts_with_no_lookahead_token(conditional.clone().into())?, ), + JsTemplate(template) => { + (is_multiline_template_starting_on_same_line(template), false) + } expr => (is_simple_expression(expr)?, false), }, }; @@ -125,3 +128,59 @@ impl FormatNodeRule for FormatJsArrowFunctionExpressi } } } + +/// Returns `true` if the template contains any new lines inside of its text chunks. +fn template_literal_contains_new_line(template: &JsTemplate) -> bool { + template.elements().iter().any(|element| match element { + JsAnyTemplateElement::JsTemplateChunkElement(chunk) => chunk + .template_chunk_token() + .map_or(false, |chunk| chunk.text().contains('\n')), + JsAnyTemplateElement::JsTemplateElement(_) => false, + }) +} + +/// Returns `true` for a template that starts on the same line as the previous token and contains a line break. +/// +/// +/// # Examples +// +/// ```javascript +/// "test" + ` +/// some content +/// `; +/// ``` +/// +/// Returns `true` because the template starts on the same line as the `+` token and its text contains a line break. +/// +/// ```javascript +/// "test" + `no line break` +/// ``` +/// +/// Returns `false` because the template text contains no line break. +/// +/// ```javascript +/// "test" + +/// `template +/// with line break`; +/// ``` +/// +/// Returns `false` because the template isn't on the same line as the '+' token. +fn is_multiline_template_starting_on_same_line(template: &JsTemplate) -> bool { + let contains_new_line = template_literal_contains_new_line(template); + + let starts_on_same_line = template.syntax().first_token().map_or(false, |token| { + for piece in token.leading_trivia().pieces() { + if let Some(comment) = piece.as_comments() { + if comment.has_newline() { + return false; + } + } else if piece.is_newline() { + return false; + } + } + + true + }); + + contains_new_line && starts_on_same_line +} diff --git a/crates/rome_js_formatter/src/js/expressions/template.rs b/crates/rome_js_formatter/src/js/expressions/template.rs index 65abf36d64c..c268e424f5f 100644 --- a/crates/rome_js_formatter/src/js/expressions/template.rs +++ b/crates/rome_js_formatter/src/js/expressions/template.rs @@ -1,32 +1,79 @@ use crate::prelude::*; use rome_formatter::write; -use rome_js_syntax::JsTemplate; -use rome_js_syntax::JsTemplateFields; +use rome_js_syntax::{ + JsAnyExpression, JsSyntaxToken, JsTemplate, TsTemplateLiteralType, TsTypeArguments, +}; +use rome_rowan::{declare_node_union, SyntaxResult}; #[derive(Debug, Clone, Default)] pub struct FormatJsTemplate; impl FormatNodeRule for FormatJsTemplate { fn fmt_fields(&self, node: &JsTemplate, f: &mut JsFormatter) -> FormatResult<()> { - let JsTemplateFields { - tag, - type_arguments, - l_tick_token, - elements, - r_tick_token, - } = node.as_fields(); - - write![ + JsAnyTemplate::from(node.clone()).fmt(f) + } +} + +declare_node_union! { + JsAnyTemplate = JsTemplate | TsTemplateLiteralType +} + +impl Format for JsAnyTemplate { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + write!( f, [ - tag.format(), - type_arguments.format(), + self.tag().format(), + self.type_arguments().format(), line_suffix_boundary(), - l_tick_token.format(), - elements.format(), - r_tick_token.format() + self.l_tick_token().format(), ] - ] + )?; + + self.write_elements(f)?; + + write!(f, [self.r_tick_token().format()]) + } +} + +impl JsAnyTemplate { + fn tag(&self) -> Option { + match self { + JsAnyTemplate::JsTemplate(template) => template.tag(), + JsAnyTemplate::TsTemplateLiteralType(_) => None, + } + } + + fn type_arguments(&self) -> Option { + match self { + JsAnyTemplate::JsTemplate(template) => template.type_arguments(), + JsAnyTemplate::TsTemplateLiteralType(_) => None, + } + } + + fn l_tick_token(&self) -> SyntaxResult { + match self { + JsAnyTemplate::JsTemplate(template) => template.l_tick_token(), + JsAnyTemplate::TsTemplateLiteralType(template) => template.l_tick_token(), + } + } + + fn write_elements(&self, f: &mut JsFormatter) -> FormatResult<()> { + match self { + JsAnyTemplate::JsTemplate(template) => { + write!(f, [template.elements().format()]) + } + JsAnyTemplate::TsTemplateLiteralType(template) => { + write!(f, [template.elements().format()]) + } + } + } + + fn r_tick_token(&self) -> SyntaxResult { + match self { + JsAnyTemplate::JsTemplate(template) => template.r_tick_token(), + JsAnyTemplate::TsTemplateLiteralType(template) => template.r_tick_token(), + } } } diff --git a/crates/rome_js_formatter/src/js/expressions/template_chunk_element.rs b/crates/rome_js_formatter/src/js/expressions/template_chunk_element.rs index 9d8a4579d59..28ee4419386 100644 --- a/crates/rome_js_formatter/src/js/expressions/template_chunk_element.rs +++ b/crates/rome_js_formatter/src/js/expressions/template_chunk_element.rs @@ -1,7 +1,8 @@ use crate::prelude::*; -use crate::utils::format_template_chunk; +use rome_formatter::write; -use rome_js_syntax::{JsTemplateChunkElement, JsTemplateChunkElementFields}; +use rome_js_syntax::{JsSyntaxToken, JsTemplateChunkElement, TsTemplateChunkElement}; +use rome_rowan::{declare_node_union, SyntaxResult}; #[derive(Debug, Clone, Default)] pub struct FormatJsTemplateChunkElement; @@ -12,11 +13,39 @@ impl FormatNodeRule for FormatJsTemplateChunkElement { node: &JsTemplateChunkElement, formatter: &mut JsFormatter, ) -> FormatResult<()> { - let JsTemplateChunkElementFields { - template_chunk_token, - } = node.as_fields(); + AnyTemplateChunkElement::from(node.clone()).fmt(formatter) + } +} + +declare_node_union! { + pub(crate) AnyTemplateChunkElement = JsTemplateChunkElement | TsTemplateChunkElement +} + +impl AnyTemplateChunkElement { + pub(crate) fn template_chunk_token(&self) -> SyntaxResult { + match self { + AnyTemplateChunkElement::JsTemplateChunkElement(chunk) => chunk.template_chunk_token(), + AnyTemplateChunkElement::TsTemplateChunkElement(chunk) => chunk.template_chunk_token(), + } + } +} + +impl Format for AnyTemplateChunkElement { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let chunk = self.template_chunk_token()?; - let chunk = template_chunk_token?; - format_template_chunk(chunk, formatter) + write!( + f, + [format_replaced( + &chunk, + &syntax_token_cow_slice( + // Per https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#sec-static-semantics-trv: + // In template literals, the '\r' and '\r\n' line terminators are normalized to '\n' + normalize_newlines(chunk.text_trimmed(), ['\r']), + &chunk, + chunk.text_trimmed_range().start(), + ) + )] + ) } } diff --git a/crates/rome_js_formatter/src/js/expressions/template_element.rs b/crates/rome_js_formatter/src/js/expressions/template_element.rs index cf1b2c739bb..683ba0049d3 100644 --- a/crates/rome_js_formatter/src/js/expressions/template_element.rs +++ b/crates/rome_js_formatter/src/js/expressions/template_element.rs @@ -1,10 +1,27 @@ use crate::prelude::*; -use crate::utils::{format_template_literal, TemplateElement}; +use rome_formatter::printer::{PrintWidth, Printer}; +use rome_formatter::{format_args, write, FormatContext, FormatRuleWithOptions, VecBuffer}; -use rome_js_syntax::JsTemplateElement; +use crate::context::TabWidth; +use crate::js::lists::template_element_list::{TemplateElementIndention, TemplateElementLayout}; +use rome_js_syntax::{ + JsAnyExpression, JsSyntaxNode, JsSyntaxToken, JsTemplateElement, TsTemplateElement, +}; +use rome_rowan::{declare_node_union, AstNode, SyntaxResult}; #[derive(Debug, Clone, Default)] -pub struct FormatJsTemplateElement; +pub struct FormatJsTemplateElement { + options: TemplateElementOptions, +} + +impl FormatRuleWithOptions for FormatJsTemplateElement { + type Options = TemplateElementOptions; + + fn with_options(mut self, options: Self::Options) -> Self { + self.options = options; + self + } +} impl FormatNodeRule for FormatJsTemplateElement { fn fmt_fields( @@ -12,6 +29,214 @@ impl FormatNodeRule for FormatJsTemplateElement { node: &JsTemplateElement, formatter: &mut JsFormatter, ) -> FormatResult<()> { - format_template_literal(TemplateElement::Js(node.clone()), formatter) + let element = AnyTemplateElement::from(node.clone()); + + FormatTemplateElement::new(element, self.options).fmt(formatter) } } + +declare_node_union! { + pub(crate) AnyTemplateElement = JsTemplateElement | TsTemplateElement +} + +#[derive(Debug, Copy, Clone, Default)] +pub struct TemplateElementOptions { + pub(crate) layout: TemplateElementLayout, + + /// The indention to use for this element + pub(crate) indention: TemplateElementIndention, + + /// Does the last template chunk (text element) end with a new line? + pub(crate) after_new_line: bool, +} + +pub(crate) struct FormatTemplateElement { + element: AnyTemplateElement, + options: TemplateElementOptions, +} + +impl FormatTemplateElement { + pub(crate) fn new(element: AnyTemplateElement, options: TemplateElementOptions) -> Self { + Self { element, options } + } +} + +impl Format for FormatTemplateElement { + fn fmt(&self, f: &mut JsFormatter) -> FormatResult<()> { + let has_comments = self.element.syntax().has_comments_direct(); + + let format_expression = format_with(|f| match &self.element { + AnyTemplateElement::JsTemplateElement(template) => { + write!(f, [template.expression().format()]) + } + AnyTemplateElement::TsTemplateElement(template) => { + write!(f, [template.ty().format()]) + } + }); + + let format_inner = format_with(|f: &mut JsFormatter| match self.options.layout { + TemplateElementLayout::SingleLine => { + // The goal is to print the expression on a single line, even if it exceeds the configured print width. + // + // Ideally, it would be possible to use a custom buffer that drops all soft line breaks + // (or converts them to spaces). However, this isn't straightforward with our + // nested IR (but would be with a flat ir). + // + // That's why we write the expression into a temporary buffer and print it + // with a printer that uses a print width so large, that the expression never exceeds + // the print width. + let mut buffer = VecBuffer::new(f.state_mut()); + write!(buffer, [format_expression])?; + let root = buffer.into_element(); + + let print_options = f + .context() + .as_print_options() + .with_print_width(PrintWidth::infinite()); + let printed = Printer::new(print_options).print(&root); + + write!( + f, + [dynamic_text( + printed.as_code(), + self.element.inner_syntax()?.text_trimmed_range().start() + )] + ) + } + TemplateElementLayout::Fit => { + use JsAnyExpression::*; + + let expression = self.element.expression(); + + // It's preferred to break after/before `${` and `}` rather than breaking in the + // middle of some expressions. + let indent = has_comments + || matches!( + expression, + Some( + JsStaticMemberExpression(_) + | JsComputedMemberExpression(_) + | JsConditionalExpression(_) + | JsSequenceExpression(_) + | TsAsExpression(_) + | JsBinaryExpression(_) + | JsLogicalExpression(_) + | JsInstanceofExpression(_) + | JsInExpression(_) + ) + ); + + if indent { + write!(f, [soft_block_indent(&format_expression)]) + } else { + write!(f, [format_expression]) + } + } + }); + + let format_indented = format_with(|f: &mut JsFormatter| { + if self.options.after_new_line { + write!(f, [dedent_to_root(&format_inner)]) + } else { + write_with_indention( + &format_inner, + self.options.indention, + f.context().tab_width(), + f, + ) + } + }); + + write!( + f, + [group(&format_args![ + self.element.dollar_curly_token().format(), + format_indented, + line_suffix_boundary(), + self.element.r_curly_token().format() + ])] + ) + } +} + +impl AnyTemplateElement { + fn dollar_curly_token(&self) -> SyntaxResult { + match self { + AnyTemplateElement::JsTemplateElement(template) => template.dollar_curly_token(), + AnyTemplateElement::TsTemplateElement(template) => template.dollar_curly_token(), + } + } + + fn inner_syntax(&self) -> SyntaxResult { + match self { + AnyTemplateElement::JsTemplateElement(template) => { + template.expression().map(AstNode::into_syntax) + } + AnyTemplateElement::TsTemplateElement(template) => { + template.ty().map(AstNode::into_syntax) + } + } + } + + fn expression(&self) -> Option { + match self { + AnyTemplateElement::JsTemplateElement(template) => template.expression().ok(), + AnyTemplateElement::TsTemplateElement(_) => None, + } + } + + fn r_curly_token(&self) -> SyntaxResult { + match self { + AnyTemplateElement::JsTemplateElement(template) => template.r_curly_token(), + AnyTemplateElement::TsTemplateElement(template) => template.r_curly_token(), + } + } +} + +/// Writes `content` with the specified `indention`. +fn write_with_indention( + content: &Content, + indention: TemplateElementIndention, + tab_width: TabWidth, + f: &mut JsFormatter, +) -> FormatResult<()> +where + Content: Format, +{ + let level = indention.level(tab_width); + let spaces = indention.align(tab_width); + + if level == 0 && spaces == 0 { + return write!(f, [content]); + } + + // Adds as many nested `indent` elements until it reaches the desired indention level. + let format_indented = format_with(|f| { + if level == 0 { + write!(f, [content]) + } else { + let mut buffer = VecBuffer::new(f.state_mut()); + + write!(buffer, [content])?; + + let mut indented = buffer.into_element(); + + for _ in 0..level { + indented = FormatElement::Indent(vec![indented].into_boxed_slice()); + } + + f.write_element(indented) + } + }); + + // Adds any necessary `align` for spaces not covered by indent level. + let format_aligned = format_with(|f| { + if spaces == 0 { + write!(f, [format_indented]) + } else { + write!(f, [align(spaces, &format_indented)]) + } + }); + + write!(f, [dedent_to_root(&format_aligned)]) +} diff --git a/crates/rome_js_formatter/src/js/lists/template_element_list.rs b/crates/rome_js_formatter/src/js/lists/template_element_list.rs index 292af8a25a6..18033616fd9 100644 --- a/crates/rome_js_formatter/src/js/lists/template_element_list.rs +++ b/crates/rome_js_formatter/src/js/lists/template_element_list.rs @@ -1,5 +1,14 @@ +use crate::js::expressions::template_chunk_element::AnyTemplateChunkElement; +use crate::js::expressions::template_element::{AnyTemplateElement, TemplateElementOptions}; + +use crate::context::TabWidth; use crate::prelude::*; -use rome_js_syntax::JsTemplateElementList; +use rome_js_syntax::{ + JsAnyExpression, JsAnyLiteralExpression, JsAnyTemplateElement, JsLanguage, + JsTemplateElementList, TsAnyTemplateElement, TsTemplateElementList, +}; +use rome_rowan::{declare_node_union, AstNodeListIterator, SyntaxResult}; +use std::iter::FusedIterator; #[derive(Debug, Clone, Default)] pub struct FormatJsTemplateElementList; @@ -8,12 +17,262 @@ impl FormatRule for FormatJsTemplateElementList { type Context = JsFormatContext; fn fmt(&self, node: &JsTemplateElementList, f: &mut JsFormatter) -> FormatResult<()> { - let mut join = f.join(); + AnyTemplateElementList::JsTemplateElementList(node.clone()).fmt(f) + } +} + +pub(crate) enum AnyTemplateElementList { + JsTemplateElementList(JsTemplateElementList), + TsTemplateElementList(TsTemplateElementList), +} + +impl Format for AnyTemplateElementList { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let layout = if self.is_simple() { + TemplateElementLayout::SingleLine + } else { + TemplateElementLayout::Fit + }; + + let mut indention = TemplateElementIndention::default(); + let mut after_new_line = false; + + for element in self.elements() { + match element { + AnyTemplateElementOrChunk::AnyTemplateElement(element) => { + let options = TemplateElementOptions { + after_new_line, + indention, + layout, + }; - for element in node { - join.entry(&element.format()); + match &element { + AnyTemplateElement::JsTemplateElement(element) => { + element.format().with_options(options).fmt(f)?; + } + AnyTemplateElement::TsTemplateElement(element) => { + element.format().with_options(options).fmt(f)?; + } + } + } + AnyTemplateElementOrChunk::AnyTemplateChunkElement(chunk) => { + match &chunk { + AnyTemplateChunkElement::JsTemplateChunkElement(chunk) => { + chunk.format().fmt(f)?; + } + AnyTemplateChunkElement::TsTemplateChunkElement(chunk) => { + chunk.format().fmt(f)?; + } + } + + let chunk_token = chunk.template_chunk_token()?; + let chunk_text = chunk_token.text(); + + let tab_width = f.context().tab_width(); + + indention = + TemplateElementIndention::after_last_new_line(chunk_text, tab_width); + after_new_line = chunk_text.ends_with('\n'); + } + } } - join.finish() + Ok(()) + } +} + +impl AnyTemplateElementList { + /// Returns `true` for `JsTemplate` if all elements are simple expressions that should be printed on a single line. + /// + /// Simple expressions are: + /// * Identifiers: `this`, `a` + /// * Members: `a.b`, `a[b]`, `a.b[c].d`, `a.b[5]`, `a.b["test"]` + fn is_simple(&self) -> bool { + match self { + AnyTemplateElementList::JsTemplateElementList(list) => { + if list.is_empty() { + return false; + } + + let mut expression_elements = list.iter().filter_map(|element| match element { + JsAnyTemplateElement::JsTemplateElement(element) => Some(element), + _ => None, + }); + + expression_elements.all(|expression_element| { + match expression_element.expression() { + Ok(expression) => is_simple_member_expression(expression).unwrap_or(false), + Err(_) => false, + } + }) + } + AnyTemplateElementList::TsTemplateElementList(_) => false, + } + } + + fn elements(&self) -> TemplateElementIterator { + match self { + AnyTemplateElementList::JsTemplateElementList(list) => { + TemplateElementIterator::JsTemplateElementList(list.iter()) + } + AnyTemplateElementList::TsTemplateElementList(list) => { + TemplateElementIterator::TsTemplateElementList(list.iter()) + } + } + } +} + +#[derive(Debug, Copy, Clone)] +pub enum TemplateElementLayout { + /// Applied when all expressions are identifiers, `this`, static member expressions, or computed member expressions with number or string literals. + /// Formats the expressions on a single line, even if their width otherwise would exceed the print width. + SingleLine, + + /// Tries to format the expression on a single line but may break the expression if the line otherwise exceeds the print width. + Fit, +} + +impl Default for TemplateElementLayout { + fn default() -> Self { + TemplateElementLayout::Fit + } +} + +declare_node_union! { + AnyTemplateElementOrChunk = AnyTemplateElement | AnyTemplateChunkElement +} + +fn is_simple_member_expression(expression: JsAnyExpression) -> SyntaxResult { + let mut current = expression; + + loop { + if current.syntax().has_comments_direct() { + return Ok(false); + } + + current = match current { + JsAnyExpression::JsStaticMemberExpression(expression) => expression.object()?, + JsAnyExpression::JsComputedMemberExpression(expression) => { + if matches!( + expression.member()?, + JsAnyExpression::JsAnyLiteralExpression( + JsAnyLiteralExpression::JsStringLiteralExpression(_) + | JsAnyLiteralExpression::JsNumberLiteralExpression(_) + ) | JsAnyExpression::JsIdentifierExpression(_) + ) { + expression.object()? + } else { + break; + } + } + JsAnyExpression::JsParenthesizedExpression(expression) => expression.expression()?, + JsAnyExpression::JsIdentifierExpression(_) | JsAnyExpression::JsThisExpression(_) => { + return Ok(true); + } + _ => { + break; + } + } + } + + Ok(false) +} + +enum TemplateElementIterator { + JsTemplateElementList(AstNodeListIterator), + TsTemplateElementList(AstNodeListIterator), +} + +impl Iterator for TemplateElementIterator { + type Item = AnyTemplateElementOrChunk; + + fn next(&mut self) -> Option { + match self { + TemplateElementIterator::JsTemplateElementList(inner) => { + let result = match inner.next()? { + JsAnyTemplateElement::JsTemplateChunkElement(chunk) => { + AnyTemplateElementOrChunk::from(AnyTemplateChunkElement::from(chunk)) + } + JsAnyTemplateElement::JsTemplateElement(element) => { + AnyTemplateElementOrChunk::from(AnyTemplateElement::from(element)) + } + }; + Some(result) + } + TemplateElementIterator::TsTemplateElementList(inner) => { + let result = match inner.next()? { + TsAnyTemplateElement::TsTemplateChunkElement(chunk) => { + AnyTemplateElementOrChunk::from(AnyTemplateChunkElement::from(chunk)) + } + TsAnyTemplateElement::TsTemplateElement(element) => { + AnyTemplateElementOrChunk::from(AnyTemplateElement::from(element)) + } + }; + + Some(result) + } + } + } +} + +impl ExactSizeIterator for TemplateElementIterator { + fn len(&self) -> usize { + match self { + TemplateElementIterator::JsTemplateElementList(inner) => inner.len(), + TemplateElementIterator::TsTemplateElementList(inner) => inner.len(), + } + } +} + +impl FusedIterator for TemplateElementIterator {} + +/// The indention derived from a position in the source document. Consists of indention level and spaces +#[derive(Debug, Copy, Clone, Default)] +pub struct TemplateElementIndention(u32); + +impl TemplateElementIndention { + /// Returns the indention level + pub(crate) fn level(&self, tab_width: TabWidth) -> u32 { + self.0 / (u8::from(tab_width) as u32) + } + + /// Returns the number of space indents on top of the indent level + pub(crate) fn align(&self, tab_width: TabWidth) -> u8 { + (self.0 % u8::from(tab_width) as u32) as u8 + } + + /// Computes the indention after the last new line character. + fn after_last_new_line(text: &str, tab_width: TabWidth) -> Self { + let by_new_line = text.rsplit_once('\n'); + + let size = match by_new_line { + None => 0, + Some((_, after_new_line)) => { + let tab_width: u32 = u8::from(tab_width).into(); + let mut size: u32 = 0; + + for c in after_new_line.chars() { + match c { + '\t' => { + // Tabs behave in a way that they are aligned to the nearest + // multiple of tab_width: + // number of spaces -> added size + // 0 -> 4, 1 -> 4, 2 -> 4, 3 -> 4 + // 4 -> 8, 5 -> 8, 6 -> 8, 7 -> 8 .. + // Or in other words, it clips the size to the next multiple of tab width. + size = size + tab_width - (size % tab_width); + } + ' ' => { + size += 1; + } + _ => break, + }; + } + + size + } + }; + + TemplateElementIndention(size) } } diff --git a/crates/rome_js_formatter/src/ts/expressions/template_chunk_element.rs b/crates/rome_js_formatter/src/ts/expressions/template_chunk_element.rs index 538633cd549..35b2c1bd5a1 100644 --- a/crates/rome_js_formatter/src/ts/expressions/template_chunk_element.rs +++ b/crates/rome_js_formatter/src/ts/expressions/template_chunk_element.rs @@ -1,7 +1,7 @@ use crate::prelude::*; -use crate::utils::format_template_chunk; -use rome_js_syntax::{TsTemplateChunkElement, TsTemplateChunkElementFields}; +use crate::js::expressions::template_chunk_element::AnyTemplateChunkElement; +use rome_js_syntax::TsTemplateChunkElement; #[derive(Debug, Clone, Default)] pub struct FormatTsTemplateChunkElement; @@ -12,11 +12,6 @@ impl FormatNodeRule for FormatTsTemplateChunkElement { node: &TsTemplateChunkElement, formatter: &mut JsFormatter, ) -> FormatResult<()> { - let TsTemplateChunkElementFields { - template_chunk_token, - } = node.as_fields(); - - let chunk = template_chunk_token?; - format_template_chunk(chunk, formatter) + AnyTemplateChunkElement::from(node.clone()).fmt(formatter) } } diff --git a/crates/rome_js_formatter/src/ts/expressions/template_element.rs b/crates/rome_js_formatter/src/ts/expressions/template_element.rs index e3f8de4c902..f40880fa973 100644 --- a/crates/rome_js_formatter/src/ts/expressions/template_element.rs +++ b/crates/rome_js_formatter/src/ts/expressions/template_element.rs @@ -1,10 +1,24 @@ use crate::prelude::*; -use crate::utils::{format_template_literal, TemplateElement}; +use rome_formatter::FormatRuleWithOptions; +use crate::js::expressions::template_element::{ + AnyTemplateElement, FormatTemplateElement, TemplateElementOptions, +}; use rome_js_syntax::TsTemplateElement; #[derive(Debug, Clone, Default)] -pub struct FormatTsTemplateElement; +pub struct FormatTsTemplateElement { + options: TemplateElementOptions, +} + +impl FormatRuleWithOptions for FormatTsTemplateElement { + type Options = TemplateElementOptions; + + fn with_options(mut self, options: Self::Options) -> Self { + self.options = options; + self + } +} impl FormatNodeRule for FormatTsTemplateElement { fn fmt_fields( @@ -12,6 +26,7 @@ impl FormatNodeRule for FormatTsTemplateElement { node: &TsTemplateElement, formatter: &mut JsFormatter, ) -> FormatResult<()> { - format_template_literal(TemplateElement::Ts(node.clone()), formatter) + let element = AnyTemplateElement::from(node.clone()); + FormatTemplateElement::new(element, self.options).fmt(formatter) } } diff --git a/crates/rome_js_formatter/src/ts/lists/template_element_list.rs b/crates/rome_js_formatter/src/ts/lists/template_element_list.rs index 2536087b2f9..8d21848b890 100644 --- a/crates/rome_js_formatter/src/ts/lists/template_element_list.rs +++ b/crates/rome_js_formatter/src/ts/lists/template_element_list.rs @@ -1,3 +1,4 @@ +use crate::js::lists::template_element_list::AnyTemplateElementList; use crate::prelude::*; use rome_js_syntax::TsTemplateElementList; @@ -8,12 +9,6 @@ impl FormatRule for FormatTsTemplateElementList { type Context = JsFormatContext; fn fmt(&self, node: &TsTemplateElementList, f: &mut JsFormatter) -> FormatResult<()> { - let mut join = f.join(); - - for item in node { - join.entry(&group(&item.format())); - } - - join.finish() + AnyTemplateElementList::TsTemplateElementList(node.clone()).fmt(f) } } diff --git a/crates/rome_js_formatter/src/utils/mod.rs b/crates/rome_js_formatter/src/utils/mod.rs index e787b0c1984..d8cfc897f9a 100644 --- a/crates/rome_js_formatter/src/utils/mod.rs +++ b/crates/rome_js_formatter/src/utils/mod.rs @@ -27,13 +27,10 @@ pub(crate) use member_chain::format_call_expression; pub(crate) use object_like::JsObjectLike; pub(crate) use object_pattern_like::JsObjectPatternLike; pub(crate) use parens::starts_with_no_lookahead_token; -use rome_formatter::{format_args, normalize_newlines, write, Buffer, VecBuffer}; -use rome_js_syntax::{ - JsAnyExpression, JsAnyFunction, JsAnyStatement, JsInitializerClause, JsLanguage, - JsTemplateElement, Modifiers, TsTemplateElement, TsType, -}; +use rome_formatter::{format_args, write, Buffer, VecBuffer}; +use rome_js_syntax::{JsAnyExpression, JsAnyStatement, JsInitializerClause, JsLanguage, Modifiers}; use rome_js_syntax::{JsSyntaxKind, JsSyntaxNode, JsSyntaxToken}; -use rome_rowan::{AstNode, AstNodeList, Direction, SyntaxResult}; +use rome_rowan::{AstNode, AstNodeList, Direction}; pub(crate) use simple::*; use std::fmt::Debug; pub(crate) use string_utils::*; @@ -213,151 +210,6 @@ where nodes_and_modifiers } -/// Utility to format -pub(crate) fn format_template_chunk(chunk: JsSyntaxToken, f: &mut JsFormatter) -> FormatResult<()> { - // Per https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#sec-static-semantics-trv: - // In template literals, the '\r' and '\r\n' line terminators are normalized to '\n' - - write!( - f, - [format_replaced( - &chunk, - &syntax_token_cow_slice( - normalize_newlines(chunk.text_trimmed(), ['\r']), - &chunk, - chunk.text_trimmed_range().start(), - ) - )] - ) -} - -/// Function to format template literals and template literal types -pub(crate) fn format_template_literal( - literal: TemplateElement, - formatter: &mut JsFormatter, -) -> FormatResult<()> { - write!(formatter, [literal]) -} - -pub(crate) enum TemplateElement { - Js(JsTemplateElement), - Ts(TsTemplateElement), -} - -impl Format for TemplateElement { - fn fmt(&self, f: &mut JsFormatter) -> FormatResult<()> { - let expression_is_plain = self.is_plain_expression()?; - let has_comments = self.has_comments(); - let should_hard_group = expression_is_plain && !has_comments; - - let content = format_with(|f| { - match self { - TemplateElement::Js(template) => { - write!(f, [template.expression().format()])?; - } - TemplateElement::Ts(template) => { - write!(f, [template.ty().format()])?; - } - } - - write!(f, [line_suffix_boundary()]) - }); - - if should_hard_group { - write!( - f, - [ - self.dollar_curly_token().format(), - content, - self.r_curly_token().format() - ] - ) - } else { - write!( - f, - [format_delimited( - &self.dollar_curly_token()?, - &content, - &self.r_curly_token()? - ) - .soft_block_indent()] - ) - } - } -} - -impl TemplateElement { - fn dollar_curly_token(&self) -> SyntaxResult { - match self { - TemplateElement::Js(template) => template.dollar_curly_token(), - TemplateElement::Ts(template) => template.dollar_curly_token(), - } - } - - fn r_curly_token(&self) -> SyntaxResult { - match self { - TemplateElement::Js(template) => template.r_curly_token(), - TemplateElement::Ts(template) => template.r_curly_token(), - } - } - - /// We want to break the template element only when we have articulated expressions inside it. - /// - /// We a plain expression is when it's one of the following: - /// - `loreum ${this.something} ipsum` - /// - `loreum ${a.b.c} ipsum` - /// - `loreum ${a} ipsum` - fn is_plain_expression(&self) -> FormatResult { - match self { - TemplateElement::Js(template_element) => { - let current_expression = template_element.expression()?; - match current_expression { - JsAnyExpression::JsStaticMemberExpression(_) - | JsAnyExpression::JsComputedMemberExpression(_) - | JsAnyExpression::JsIdentifierExpression(_) - | JsAnyExpression::JsAnyLiteralExpression(_) - | JsAnyExpression::JsCallExpression(_) => Ok(true), - - JsAnyExpression::JsParenthesizedExpression(expression) => { - // binary and logical expression have their own grouping inside parenthesis, - // so we mark the current parenthesized expression as not plain - match expression.expression()? { - JsAnyExpression::JsLogicalExpression(_) - | JsAnyExpression::JsBinaryExpression(_) => Ok(false), - _ => Ok(true), - } - } - - _ => { - if let Some(function) = - JsAnyFunction::cast(current_expression.syntax().clone()) - { - Ok(is_simple_function_expression(function)?) - } else { - Ok(false) - } - } - } - } - TemplateElement::Ts(template_element) => { - let is_mapped_type = matches!(template_element.ty()?, TsType::TsMappedType(_)); - Ok(!is_mapped_type) - } - } - } - - fn has_comments(&self) -> bool { - match self { - TemplateElement::Js(template_element) => { - template_element.syntax().has_comments_descendants() - } - TemplateElement::Ts(template_element) => { - template_element.syntax().has_comments_descendants() - } - } - } -} - /// This enum is used to extract a precedence from certain nodes. By comparing the precedence /// of two nodes, it's possible to change the way certain node should be formatted. /// diff --git a/crates/rome_js_formatter/tests/specs/js/module/expression/logical_expression.js.snap b/crates/rome_js_formatter/tests/specs/js/module/expression/logical_expression.js.snap index 390c50423ac..c55ae740354 100644 --- a/crates/rome_js_formatter/tests/specs/js/module/expression/logical_expression.js.snap +++ b/crates/rome_js_formatter/tests/specs/js/module/expression/logical_expression.js.snap @@ -245,11 +245,9 @@ undefined === function () { throw undefined; }; -const b = `${ - ( - veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFoo + veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongBar - ) -}`; +const b = `${( + veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFoo + veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongBar +)}`; const a = ( @@ -321,6 +319,6 @@ a in ## Lines exceeding width of 80 characters - 124: veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFoo + veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongBar - 192: veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFoo instanceof String && veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongBar instanceof Number + 123: veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFoo + veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongBar + 190: veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFoo instanceof String && veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongBar instanceof Number diff --git a/crates/rome_js_formatter/tests/specs/js/module/template/template.js b/crates/rome_js_formatter/tests/specs/js/module/template/template.js index db9177d31da..7c4d033b70d 100644 --- a/crates/rome_js_formatter/tests/specs/js/module/template/template.js +++ b/crates/rome_js_formatter/tests/specs/js/module/template/template.js @@ -17,16 +17,18 @@ output output `; -// don't break -const bar =`but where will ${this.fanta} wrap ${baz} ${"hello"} template literal? ${bar.ff.sss} long long long long ${foo.[3]} long long long long long long`; +// Single Line +const bar =`but where will ${this.fanta} wrap ${baz} ${"hello"} template literal? ${bar.ff.sss} long long long long ${foo[3]} long long long long long long`; + +// Fit const foo = `but where will ${a && b && bar || c && d && g} wrap long long long long long long`; const foo = `but where will ${lorem && loremlorem && loremlorem || loremc && lorem && loremlorem} wrap long long long long long long`; const a = ` let expression_is_simple = is_plain_expression(&expression)?; -${loooooong || loooooong || loooooong || loooooong || loooooong || loooooong || loooooong || loooooong } +${loooooong || loooooong || loooooong || loooooong || loooooong || loooooong || loooooong || loooooong } let expression_is_simple = is_plain_expression(&expression)?; `; @@ -42,4 +44,4 @@ const foo = `but where will ${ `${// $FlowFixMe found when converting React.createClass to ES6 ExampleStory.getFragment('story')} -`; \ No newline at end of file +`; diff --git a/crates/rome_js_formatter/tests/specs/js/module/template/template.js.snap b/crates/rome_js_formatter/tests/specs/js/module/template/template.js.snap index fdff19c69e6..892e141c36e 100644 --- a/crates/rome_js_formatter/tests/specs/js/module/template/template.js.snap +++ b/crates/rome_js_formatter/tests/specs/js/module/template/template.js.snap @@ -22,16 +22,18 @@ output output `; -// don't break -const bar =`but where will ${this.fanta} wrap ${baz} ${"hello"} template literal? ${bar.ff.sss} long long long long ${foo.[3]} long long long long long long`; +// Single Line +const bar =`but where will ${this.fanta} wrap ${baz} ${"hello"} template literal? ${bar.ff.sss} long long long long ${foo[3]} long long long long long long`; + +// Fit const foo = `but where will ${a && b && bar || c && d && g} wrap long long long long long long`; const foo = `but where will ${lorem && loremlorem && loremlorem || loremc && lorem && loremlorem} wrap long long long long long long`; const a = ` let expression_is_simple = is_plain_expression(&expression)?; -${loooooong || loooooong || loooooong || loooooong || loooooong || loooooong || loooooong || loooooong } +${loooooong || loooooong || loooooong || loooooong || loooooong || loooooong || loooooong || loooooong } let expression_is_simple = is_plain_expression(&expression)?; `; @@ -48,6 +50,7 @@ const foo = `but where will ${ `${// $FlowFixMe found when converting React.createClass to ES6 ExampleStory.getFragment('story')} `; + ============================= # Outputs ## Output 1 @@ -77,16 +80,21 @@ output `test abcd ${() => { - var hey; - const looooooooooong_expression = "loooooooooong_expression"; - return hey; -}} + var hey; + const looooooooooong_expression = "loooooooooong_expression"; + return hey; + }} output `; -// don't break -const bar =`but where will ${this.fanta} wrap ${baz} ${"hello"} template literal? ${bar.ff.sss} long long long long ${foo.[3]} long long long long long long`; +// Single Line +const bar = `but where will ${ + this.fanta +} wrap ${baz} ${"hello"} template literal? ${bar.ff.sss} long long long long ${ + foo[3] +} long long long long long long`; +// Fit const foo = `but where will ${ (a && b && bar) || (c && d && g) } wrap long long long long long long`; @@ -106,7 +114,7 @@ ${ loooooong || loooooong || loooooong -} +} let expression_is_simple = is_plain_expression(&expression)?; `; @@ -117,22 +125,15 @@ const foo = `but where will ${ `
${ this.set && this.set.artist - /* avoid console errors if `this.set` is undefined */ -}
`; +/* avoid console errors if `this.set` is undefined */}`; -`
${ - /* avoid console errors if `this.set` is undefined */ +`
${/* avoid console errors if `this.set` is undefined */ this.set && this.set.artist }
`; -`${ +`${ExampleStory.getFragment( // $FlowFixMe found when converting React.createClass to ES6 - ExampleStory.getFragment("story") -} + "story", +)} `; - -## Lines exceeding width of 80 characters - - 30: const bar =`but where will ${this.fanta} wrap ${baz} ${"hello"} template literal? ${bar.ff.sss} long long long long ${foo.[3]} long long long long long long`; - diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/comments/template-literal.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/comments/template-literal.js.snap index 6ffc1ea8fd6..457e8fc50f4 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/comments/template-literal.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/comments/template-literal.js.snap @@ -25,32 +25,37 @@ d //comment ```diff --- Prettier +++ Rome -@@ -5,7 +5,7 @@ +@@ -1,14 +1,11 @@ + ` +-${ +- a // comment ++${a // comment + } ${b /* comment */} --${/* comment */ c /* comment */} -+${ /* comment */ c /* comment */} + ${/* comment */ c /* comment */} - ${ - // comment +-${ +- // comment +- d //comment ++${d // comment //comment + }; + `; ``` # Output ```js ` -${ - a // comment +${a // comment } ${b /* comment */} -${ /* comment */ c /* comment */} +${/* comment */ c /* comment */} -${ - // comment - d //comment +${d // comment //comment }; `; ``` diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/line-suffix-boundary/boundary.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/line-suffix-boundary/boundary.js.snap index 6e77c7a49ab..5f0c526e8a0 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/line-suffix-boundary/boundary.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/line-suffix-boundary/boundary.js.snap @@ -37,16 +37,32 @@ ExampleStory.getFragment('story')} ```diff --- Prettier +++ Rome -@@ -9,7 +9,7 @@ +@@ -3,27 +3,25 @@ + a + } + +-${ +- a // comment ++${a // comment + } ${b /* comment */} --${/* comment */ c /* comment */} -+${ /* comment */ c /* comment */} + ${/* comment */ c /* comment */} + +-${ +- // comment +- d //comment ++${d // comment //comment + } - ${ - // comment -@@ -23,7 +23,8 @@ +-${ ++${ExampleStory.getFragment( + // $FlowFixMe found when converting React.createClass to ES6 +- ExampleStory.getFragment("story") +-} ++ "story", ++)} `;
@@ -67,23 +83,20 @@ ExampleStory.getFragment('story')} a } -${ - a // comment +${a // comment } ${b /* comment */} -${ /* comment */ c /* comment */} +${/* comment */ c /* comment */} -${ - // comment - d //comment +${d // comment //comment } -${ +${ExampleStory.getFragment( // $FlowFixMe found when converting React.createClass to ES6 - ExampleStory.getFragment("story") -} + "story", +)} `;
diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-comments/comment-inside.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-comments/comment-inside.js.snap index 2d3db4bb5b9..2f63f1fdd1d 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-comments/comment-inside.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-comments/comment-inside.js.snap @@ -74,30 +74,29 @@ expr1 = html` ```diff --- Prettier +++ Rome -@@ -2,9 +2,9 @@ - html` +@@ -3,56 +3,43 @@
${ -- this.set && this.set.artist + this.set && this.set.artist - /* avoid console errors if `this.set` is undefined */ - } -+ this.set && this.set.artist -+ /* avoid console errors if `this.set` is undefined */ -+} ++/* avoid console errors if `this.set` is undefined */}
`; -@@ -13,36 +13,32 @@ - /* comment */ +-html`${ +- foo +- /* comment */ ++html`${foo ++/* comment */ }`; html` - ${ - foo - /* comment */ - } -+${ -+ foo -+ /* comment */ ++${foo ++/* comment */ +} `; @@ -107,18 +106,16 @@ expr1 = html` - /* comment */ - } -`; -+graphql`${ -+ foo -+ /* comment */ ++graphql`${foo ++/* comment */ +}`; graphql` - ${ - foo - /* comment */ - } -+${ -+ foo -+ /* comment */ ++${foo ++/* comment */ +} `; @@ -128,37 +125,41 @@ expr1 = html` - /* comment */ - } -`; -+css`${ -+ foo -+ /* comment */ ++css`${foo ++/* comment */ +}`; css` - ${ - foo - /* comment */ - } -+${ -+ foo -+ /* comment */ ++${foo ++/* comment */ +} `; - markdown`${ -@@ -59,9 +55,10 @@ - // https://github.com/prettier/prettier/pull/9278#issuecomment-700589195 - expr1 = html` -
-- ${x( -- foo, // fg -- bar, +-markdown`${ +- foo +- /* comment */ ++markdown`${foo ++/* comment */ + }`; + markdown` +-${ +- foo +- /* comment */ ++${foo ++/* comment */ + } + `; + +@@ -62,6 +49,5 @@ + ${x( + foo, // fg + bar, - )} -
-+ ${ -+ x( -+ foo, // fg -+ bar, -+ ) -+}
++ )}
`; ``` @@ -169,65 +170,54 @@ expr1 = html` html`
${ - this.set && this.set.artist - /* avoid console errors if `this.set` is undefined */ -} + this.set && this.set.artist +/* avoid console errors if `this.set` is undefined */}
`; -html`${ - foo - /* comment */ +html`${foo +/* comment */ }`; html` -${ - foo - /* comment */ +${foo +/* comment */ } `; -graphql`${ - foo - /* comment */ +graphql`${foo +/* comment */ }`; graphql` -${ - foo - /* comment */ +${foo +/* comment */ } `; -css`${ - foo - /* comment */ +css`${foo +/* comment */ }`; css` -${ - foo - /* comment */ +${foo +/* comment */ } `; -markdown`${ - foo - /* comment */ +markdown`${foo +/* comment */ }`; markdown` -${ - foo - /* comment */ +${foo +/* comment */ } `; // https://github.com/prettier/prettier/pull/9278#issuecomment-700589195 expr1 = html`
- ${ - x( - foo, // fg - bar, - ) -}
+ ${x( + foo, // fg + bar, + )}
`; ``` diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-css/issue-5697.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-css/issue-5697.js.snap deleted file mode 100644 index 0b3e9092c60..00000000000 --- a/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-css/issue-5697.js.snap +++ /dev/null @@ -1,85 +0,0 @@ ---- -source: crates/rome_js_formatter/tests/prettier_tests.rs ---- - -# Input - -```js -const StyledH1 = styled.div` - font-size: 2.5em; - font-weight: ${(props) => (props.strong ? 500 : 100)}; - font-family: ${constants.text.displayFont.fontFamily}; - letter-spacing: ${(props) => (props.light ? '0.04em' : 0)}; - color: ${(props) => props.textColor}; - ${(props) => - props.center - ? ` display: flex; - align-items: center; - justify-content: center; - text-align: center;` - : ''} - @media (max-width: ${(props) => (props.noBreakPoint ? '0' : constants.layout.breakpoint.break1)}px) { - font-size: 2em; - } -`; -``` - - -# Prettier differences - -```diff ---- Prettier -+++ Rome -@@ -4,15 +4,18 @@ - font-family: ${constants.text.displayFont.fontFamily}; - letter-spacing: ${(props) => (props.light ? "0.04em" : 0)}; - color: ${(props) => props.textColor}; -- ${(props) => -+ ${ -+ (props) => - props.center - ? ` display: flex; - align-items: center; - justify-content: center; - text-align: center;` -- : ""} -- @media (max-width: ${(props) => -- props.noBreakPoint ? "0" : constants.layout.breakpoint.break1}px) { -+ : "" -+} -+ @media (max-width: ${ -+ (props) => (props.noBreakPoint ? "0" : constants.layout.breakpoint.break1) -+}px) { - font-size: 2em; - } - `; -``` - -# Output - -```js -const StyledH1 = styled.div` - font-size: 2.5em; - font-weight: ${(props) => (props.strong ? 500 : 100)}; - font-family: ${constants.text.displayFont.fontFamily}; - letter-spacing: ${(props) => (props.light ? "0.04em" : 0)}; - color: ${(props) => props.textColor}; - ${ - (props) => - props.center - ? ` display: flex; - align-items: center; - justify-content: center; - text-align: center;` - : "" -} - @media (max-width: ${ - (props) => (props.noBreakPoint ? "0" : constants.layout.breakpoint.break1) -}px) { - font-size: 2em; - } -`; -``` - - - diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-css/styled-components.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-css/styled-components.js.snap index 8ac449ef06e..60e4754dc85 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-css/styled-components.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-css/styled-components.js.snap @@ -296,11 +296,11 @@ const StyledDiv = styled.div` const TomatoButton = Button.extend` - color: tomato; + color : tomato ; -+ -+border-color : tomato -+ ; - border-color: tomato; ++border-color : tomato ++ ; ++ `; Button.extend.attr({})` @@ -404,84 +404,50 @@ const StyledDiv = styled.div` `; const Single2 = styled.div` -@@ -198,11 +196,12 @@ - height: 40px; +@@ -199,8 +197,7 @@ width: 40px; -- ${(props) => + ${(props) => - (props.complete || props.inProgress) && - css` -+ ${ -+ (props) => + (props.complete || props.inProgress) && css` border-color: rgba(var(--green-rgb), 0.15); -- `} -+ ` -+} + `} - div { - background-color: var(--purpleTT); -@@ -211,39 +210,44 @@ - color: var(--purpleTT); +@@ -212,15 +209,13 @@ display: inline-flex; -- ${(props) => + ${(props) => - props.complete && - css` -+ ${ -+ (props) => -+ props.complete && css` ++ props.complete && css` background-color: var(--green); border-width: 7px; -- `} -+ ` -+} + `} -- ${(props) => + ${(props) => - (props.complete || props.inProgress) && - css` -+ ${ -+ (props) => -+ (props.complete || props.inProgress) && css` ++ (props.complete || props.inProgress) && css` border-color: var(--green); -- `} -+ ` -+} + `} } - `; - - const A = styled.a` +@@ -230,11 +225,10 @@ display: inline-block; color: #fff; -- ${(props) => + ${(props) => - props.a && - css` - display: none; - `} - height: 30px; -+ ${ -+ (props) => + props.a && css` + display: none; -+ ` -+} ++ `} + height: 30px; `; const Foo = styled.p` - max-width: 980px; -- ${mediaBreakpointOnlyXs` -+ ${ -+ mediaBreakpointOnlyXs` - && { - font-size: 0.8rem; - } -- `} -+ ` -+} - - &.bottom { - margin-top: 3rem; ``` # Output @@ -685,12 +651,10 @@ const bar = styled.div` height: 40px; width: 40px; - ${ - (props) => + ${(props) => (props.complete || props.inProgress) && css` border-color: rgba(var(--green-rgb), 0.15); - ` -} + `} div { background-color: var(--purpleTT); @@ -699,44 +663,36 @@ const bar = styled.div` color: var(--purpleTT); display: inline-flex; - ${ - (props) => - props.complete && css` + ${(props) => + props.complete && css` background-color: var(--green); border-width: 7px; - ` -} + `} - ${ - (props) => - (props.complete || props.inProgress) && css` + ${(props) => + (props.complete || props.inProgress) && css` border-color: var(--green); - ` -} + `} } `; const A = styled.a` display: inline-block; color: #fff; - ${ - (props) => + ${(props) => props.a && css` display: none; - ` -} + `} height: 30px; `; const Foo = styled.p` max-width: 980px; - ${ - mediaBreakpointOnlyXs` + ${mediaBreakpointOnlyXs` && { font-size: 0.8rem; } - ` -} + `} &.bottom { margin-top: 3rem; diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-graphql/graphql-tag.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-graphql/graphql-tag.js.snap index 12d192156f3..f4c3b69504e 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-graphql/graphql-tag.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-graphql/graphql-tag.js.snap @@ -233,30 +233,30 @@ fragment another on User { name +# comment +${one}${two} ${three} +${four} -+ + +- ${five} +- # comment +- ${six} +${five} +# comment +${six} -+ + +- # comment +- ${seven} +- # comment +# comment +${seven} +# comment + +${eight} - -- ${five} -- # comment -- ${six} ++ + # comment with trailing whitespace -- # comment -- ${seven} -- # comment - - ${eight} -+# blank line above this comment - # comment with trailing whitespace ++# blank line above this comment ++ - # blank line above this comment `; @@ -344,20 +344,18 @@ fragment another on User { name + + +${USER_DETAILS_FRAGMENT} -+ + +- ${FRIENDS_FRAGMENT} + # Comment + # that continues on a new line + + + # and has a blank line in the middle - -- ${FRIENDS_FRAGMENT} ++ + ${FRIENDS_FRAGMENT} ${generateFragment({ -- totally: "a good idea", -- })} -+ totally: "a good idea", -+})} + totally: "a good idea", + })} - ${fragment} - #comment @@ -514,8 +512,8 @@ ${USER_DETAILS_FRAGMENT} ${FRIENDS_FRAGMENT} ${generateFragment({ - totally: "a good idea", -})} + totally: "a good idea", + })} ${fragment}#comment diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-html/lit-html.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-html/lit-html.js.snap index 964a1b14646..003bf398251 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-html/lit-html.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/multiparser-html/lit-html.js.snap @@ -115,7 +115,7 @@ ${ foo}:${bar}; ```diff --- Prettier +++ Rome -@@ -14,48 +14,64 @@ +@@ -14,48 +14,63 @@ render() { return html` @@ -175,11 +175,10 @@ ${ foo}:${bar};

Bar List

- ${bars.map((bar) => html`

${bar}

`)} + ${bars.map( -+ (bar) => -+ html` ++ (bar) => html` +

${bar}

+ `, -+ )} ++ )} `; } @@ -199,7 +198,7 @@ ${ foo}:${bar}; const closingScriptTagShouldBeEscapedProperly = /* HTML */ ` `; @@ -311,11 +310,10 @@ function HelloWorld() { return html`

Bar List

${bars.map( - (bar) => - html` + (bar) => html`

${bar}

`, - )} + )} `; } @@ -360,6 +358,6 @@ ${foo}:${bar}; # Lines exceeding max width of 80 characters ``` - 82: const closingScriptTag2 = /* HTML */ `