diff --git a/crates/rome_formatter/src/buffer.rs b/crates/rome_formatter/src/buffer.rs index e989f157465..165625d814f 100644 --- a/crates/rome_formatter/src/buffer.rs +++ b/crates/rome_formatter/src/buffer.rs @@ -164,10 +164,11 @@ pub struct VecBuffer<'a, Context> { impl<'a, Context> VecBuffer<'a, Context> { pub fn new(state: &'a mut FormatState) -> Self { - Self { - state, - elements: vec![], - } + Self::new_with_vec(state, Vec::new()) + } + + pub fn new_with_vec(state: &'a mut FormatState, elements: Vec) -> Self { + Self { state, elements } } /// Creates a buffer with the specified capacity diff --git a/crates/rome_formatter/src/builders.rs b/crates/rome_formatter/src/builders.rs index b001b7b8f81..de5c8e3a655 100644 --- a/crates/rome_formatter/src/builders.rs +++ b/crates/rome_formatter/src/builders.rs @@ -2212,60 +2212,6 @@ impl<'a, 'buf, Context> FillBuilder<'a, 'buf, Context> { self } - /// Adds an iterator of entries to the fill output, flattening any [FormatElement::List] - /// entries by adding the list's elements to the fill output. - /// - /// ## Warning - /// - /// The usage of this method is highly discouraged and it's better to use - /// other APIs on ways: for example progressively format the items based on their type. - pub fn flatten_entries( - &mut self, - separator: &dyn Format, - entries: I, - ) -> &mut Self - where - F: Format, - I: IntoIterator, - { - for entry in entries { - self.flatten_entry(separator, &entry); - } - - self - } - - /// Adds a new entry to the fill output. If the entry is a [FormatElement::List], - /// then adds the list's entries to the fill output instead of the list itself. - fn flatten_entry( - &mut self, - separator: &dyn Format, - entry: &dyn Format, - ) -> &mut Self { - self.result = self.result.and_then(|_| { - let mut buffer = VecBuffer::new(self.fmt.state_mut()); - write!(buffer, [entry])?; - - let entries = buffer.into_vec(); - self.items.reserve((entries.len() * 2).saturating_sub(1)); - - let mut buffer = VecBuffer::new(self.fmt.state_mut()); - for item in entries.into_iter() { - if !self.items.is_empty() { - write!(buffer, [separator])?; - - self.items.push(buffer.take_element()); - } - - self.items.push(item); - } - - Ok(()) - }); - - self - } - /// Adds a new entry to the fill output. The `separator` isn't written if this is the first element in the list. pub fn entry( &mut self, diff --git a/crates/rome_formatter/src/format_element.rs b/crates/rome_formatter/src/format_element.rs index 5ede04a8129..64c21bdba7f 100644 --- a/crates/rome_formatter/src/format_element.rs +++ b/crates/rome_formatter/src/format_element.rs @@ -224,6 +224,12 @@ pub enum LineMode { Empty, } +impl LineMode { + pub const fn is_hard(&self) -> bool { + matches!(self, LineMode::Hard) + } +} + /// A token used to gather a list of elements; see [crate::Formatter::join_with]. #[derive(Clone, Default, Eq, PartialEq)] pub struct List { @@ -984,9 +990,15 @@ impl Format for FormatElement { [ dynamic_text(&std::format!(""), TextSize::default()), space(), - &interned.0.as_ref() ] - ) + )?; + + match interned.0.as_ref() { + element @ FormatElement::Text(_) | element @ FormatElement::Space => { + write!(f, [text("\""), element, text("\"")]) + } + element => element.fmt(f), + } } else { write!( f, @@ -1017,13 +1029,9 @@ impl<'a> Format for &'a [FormatElement] { matches!(element, FormatElement::Text(_) | FormatElement::Space); if print_as_str { - write!(f, [text("\"")])?; - } - - write!(f, [group(&element)])?; - - if print_as_str { - write!(f, [text("\"")])?; + write!(f, [text("\""), &element, text("\"")])?; + } else { + write!(f, [group(&element)])?; } if index < len - 1 { diff --git a/crates/rome_formatter/src/printed_tokens.rs b/crates/rome_formatter/src/printed_tokens.rs index bcd8e001546..bf66c11c854 100644 --- a/crates/rome_formatter/src/printed_tokens.rs +++ b/crates/rome_formatter/src/printed_tokens.rs @@ -38,7 +38,8 @@ impl PrintedTokens { descendant_offset if descendant_offset < *offset => { panic!("token has not been seen by the formatter: {descendant:#?}.\ \nUse `format_replaced` if you want to replace a token from the formatted output.\ - \nUse `format_removed` if you to remove a token from the formatted output") + \nUse `format_removed` if you want to remove a token from the formatted output.\n\ + parent: {:#?}", descendant.parent()) } descendant_offset if descendant_offset > *offset => { panic!("tracked offset {offset:?} doesn't match any token of {root:#?}. Have you passed a token from another tree?"); diff --git a/crates/rome_formatter/src/printer/mod.rs b/crates/rome_formatter/src/printer/mod.rs index 39b8cc8ce9a..b53fde79814 100644 --- a/crates/rome_formatter/src/printer/mod.rs +++ b/crates/rome_formatter/src/printer/mod.rs @@ -474,12 +474,31 @@ impl<'a> Printer<'a> { current_fits = next_fits; } } - (Some(_), _) => { - // Don't print a trailing separator + // Trailing separator + (Some(separator), _) => { + let print_mode = if current_fits + && fits_on_line( + [separator], + args.with_print_mode(PrintMode::Flat), + &empty_rest, + self, + ) { + PrintMode::Flat + } else { + PrintMode::Expanded + }; + + self.print_all(queue, &[separator], args.with_print_mode(print_mode)); } - (None, _) => { + (None, None) => { break; } + (None, Some(_)) => { + // Unreachable for iterators implementing [FusedIterator] which slice.iter implements. + // Reaching this means that the first `iter.next()` returned `None` but calling `iter.next()` + // again returns `Some` + unreachable!() + } } } } diff --git a/crates/rome_js_formatter/src/jsx/attribute/expression_attribute_value.rs b/crates/rome_js_formatter/src/jsx/attribute/expression_attribute_value.rs index 3a7c525bdf3..6563f02ea82 100644 --- a/crates/rome_js_formatter/src/jsx/attribute/expression_attribute_value.rs +++ b/crates/rome_js_formatter/src/jsx/attribute/expression_attribute_value.rs @@ -1,8 +1,8 @@ use crate::prelude::*; -use rome_formatter::write; +use rome_formatter::{format_args, write}; use rome_js_syntax::{ - JsAnyExpression, JsxExpressionAttributeValue, JsxExpressionAttributeValueFields, + JsAnyExpression, JsxAnyTag, JsxExpressionAttributeValue, JsxExpressionAttributeValueFields, }; #[derive(Debug, Clone, Default)] @@ -19,58 +19,85 @@ impl FormatNodeRule for FormatJsxExpressionAttribut expression, r_curly_token, } = node.as_fields(); - write!( - f, - [group(&format_with(|f| { - write!(f, [l_curly_token.format()])?; - let expression = expression.as_ref()?; + let expression = expression?; - // When the inner expression for a prop is an object, array, or call expression, we want to combine the - // delimiters of the expression (`{`, `}`, `[`, `]`, or `(`, `)`) with the delimiters of the JSX - // attribute (`{`, `}`), so that we don't end up with redundant indents. Therefore we do not - // soft indent the expression - // - // Good: - // ```jsx - // - // ``` - // - // Bad: - // ```jsx - // - // ``` - // - if matches!( - expression, - JsAnyExpression::JsObjectExpression(_) - | JsAnyExpression::JsArrayExpression(_) - | JsAnyExpression::JsCallExpression(_) - | JsAnyExpression::JsArrowFunctionExpression(_) - ) { - write!(f, [expression.format()])?; - } else { - write!(f, [soft_block_indent(&expression.format())])?; - }; + let should_inline = should_inline_jsx_expression(&expression); - write!(f, [line_suffix_boundary(), r_curly_token.format()]) - }))] - ) + if should_inline { + write!( + f, + [ + l_curly_token.format(), + expression.format(), + line_suffix_boundary(), + r_curly_token.format() + ] + ) + } else { + write!( + f, + [group(&format_args![ + l_curly_token.format(), + soft_block_indent(&expression.format()), + line_suffix_boundary(), + r_curly_token.format() + ])] + ) + } + } +} + +/// Tests if an expression inside of a [JsxExpressionChild] or [JsxExpressionAttributeValue] should be inlined. +/// Good: +/// ```jsx +/// +/// ``` +/// +/// Bad: +/// ```jsx +/// +/// ``` +pub(crate) fn should_inline_jsx_expression(expression: &JsAnyExpression) -> bool { + use JsAnyExpression::*; + + if expression.syntax().has_comments_direct() { + return false; + } + + match expression { + JsArrayExpression(_) + | JsObjectExpression(_) + | JsArrowFunctionExpression(_) + | JsCallExpression(_) + | JsNewExpression(_) + | JsImportCallExpression(_) + | ImportMeta(_) + | JsFunctionExpression(_) + | JsTemplate(_) => true, + JsAwaitExpression(await_expression) => match await_expression.argument() { + Ok(JsxTagExpression(argument)) => { + matches!(argument.tag(), Ok(JsxAnyTag::JsxElement(_))) + && should_inline_jsx_expression(&argument.into()) + } + _ => false, + }, + _ => false, } } diff --git a/crates/rome_js_formatter/src/jsx/auxiliary/expression_child.rs b/crates/rome_js_formatter/src/jsx/auxiliary/expression_child.rs index f9f3dcc13d6..c43175a0a21 100644 --- a/crates/rome_js_formatter/src/jsx/auxiliary/expression_child.rs +++ b/crates/rome_js_formatter/src/jsx/auxiliary/expression_child.rs @@ -1,10 +1,10 @@ +use crate::jsx::attribute::expression_attribute_value::should_inline_jsx_expression; use crate::prelude::*; use crate::prelude::{format_args, write}; -use crate::utils::jsx::JsxSpace; -use rome_formatter::{group, CstFormatContext, FormatResult}; -use rome_js_syntax::{ - JsAnyExpression, JsAnyLiteralExpression, JsxExpressionChild, JsxExpressionChildFields, -}; + +use crate::utils::JsAnyBinaryLikeExpression; +use rome_formatter::{group, FormatResult}; +use rome_js_syntax::{JsAnyExpression, JsxExpressionChild, JsxExpressionChildFields}; #[derive(Debug, Clone, Default)] pub struct FormatJsxExpressionChild; @@ -17,47 +17,36 @@ impl FormatNodeRule for FormatJsxExpressionChild { r_curly_token, } = node.as_fields(); - let l_curly_token = l_curly_token?; - let r_curly_token = r_curly_token?; - - // If the expression child is just a string literal with one space in it, it's a JSX space - if let Some(JsAnyExpression::JsAnyLiteralExpression( - JsAnyLiteralExpression::JsStringLiteralExpression(string_literal), - )) = &expression - { - let str_token = string_literal.value_token()?; - let trimmed_text = str_token.text_trimmed(); - - let has_space_text = trimmed_text == "' '" || trimmed_text == "\" \""; - let no_trivia = !str_token.has_leading_non_whitespace_trivia() - && !str_token.has_trailing_comments() - && !l_curly_token.has_trailing_comments() - && !r_curly_token.has_leading_non_whitespace_trivia(); - let is_suppressed = f - .context() - .comments() - .is_suppressed(string_literal.syntax()); - - if has_space_text && no_trivia && !is_suppressed { - return write![ - f, - [ - format_removed(&l_curly_token), - format_replaced(&str_token, &JsxSpace::default()), - format_removed(&r_curly_token) - ] - ]; + let should_inline = expression.as_ref().map_or(true, |expression| { + if matches!(expression, JsAnyExpression::JsConditionalExpression(_)) + || JsAnyBinaryLikeExpression::can_cast(expression.syntax().kind()) + { + true + } else { + should_inline_jsx_expression(expression) } - } + }); - write![ - f, - [group(&format_args![ - l_curly_token.format(), - expression.format(), - line_suffix_boundary(), - r_curly_token.format() - ])] - ] + if should_inline { + write!( + f, + [ + l_curly_token.format(), + expression.format(), + line_suffix_boundary(), + r_curly_token.format() + ] + ) + } else { + write!( + f, + [group(&format_args![ + l_curly_token.format(), + soft_block_indent(&expression.format()), + line_suffix_boundary(), + r_curly_token.format() + ])] + ) + } } } diff --git a/crates/rome_js_formatter/src/jsx/auxiliary/spread_child.rs b/crates/rome_js_formatter/src/jsx/auxiliary/spread_child.rs index 89ea385c8d7..db7751ee4d5 100644 --- a/crates/rome_js_formatter/src/jsx/auxiliary/spread_child.rs +++ b/crates/rome_js_formatter/src/jsx/auxiliary/spread_child.rs @@ -1,13 +1,28 @@ use crate::prelude::*; +use rome_formatter::write; -use rome_js_syntax::JsxSpreadChild; -use rome_rowan::AstNode; +use rome_js_syntax::{JsxSpreadChild, JsxSpreadChildFields}; #[derive(Debug, Clone, Default)] pub struct FormatJsxSpreadChild; impl FormatNodeRule for FormatJsxSpreadChild { - fn fmt_fields(&self, node: &JsxSpreadChild, formatter: &mut JsFormatter) -> FormatResult<()> { - format_verbatim_node(node.syntax()).fmt(formatter) + fn fmt_fields(&self, node: &JsxSpreadChild, f: &mut JsFormatter) -> FormatResult<()> { + let JsxSpreadChildFields { + l_curly_token, + dotdotdot_token, + expression, + r_curly_token, + } = node.as_fields(); + + write!( + f, + [ + l_curly_token.format(), + dotdotdot_token.format(), + expression.format(), + r_curly_token.format() + ] + ) } } diff --git a/crates/rome_js_formatter/src/jsx/auxiliary/text.rs b/crates/rome_js_formatter/src/jsx/auxiliary/text.rs index b375919ba2c..b25017a9214 100644 --- a/crates/rome_js_formatter/src/jsx/auxiliary/text.rs +++ b/crates/rome_js_formatter/src/jsx/auxiliary/text.rs @@ -1,409 +1,15 @@ use crate::prelude::*; -use crate::utils::jsx::{JsxSpace, JSX_WHITESPACE_CHARS}; -use rome_formatter::{write, FormatResult}; -use rome_js_syntax::{JsxText, JsxTextFields, TextSize}; -use std::borrow::Cow; -use std::ops::Range; -use std::str::CharIndices; + +use rome_formatter::FormatResult; +use rome_js_syntax::JsxText; #[derive(Debug, Clone, Default)] pub struct FormatJsxText; impl FormatNodeRule for FormatJsxText { fn fmt_fields(&self, node: &JsxText, f: &mut JsFormatter) -> FormatResult<()> { - let JsxTextFields { value_token } = node.as_fields(); - let token = value_token?; - let (leading_whitespace_type, new_text, start, trailing_whitespace_type) = - clean_jsx_text(token.text(), token.text_range().start()); - if matches!( - leading_whitespace_type, - Some(WhitespaceType::HasNewline) | None - ) && new_text.is_empty() - && matches!( - trailing_whitespace_type, - Some(WhitespaceType::HasNewline) | None - ) - { - return write![f, [format_removed(&token)]]; - } - - let new_token = syntax_token_cow_slice(new_text, &token, start); - let new_text = format_replaced(&token, &new_token); - - write![ - f, - [leading_whitespace_type, new_text, trailing_whitespace_type] - ] - } -} - -struct TextCleaner<'a> { - pub text: &'a str, - pub leading_whitespace_type: Option, - pub start_idx: usize, - /// Whitespace ranges are the ranges of text that contain whitespace. We keep track of them - /// so that on our second pass, we strip them out. - /// - /// "A Brighter \n\t Summer \n\n Day" - /// ^^ ^^^^^^ ^^^^^^^ - pub whitespace_ranges: Vec>, - pub trailing_whitespace_type: Option, -} - -impl<'a> TextCleaner<'a> { - fn build(text: &'a str) -> Self { - let mut char_indices = text.char_indices(); - - // Once `get_leading_whitespace_type` is done, we have consumed our first non-whitespace character - let (leading_whitespace_type, start_idx) = get_leading_whitespace_type(&mut char_indices); - - let mut whitespace_ranges = Vec::new(); - let mut current_whitespace_range_start: Option = None; - - for (idx, c) in char_indices { - // If we've already started a whitespace range... - if let Some(start) = current_whitespace_range_start { - // If the character is *not* a whitespace character... - // - // input: "Yi Yi" - // ^ - if !JSX_WHITESPACE_CHARS.contains(&c) { - // We push the range into the vector - whitespace_ranges.push(start..idx); - current_whitespace_range_start = None; - } - } else { - // If we have not started a whitespace range - // and we come across a whitespace character, - // - // input: "Yi Yi" - // ^ - if JSX_WHITESPACE_CHARS.contains(&c) { - // We start a whitespace range - current_whitespace_range_start = Some(idx); - } - } - } - - // If, at the end of the loop, we still have a `current_whitespace_range_start` that is - // Some, this indicates we have trailing whitespace: - // - // input: "Taipei Story \t" - // ^ started unterminated whitespace range here - // - let trailing_whitespace_type = current_whitespace_range_start - .and_then(|start| get_trailing_whitespace_type(&text[start..])); - - TextCleaner { - text, - start_idx, - leading_whitespace_type, - whitespace_ranges, - trailing_whitespace_type, - } - } - - /// Tries to clean the text with the whitespace ranges. If we have no ranges, we return None - /// because there's no cleaning to be done. - /// Does *not* add leading or trailing whitespace. Leading or trailing whitespace must be a JSX - /// space. - fn clean_text(&self) -> Option { - if self.whitespace_ranges.is_empty() { - return None; - } - - let mut char_indices = self.text.char_indices(); - - let mut output_string = String::new(); - - if self.leading_whitespace_type.is_some() { - for (_, c) in char_indices.by_ref() { - if !JSX_WHITESPACE_CHARS.contains(&c) { - output_string.push(c); - break; - } - } - } - - let mut current_whitespace_range_idx = 0; - - // Invariant: idx is **never** larger than the end of the current whitespace range - for (idx, c) in char_indices { - let current_whitespace_range = self.whitespace_ranges.get(current_whitespace_range_idx); - if let Some(range) = current_whitespace_range { - // If the index is the end of the current whitespace range, - // then we increment the whitespace range index and - // push on a space character. - // - // input: "hello world" - // ^ - // output: "hello " - if idx == range.end - 1 { - output_string.push(' '); - current_whitespace_range_idx += 1; - } - - // If our index is less than the start of the current whitespace range - // we push on characters. - // - // input: "hello world" - // ^ - // output: "hel" - // - if idx < range.start { - output_string.push(c) - } - } else { - // If None, we are past the whitespace ranges - // - // input: "hello world" - // ^ - // output: "hello wor" - // - // If the character is not whitespace, we push it on. - // If it is whitespace, it is trailing whitespace, so we ignore it. - if !JSX_WHITESPACE_CHARS.contains(&c) { - output_string.push(c) - } - } - } - - Some(output_string) - } -} - -impl Format for WhitespaceType { - fn fmt(&self, f: &mut JsFormatter) -> FormatResult<()> { - if self == &WhitespaceType::NoNewline { - write![f, [JsxSpace::default()]]?; - } - - Ok(()) - } -} - -/// Leading and trailing whitespace can either have newlines or not -/// If whitespace has newlines, we normalize it to no spaces. -/// If whitespace has no newlines, we normalize it to a single space -#[derive(Debug, Copy, Clone, PartialEq)] -enum WhitespaceType { - NoNewline, - HasNewline, -} - -/// We push the CharIndices iterator forward until we get to a non-whitespace character -/// -/// Returns the whitespace type (if whitespace exists), ond the start index of the non-whitespace -/// text -/// -/// NOTE: It's okay that we consume this non-whitespace character, as it won't affect our -/// whitespace group finding logic. -fn get_leading_whitespace_type(char_indices: &mut CharIndices) -> (Option, usize) { - let mut leading_type = None; - let mut start_idx = 0; - - for (i, c) in char_indices.by_ref() { - if !JSX_WHITESPACE_CHARS.contains(&c) { - return (leading_type, i); - } - start_idx = i; - if c == '\n' { - leading_type = Some(WhitespaceType::HasNewline); - } else if leading_type.is_none() { - leading_type = Some(WhitespaceType::NoNewline); - } - } - - (leading_type, start_idx + 1) -} - -/// Get the whitespace type for the trailing whitespace. -/// This uses a slice instead of an iterator because we cannot know what is the trailing -/// whitespace a priori. -fn get_trailing_whitespace_type(end_whitespace: &str) -> Option { - let mut trailing_type = None; - for c in end_whitespace.chars() { - if JSX_WHITESPACE_CHARS.contains(&c) { - if c == '\n' { - trailing_type = Some(WhitespaceType::HasNewline); - } else if trailing_type.is_none() { - trailing_type = Some(WhitespaceType::NoNewline); - } - } - } - - trailing_type -} - -fn clean_jsx_text( - text: &str, - text_start: TextSize, -) -> ( - Option, - Cow, - TextSize, - Option, -) { - if text.is_empty() { - return (None, Cow::Borrowed(text), text_start, None); - } - - let text_cleaner = TextCleaner::build(text); - - let cleaned_text = if let Some(normalized_text) = text_cleaner.clean_text() { - Cow::Owned(normalized_text) - } else { - Cow::Borrowed(text_cleaner.text.trim_matches(&JSX_WHITESPACE_CHARS[..])) - }; - - let start_idx: TextSize = text_cleaner - .start_idx - .try_into() - .expect("index is larger than 2^32 bits"); - - ( - text_cleaner.leading_whitespace_type, - cleaned_text, - text_start + start_idx, - text_cleaner.trailing_whitespace_type, - ) -} - -#[cfg(test)] -mod tests { - use crate::jsx::auxiliary::text::{clean_jsx_text, WhitespaceType}; - use std::borrow::Cow; - - #[test] - fn clean_jsx_text_works() { - assert_eq!( - (None, Cow::Borrowed(""), 0.into(), None), - clean_jsx_text("", 0.into()) - ); - assert_eq!( - ( - Some(WhitespaceType::NoNewline), - Cow::Borrowed(""), - 1.into(), - None - ), - clean_jsx_text(" ", 0.into()) - ); - assert_eq!( - (None, Cow::Borrowed("Foo"), 0.into(), None), - clean_jsx_text("Foo", 0.into()) - ); - assert_eq!( - ( - Some(WhitespaceType::NoNewline), - Cow::Borrowed("Foo"), - 1.into(), - None - ), - clean_jsx_text(" Foo", 0.into()) - ); - assert_eq!( - ( - Some(WhitespaceType::HasNewline), - Cow::Borrowed("Foo"), - 1.into(), - None - ), - clean_jsx_text("\nFoo", 0.into()) - ); - assert_eq!( - ( - Some(WhitespaceType::NoNewline), - Cow::Borrowed("Foo"), - 1.into(), - None - ), - clean_jsx_text("\tFoo", 0.into()) - ); - assert_eq!( - ( - Some(WhitespaceType::HasNewline), - Cow::Borrowed("Foo"), - 4.into(), - None - ), - clean_jsx_text("\n \t Foo", 0.into()) - ); - assert_eq!( - ( - Some(WhitespaceType::HasNewline), - Cow::Borrowed("Foo"), - 8.into(), - None - ), - clean_jsx_text("\n \t \n \t\nFoo", 0.into()) - ); - assert_eq!( - ( - Some(WhitespaceType::NoNewline), - Cow::Borrowed("Foo bar lorem"), - 1.into(), - None - ), - clean_jsx_text(" Foo bar lorem", 0.into()) - ); - assert_eq!( - ( - None, - Cow::Borrowed("Foo"), - 0.into(), - Some(WhitespaceType::NoNewline) - ), - clean_jsx_text("Foo ", 0.into()) - ); - assert_eq!( - ( - None, - Cow::Borrowed("Foo"), - 0.into(), - Some(WhitespaceType::HasNewline) - ), - clean_jsx_text("Foo\n", 0.into()) - ); - assert_eq!( - ( - None, - Cow::Borrowed("Foo"), - 0.into(), - Some(WhitespaceType::NoNewline) - ), - clean_jsx_text("Foo\t", 0.into()) - ); - assert_eq!( - ( - None, - Cow::Borrowed("Foo"), - 0.into(), - Some(WhitespaceType::HasNewline) - ), - clean_jsx_text("Foo\t \n ", 0.into()) - ); - assert_eq!( - ( - None, - Cow::Borrowed("Foo"), - 0.into(), - Some(WhitespaceType::HasNewline) - ), - clean_jsx_text("Foo\n \t \n \t\n", 0.into()) - ); - assert_eq!( - (None, Cow::Owned("Foo Bar".to_string()), 0.into(), None), - clean_jsx_text("Foo\n \t\t\n \tBar", 0.into()) - ); - assert_eq!( - ( - Some(WhitespaceType::HasNewline), - Cow::Owned("Foo Bar".to_string()), - 7.into(), - Some(WhitespaceType::HasNewline) - ), - clean_jsx_text("\n \t\t\n \tFoo\n \t\t\n \tBar\n \t\t\n \t", 0.into()) - ); + // Formatting a [JsxText] on its own isn't supported. Format as verbatim. A text should always be formatted + // through its [JsxChildList] + format_verbatim_node(node.syntax()).fmt(f) } } diff --git a/crates/rome_js_formatter/src/jsx/expressions/tag_expression.rs b/crates/rome_js_formatter/src/jsx/expressions/tag_expression.rs index cf97d0b02df..69952ab53fa 100644 --- a/crates/rome_js_formatter/src/jsx/expressions/tag_expression.rs +++ b/crates/rome_js_formatter/src/jsx/expressions/tag_expression.rs @@ -1,9 +1,10 @@ use crate::parentheses::{is_callee, is_tag, NeedsParentheses}; use crate::prelude::*; use crate::utils::jsx::{get_wrap_state, WrapState}; -use rome_formatter::{format_args, write}; +use rome_formatter::write; use rome_js_syntax::{ - JsBinaryExpression, JsBinaryOperator, JsSyntaxKind, JsSyntaxNode, JsxTagExpression, + JsArrowFunctionExpression, JsBinaryExpression, JsBinaryOperator, JsCallArgumentList, + JsCallExpression, JsSyntaxKind, JsSyntaxNode, JsxExpressionChild, JsxTagExpression, }; use rome_rowan::AstNode; @@ -12,16 +13,32 @@ pub struct FormatJsxTagExpression; impl FormatNodeRule for FormatJsxTagExpression { fn fmt_fields(&self, node: &JsxTagExpression, f: &mut JsFormatter) -> FormatResult<()> { - match get_wrap_state(node) { - WrapState::WrapOnBreak => write![ - f, - [group(&format_args![ - if_group_breaks(&text("(")), - soft_block_indent(&format_args![node.tag().format()]), - if_group_breaks(&text(")")) - ])] - ], - WrapState::NoWrap => write![f, [node.tag().format()]], + let wrap = get_wrap_state(node); + + match wrap { + WrapState::NoWrap => { + write![f, [node.tag().format()]] + } + WrapState::WrapOnBreak => { + let should_expand = should_expand(node); + let needs_parentheses = node.needs_parentheses(); + + let format_inner = format_with(|f| { + if !needs_parentheses { + write!(f, [if_group_breaks(&text("("))])?; + } + + write!(f, [soft_block_indent(&node.tag().format())])?; + + if !needs_parentheses { + write!(f, [if_group_breaks(&text(")"))])?; + } + + Ok(()) + }); + + write!(f, [group(&format_inner).should_expand(should_expand)]) + } } } @@ -30,6 +47,43 @@ impl FormatNodeRule for FormatJsxTagExpression { } } +/// This is a very special situation where we're returning a JsxElement +/// from an arrow function that's passed as an argument to a function, +/// which is itself inside a JSX expression child. +/// +/// If you're wondering why this is the only other case, it's because +/// Prettier defines it to be that way. +/// +/// ```jsx +/// let bar =
+/// {foo(() =>
the quick brown fox jumps over the lazy dog
)} +///
; +/// ``` +pub fn should_expand(expression: &JsxTagExpression) -> bool { + let arrow = match expression.syntax().parent() { + Some(parent) if JsArrowFunctionExpression::can_cast(parent.kind()) => parent, + _ => return false, + }; + + let call = match arrow.parent() { + // Argument + Some(grand_parent) if JsCallArgumentList::can_cast(grand_parent.kind()) => { + let maybe_call_expression = grand_parent.grand_parent(); + + match maybe_call_expression { + Some(call) if JsCallExpression::can_cast(call.kind()) => call, + _ => return false, + } + } + // Callee + Some(grand_parent) if JsCallExpression::can_cast(grand_parent.kind()) => grand_parent, + _ => return false, + }; + + call.parent() + .map_or(false, |parent| JsxExpressionChild::can_cast(parent.kind())) +} + impl NeedsParentheses for JsxTagExpression { fn needs_parentheses_with_parent(&self, parent: &JsSyntaxNode) -> bool { match parent.kind() { diff --git a/crates/rome_js_formatter/src/jsx/lists/attribute_list.rs b/crates/rome_js_formatter/src/jsx/lists/attribute_list.rs index dffced40fc6..52694a75607 100644 --- a/crates/rome_js_formatter/src/jsx/lists/attribute_list.rs +++ b/crates/rome_js_formatter/src/jsx/lists/attribute_list.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use rome_formatter::write; + use rome_js_syntax::JsxAttributeList; #[derive(Debug, Clone, Default)] @@ -9,12 +9,8 @@ impl FormatRule for FormatJsxAttributeList { type Context = JsFormatContext; fn fmt(&self, node: &JsxAttributeList, f: &mut JsFormatter) -> FormatResult<()> { - let attributes = format_with(|f| { - f.join_with(&soft_line_break_or_space()) - .entries(node.iter().formatted()) - .finish() - }); - - write!(f, [group(&soft_block_indent(&attributes))]) + f.join_with(&soft_line_break_or_space()) + .entries(node.iter().formatted()) + .finish() } } diff --git a/crates/rome_js_formatter/src/jsx/lists/child_list.rs b/crates/rome_js_formatter/src/jsx/lists/child_list.rs index 7a2377337ca..ad9d75ad099 100644 --- a/crates/rome_js_formatter/src/jsx/lists/child_list.rs +++ b/crates/rome_js_formatter/src/jsx/lists/child_list.rs @@ -1,25 +1,567 @@ use crate::prelude::*; -use crate::utils::jsx::contains_meaningful_jsx_text; +use crate::utils::jsx::{ + is_meaningful_jsx_text, is_whitespace_jsx_expression, jsx_split_children, JsxChild, + JsxRawSpace, JsxSpace, +}; use crate::JsFormatter; -use rome_js_syntax::JsxChildList; +use rome_formatter::{format_args, write, FormatRuleWithOptions, VecBuffer}; +use rome_js_syntax::{JsxAnyChild, JsxChildList}; #[derive(Debug, Clone, Default)] -pub struct FormatJsxChildList; +pub struct FormatJsxChildList { + layout: JsxChildListLayout, +} + +impl FormatRuleWithOptions for FormatJsxChildList { + type Options = JsxChildListLayout; + + fn with_options(mut self, options: Self::Options) -> Self { + self.layout = options; + self + } +} impl FormatRule for FormatJsxChildList { type Context = JsFormatContext; - fn fmt(&self, node: &JsxChildList, formatter: &mut JsFormatter) -> FormatResult<()> { - if contains_meaningful_jsx_text(node) { - formatter - .fill() - .flatten_entries(&soft_line_break(), node.iter().formatted()) - .finish() + fn fmt(&self, list: &JsxChildList, f: &mut JsFormatter) -> FormatResult<()> { + self.disarm_debug_assertions(list, f); + + let children_meta = self.children_meta(list); + let layout = self.layout(children_meta); + + let multiline_layout = if children_meta.meaningful_text { + MultilineLayout::Fill } else { - formatter - .join_with(soft_line_break()) - .entries(node.iter().formatted()) - .finish() + MultilineLayout::NoFill + }; + + let mut flat = FlatBuilder::new(); + let mut multiline = MultilineBuilder::new(multiline_layout); + + let mut last: Option = None; + let mut force_multiline = layout.is_multiline(); + + let mut children = jsx_split_children(list)?; + + // Trim trailing new lines + if let Some(JsxChild::EmptyLine | JsxChild::Newline) = children.last() { + children.pop(); + } + + let mut children_iter = children.into_iter().peekable(); + + // Trim leading new lines + if let Some(JsxChild::Newline | JsxChild::EmptyLine) = children_iter.peek() { + children_iter.next(); + } + + while let Some(child) = children_iter.next() { + let mut child_breaks = false; + + match &child { + // A single word: Both `a` and `b` are a word in `a b` because they're separated by JSX Whitespace. + JsxChild::Word(word) => { + let separator = match children_iter.peek() { + Some(JsxChild::Word(_)) => { + // Separate words by a space or line break in extended mode + Some(WordSeparator::BetweenWords) + } + + // Last word or last word before an element without any whitespace in between + Some(JsxChild::NonText(child)) => Some(WordSeparator::EndOfText { + is_next_self_closing: matches!( + child, + JsxAnyChild::JsxSelfClosingElement(_) + ), + }), + + _ => None, + }; + + child_breaks = separator.map_or(false, |separator| separator.will_break()); + + flat.write(&format_args![word, separator], f); + + if let Some(separator) = separator { + multiline.write(word, &separator, f); + } else { + multiline.write_with_empty_separator(word, f); + } + } + + // * Whitespace after the opening tag and before a meaningful text: `
a` + // * Whitespace before the closing tag: `a
` + // * Whitespace before an opening tag: `a
` + JsxChild::Whitespace => { + flat.write(&JsxSpace, f); + + // ```javascript + //
a + // {' '}
+ // ``` + let is_after_line_break = + last.as_ref().map_or(false, |last| last.is_any_line()); + + // `
aaa
` or `
` + let is_trailing_or_only_whitespace = children_iter.peek().is_none(); + + if is_trailing_or_only_whitespace || is_after_line_break { + multiline.write_with_empty_separator(&JsxRawSpace, f); + } + // Leading whitespace. Only possible if used together with a expression child + // + // ``` + //
+ // + // {' '} + // + //
+ // ``` + else if last.is_none() { + multiline.write(&JsxRawSpace, &hard_line_break(), f); + } else { + multiline.write_with_empty_separator(&JsxSpace, f); + } + } + + // A new line between some JSX text and an element + JsxChild::Newline => { + child_breaks = true; + + multiline.write_with_empty_separator(&hard_line_break(), f); + } + + // An empty line between some JSX text and an element + JsxChild::EmptyLine => { + child_breaks = true; + + multiline.write_with_empty_separator(&empty_line(), f); + } + + // Any child that isn't text + JsxChild::NonText(non_text) => { + let line_mode = match children_iter.peek() { + Some(JsxChild::Newline | JsxChild::Word(_) | JsxChild::Whitespace) => { + // Break if the current or next element is a self closing element + // ```javascript + //
adefg
+                            // ```
+                            // Becomes
+                            // ```javascript
+                            // 
+                            // adefg
+                            // ```
+                            if matches!(non_text, JsxAnyChild::JsxSelfClosingElement(_)) {
+                                Some(LineMode::Hard)
+                            } else {
+                                Some(LineMode::Soft)
+                            }
+                        }
+
+                        // Add a hard line break if what comes after the element is not a text or is all whitespace
+                        Some(_) => Some(LineMode::Hard),
+
+                        // Don't insert trailing line breaks
+                        None => None,
+                    };
+
+                    child_breaks = line_mode.map_or(false, |mode| mode.is_hard());
+
+                    let format_separator = format_with(|f| match line_mode {
+                        Some(mode) => f.write_element(FormatElement::Line(mode)),
+                        None => Ok(()),
+                    });
+
+                    if force_multiline {
+                        multiline.write(&non_text.format(), &format_separator, f);
+                    } else {
+                        let mut memoized = non_text.format().memoized();
+
+                        force_multiline = memoized.inspect(f)?.will_break();
+
+                        flat.write(&format_args![memoized, format_separator], f);
+                        multiline.write(&memoized, &format_separator, f);
+                    }
+                }
+            }
+
+            if child_breaks {
+                flat.disable();
+                force_multiline = true;
+            }
+
+            last = Some(child);
+        }
+
+        let format_multiline = format_once(|f| write!(f, [block_indent(&multiline.finish())]));
+        let format_flat_children = flat.finish();
+
+        if force_multiline {
+            write!(f, [format_multiline])
+        } else {
+            write!(f, [best_fitting![format_flat_children, format_multiline]])
+        }
+    }
+}
+
+impl FormatJsxChildList {
+    /// Tracks the tokens of [JsxText] and [JsxExpressionChild] nodes to be formatted and
+    /// asserts that the suppression comments are checked (they get ignored).
+    ///
+    /// This is necessary because the formatting of [JsxChildList] bypasses the node formatting for
+    /// [JsxText] and [JsxExpressionChild] and instead, formats the nodes itself.
+    #[cfg(debug_assertions)]
+    fn disarm_debug_assertions(&self, node: &JsxChildList, f: &mut JsFormatter) {
+        use rome_formatter::CstFormatContext;
+        use rome_js_syntax::{JsAnyExpression, JsAnyLiteralExpression};
+        use JsxAnyChild::*;
+
+        for child in node {
+            match child {
+                JsxExpressionChild(expression) if is_whitespace_jsx_expression(&expression) => {
+                    f.context()
+                        .comments()
+                        .mark_suppression_checked(expression.syntax());
+
+                    match expression.expression().unwrap() {
+                        JsAnyExpression::JsAnyLiteralExpression(
+                            JsAnyLiteralExpression::JsStringLiteralExpression(string_literal),
+                        ) => {
+                            f.context()
+                                .comments()
+                                .mark_suppression_checked(string_literal.syntax());
+
+                            f.state_mut()
+                                .track_token(&string_literal.value_token().unwrap());
+
+                            f.state_mut()
+                                .track_token(&expression.l_curly_token().unwrap());
+                            f.state_mut()
+                                .track_token(&expression.r_curly_token().unwrap());
+                        }
+                        _ => unreachable!(),
+                    }
+                }
+                JsxText(text) => {
+                    f.state_mut().track_token(&text.value_token().unwrap());
+
+                    // You can't suppress a text node
+                    f.context()
+                        .comments()
+                        .mark_suppression_checked(text.syntax());
+                }
+                _ => {
+                    continue;
+                }
+            }
+        }
+    }
+
+    #[cfg(not(debug_assertions))]
+    fn disarm_debug_assertions(&self, _: &JsxChildList, _: &mut JsFormatter) {}
+
+    fn layout(&self, meta: ChildrenMeta) -> JsxChildListLayout {
+        match self.layout {
+            JsxChildListLayout::BestFitting => {
+                if meta.any_tag || meta.multiple_expressions {
+                    JsxChildListLayout::Multiline
+                } else {
+                    JsxChildListLayout::BestFitting
+                }
+            }
+            JsxChildListLayout::Multiline => JsxChildListLayout::Multiline,
+        }
+    }
+
+    /// Computes additional meta data about the children by iterating once over all children.
+    fn children_meta(&self, list: &JsxChildList) -> ChildrenMeta {
+        let mut has_expression = false;
+
+        let mut meta = ChildrenMeta::default();
+
+        for child in list {
+            use JsxAnyChild::*;
+
+            match child {
+                JsxElement(_) | JsxFragment(_) | JsxSelfClosingElement(_) => meta.any_tag = true,
+                JsxExpressionChild(expression) if !is_whitespace_jsx_expression(&expression) => {
+                    meta.multiple_expressions = has_expression;
+                    has_expression = true;
+                }
+                JsxText(text) => {
+                    meta.meaningful_text = meta.meaningful_text
+                        || text
+                            .value_token()
+                            .map_or(false, |token| is_meaningful_jsx_text(token.text()));
+                }
+                _ => {}
+            }
         }
+
+        meta
+    }
+}
+
+#[derive(Debug, Default, Copy, Clone)]
+pub enum JsxChildListLayout {
+    /// Prefers to format the children on a single line if possible.
+    #[default]
+    BestFitting,
+
+    /// Forces the children to be formatted over multiple lines
+    Multiline,
+}
+
+impl JsxChildListLayout {
+    const fn is_multiline(&self) -> bool {
+        matches!(self, JsxChildListLayout::Multiline)
+    }
+}
+
+#[derive(Copy, Clone, Debug, Default)]
+struct ChildrenMeta {
+    /// `true` if children contains a [JsxElement] or [JsxFragment]
+    any_tag: bool,
+
+    /// `true` if children contains more than one [JsxExpressionChild]
+    multiple_expressions: bool,
+
+    /// `true` if any child contains meaningful a [JsxText] with meaningful text.
+    meaningful_text: bool,
+}
+
+#[derive(Copy, Clone, Debug)]
+enum WordSeparator {
+    /// Separator between two words. Creates a soft line break or space.
+    ///
+    /// `a b`
+    BetweenWords,
+
+    /// A separator of a word at the end of a [JsxText] element. Either because it is the last
+    /// child in its parent OR it is right before the start of another child (element, expression, ...).
+    ///
+    /// ```javascript
+    /// 
a
; // last element of parent + ///
a
// last element before another element + ///
a{expression}
// last element before expression + /// ``` + /// + /// Creates a soft line break EXCEPT if the next element is a self closing element, which results in a hard line break: + /// + /// ```javascript + /// a =
ab
; + /// + /// // becomes + /// + /// a = ( + ///
+ /// ab + ///
+ ///
+ /// ); + /// ``` + EndOfText { + /// `true` if the next element is a [JsxSelfClosingElement] + is_next_self_closing: bool, + }, +} + +impl WordSeparator { + /// Returns if formatting this separator will result in a child that expands + fn will_break(&self) -> bool { + matches!( + self, + WordSeparator::EndOfText { + is_next_self_closing: true + } + ) + } +} + +impl Format for WordSeparator { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + match self { + WordSeparator::BetweenWords => soft_line_break_or_space().fmt(f), + WordSeparator::EndOfText { + is_next_self_closing: self_closing, + } => { + // ```javascript + //
ab
+ // ``` + // Becomes + // + // ```javascript + //
+ // ab + //
+ //
+ // ``` + if *self_closing { + hard_line_break().fmt(f) + } + // Try to fit everything else on a single line + else { + soft_line_break().fmt(f) + } + } + } + } +} + +#[derive(Copy, Clone, Debug, Default)] +enum MultilineLayout { + Fill, + #[default] + NoFill, +} + +/// Builder that helps to create the output for the multiline layout. +/// +/// The multiline layout may use [FormatElement::Fill] element that requires that its children +/// are an alternating sequence of `[element, separator, element, separator, ...]`. +/// +/// This requires that each element is wrapped inside of a list if it emits more than one element to uphold +/// the constraints of [FormatElement::Fill]. +/// +/// However, the wrapping is only necessary for [MultilineLayout::Fill] for when the [FormatElement::Fill] element is used. +/// +/// This builder takes care of doing the least amount of work necessary for the chosen layout while also guaranteeing +/// that the written element is valid +#[derive(Debug, Clone)] +struct MultilineBuilder { + layout: MultilineLayout, + result: FormatResult>, +} + +impl MultilineBuilder { + fn new(layout: MultilineLayout) -> Self { + Self { + layout, + result: Ok(Vec::new()), + } + } + + /// Formats an element that does not require a separator + fn write_with_empty_separator( + &mut self, + content: &dyn Format, + f: &mut JsFormatter, + ) { + let result = std::mem::replace(&mut self.result, Ok(Vec::new())); + + self.result = result.and_then(|mut elements| { + let elements = match self.layout { + MultilineLayout::Fill => { + // Make sure that the separator and content only ever write a single element + let mut buffer = VecBuffer::new(f.state_mut()); + write!(buffer, [content])?; + + elements.push(buffer.into_element()); + + // Fill requires a sequence of [element, separator, element, separator] + // Push an empty list as separator + elements.push(FormatElement::List(List::default())); + elements + } + MultilineLayout::NoFill => { + let mut buffer = VecBuffer::new_with_vec(f.state_mut(), elements); + write!(buffer, [content])?; + + buffer.into_vec() + } + }; + + Ok(elements) + }) + } + + fn write( + &mut self, + content: &dyn Format, + separator: &dyn Format, + f: &mut JsFormatter, + ) { + let result = std::mem::replace(&mut self.result, Ok(Vec::new())); + + self.result = result.and_then(|mut elements| { + let elements = match self.layout { + MultilineLayout::Fill => { + // Make sure that the separator and content only ever write a single element + let mut buffer = VecBuffer::new(f.state_mut()); + write!(buffer, [content])?; + + elements.push(buffer.into_element()); + + let mut buffer = VecBuffer::new_with_vec(f.state_mut(), elements); + write!(buffer, [separator])?; + buffer.into_vec() + } + MultilineLayout::NoFill => { + let mut buffer = VecBuffer::new_with_vec(f.state_mut(), elements); + write!(buffer, [content, separator])?; + + buffer.into_vec() + } + }; + Ok(elements) + }) + } + + fn finish(self) -> impl Format { + format_once(move |f| { + let elements = self.result?; + + match self.layout { + MultilineLayout::Fill => { + f.write_element(FormatElement::Fill(elements.into_boxed_slice())) + } + MultilineLayout::NoFill => f.write_elements(elements), + } + }) + } +} + +#[derive(Debug)] +struct FlatBuilder { + result: FormatResult>, + disabled: bool, +} + +impl FlatBuilder { + fn new() -> Self { + Self { + result: Ok(Vec::new()), + disabled: false, + } + } + + fn write(&mut self, content: &dyn Format, f: &mut JsFormatter) { + if self.disabled { + return; + } + + let result = std::mem::replace(&mut self.result, Ok(Vec::new())); + + self.result = result.and_then(|elements| { + let mut buffer = VecBuffer::new_with_vec(f.state_mut(), elements); + + write!(buffer, [content])?; + + Ok(buffer.into_vec()) + }) + } + + fn disable(&mut self) { + self.disabled = true; + } + + fn finish(self) -> impl Format { + format_once(move |f| { + assert!(!self.disabled, "The flat builder has been disabled and thus, does no longer store any elements. Make sure you don't call disable if you later intend to format the flat content."); + + let elements = self.result?; + f.write_elements(elements) + }) } } diff --git a/crates/rome_js_formatter/src/jsx/tag/element.rs b/crates/rome_js_formatter/src/jsx/tag/element.rs index 2a65aef7cee..e9d552b5fa9 100644 --- a/crates/rome_js_formatter/src/jsx/tag/element.rs +++ b/crates/rome_js_formatter/src/jsx/tag/element.rs @@ -1,32 +1,177 @@ use crate::prelude::*; -use crate::soft_block_indent; -use crate::utils::jsx::is_jsx_inside_arrow_function_inside_call_inside_expression_child; -use rome_formatter::{format_args, write, FormatResult}; -use rome_js_syntax::{JsxElement, JsxElementFields}; + +use crate::jsx::lists::child_list::JsxChildListLayout; +use crate::utils::jsx::is_meaningful_jsx_text; +use rome_formatter::{write, CstFormatContext, FormatResult}; +use rome_js_syntax::{ + JsAnyExpression, JsxAnyChild, JsxChildList, JsxElement, JsxExpressionChild, JsxFragment, +}; +use rome_rowan::{declare_node_union, SyntaxResult}; #[derive(Debug, Clone, Default)] pub struct FormatJsxElement; impl FormatNodeRule for FormatJsxElement { - fn fmt_fields(&self, node: &JsxElement, formatter: &mut JsFormatter) -> FormatResult<()> { - let JsxElementFields { - opening_element, - children, - closing_element, - } = node.as_fields(); - - let expand_if_special_case = - is_jsx_inside_arrow_function_inside_call_inside_expression_child(node.syntax()) - .then(expand_parent); - - write![ - formatter, - [group(&format_args![ - opening_element.format(), - expand_if_special_case, - soft_block_indent(&children.format()), - closing_element.format() - ])] - ] + fn fmt_fields(&self, node: &JsxElement, f: &mut JsFormatter) -> FormatResult<()> { + JsxAnyTagWithChildren::from(node.clone()).fmt(f) + } +} + +declare_node_union! { + pub(super) JsxAnyTagWithChildren = JsxElement | JsxFragment +} + +impl Format for JsxAnyTagWithChildren { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let format_opening = format_with(|f| self.fmt_opening(f)); + let format_closing = format_with(|f| self.fmt_closing(f)); + + let layout = self.layout(f)?; + + match layout { + ElementLayout::NoChildren => { + write!(f, [format_opening, format_closing]) + } + + ElementLayout::Template(expression) => { + write!(f, [format_opening, expression.format(), format_closing]) + } + + ElementLayout::Default => { + let mut format_opening = format_opening.memoized(); + + let opening_breaks = format_opening.inspect(f)?.will_break(); + + let multiple_attributes = match self { + JsxAnyTagWithChildren::JsxElement(element) => { + element.opening_element()?.attributes().len() > 1 + } + JsxAnyTagWithChildren::JsxFragment(_) => false, + }; + + let list_layout = if multiple_attributes || opening_breaks { + JsxChildListLayout::Multiline + } else { + JsxChildListLayout::BestFitting + }; + + write!( + f, + [ + format_opening, + self.children().format().with_options(list_layout), + format_closing + ] + ) + } + } + } +} + +impl JsxAnyTagWithChildren { + fn fmt_opening(&self, f: &mut JsFormatter) -> FormatResult<()> { + match self { + JsxAnyTagWithChildren::JsxElement(element) => { + write!(f, [element.opening_element().format()]) + } + JsxAnyTagWithChildren::JsxFragment(fragment) => { + write!(f, [fragment.opening_fragment().format()]) + } + } + } + + fn fmt_closing(&self, f: &mut JsFormatter) -> FormatResult<()> { + match self { + JsxAnyTagWithChildren::JsxElement(element) => { + write!(f, [element.closing_element().format()]) + } + JsxAnyTagWithChildren::JsxFragment(fragment) => { + write!(f, [fragment.closing_fragment().format()]) + } + } + } + + fn children(&self) -> JsxChildList { + match self { + JsxAnyTagWithChildren::JsxElement(element) => element.children(), + JsxAnyTagWithChildren::JsxFragment(fragment) => fragment.children(), + } + } + + fn layout(&self, f: &mut JsFormatter) -> SyntaxResult { + use JsAnyExpression::*; + use JsxAnyChild::*; + + let children = self.children(); + + let layout = match children.len() { + 0 => ElementLayout::NoChildren, + 1 => { + // SAFETY: Safe because of length check above + let child = children.first().unwrap(); + + match child { + JsxText(text) => { + let value_token = text.value_token()?; + if !is_meaningful_jsx_text(value_token.text()) { + // Text nodes can't have suppressions + f.context_mut() + .comments() + .mark_suppression_checked(text.syntax()); + // It's safe to ignore the tokens here because JSX text tokens can't have comments (nor whitespace) attached. + f.state_mut().track_token(&value_token); + + ElementLayout::NoChildren + } else { + ElementLayout::Default + } + } + JsxExpressionChild(expression) => match expression.expression() { + Some(JsTemplate(_)) => ElementLayout::Template(expression), + _ => ElementLayout::Default, + }, + _ => ElementLayout::Default, + } + } + _ => ElementLayout::Default, + }; + + Ok(layout) } } + +#[derive(Debug, Clone)] +enum ElementLayout { + /// Empty Tag with no children or contains no meaningful text. + NoChildren, + + /// Prefer breaking the template if it is the only child of the element + /// ```javascript + ///
{`A Long Tempalte String That uses ${ + /// 5 + 4 + /// } that will eventually break across multiple lines ${(40 / 3) * 45}`}
; + /// ``` + /// + /// instead of + /// + /// ```javascript + ///
+ /// {`A Long Template String That uses ${ + /// 5 + 4 + /// } that will eventually break across multiple lines ${(40 / 3) * 45}`} + ///
; + /// ``` + Template(JsxExpressionChild), + + /// Default layout used for all elements that have children and [ElementLayout::Template] does not apply. + /// + /// ```javascript + /// + /// Some more content + /// + /// + /// + /// ; + /// ``` + Default, +} diff --git a/crates/rome_js_formatter/src/jsx/tag/fragment.rs b/crates/rome_js_formatter/src/jsx/tag/fragment.rs index 401243178bc..b8a4a7335db 100644 --- a/crates/rome_js_formatter/src/jsx/tag/fragment.rs +++ b/crates/rome_js_formatter/src/jsx/tag/fragment.rs @@ -1,26 +1,14 @@ use crate::prelude::*; +use crate::jsx::tag::element::JsxAnyTagWithChildren; use rome_formatter::write; -use rome_js_syntax::{JsxFragment, JsxFragmentFields}; +use rome_js_syntax::JsxFragment; #[derive(Debug, Clone, Default)] pub struct FormatJsxFragment; impl FormatNodeRule for FormatJsxFragment { fn fmt_fields(&self, node: &JsxFragment, f: &mut JsFormatter) -> FormatResult<()> { - let JsxFragmentFields { - opening_fragment, - children, - closing_fragment, - } = node.as_fields(); - - write![ - f, - [ - opening_fragment.format(), - children.format(), - closing_fragment.format() - ] - ] + write!(f, [JsxAnyTagWithChildren::from(node.clone())]) } } diff --git a/crates/rome_js_formatter/src/jsx/tag/opening_element.rs b/crates/rome_js_formatter/src/jsx/tag/opening_element.rs index 03383c7f0fa..27358b9a9c6 100644 --- a/crates/rome_js_formatter/src/jsx/tag/opening_element.rs +++ b/crates/rome_js_formatter/src/jsx/tag/opening_element.rs @@ -1,34 +1,248 @@ use crate::prelude::*; use rome_formatter::write; -use rome_js_syntax::{JsxOpeningElement, JsxOpeningElementFields}; +use rome_js_syntax::{ + JsSyntaxToken, JsxAnyAttribute, JsxAnyAttributeValue, JsxAnyElementName, JsxAttributeList, + JsxOpeningElement, JsxSelfClosingElement, JsxString, TsTypeArguments, +}; +use rome_rowan::{declare_node_union, SyntaxResult}; #[derive(Debug, Clone, Default)] pub struct FormatJsxOpeningElement; impl FormatNodeRule for FormatJsxOpeningElement { fn fmt_fields(&self, node: &JsxOpeningElement, f: &mut JsFormatter) -> FormatResult<()> { - let JsxOpeningElementFields { - l_angle_token, - name, - type_arguments, - attributes, - r_angle_token, - } = node.as_fields(); - write!( - f, - [ - l_angle_token.format(), - name.format(), - type_arguments.format(), - line_suffix_boundary(), - ] - )?; - - if !attributes.is_empty() { - write!(f, [space(), attributes.format(), line_suffix_boundary()])?; + JsxAnyOpeningElement::from(node.clone()).fmt(f) + } +} + +declare_node_union! { + pub(super) JsxAnyOpeningElement = JsxSelfClosingElement | JsxOpeningElement +} + +impl Format for JsxAnyOpeningElement { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let layout = self.compute_layout()?; + + let l_angle_token = self.l_angle_token()?; + let name = self.name()?; + let type_arguments = self.type_arguments(); + let attributes = self.attributes(); + + let format_close = format_with(|f| { + if let JsxAnyOpeningElement::JsxSelfClosingElement(element) = self { + write!(f, [element.slash_token().format()])?; + } + + write!(f, [self.r_angle_token().format()]) + }); + + match layout { + OpeningElementLayout::Inline => { + write!( + f, + [ + l_angle_token.format(), + name.format(), + type_arguments.format(), + line_suffix_boundary(), + space(), + format_close + ] + ) + } + OpeningElementLayout::SingleStringAttribute => { + let attribute_spacing = if self.is_self_closing() { + Some(space()) + } else { + None + }; + write!( + f, + [ + l_angle_token.format(), + name.format(), + type_arguments.format(), + line_suffix_boundary(), + space(), + attributes.format(), + line_suffix_boundary(), + attribute_spacing, + format_close + ] + ) + } + OpeningElementLayout::IndentAttributes { name_has_comments } => { + let format_inner = format_with(|f| { + write!( + f, + [ + l_angle_token.format(), + name.format(), + type_arguments.format(), + line_suffix_boundary(), + soft_line_indent_or_space(&attributes.format()), + line_suffix_boundary(), + ] + )?; + + let bracket_same_line = attributes.is_empty() && !name_has_comments; + + if self.is_self_closing() { + write!(f, [soft_line_break_or_space(), format_close]) + } else if bracket_same_line { + write!(f, [format_close]) + } else { + write!(f, [soft_line_break(), format_close]) + } + }); + + let has_multiline_string_attribute = attributes + .iter() + .any(|attribute| is_multiline_string_literal_attribute(&attribute)); + + write![ + f, + [group(&format_inner).should_expand(has_multiline_string_attribute)] + ] + } } + } +} + +impl JsxAnyOpeningElement { + fn l_angle_token(&self) -> SyntaxResult { + match self { + JsxAnyOpeningElement::JsxSelfClosingElement(element) => element.l_angle_token(), + JsxAnyOpeningElement::JsxOpeningElement(element) => element.l_angle_token(), + } + } + + fn name(&self) -> SyntaxResult { + match self { + JsxAnyOpeningElement::JsxSelfClosingElement(element) => element.name(), + JsxAnyOpeningElement::JsxOpeningElement(element) => element.name(), + } + } + + fn type_arguments(&self) -> Option { + match self { + JsxAnyOpeningElement::JsxSelfClosingElement(element) => element.type_arguments(), + JsxAnyOpeningElement::JsxOpeningElement(element) => element.type_arguments(), + } + } - write!(f, [r_angle_token.format()]) + fn attributes(&self) -> JsxAttributeList { + match self { + JsxAnyOpeningElement::JsxSelfClosingElement(element) => element.attributes(), + JsxAnyOpeningElement::JsxOpeningElement(element) => element.attributes(), + } + } + + fn r_angle_token(&self) -> SyntaxResult { + match self { + JsxAnyOpeningElement::JsxSelfClosingElement(element) => element.r_angle_token(), + JsxAnyOpeningElement::JsxOpeningElement(element) => element.r_angle_token(), + } + } + + fn is_self_closing(&self) -> bool { + matches!(self, JsxAnyOpeningElement::JsxSelfClosingElement(_)) + } + + fn compute_layout(&self) -> SyntaxResult { + let attributes = self.attributes(); + let l_angle_token = self.l_angle_token()?; + let name = self.name()?; + + let name_has_comments = l_angle_token.has_trailing_comments() + || name.syntax().has_comments_direct() + || self + .type_arguments() + .map_or(false, |arguments| arguments.syntax().has_comments_direct()); + + let layout = if self.is_self_closing() && attributes.is_empty() && !name_has_comments { + OpeningElementLayout::Inline + } else if attributes.len() == 1 + && attributes.iter().all(|attribute| { + is_single_line_string_literal_attribute(&attribute) + && !attribute.syntax().has_comments_direct() + }) + && !name_has_comments + { + OpeningElementLayout::SingleStringAttribute + } else { + OpeningElementLayout::IndentAttributes { name_has_comments } + }; + + Ok(layout) + } +} + +#[derive(Copy, Clone, Debug)] +enum OpeningElementLayout { + /// Don't create a group around the element to avoid it breaking ever. + /// + /// Applied for elements that have no attributes nor any comment attached to their name. + /// + /// ```javascript + /// > + /// ``` + Inline, + + /// Opening element with a single attribute that contains no line breaks, nor has comments. + /// + /// ```javascript + ///
; + /// ``` + SingleStringAttribute, + + /// Default layout that indents the attributes and formats each attribute on its own line. + /// + /// ```javascript + ///
; + /// ``` + IndentAttributes { name_has_comments: bool }, +} + +/// Returns `true` if this is an attribute with a [JsxString] initializer that does not contain any new line characters. +fn is_single_line_string_literal_attribute(attribute: &JsxAnyAttribute) -> bool { + as_string_literal_attribute_value(attribute).map_or(false, |string| { + string + .value_token() + .map_or(false, |text| !text.text_trimmed().contains('\n')) + }) +} + +/// Returns `true` if this is an attribute with a [JsxString] initializer that contains at least one new line character. +fn is_multiline_string_literal_attribute(attribute: &JsxAnyAttribute) -> bool { + as_string_literal_attribute_value(attribute).map_or(false, |string| { + string + .value_token() + .map_or(false, |text| text.text_trimmed().contains('\n')) + }) +} + +/// Returns `Some` if the initializer value of this attribute is a [JsxString]. +/// Returns [None] otherwise. +fn as_string_literal_attribute_value(attribute: &JsxAnyAttribute) -> Option { + use JsxAnyAttribute::*; + use JsxAnyAttributeValue::*; + + match attribute { + JsxAttribute(attribute) => { + attribute + .initializer() + .and_then(|initializer| match initializer.value() { + Ok(JsxString(string)) => Some(string), + + _ => None, + }) + } + JsxSpreadAttribute(_) => None, } } diff --git a/crates/rome_js_formatter/src/jsx/tag/self_closing_element.rs b/crates/rome_js_formatter/src/jsx/tag/self_closing_element.rs index d41dd05aa17..22f0a599793 100644 --- a/crates/rome_js_formatter/src/jsx/tag/self_closing_element.rs +++ b/crates/rome_js_formatter/src/jsx/tag/self_closing_element.rs @@ -1,34 +1,14 @@ use crate::prelude::*; -use rome_formatter::write; -use rome_js_syntax::{JsxSelfClosingElement, JsxSelfClosingElementFields}; +use crate::jsx::tag::opening_element::JsxAnyOpeningElement; + +use rome_js_syntax::JsxSelfClosingElement; #[derive(Debug, Clone, Default)] pub struct FormatJsxSelfClosingElement; impl FormatNodeRule for FormatJsxSelfClosingElement { fn fmt_fields(&self, node: &JsxSelfClosingElement, f: &mut JsFormatter) -> FormatResult<()> { - let JsxSelfClosingElementFields { - l_angle_token, - name, - type_arguments, - attributes, - slash_token, - r_angle_token, - } = node.as_fields(); - - write![ - f, - [ - l_angle_token.format(), - name.format(), - type_arguments.format(), - space(), - attributes.format(), - space(), - slash_token.format(), - r_angle_token.format() - ] - ] + JsxAnyOpeningElement::from(node.clone()).fmt(f) } } diff --git a/crates/rome_js_formatter/src/utils/jsx.rs b/crates/rome_js_formatter/src/utils/jsx.rs index 86e439b9e7e..9c25c018cd0 100644 --- a/crates/rome_js_formatter/src/utils/jsx.rs +++ b/crates/rome_js_formatter/src/utils/jsx.rs @@ -1,26 +1,15 @@ use crate::context::QuoteStyle; - use crate::prelude::*; use rome_formatter::{format_args, write}; -use rome_js_syntax::{JsSyntaxKind, JsSyntaxNode, JsxAnyChild, JsxChildList, JsxTagExpression}; - -/// Checks if the children of an element contain meaningful text. See [is_meaningful_jsx_text] for -/// definition of meaningful JSX text. -pub fn contains_meaningful_jsx_text(children: &JsxChildList) -> bool { - children.iter().any(|child| { - if let JsxAnyChild::JsxText(jsx_text) = child { - if let Ok(token) = jsx_text.value_token() { - if is_meaningful_jsx_text(token.text()) { - return true; - } - } - } - - false - }) -} +use rome_js_syntax::{ + JsAnyExpression, JsAnyLiteralExpression, JsComputedMemberExpression, JsStaticMemberExpression, + JsSyntaxKind, JsxAnyChild, JsxExpressionChild, JsxTagExpression, TextLen, +}; +use rome_rowan::{SyntaxResult, SyntaxTokenText, TextRange, TextSize}; +use std::iter::{FusedIterator, Peekable}; +use std::str::Chars; -pub static JSX_WHITESPACE_CHARS: [char; 4] = [' ', '\n', '\t', '\r']; +pub(crate) static JSX_WHITESPACE_CHARS: [char; 4] = [' ', '\n', '\t', '\r']; /// Meaningful JSX text is defined to be text that has either non-whitespace /// characters, or does not contain a newline. Whitespace is defined as ASCII @@ -52,7 +41,8 @@ pub fn is_meaningful_jsx_text(text: &str) -> bool { /// Indicates that an element should always be wrapped in parentheses, should be wrapped /// only when it's line broken, or should not be wrapped at all. -pub enum WrapState { +#[derive(Copy, Clone, Debug)] +pub(crate) enum WrapState { /// For a JSX element that is never wrapped in parentheses. /// For instance, a JSX element that is another element's attribute /// should never be wrapped: @@ -74,63 +64,40 @@ pub enum WrapState { /// Checks if a JSX Element should be wrapped in parentheses. Returns a [WrapState] which /// indicates when the element should be wrapped in parentheses. -pub fn get_wrap_state(node: &JsxTagExpression) -> WrapState { +pub(crate) fn get_wrap_state(node: &JsxTagExpression) -> WrapState { // We skip the first item because the first item in ancestors is the node itself, i.e. // the JSX Element in this case. let parent = node.syntax().parent(); parent.map_or(WrapState::NoWrap, |parent| match parent.kind() { - JsSyntaxKind::JS_ARRAY_EXPRESSION + JsSyntaxKind::JS_ARRAY_ELEMENT_LIST | JsSyntaxKind::JSX_ATTRIBUTE - | JsSyntaxKind::JSX_ELEMENT + | JsSyntaxKind::JSX_EXPRESSION_ATTRIBUTE_VALUE | JsSyntaxKind::JSX_EXPRESSION_CHILD - | JsSyntaxKind::JSX_FRAGMENT | JsSyntaxKind::JS_EXPRESSION_STATEMENT - | JsSyntaxKind::JS_STATIC_MEMBER_EXPRESSION - | JsSyntaxKind::JS_COMPUTED_MEMBER_EXPRESSION - | JsSyntaxKind::JS_CALL_ARGUMENT_LIST => WrapState::NoWrap, - _ => WrapState::WrapOnBreak, - }) -} + | JsSyntaxKind::JS_CALL_ARGUMENT_LIST + | JsSyntaxKind::JS_EXPRESSION_SNIPPED + | JsSyntaxKind::JS_CONDITIONAL_EXPRESSION => WrapState::NoWrap, + JsSyntaxKind::JS_STATIC_MEMBER_EXPRESSION => { + let member = JsStaticMemberExpression::unwrap_cast(parent); -/// This is a very special situation where we're returning a JsxElement -/// from an arrow function that's passed as an argument to a function, -/// which is itself inside a JSX expression child. -/// -/// If you're wondering why this is the only other case, it's because -/// Prettier defines it to be that way. -/// -/// ```jsx -/// let bar =
-/// {foo(() =>
the quick brown fox jumps over the lazy dog
)} -///
; -/// ``` -pub fn is_jsx_inside_arrow_function_inside_call_inside_expression_child( - node: &JsSyntaxNode, -) -> bool { - // We skip the first item because the first item in ancestors is the node itself, i.e. - // the JSX Element in this case. - let mut ancestors = node.ancestors().skip(2).peekable(); - - let required_ancestors = [ - JsSyntaxKind::JS_ARROW_FUNCTION_EXPRESSION, - JsSyntaxKind::JS_CALL_ARGUMENT_LIST, - JsSyntaxKind::JS_CALL_ARGUMENTS, - JsSyntaxKind::JS_CALL_EXPRESSION, - JsSyntaxKind::JSX_EXPRESSION_CHILD, - ]; - - for required_ancestor in required_ancestors { - let is_required_ancestor = ancestors - .next() - .map(|ancestor| ancestor.kind() == required_ancestor) - .unwrap_or(false); - if !is_required_ancestor { - return false; + if member.is_optional_chain() { + WrapState::NoWrap + } else { + WrapState::WrapOnBreak + } } - } + JsSyntaxKind::JS_COMPUTED_MEMBER_EXPRESSION => { + let member = JsComputedMemberExpression::unwrap_cast(parent); - true + if member.is_optional_chain() { + WrapState::NoWrap + } else { + WrapState::WrapOnBreak + } + } + _ => WrapState::WrapOnBreak, + }) } /// Creates either a space using an expression child and a string literal, @@ -148,21 +115,484 @@ pub fn is_jsx_inside_arrow_function_inside_call_inside_expression_child( ///
/// ``` #[derive(Default)] -pub struct JsxSpace {} +pub(crate) struct JsxSpace; impl Format for JsxSpace { fn fmt(&self, formatter: &mut JsFormatter) -> FormatResult<()> { - let jsx_space = match formatter.options().quote_style() { - QuoteStyle::Double => "{\" \"}", - QuoteStyle::Single => "{\' \'}", - }; - write![ formatter, [ - if_group_breaks(&format_args![text(jsx_space), hard_line_break()]), + if_group_breaks(&format_args![JsxRawSpace, hard_line_break()]), if_group_fits_on_line(&space()) ] ] } } + +pub(crate) struct JsxRawSpace; + +impl Format for JsxRawSpace { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let jsx_space = match f.options().quote_style() { + QuoteStyle::Double => r#"{" "}"#, + QuoteStyle::Single => "{' '}", + }; + + write!(f, [text(jsx_space)]) + } +} + +pub(crate) fn is_whitespace_jsx_expression(child: &JsxExpressionChild) -> bool { + match child.expression() { + Some(JsAnyExpression::JsAnyLiteralExpression( + JsAnyLiteralExpression::JsStringLiteralExpression(literal), + )) => { + match ( + child.l_curly_token(), + literal.value_token(), + child.r_curly_token(), + ) { + (Ok(l_curly_token), Ok(value_token), Ok(r_curly_token)) => { + let is_empty = matches!(value_token.text_trimmed(), "\" \"" | "' '"); + + let has_comments = l_curly_token.has_trailing_comments() + || r_curly_token.has_leading_comments() + || value_token.has_leading_non_whitespace_trivia() + || value_token.has_trailing_comments(); + + is_empty && !has_comments + } + _ => false, + } + } + _ => false, + } +} + +pub(crate) fn jsx_split_children(children: I) -> SyntaxResult> +where + I: IntoIterator, +{ + let mut result = Vec::new(); + + for child in children.into_iter() { + match child { + JsxAnyChild::JsxText(text) => { + // Split the text into words + // Keep track if there's any leading/trailing empty line, new line or whitespace + + let value_token = text.value_token()?; + let mut chunks = JsxSplitChunksIterator::new(value_token.text()).peekable(); + + // Text starting with a whitespace + if let Some((_, JsxTextChunk::Whitespace(_whitespace))) = chunks.peek() { + match chunks.next() { + Some((_, JsxTextChunk::Whitespace(whitespace))) => { + if whitespace.contains('\n') { + if chunks.peek().is_none() { + // A text only consisting of whitespace that also contains a new line isn't considered meaningful text. + // It can be entirely removed from the content without changing the semantics. + let newlines = + whitespace.chars().filter(|c| *c == '\n').count(); + + // Keep up to one blank line between tags/expressions and text. + // ```javascript + //
+ // + // + //
+ // ``` + if newlines > 1 { + result.push(JsxChild::EmptyLine); + } + + continue; + } + + result.push(JsxChild::Newline) + } else if !matches!(result.last(), Some(JsxChild::Whitespace)) { + result.push(JsxChild::Whitespace) + } + } + _ => unreachable!(), + } + } + + while let Some(chunk) = chunks.next() { + match chunk { + (_, JsxTextChunk::Whitespace(whitespace)) => { + // Only handle trailing whitespace. Words must always be joined by new lines + if chunks.peek().is_none() { + if whitespace.contains('\n') { + result.push(JsxChild::Newline); + } else { + result.push(JsxChild::Whitespace) + } + } + } + + (relative_start, JsxTextChunk::Word(word)) => { + result.push(JsxChild::Word(JsxWord { + text: value_token + .token_text() + .slice(TextRange::at(relative_start, word.text_len())), + source_position: value_token.text_range().start() + relative_start, + })); + } + } + } + } + + JsxAnyChild::JsxExpressionChild(child) => { + if is_whitespace_jsx_expression(&child) { + match result.last() { + Some(JsxChild::Whitespace) => { + // Ignore + } + _ => result.push(JsxChild::Whitespace), + } + } else { + result.push(JsxChild::NonText(child.into())) + } + } + child => { + result.push(JsxChild::NonText(child)); + } + } + } + + Ok(result) +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) enum JsxChild { + /// A Single word in a JSX text. For example, the words for `a b\nc` are `[a, b, c]` + Word(JsxWord), + + /// A ` ` or `${" "}` whitespace + /// + /// ```javascript + ///
+ ///
a
+ ///
a
+ ///
{' '}a
+ ///
a{' '}
+ ///
{' '}
+ ///
a + /// {' '}b
+ /// ``` + /// + /// Whitespace between two words is not represented as whitespace + /// ```javascript + ///
a b
+ /// ``` + /// The space between `a` and `b` is not considered a whitespace. + Whitespace, + + /// A new line at the start or end of a [JsxText] with meaningful content. (that isn't all whitespace + /// and contains a new line). + /// + /// ```javascript + ///
+ /// a + ///
+ /// ``` + Newline, + + /// A [JsxText] that only consists of whitespace and has at least two line breaks; + /// + /// ```javascript + ///
+ /// + /// + ///
+ /// ``` + /// + /// The text between `
` and `` is an empty line text. + EmptyLine, + + /// Any other content that isn't a text. Should be formatted as is. + NonText(JsxAnyChild), +} + +impl JsxChild { + pub(crate) const fn is_any_line(&self) -> bool { + matches!(self, JsxChild::EmptyLine | JsxChild::Newline) + } +} + +/// A word in a Jsx Text. A word is string sequence that isn't separated by any JSX whitespace. +#[derive(Debug, Clone, Eq, PartialEq)] +pub(crate) struct JsxWord { + text: SyntaxTokenText, + source_position: TextSize, +} + +impl Format for JsxWord { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + f.write_element(FormatElement::Text(Text::SyntaxTokenTextSlice { + source_position: self.source_position, + slice: self.text.clone(), + })) + } +} + +#[derive(Eq, PartialEq, Copy, Clone, Debug)] +enum JsxTextChunk<'a> { + Whitespace(&'a str), + Word(&'a str), +} + +/// Splits a text into whitespace only and non-whitespace chunks. +/// +/// See `jsx_split_chunks_iterator` test for examples +struct JsxSplitChunksIterator<'a> { + position: TextSize, + text: &'a str, + chars: Peekable>, +} + +impl<'a> JsxSplitChunksIterator<'a> { + fn new(text: &'a str) -> Self { + Self { + position: TextSize::default(), + text, + chars: text.chars().peekable(), + } + } +} + +impl<'a> Iterator for JsxSplitChunksIterator<'a> { + type Item = (TextSize, JsxTextChunk<'a>); + + fn next(&mut self) -> Option { + let char = self.chars.next()?; + + let start = self.position; + self.position += char.text_len(); + + let is_whitespace = matches!(char, ' ' | '\n' | '\t' | '\r'); + + while let Some(next) = self.chars.peek() { + let next_is_whitespace = matches!(next, ' ' | '\n' | '\t' | '\r'); + + if is_whitespace != next_is_whitespace { + break; + } + + self.position += next.text_len(); + self.chars.next(); + } + + let range = TextRange::new(start, self.position); + let slice = &self.text[range]; + + let chunk = if is_whitespace { + JsxTextChunk::Whitespace(slice) + } else { + JsxTextChunk::Word(slice) + }; + + Some((start, chunk)) + } +} + +impl FusedIterator for JsxSplitChunksIterator<'_> {} + +#[cfg(test)] +mod tests { + use crate::utils::jsx::{jsx_split_children, JsxChild, JsxSplitChunksIterator, JsxTextChunk}; + use rome_js_parser::parse; + use rome_js_syntax::{JsxChildList, JsxText, SourceType}; + use rome_rowan::{AstNode, TextSize}; + + fn assert_jsx_text_chunks(text: &str, expected_chunks: Vec<(TextSize, JsxTextChunk)>) { + let parse = parse(&std::format!("<>{text}"), 0, SourceType::jsx()); + assert!( + !parse.has_errors(), + "Source should not have any errors {:?}", + parse.diagnostics() + ); + + let jsx_text = parse + .syntax() + .descendants() + .find_map(JsxText::cast) + .expect("Expected a JSX Text child"); + + let value_token = jsx_text.value_token().unwrap(); + let chunks = JsxSplitChunksIterator::new(value_token.text()).collect::>(); + assert_eq!(chunks, expected_chunks); + } + + #[test] + fn jsx_split_chunks_iterator() { + assert_jsx_text_chunks( + "a b c", + vec![ + (TextSize::from(0), JsxTextChunk::Word("a")), + (TextSize::from(1), JsxTextChunk::Whitespace(" ")), + (TextSize::from(2), JsxTextChunk::Word("b")), + (TextSize::from(3), JsxTextChunk::Whitespace(" ")), + (TextSize::from(4), JsxTextChunk::Word("c")), + ], + ); + + // merges consequent spaces + assert_jsx_text_chunks( + "a\n\rb", + vec![ + (TextSize::from(0), JsxTextChunk::Word("a")), + (TextSize::from(1), JsxTextChunk::Whitespace("\n\r")), + (TextSize::from(3), JsxTextChunk::Word("b")), + ], + ); + + // merges consequent non whitespace characters + assert_jsx_text_chunks( + "abcd efg", + vec![ + (TextSize::from(0), JsxTextChunk::Word("abcd")), + (TextSize::from(4), JsxTextChunk::Whitespace(" ")), + (TextSize::from(5), JsxTextChunk::Word("efg")), + ], + ); + + // whitespace at the beginning + assert_jsx_text_chunks( + "\n\n abcd", + vec![ + (TextSize::from(0), JsxTextChunk::Whitespace("\n\n ")), + (TextSize::from(3), JsxTextChunk::Word("abcd")), + ], + ); + + // whitespace at the end + assert_jsx_text_chunks( + "abcd \n\n", + vec![ + (TextSize::from(0), JsxTextChunk::Word("abcd")), + (TextSize::from(4), JsxTextChunk::Whitespace(" \n\n")), + ], + ); + } + + fn parse_jsx_children(children: &str) -> JsxChildList { + let parse = parse(&std::format!("
{children}
"), 0, SourceType::jsx()); + + assert!( + !parse.has_errors(), + "Expected source text to not have any errors: {:?}", + parse.diagnostics() + ); + + parse + .syntax() + .descendants() + .find_map(JsxChildList::cast) + .expect("Expect a JsxChildList") + } + + #[test] + fn split_children_words_only() { + let child_list = parse_jsx_children("a b c"); + + let children = jsx_split_children(&child_list).unwrap(); + + assert_eq!(3, children.len()); + assert_word(&children[0], "a"); + assert_word(&children[1], "b"); + assert_word(&children[2], "c"); + } + + #[test] + fn split_non_meaningful_text() { + let child_list = parse_jsx_children(" \n "); + + let children = jsx_split_children(&child_list).unwrap(); + + assert_eq!(children, vec![]); + } + + #[test] + fn split_non_meaningful_leading_multiple_lines() { + let child_list = parse_jsx_children(" \n \n "); + + let children = jsx_split_children(&child_list).unwrap(); + + assert_eq!(children, vec![JsxChild::EmptyLine]); + } + + #[test] + fn split_meaningful_whitespace() { + let child_list = parse_jsx_children(" "); + + let children = jsx_split_children(&child_list).unwrap(); + + assert_eq!(children, vec![JsxChild::Whitespace]); + } + + #[test] + fn split_children_leading_newlines() { + let child_list = parse_jsx_children(" \n a b"); + + let children = jsx_split_children(&child_list).unwrap(); + + assert_eq!(3, children.len()); + assert_eq!(children[0], JsxChild::Newline); + assert_word(&children[1], "a"); + assert_word(&children[2], "b"); + } + + #[test] + fn split_children_trailing_whitespace() { + let child_list = parse_jsx_children("a b \t "); + + let children = jsx_split_children(&child_list).unwrap(); + + assert_eq!(3, children.len()); + assert_word(&children[0], "a"); + assert_word(&children[1], "b"); + assert_eq!(children[2], JsxChild::Whitespace); + } + + #[test] + fn split_children_trailing_newline() { + let child_list = parse_jsx_children("a b \n \t "); + + let children = jsx_split_children(&child_list).unwrap(); + + assert_eq!(3, children.len()); + assert_word(&children[0], "a"); + assert_word(&children[1], "b"); + assert_eq!(children[2], JsxChild::Newline); + } + + #[test] + fn split_children_empty_expression() { + let child_list = parse_jsx_children(r#"a{' '}c{" "}"#); + + let children = jsx_split_children(&child_list).unwrap(); + + assert_eq!( + 4, + children.len(), + "Expected to contain four elements. Actual:\n{children:#?} " + ); + assert_word(&children[0], "a"); + assert_eq!(children[1], JsxChild::Whitespace); + assert_word(&children[2], "c"); + assert_eq!(children[3], JsxChild::Whitespace); + } + + fn assert_word(child: &JsxChild, text: &str) { + match child { + JsxChild::Word(word) => { + assert_eq!(word.text.text(), text) + } + child => { + panic!("Expected a word but found {child:#?}"); + } + } + } +} diff --git a/crates/rome_js_formatter/tests/specs/jsx/attributes.jsx b/crates/rome_js_formatter/tests/specs/jsx/attributes.jsx index 9220a88fb5d..562347d00b3 100644 --- a/crates/rome_js_formatter/tests/specs/jsx/attributes.jsx +++ b/crates/rome_js_formatter/tests/specs/jsx/attributes.jsx @@ -48,4 +48,23 @@ // https://github.com/rome/tools/issues/2944
;
\ No newline at end of file + /* comment */ asdf } />; + +// Wrapping JSX in attribute +const a = + Are you sure delete this task? let + + ) + } + okText="Yes" + cancelText="No" + mouseEnterDelay={0} + mouseLeaveDelay={0} + onVisibleChange={onVisibleChange} + > + Delete + +; diff --git a/crates/rome_js_formatter/tests/specs/jsx/attributes.jsx.snap b/crates/rome_js_formatter/tests/specs/jsx/attributes.jsx.snap index 51cec752639..ac681932caf 100644 --- a/crates/rome_js_formatter/tests/specs/jsx/attributes.jsx.snap +++ b/crates/rome_js_formatter/tests/specs/jsx/attributes.jsx.snap @@ -53,7 +53,27 @@ expression: attributes.jsx // https://github.com/rome/tools/issues/2944
;
+ /* comment */ asdf } />; + +// Wrapping JSX in attribute +const a = + Are you sure delete this task? let + + ) + } + okText="Yes" + cancelText="No" + mouseEnterDelay={0} + mouseLeaveDelay={0} + onVisibleChange={onVisibleChange} + > + Delete + +; + ============================= # Outputs ## Output 1 @@ -125,6 +145,24 @@ let a = ( /* comment */asdf } />; +// Wrapping JSX in attribute +const a = ( + + Are you sure delete this task? let + + } + okText="Yes" + cancelText="No" + mouseEnterDelay={0} + mouseLeaveDelay={0} + onVisibleChange={onVisibleChange} + > + Delete + +); + ## Lines exceeding width of 80 characters diff --git a/crates/rome_js_formatter/tests/specs/jsx/element.jsx b/crates/rome_js_formatter/tests/specs/jsx/element.jsx index e1bc1268df2..77f6f14a310 100644 --- a/crates/rome_js_formatter/tests/specs/jsx/element.jsx +++ b/crates/rome_js_formatter/tests/specs/jsx/element.jsx @@ -1,3 +1,46 @@ +// Single string attribute +
; + +// Not single string because of the new line +a =
; + +// Inline +a = ; + +// IndentAttributes +a = ; + +// Empty +a =
; +<> + + +; + +// Not empty +a =
; + +// Template +a =
{`A Long Tempalte String That uses ${5 + 4} that will eventually break across multiple lines ${40 / 3 * 45}`}
; + +// Meaningful text after self closing element adds a hard line break +a =
adefg
; + +// Meaningful text after a non-self closing element should add a soft line break +b = a =
a
abcd
; + +// A word right before a self-closing element inserts a hard line break +a =
ab
; + +// A Word not right before a self-closing element inserts a soft line break. +a =
ab
text
; + +// whitespaces +c =
a{' '}{' '}{' '}{' '}{' '}{' '}{' '}{' '}b{' '}{' '}{' '}{' '}{' '}{' '}
; + +c2 =
a{' '}{' '}{' '}{' '}{' '}{' '}{' '}{' '}
content{' '}{' '}{' '}{' '}{' '}{' '}
; + ; const Essay = () =>
The films of Wong Kar-Wai exemplify the synthesis of French New Wave cinema—specifically the unrelenting @@ -128,14 +171,14 @@ function MapoTofuRecipe() { let component =
La Haine dir. Mathieu Kassovitz
; let component = ( -
Uncle Boonmee Who Can Recall His Past Lives dir. Apichatpong Weerasethakul
+
Uncle Boonmee Who Can Recall His Past Lives dir. Apichatpong Weerasethakul
); (
Badlands
).property; - let bar =
- {foo(() =>
the quick brown fox jumps over the lazy dog and then jumps over the lazy cat and then over the lazy fish.
)} -
; +let bar =
+ {foo(() =>
the quick brown fox jumps over the lazy dog and then jumps over the lazy cat and then over the lazy fish.
)} +
; { /* comment */ " " /* comment */ }; let a = { " " /* comment */ }; let a = { /* comment */ " " }; + +// in array +const breadcrumbItems = [ + ( + + Home + + ), +].concat(extraBreadcrumbItems); diff --git a/crates/rome_js_formatter/tests/specs/jsx/element.jsx.snap b/crates/rome_js_formatter/tests/specs/jsx/element.jsx.snap index 0fb247f1d7f..7eac0eacc56 100644 --- a/crates/rome_js_formatter/tests/specs/jsx/element.jsx.snap +++ b/crates/rome_js_formatter/tests/specs/jsx/element.jsx.snap @@ -3,6 +3,49 @@ source: crates/rome_js_formatter/tests/spec_test.rs expression: element.jsx --- # Input +// Single string attribute +
; + +// Not single string because of the new line +a =
; + +// Inline +a = ; + +// IndentAttributes +a = ; + +// Empty +a =
; +<> + + +; + +// Not empty +a =
; + +// Template +a =
{`A Long Tempalte String That uses ${5 + 4} that will eventually break across multiple lines ${40 / 3 * 45}`}
; + +// Meaningful text after self closing element adds a hard line break +a =
adefg
; + +// Meaningful text after a non-self closing element should add a soft line break +b = a =
a
abcd
; + +// A word right before a self-closing element inserts a hard line break +a =
ab
; + +// A Word not right before a self-closing element inserts a soft line break. +a =
ab
text
; + +// whitespaces +c =
a{' '}{' '}{' '}{' '}{' '}{' '}{' '}{' '}b{' '}{' '}{' '}{' '}{' '}{' '}
; + +c2 =
a{' '}{' '}{' '}{' '}{' '}{' '}{' '}{' '}
content{' '}{' '}{' '}{' '}{' '}{' '}
; + ; const Essay = () =>
The films of Wong Kar-Wai exemplify the synthesis of French New Wave cinema—specifically the unrelenting @@ -133,14 +176,14 @@ function MapoTofuRecipe() { let component =
La Haine dir. Mathieu Kassovitz
; let component = ( -
Uncle Boonmee Who Can Recall His Past Lives dir. Apichatpong Weerasethakul
+
Uncle Boonmee Who Can Recall His Past Lives dir. Apichatpong Weerasethakul
); (
Badlands
).property; - let bar =
- {foo(() =>
the quick brown fox jumps over the lazy dog and then jumps over the lazy cat and then over the lazy fish.
)} -
; +let bar =
+ {foo(() =>
the quick brown fox jumps over the lazy dog and then jumps over the lazy cat and then over the lazy fish.
)} +
; { " " /* comment */ }; let a = { /* comment */ " " }; +// in array +const breadcrumbItems = [ + ( + + Home + + ), +].concat(extraBreadcrumbItems); + ============================= # Outputs ## Output 1 @@ -165,11 +217,96 @@ Line width: 80 Quote style: Double Quotes Quote properties: As needed ----- -; +// Single string attribute +
; + +// Not single string because of the new line +a = ( +
+); + +// Inline +a = ( + +); + +// IndentAttributes +a = ( + +); + +// Empty +a =
; +<>; + +// Not empty +a =
; + +// Template +a = ( +
{`A Long Tempalte String That uses ${ + 5 + 4 + } that will eventually break across multiple lines ${(40 / 3) * 45}`}
+); + +// Meaningful text after self closing element adds a hard line break +a = ( +
+
+		adefg
+	
+); + +// Meaningful text after a non-self closing element should add a soft line break +b = a = ( +
+
a
abcd +
+); + +// A word right before a self-closing element inserts a hard line break +a = ( +
+ ab +
+
+); + +// A Word not right before a self-closing element inserts a soft line break. +a = ( +
+ ab
text
+
+); + +// whitespaces +c =
a b
; + +c2 = ( +
+ a
content{" "} +
+); + +; const Essay = () => (
- The films of Wong Kar-Wai exemplify the synthesis of French New Wave cinema—specifically the unrelenting experimental technique and fascination with American/western culture—with more conventional melodramatic, romantic narratives. + The films of Wong Kar-Wai exemplify the synthesis of French New Wave + cinema—specifically the unrelenting experimental technique and fascination + with American/western culture—with more conventional melodramatic, romantic + narratives.
); @@ -264,9 +401,7 @@ function Tabs() {
{prettierOutput.ir}
-
+				
 					{errors}
 				
@@ -295,10 +430,14 @@ function LoginForm() { function MapoTofuRecipe() { return (
    - Mapo tofu recipe
  • 2 packets soft or silken tofu
  • -
  • 1 tablespoon minced garlic
  • 1 tablespoon minced ginger
  • -
  • 2 tablespoons doubanjiang
  • 1 tablespoon douchi
  • -
  • 1 tablespoon corn or potato starch
  • 2 scallions or jiu cai
  • + Mapo tofu recipe +
  • 2 packets soft or silken tofu
  • +
  • 1 tablespoon minced garlic
  • +
  • 1 tablespoon minced ginger
  • +
  • 2 tablespoons doubanjiang
  • +
  • 1 tablespoon douchi
  • +
  • 1 tablespoon corn or potato starch
  • +
  • 2 scallions or jiu cai
  • 6 ounces of ground beef or pork
); @@ -310,7 +449,8 @@ let component =
La Haine dir. Mathieu Kassovitz
; let component = (
- Uncle Boonmee Who Can Recall His Past Lives dir. Apichatpong Weerasethakul + {" "} + Uncle Boonmee Who Can Recall His Past Lives dir. Apichatpong Weerasethakul{" "}
); @@ -321,7 +461,9 @@ let bar = ( {foo( () => (
- the quick brown fox jumps over the lazy dog and then jumps over the lazy cat and then over the lazy fish. + {" "} + the quick brown fox jumps over the lazy dog and then jumps over the + lazy cat and then over the lazy fish.{" "}
), )} @@ -342,19 +484,29 @@ let b = ; let a = {/* comment */ " " /* comment */}; let a = ( - {" " + { + " " /* comment */} ); let a = {/* comment */ " "}; +// in array +const breadcrumbItems = [ + + Home + , +].concat(extraBreadcrumbItems); + ## Lines exceeding width of 80 characters - 5: The films of Wong Kar-Wai exemplify the synthesis of French New Wave cinema—specifically the unrelenting experimental technique and fascination with American/western culture—with more conventional melodramatic, romantic narratives. - 41: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", - 62: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", - 75: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", - 101: className="h-screen overflow-y-scroll whitespace-pre-wrap text-red-500 text-xs" - 157: the quick brown fox jumps over the lazy dog and then jumps over the lazy cat and then over the lazy fish. + 2:
; + 7: tooltip="A very long tooltip text that would otherwise make the attribute break + 14: + 126: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", + 147: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", + 160: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", + 185:
+  234: 		Uncle Boonmee Who Can Recall His Past Lives dir. Apichatpong Weerasethakul{" "}
 
diff --git a/crates/rome_js_formatter/tests/specs/jsx/fragment.jsx b/crates/rome_js_formatter/tests/specs/jsx/fragment.jsx
index 4a80b22beef..59a7d1cf5e8 100644
--- a/crates/rome_js_formatter/tests/specs/jsx/fragment.jsx
+++ b/crates/rome_js_formatter/tests/specs/jsx/fragment.jsx
@@ -1 +1,2 @@
 <>
+<> 
diff --git a/crates/rome_js_formatter/tests/specs/jsx/fragment.jsx.snap b/crates/rome_js_formatter/tests/specs/jsx/fragment.jsx.snap
index 243ec7b150e..c0b21e99fe2 100644
--- a/crates/rome_js_formatter/tests/specs/jsx/fragment.jsx.snap
+++ b/crates/rome_js_formatter/tests/specs/jsx/fragment.jsx.snap
@@ -4,6 +4,7 @@ expression: fragment.jsx
 ---
 # Input
 <>
+<> 
 
 =============================
 # Outputs
@@ -14,5 +15,7 @@ Line width: 80
 Quote style: Double Quotes
 Quote properties: As needed
 -----
-<>;
+<>
+<>
+
 
diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/binary-expressions/inline-jsx.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/binary-expressions/inline-jsx.js.snap
deleted file mode 100644
index e3269f11a5e..00000000000
--- a/crates/rome_js_formatter/tests/specs/prettier/js/binary-expressions/inline-jsx.js.snap
+++ /dev/null
@@ -1,64 +0,0 @@
----
-source: crates/rome_js_formatter/tests/prettier_tests.rs
----
-
-# Input
-
-```js
-const user = renderedUser || 
; - -const user2 = renderedUser || shouldRenderUser &&
; - -const avatar = hasAvatar && ; - -const avatar2 = (hasAvatar || showPlaceholder) && ; -``` - - -# Prettier differences - -```diff ---- Prettier -+++ Rome -@@ -1,15 +1,11 @@ - const user = renderedUser || ( --
-- --
-+
- ); - - const user2 = - renderedUser || - (shouldRenderUser && ( --
-- --
-+
- )); - - const avatar = hasAvatar && ; -``` - -# Output - -```js -const user = renderedUser || ( -
-); - -const user2 = - renderedUser || - (shouldRenderUser && ( -
- )); - -const avatar = hasAvatar && ; - -const avatar2 = (hasAvatar || showPlaceholder) && ( - -); -``` - - - diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/binary-expressions/jsx_parent.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/binary-expressions/jsx_parent.js.snap deleted file mode 100644 index 5dd538f4034..00000000000 --- a/crates/rome_js_formatter/tests/specs/prettier/js/binary-expressions/jsx_parent.js.snap +++ /dev/null @@ -1,111 +0,0 @@ ---- -source: crates/rome_js_formatter/tests/prettier_tests.rs ---- - -# Input - -```js -
; - -
- {!isJellyfishEnabled && - diffUpdateMessageInput != null && - this.state.isUpdateMessageEmpty} -
; - -
; - -
- {!isJellyfishEnabled && - diffUpdateMessageInput != null &&
Text
} -
; - -
- {!isJellyfishEnabled && - diffUpdateMessageInput != null && child ||
Text
} -
; -``` - - -# Prettier differences - -```diff ---- Prettier -+++ Rome -@@ -24,16 +24,12 @@ - -
- {!isJellyfishEnabled && diffUpdateMessageInput != null && ( --
-- Text --
-+
Text
- )} -
; - -
- {(!isJellyfishEnabled && diffUpdateMessageInput != null && child) || ( --
-- Text --
-+
Text
- )} -
; -``` - -# Output - -```js -
; - -
- {!isJellyfishEnabled && - diffUpdateMessageInput != null && - this.state.isUpdateMessageEmpty} -
; - -
; - -
- {!isJellyfishEnabled && diffUpdateMessageInput != null && ( -
Text
- )} -
; - -
- {(!isJellyfishEnabled && diffUpdateMessageInput != null && child) || ( -
Text
- )} -
; -``` - - - diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/comments/issue-3532.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/comments/issue-3532.js.snap deleted file mode 100644 index b67f4d0b343..00000000000 --- a/crates/rome_js_formatter/tests/specs/prettier/js/comments/issue-3532.js.snap +++ /dev/null @@ -1,120 +0,0 @@ ---- -source: crates/rome_js_formatter/tests/prettier_tests.rs ---- - -# Input - -```js -import React from 'react'; - -/* -import styled from 'react-emotion'; - -const AspectRatioBox = styled.div` - &::before { - content: ''; - width: 1px; - margin-left: -1px; - float: left; - height: 0; - padding-top: ${props => 100 / props.aspectRatio}%; - } - - &::after { - /* To clear float *//* - content: ''; - display: table; - clear: both; - } -`; -*/ - -const AspectRatioBox = ({ - aspectRatio, - children, - ...props -}) => ( -
100 / props.aspectRatio}%; - background: white; - position: relative;`} - > -
{children}
-
-); - -export default AspectRatioBox; -``` - - -# Prettier differences - -```diff ---- Prettier -+++ Rome -@@ -24,11 +24,13 @@ - - const AspectRatioBox = ({ aspectRatio, children, ...props }) => ( -
100 / props.aspectRatio}%; - background: white; -- position: relative;`} -+ position: relative;` -+ } - > -
{children}
-
-``` - -# Output - -```js -import React from "react"; - -/* -import styled from 'react-emotion'; - -const AspectRatioBox = styled.div` - &::before { - content: ''; - width: 1px; - margin-left: -1px; - float: left; - height: 0; - padding-top: ${props => 100 / props.aspectRatio}%; - } - - &::after { - /* To clear float */ /* - content: ''; - display: table; - clear: both; - } -`; -*/ - -const AspectRatioBox = ({ aspectRatio, children, ...props }) => ( -
100 / props.aspectRatio}%; - background: white; - position: relative;` - } - > -
{children}
-
-); - -export default AspectRatioBox; -``` - - - diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/comments/issues.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/comments/issues.js.snap index 17e9edc759d..3d07a8c4ed3 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/comments/issues.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/comments/issues.js.snap @@ -84,7 +84,18 @@ foo({} ```diff --- Prettier +++ Rome -@@ -64,5 +64,5 @@ +@@ -39,7 +39,9 @@ + .takeUntil(exit); + + // Comments disappear inside of JSX +-
{/* Some comment */}
; ++
++ {/* Some comment */} ++
; + + // Comments in JSX tag are placed in a non optimal way +
{/* Some comment */}
; +
+ {/* Some comment */} +
; // Comments in JSX tag are placed in a non optimal way
-
-
-); - -/** - * @type {object} - */ -() => ( -
- sajdfpoiasdjfpoiasdjfpoiasdjfpoiadsjfpaoisdjfapsdiofjapioisadfaskfaspiofjp -
-); - -/** - * @type {object} - */ -function HelloWorld() { - return ( -
- Test -
- ); -}``` - - -# Prettier differences - -```diff ---- Prettier -+++ Rome -@@ -1,9 +1,5 @@ - /** @type {any} */ --const x = ( --
--
--
--); -+const x =
; - - /** - * @type {object} -@@ -18,9 +14,5 @@ - * @type {object} - */ - function HelloWorld() { -- return ( --
-- Test --
-- ); -+ return
Test
; - } -``` - -# Output - -```js -/** @type {any} */ -const x =
; - -/** - * @type {object} - */ -() => ( -
- sajdfpoiasdjfpoiasdjfpoiasdjfpoiadsjfpaoisdjfapsdiofjapioisadfaskfaspiofjp -
-); - -/** - * @type {object} - */ -function HelloWorld() { - return
Test
; -} -``` - - - diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/comments/jsx.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/comments/jsx.js.snap index 1525ff06e4d..1c9b2476fad 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/comments/jsx.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/comments/jsx.js.snap @@ -133,7 +133,7 @@ onClick={() => {}}> ```diff --- Prettier +++ Rome -@@ -1,26 +1,24 @@ +@@ -1,24 +1,26 @@ -
{/* comment */}
; +
+ { @@ -150,27 +150,25 @@ onClick={() => {}}>
;
-- { + { - a - /* comment - */ -- } -+ {a /* comment -+*/} ++ a /* comment ++*/ + }
;
- { - /* comment - */ -- a -- } + {/* comment -+*/ a} ++*/ + a + }
; - -
{/* comment */}
; -@@ -29,29 +27,29 @@ +@@ -29,29 +31,29 @@
{ @@ -208,7 +206,7 @@ onClick={() => {}}> }
; -@@ -62,13 +60,15 @@ +@@ -62,13 +64,15 @@
{/** * JSDoc-y comment in JSX. I wonder what will happen to it? @@ -227,7 +225,7 @@ onClick={() => {}}>
;
; @@ -239,19 +237,6 @@ onClick={() => {}}> > {foo}
; - -
-+id="foo"> - {children} -
; - -- -- {} -- --; -+{}; ``` # Output @@ -271,13 +256,17 @@ onClick={() => {}}>
;
- {a /* comment -*/} + { + a /* comment +*/ + }
;
{/* comment -*/ a} +*/ + a + }
;
{/* comment */}
; @@ -356,17 +345,21 @@ onClick={() => {}}>
;
+ id="foo" +> {children}
; -{}; + + {} + +; ``` # Lines exceeding max width of 80 characters ``` - 52: // Some very v ery very very merry (xmas) very very long line to break line width limit - 57: {/*
Some very v ery very very long line to break line width limit
*/} + 56: // Some very v ery very very merry (xmas) very very long line to break line width limit + 61: {/*
Some very v ery very very long line to break line width limit
*/} ``` diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/last-argument-expansion/break-parent.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/last-argument-expansion/break-parent.js.snap index a3fedd9fb3c..49f729e934a 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/last-argument-expansion/break-parent.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/last-argument-expansion/break-parent.js.snap @@ -38,7 +38,7 @@ true ```diff --- Prettier +++ Rome -@@ -10,18 +10,18 @@ +@@ -10,18 +10,16 @@ ], }); @@ -61,17 +61,15 @@ true + ? test({ + a: 1, + }) -+ : ( -+
-+ ); ++ :
; ``` # Output @@ -93,17 +91,15 @@ true ? test({ a: 1, }) - : ( -
- ); + :
; ``` diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/last-argument-expansion/jsx.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/last-argument-expansion/jsx.js.snap index 73bf71173d2..6e282cdc7e0 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/last-argument-expansion/jsx.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/last-argument-expansion/jsx.js.snap @@ -18,14 +18,18 @@ const els = items.map(item => ( ```diff --- Prettier +++ Rome -@@ -1,5 +1,3 @@ +@@ -1,5 +1,7 @@ -const els = items.map((item) => ( -
- {children} -
-)); +const els = items.map( -+ (item) =>
{children}
, ++ (item) => ( ++
++ {children} ++
++ ), +); ``` @@ -33,7 +37,11 @@ const els = items.map(item => ( ```js const els = items.map( - (item) =>
{children}
, + (item) => ( +
+ {children} +
+ ), ); ``` 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 5f0c526e8a0..810ee274d56 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,7 +37,7 @@ ExampleStory.getFragment('story')} ```diff --- Prettier +++ Rome -@@ -3,27 +3,25 @@ +@@ -3,23 +3,20 @@ a } @@ -66,13 +66,6 @@ ExampleStory.getFragment('story')} `;
-- { -- ExampleStory.getFragment("story") // $FlowFixMe found when converting React.createClass to ES6 -+ {ExampleStory.getFragment( -+ "story", -+ ) // $FlowFixMe found when converting React.createClass to ES6 - } -
; ``` # Output @@ -100,12 +93,15 @@ ${ExampleStory.getFragment( `;
- {ExampleStory.getFragment( - "story", - ) // $FlowFixMe found when converting React.createClass to ES6 + { + ExampleStory.getFragment("story") // $FlowFixMe found when converting React.createClass to ES6 }
; ``` +# Lines exceeding max width of 80 characters +``` + 24: ExampleStory.getFragment("story") // $FlowFixMe found when converting React.createClass to ES6 +``` diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/method-chain/pr-7889.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/method-chain/pr-7889.js.snap index 140e8d5b948..cccee2fca9a 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/method-chain/pr-7889.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/method-chain/pr-7889.js.snap @@ -24,14 +24,18 @@ const Profile2 = view.with({ name }).as((props) => ( ```diff --- Prettier +++ Rome -@@ -1,11 +1,7 @@ +@@ -1,11 +1,15 @@ -const Profile = view.with({ name: (state) => state.name }).as((props) => ( -
-

Hello, {props.name}

-
-)); +const Profile = view.with({ name: (state) => state.name }).as( -+ (props) =>

Hello, {props.name}

, ++ (props) => ( ++
++

Hello, {props.name}

++
++ ), +); -const Profile2 = view.with({ name }).as((props) => ( @@ -40,7 +44,11 @@ const Profile2 = view.with({ name }).as((props) => ( -
-)); +const Profile2 = view.with({ name }).as( -+ (props) =>

Hello, {props.name}

, ++ (props) => ( ++
++

Hello, {props.name}

++
++ ), +); ``` @@ -48,11 +56,19 @@ const Profile2 = view.with({ name }).as((props) => ( ```js const Profile = view.with({ name: (state) => state.name }).as( - (props) =>

Hello, {props.name}

, + (props) => ( +
+

Hello, {props.name}

+
+ ), ); const Profile2 = view.with({ name }).as( - (props) =>

Hello, {props.name}

, + (props) => ( +
+

Hello, {props.name}

+
+ ), ); ``` diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/template-literals/css-prop.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/template-literals/css-prop.js.snap index 158b18b0293..fc9f5fd56b3 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/template-literals/css-prop.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/template-literals/css-prop.js.snap @@ -39,15 +39,12 @@ const TestComponent = ({ children, ...props }) => ( ```diff --- Prettier +++ Rome -@@ -2,18 +2,20 @@ - // Create styles as if you're calling css and the class will be applied to the component +@@ -3,17 +3,17 @@ return (
( + & .some-class { + font-size: 20px; + } -+ ` -+ } ++ `} > This will be blue until hovered.
This font size will be 20px
-@@ -22,12 +24,5 @@ +@@ -22,12 +22,5 @@ } const TestComponent = ({ children, ...props }) => ( @@ -93,8 +89,7 @@ function SomeComponent(props) { // Create styles as if you're calling css and the class will be applied to the component return (
This will be blue until hovered.
This font size will be 20px
diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/template-literals/styled-jsx-with-expressions.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/template-literals/styled-jsx-with-expressions.js.snap index 2a1568c06df..cd5d8e7ba17 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/template-literals/styled-jsx-with-expressions.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/template-literals/styled-jsx-with-expressions.js.snap @@ -54,10 +54,8 @@ source: crates/rome_js_formatter/tests/prettier_tests.rs ```diff --- Prettier +++ Rome -@@ -1,42 +1,47 @@ --; -+`} -+; +@@ -30,13 +30,12 @@ --; -+`} -+; + `}; --; -+`} -+; + `}; ``` # Output ```js -; +`}; -; +`}; -; +`}; ``` # Lines exceeding max width of 80 characters ``` - 44: animation: 3s ease-in 1s ${(foo) => foo.getIterations()} reverse both paused slidein; + 39: animation: 3s ease-in 1s ${(foo) => foo.getIterations()} reverse both paused slidein; ``` diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/template-literals/styled-jsx.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/template-literals/styled-jsx.js.snap index d122aa4fa12..1f57b7f0575 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/template-literals/styled-jsx.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/template-literals/styled-jsx.js.snap @@ -94,57 +94,44 @@ margin: 0; ```diff --- Prettier +++ Rome -@@ -1,97 +1,85 @@ --; -+; + `};
-- -+ ++}`}
; --
+
- --
; -+
; ++ +
;
-- -+ Shouldn't be formatted.`} -+ +@@ -31,67 +24,58 @@
; const header = css` @@ -256,33 +243,29 @@ margin: 0; # Output ```js -; +`};
- +}`}
; -
; +
+ +
;
- + Shouldn't be formatted.`}
; const header = css` diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/ternaries/nested-in-condition.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/ternaries/nested-in-condition.js.snap index b0eaa4c389d..bff219caf4c 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/ternaries/nested-in-condition.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/ternaries/nested-in-condition.js.snap @@ -49,7 +49,7 @@ const value = (bifornCringerMoshedPerplexSawder ```diff --- Prettier +++ Rome -@@ -22,19 +22,6 @@ +@@ -22,19 +22,17 @@ bifornCringerMoshedPerplexSawder ? askTrovenaBeenaDependsRowans : glimseGlyphsHazardNoopsTieTie @@ -70,8 +70,19 @@ const value = (bifornCringerMoshedPerplexSawder - -); +) -+ ? -+ : ; ++ ? ++ ++ ++ ++ ++ ++ ++ ++ : ++ ++ ++ ++ ; ``` # Output @@ -102,8 +113,19 @@ const value = ( ? askTrovenaBeenaDependsRowans : glimseGlyphsHazardNoopsTieTie ) - ? - : ; + ? + + + + + + + + : + + + + ; ``` diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/ternaries/nested.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/ternaries/nested.js.snap index edeea9998f0..5d861b3d721 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/js/ternaries/nested.js.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/js/ternaries/nested.js.snap @@ -140,12 +140,12 @@ a + ? + : match.params.storyId === "deal-list" + ? -+ : ( -+ -+ {"Missing story book"} -+ -+ -+ ); ++ : ++ {"Missing story book"} ++ ++ ++ ++ ; const message = i % 3 === 0 && i % 5 === 0 @@ -188,12 +188,12 @@ const StorybookLoader = ({ match }) => ? : match.params.storyId === "deal-list" ? - : ( - - {"Missing story book"} - - - ); + : + {"Missing story book"} + + + + ; const message = i % 3 === 0 && i % 5 === 0 diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/yield/jsx-without-parenthesis.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/yield/jsx-without-parenthesis.js.snap deleted file mode 100644 index c71a7be37a3..00000000000 --- a/crates/rome_js_formatter/tests/specs/prettier/js/yield/jsx-without-parenthesis.js.snap +++ /dev/null @@ -1,42 +0,0 @@ ---- -source: crates/rome_js_formatter/tests/prettier_tests.rs ---- - -# Input - -```js -function* f() { - yield
generator
- yield

generator

-} -``` - - -# Prettier differences - -```diff ---- Prettier -+++ Rome -@@ -1,8 +1,4 @@ - function* f() { - yield
generator
; -- yield ( --
--

generator

--
-- ); -+ yield

generator

; - } -``` - -# Output - -```js -function* f() { - yield
generator
; - yield

generator

; -} -``` - - - diff --git a/crates/rome_js_formatter/tests/specs/prettier/js/yield/jsx.js.snap b/crates/rome_js_formatter/tests/specs/prettier/js/yield/jsx.js.snap deleted file mode 100644 index abe737a16c4..00000000000 --- a/crates/rome_js_formatter/tests/specs/prettier/js/yield/jsx.js.snap +++ /dev/null @@ -1,42 +0,0 @@ ---- -source: crates/rome_js_formatter/tests/prettier_tests.rs ---- - -# Input - -```js -function* f() { - yield (
generator
) - yield (

generator

) -} -``` - - -# Prettier differences - -```diff ---- Prettier -+++ Rome -@@ -1,8 +1,4 @@ - function* f() { - yield
generator
; -- yield ( --
--

generator

--
-- ); -+ yield

generator

; - } -``` - -# Output - -```js -function* f() { - yield
generator
; - yield

generator

; -} -``` - - - diff --git a/crates/rome_js_formatter/tests/specs/prettier/typescript/assignment/issue-10848.tsx.snap b/crates/rome_js_formatter/tests/specs/prettier/typescript/assignment/issue-10848.tsx.snap index 6a120a2d9d9..cedb16740b3 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/typescript/assignment/issue-10848.tsx.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/typescript/assignment/issue-10848.tsx.snap @@ -61,65 +61,8 @@ const Query: FunctionComponent = ({ ```diff --- Prettier +++ Rome -@@ -1,10 +1,6 @@ - const MyComponent: React.VoidFunctionComponent = ({ x }) => { - const a = useA(); -- return ( --
-- x = {x}; a = {a} --
-- ); -+ return
x = {x}; a = {a}
; - }; - - const MyComponent2: React.VoidFunctionComponent = ({ -@@ -12,22 +8,14 @@ - y, - }) => { - const a = useA(); -- return ( --
-- x = {x}; y = {y}; a = {a} --
-- ); -+ return
x = {x}; y = {y}; a = {a}
; - }; - - const MyComponentWithLongName1: React.VoidFunctionComponent< - MyComponentWithLongNameProps - > = ({ x, y }) => { - const a = useA(); -- return ( --
-- x = {x}; y = {y}; a = {a} --
-- ); -+ return
x = {x}; y = {y}; a = {a}
; - }; - - const MyComponentWithLongName2: React.VoidFunctionComponent< -@@ -41,29 +29,23 @@ - anotherPropWithLongName4, - }) => { - const a = useA(); -- return ( --
-- x = {x}; y = {y}; a = {a} --
-- ); -+ return
x = {x}; y = {y}; a = {a}
; - }; - - const MyGenericComponent: React.VoidFunctionComponent< - MyGenericComponentProps - > = ({ x, y }) => { - const a = useA(); -- return ( --
-- x = {x}; y = {y}; a = {a} --
-- ); -+ return
x = {x}; y = {y}; a = {a}
; +@@ -59,11 +59,13 @@ + ); }; -export const ExportToExcalidrawPlus: React.FC<{ @@ -144,7 +87,11 @@ const Query: FunctionComponent = ({ ```js const MyComponent: React.VoidFunctionComponent = ({ x }) => { const a = useA(); - return
x = {x}; a = {a}
; + return ( +
+ x = {x}; a = {a} +
+ ); }; const MyComponent2: React.VoidFunctionComponent = ({ @@ -152,14 +99,22 @@ const MyComponent2: React.VoidFunctionComponent = ({ y, }) => { const a = useA(); - return
x = {x}; y = {y}; a = {a}
; + return ( +
+ x = {x}; y = {y}; a = {a} +
+ ); }; const MyComponentWithLongName1: React.VoidFunctionComponent< MyComponentWithLongNameProps > = ({ x, y }) => { const a = useA(); - return
x = {x}; y = {y}; a = {a}
; + return ( +
+ x = {x}; y = {y}; a = {a} +
+ ); }; const MyComponentWithLongName2: React.VoidFunctionComponent< @@ -173,14 +128,22 @@ const MyComponentWithLongName2: React.VoidFunctionComponent< anotherPropWithLongName4, }) => { const a = useA(); - return
x = {x}; y = {y}; a = {a}
; + return ( +
+ x = {x}; y = {y}; a = {a} +
+ ); }; const MyGenericComponent: React.VoidFunctionComponent< MyGenericComponentProps > = ({ x, y }) => { const a = useA(); - return
x = {x}; y = {y}; a = {a}
; + return ( +
+ x = {x}; y = {y}; a = {a} +
+ ); }; export const ExportToExcalidrawPlus: React.FC< diff --git a/crates/rome_js_formatter/tests/specs/prettier/typescript/comments/after_jsx_generic.ts.snap b/crates/rome_js_formatter/tests/specs/prettier/typescript/comments/after_jsx_generic.ts.snap index ba40ac6e6ee..36413b8d1b1 100644 --- a/crates/rome_js_formatter/tests/specs/prettier/typescript/comments/after_jsx_generic.ts.snap +++ b/crates/rome_js_formatter/tests/specs/prettier/typescript/comments/after_jsx_generic.ts.snap @@ -38,51 +38,25 @@ let comp = ( ```diff --- Prettier +++ Rome -@@ -1,25 +1,23 @@ +@@ -1,7 +1,7 @@ let comp = ( <> - /* comment1 */> - foo /* comment2 */> -- /* comment3 */ bar> -- foo /* comment4 */ bar> -- -- -- // comment5 -- > -- -- foo ++ /* comment1 */ > ++ foo /* comment2 */ > + /* comment3 */ bar> + foo /* comment4 */ bar> + +@@ -10,7 +10,7 @@ + > + + foo - // comment6 -- > -- -- // comment7 -- foo -- > -- -- foo -- // comment8 -- bar -- > -+ /* comment1 */ > -+ foo /* comment2 */ > -+ /* comment3 */ bar> -+ foo /* comment4 */ bar> -+ -+ // comment5 -+ > -+ foo -+ // comment6 -+ > -+ -+ // comment7 -+ foo -+ > -+ -+ foo -+ // comment8 -+ bar -+ > - - ); ++ // comment6 + > + + // comment7 ``` # Output @@ -90,25 +64,27 @@ let comp = ( ```js let comp = ( <> - /* comment1 */ > - foo /* comment2 */ > - /* comment3 */ bar> - foo /* comment4 */ bar> - - // comment5 - > - foo - // comment6 - > - - // comment7 - foo - > - - foo - // comment8 - bar - > + /* comment1 */ > + foo /* comment2 */ > + /* comment3 */ bar> + foo /* comment4 */ bar> + + + // comment5 + > + + foo + // comment6 + > + + // comment7 + foo + > + + foo + // comment8 + bar + > ); ``` diff --git a/crates/rome_js_formatter/tests/specs/prettier/typescript/tsx/member-expression.tsx.snap b/crates/rome_js_formatter/tests/specs/prettier/typescript/tsx/member-expression.tsx.snap deleted file mode 100644 index 1b983976431..00000000000 --- a/crates/rome_js_formatter/tests/specs/prettier/typescript/tsx/member-expression.tsx.snap +++ /dev/null @@ -1,84 +0,0 @@ ---- -source: crates/rome_js_formatter/tests/prettier_tests.rs ---- - -# Input - -```js -().method(); -().property; -()["computed"]; -()["computed"](); -( - -).method(); -( -
- foo -
-).property; -( -
- foo -
-)["computed"]; -( -
- foo -
-)["computed"](); -``` - - -# Prettier differences - -```diff ---- Prettier -+++ Rome -@@ -2,23 +2,7 @@ - ().property; - ()["computed"]; - ()["computed"](); --( -- --).method(); --( --
-- foo --
--).property; --( --
-- foo --
--)["computed"]; --( --
-- foo --
--)["computed"](); -+().method(); -+().property; -+()["computed"]; -+()["computed"](); -``` - -# Output - -```js -().method(); -().property; -()["computed"]; -()["computed"](); -().method(); -().property; -()["computed"]; -()["computed"](); -``` - - - diff --git a/crates/rome_js_formatter/tests/specs/prettier/typescript/tsx/url.tsx.snap b/crates/rome_js_formatter/tests/specs/prettier/typescript/tsx/url.tsx.snap deleted file mode 100644 index d2490482479..00000000000 --- a/crates/rome_js_formatter/tests/specs/prettier/typescript/tsx/url.tsx.snap +++ /dev/null @@ -1,66 +0,0 @@ ---- -source: crates/rome_js_formatter/tests/prettier_tests.rs ---- - -# Input - -```js -const link = http://example.com; - -const first =
http://example.com
; - - const second = <>http://example.com; - - const third =

http://example.com
; - - const fourth =
http://example.com
; - - const fifth =
{}http://example.com
; -``` - - -# Prettier differences - -```diff ---- Prettier -+++ Rome -@@ -4,17 +4,8 @@ - - const second = <>http://example.com; - --const third = ( --
--
-- http://example.com --
--); -+const third =

http://example.com
; - --const fourth = ( --
-- http://example.com --
--); -+const fourth =
http://example.com
; - - const fifth =
{}http://example.com
; -``` - -# Output - -```js -const link = http://example.com; - -const first =
http://example.com
; - -const second = <>http://example.com; - -const third =

http://example.com
; - -const fourth =
http://example.com
; - -const fifth =
{}http://example.com
; -``` - - - diff --git a/website/playground/src/DesktopPlayground.tsx b/website/playground/src/DesktopPlayground.tsx index 36a0901b93d..902f52c8216 100644 --- a/website/playground/src/DesktopPlayground.tsx +++ b/website/playground/src/DesktopPlayground.tsx @@ -112,9 +112,7 @@ export default function DesktopPlayground({ settings={settings} setPlaygroundState={setPlaygroundState} /> -
+
- + + + - + + +
diff --git a/website/playground/src/MobilePlayground.tsx b/website/playground/src/MobilePlayground.tsx index fb8daf06b95..16567006b94 100644 --- a/website/playground/src/MobilePlayground.tsx +++ b/website/playground/src/MobilePlayground.tsx @@ -119,8 +119,12 @@ export function MobilePlayground({ }} /> - - + + + + + +
{formatter_ir}
@@ -128,13 +132,13 @@ export function MobilePlayground({
{prettierOutput.ir}
-
+					
 						{errors}
 					
- + + +
); diff --git a/website/playground/src/TreeView.tsx b/website/playground/src/TreeView.tsx index be8ca9552bd..2d2523b5c9a 100644 --- a/website/playground/src/TreeView.tsx +++ b/website/playground/src/TreeView.tsx @@ -3,5 +3,9 @@ interface Props { } export default function TreeView({ tree }: Props) { - return
{tree}
; + return ( +
+
{tree}
+
+ ); } diff --git a/website/playground/src/main.tsx b/website/playground/src/main.tsx index f1449065200..490999ca84a 100644 --- a/website/playground/src/main.tsx +++ b/website/playground/src/main.tsx @@ -4,6 +4,8 @@ import "./index.css"; import App from "./App"; ReactDOM.render( - , + + + , document.getElementById("root"), );