Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

feat(rome_js_formatter): Template formatting #3063

Merged
merged 3 commits into from
Aug 16, 2022
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(rome_js_formatter): Template formatting
This PR improves Rome's formatting of `JsTemplate`s and `TsTemplate`s to closer match Prettier's formatting.

It mainly implements:

* simple expressions that never break even if the template literal, as a result thereof, exceeds the line width
* Aligning expressions in template literals with the last template chunk

This PR does not implement Prettier's custom formatting of `Jest` specs, and it doesn't implement custom comments formatting.

## Tests

I manually verified the snapshot changes. There are some remaining differences but they are rooted in the fact that some, expression formatting isn't compatible with prettier yet (mainly binary expression, call arguments)
MichaReiser committed Aug 16, 2022
commit 6480ecf38cc70c7b107089f17f79b3895a57c0cc
5 changes: 1 addition & 4 deletions crates/rome_formatter/src/builders.rs
Original file line number Diff line number Diff line change
@@ -1504,6 +1504,7 @@ impl<Context> Format<Context> for ExpandParent {
/// ```
/// use rome_formatter::{format_args, format, LineWidth};
/// use rome_formatter::prelude::*;
/// use rome_formatter::printer::PrintWidth;
///
/// let context = SimpleFormatContext {
/// line_width: LineWidth::try_from(20).unwrap(),
@@ -1525,10 +1526,6 @@ impl<Context> Format<Context> for ExpandParent {
/// ])
/// ]).unwrap();
///
/// let options = PrinterOptions {
/// print_width: LineWidth::try_from(20).unwrap(),
/// ..PrinterOptions::default()
/// };
/// assert_eq!(
/// "[\n\t'A somewhat longer string to force a line break',\n\t2,\n\t3,\n]",
/// elements.print().as_code()
2 changes: 1 addition & 1 deletion crates/rome_formatter/src/format_element.rs
Original file line number Diff line number Diff line change
@@ -777,7 +777,7 @@ impl FormatContext for IrFormatContext {
fn as_print_options(&self) -> PrinterOptions {
PrinterOptions {
tab_width: 2,
print_width: self.line_width(),
print_width: self.line_width().into(),
line_ending: LineEnding::LineFeed,
indent_style: IndentStyle::Space(2),
}
2 changes: 1 addition & 1 deletion crates/rome_formatter/src/lib.rs
Original file line number Diff line number Diff line change
@@ -270,7 +270,7 @@ impl FormatContext for SimpleFormatContext {
fn as_print_options(&self) -> PrinterOptions {
PrinterOptions::default()
.with_indent(self.indent_style)
.with_print_width(self.line_width)
.with_print_width(self.line_width.into())
}
}

10 changes: 5 additions & 5 deletions crates/rome_formatter/src/printer/mod.rs
Original file line number Diff line number Diff line change
@@ -942,7 +942,7 @@ fn fits_element_on_line<'a, 'rest>(
state.line_width += char_width as usize;
}

if state.line_width > options.print_width.value().into() {
if state.line_width > options.print_width.into() {
return Fits::No;
}

@@ -1080,8 +1080,8 @@ impl<'a, 'rest> MeasureQueue<'a, 'rest> {
#[cfg(test)]
mod tests {
use crate::prelude::*;
use crate::printer::{LineEnding, Printer, PrinterOptions};
use crate::{format_args, write, FormatState, IndentStyle, LineWidth, Printed, VecBuffer};
use crate::printer::{LineEnding, PrintWidth, Printer, PrinterOptions};
use crate::{format_args, write, FormatState, IndentStyle, Printed, VecBuffer};

fn format(root: &dyn Format<()>) -> Printed {
format_with_options(
@@ -1230,7 +1230,7 @@ two lines`,
let options = PrinterOptions {
indent_style: IndentStyle::Tab,
tab_width: 4,
print_width: LineWidth::try_from(19).unwrap(),
print_width: PrintWidth::new(19),
..PrinterOptions::default()
};

@@ -1315,7 +1315,7 @@ two lines`,

let document = buffer.into_element();

let printed = Printer::new(PrinterOptions::default().with_print_width(LineWidth(10)))
let printed = Printer::new(PrinterOptions::default().with_print_width(PrintWidth::new(10)))
.print(&document);

assert_eq!(
38 changes: 35 additions & 3 deletions crates/rome_formatter/src/printer/printer_options/mod.rs
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ pub struct PrinterOptions {
pub tab_width: u8,

/// What's the max width of a line. Defaults to 80
pub print_width: LineWidth,
pub print_width: PrintWidth,

/// The type of line ending to apply to the printed input
pub line_ending: LineEnding,
@@ -16,8 +16,40 @@ pub struct PrinterOptions {
pub indent_style: IndentStyle,
}

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct PrintWidth(u32);

impl PrintWidth {
pub fn new(width: u32) -> Self {
Self(width)
}

/// Creates a print width that avoids ever breaking content because it exceeds the print width.
MichaReiser marked this conversation as resolved.
Show resolved Hide resolved
pub fn infinite() -> Self {
Self(u32::MAX)
}
}

impl Default for PrintWidth {
fn default() -> Self {
LineWidth::default().into()
}
}

impl From<LineWidth> for PrintWidth {
fn from(width: LineWidth) -> Self {
Self(u16::from(width) as u32)
}
}

impl From<PrintWidth> for usize {
fn from(width: PrintWidth) -> Self {
width.0 as usize
}
}

impl PrinterOptions {
pub fn with_print_width(mut self, width: LineWidth) -> Self {
pub fn with_print_width(mut self, width: PrintWidth) -> Self {
self.print_width = width;
self
}
@@ -69,7 +101,7 @@ impl Default for PrinterOptions {
fn default() -> Self {
PrinterOptions {
tab_width: 2,
print_width: LineWidth::default(),
print_width: PrintWidth::default(),
indent_style: Default::default(),
line_ending: LineEnding::LineFeed,
}
7 changes: 5 additions & 2 deletions crates/rome_js_formatter/src/context.rs
Original file line number Diff line number Diff line change
@@ -105,7 +105,7 @@ impl FormatContext for JsFormatContext {
fn as_print_options(&self) -> PrinterOptions {
PrinterOptions::default()
.with_indent(self.indent_style)
.with_print_width(self.line_width)
.with_print_width(self.line_width.into())
}
}

@@ -160,7 +160,10 @@ impl CommentStyle<JsLanguage> for JsCommentStyle {
fn is_group_start_token(&self, kind: JsSyntaxKind) -> bool {
matches!(
kind,
JsSyntaxKind::L_PAREN | JsSyntaxKind::L_BRACK | JsSyntaxKind::L_CURLY
JsSyntaxKind::L_PAREN
| JsSyntaxKind::L_BRACK
| JsSyntaxKind::L_CURLY
| JsSyntaxKind::DOLLAR_CURLY
)
}

Original file line number Diff line number Diff line change
@@ -3,8 +3,8 @@ use rome_formatter::{format_args, write};

use crate::utils::{is_simple_expression, resolve_expression, starts_with_no_lookahead_token};
use rome_js_syntax::{
JsAnyArrowFunctionParameters, JsAnyExpression, JsAnyFunctionBody, JsArrowFunctionExpression,
JsArrowFunctionExpressionFields,
JsAnyArrowFunctionParameters, JsAnyExpression, JsAnyFunctionBody, JsAnyTemplateElement,
JsArrowFunctionExpression, JsArrowFunctionExpressionFields, JsTemplate,
};

#[derive(Debug, Clone, Default)]
@@ -99,6 +99,9 @@ impl FormatNodeRule<JsArrowFunctionExpression> for FormatJsArrowFunctionExpressi
false,
!starts_with_no_lookahead_token(conditional.clone().into())?,
),
JsTemplate(template) => {
(is_multiline_template_starting_on_same_line(template), false)
}
expr => (is_simple_expression(expr)?, false),
},
};
@@ -125,3 +128,33 @@ impl FormatNodeRule<JsArrowFunctionExpression> for FormatJsArrowFunctionExpressi
}
}
}

/// Returns `true` if the template contains any new lines inside of its text chunks.
fn template_literal_contains_new_line(template: &JsTemplate) -> bool {
template.elements().iter().any(|element| match element {
JsAnyTemplateElement::JsTemplateChunkElement(chunk) => chunk
.template_chunk_token()
.map_or(false, |chunk| chunk.text().contains('\n')),
JsAnyTemplateElement::JsTemplateElement(_) => false,
})
}

fn is_multiline_template_starting_on_same_line(template: &JsTemplate) -> bool {
MichaReiser marked this conversation as resolved.
Show resolved Hide resolved
let contains_new_line = template_literal_contains_new_line(template);

let starts_on_same_line = template.syntax().first_token().map_or(false, |token| {
for piece in token.leading_trivia().pieces() {
if let Some(comment) = piece.as_comments() {
if comment.has_newline() {
return false;
}
} else if piece.is_newline() {
return false;
}
}

true
});

contains_new_line && starts_on_same_line
}
81 changes: 64 additions & 17 deletions crates/rome_js_formatter/src/js/expressions/template.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,79 @@
use crate::prelude::*;
use rome_formatter::write;

use rome_js_syntax::JsTemplate;
use rome_js_syntax::JsTemplateFields;
use rome_js_syntax::{
JsAnyExpression, JsSyntaxToken, JsTemplate, TsTemplateLiteralType, TsTypeArguments,
};
use rome_rowan::{declare_node_union, SyntaxResult};

#[derive(Debug, Clone, Default)]
pub struct FormatJsTemplate;

impl FormatNodeRule<JsTemplate> for FormatJsTemplate {
fn fmt_fields(&self, node: &JsTemplate, f: &mut JsFormatter) -> FormatResult<()> {
let JsTemplateFields {
tag,
type_arguments,
l_tick_token,
elements,
r_tick_token,
} = node.as_fields();

write![
JsAnyTemplate::from(node.clone()).fmt(f)
}
}

declare_node_union! {
JsAnyTemplate = JsTemplate | TsTemplateLiteralType
}

impl Format<JsFormatContext> for JsAnyTemplate {
fn fmt(&self, f: &mut Formatter<JsFormatContext>) -> FormatResult<()> {
write!(
f,
[
tag.format(),
type_arguments.format(),
self.tag().format(),
self.type_arguments().format(),
line_suffix_boundary(),
l_tick_token.format(),
elements.format(),
r_tick_token.format()
self.l_tick_token().format(),
]
]
)?;

self.write_elements(f)?;

write!(f, [self.r_tick_token().format()])
}
}

impl JsAnyTemplate {
fn tag(&self) -> Option<JsAnyExpression> {
match self {
JsAnyTemplate::JsTemplate(template) => template.tag(),
JsAnyTemplate::TsTemplateLiteralType(_) => None,
}
}

fn type_arguments(&self) -> Option<TsTypeArguments> {
match self {
JsAnyTemplate::JsTemplate(template) => template.type_arguments(),
JsAnyTemplate::TsTemplateLiteralType(_) => None,
}
}

fn l_tick_token(&self) -> SyntaxResult<JsSyntaxToken> {
match self {
JsAnyTemplate::JsTemplate(template) => template.l_tick_token(),
JsAnyTemplate::TsTemplateLiteralType(template) => template.l_tick_token(),
}
}

fn write_elements(&self, f: &mut JsFormatter) -> FormatResult<()> {
match self {
JsAnyTemplate::JsTemplate(template) => {
write!(f, [template.elements().format()])
}
JsAnyTemplate::TsTemplateLiteralType(template) => {
write!(f, [template.elements().format()])
}
}
}

fn r_tick_token(&self) -> SyntaxResult<JsSyntaxToken> {
match self {
JsAnyTemplate::JsTemplate(template) => template.r_tick_token(),
JsAnyTemplate::TsTemplateLiteralType(template) => template.r_tick_token(),
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::prelude::*;
use crate::utils::format_template_chunk;
use rome_formatter::write;

use rome_js_syntax::{JsTemplateChunkElement, JsTemplateChunkElementFields};
use rome_js_syntax::{JsSyntaxToken, JsTemplateChunkElement, TsTemplateChunkElement};
use rome_rowan::{declare_node_union, SyntaxResult};

#[derive(Debug, Clone, Default)]
pub struct FormatJsTemplateChunkElement;
@@ -12,11 +13,40 @@ impl FormatNodeRule<JsTemplateChunkElement> for FormatJsTemplateChunkElement {
node: &JsTemplateChunkElement,
formatter: &mut JsFormatter,
) -> FormatResult<()> {
let JsTemplateChunkElementFields {
template_chunk_token,
} = node.as_fields();
AnyTemplateChunkElement::from(node.clone()).fmt(formatter)
}
}

declare_node_union! {
pub(crate) AnyTemplateChunkElement = JsTemplateChunkElement | TsTemplateChunkElement
}

impl AnyTemplateChunkElement {
pub(crate) fn template_chunk_token(&self) -> SyntaxResult<JsSyntaxToken> {
match self {
AnyTemplateChunkElement::JsTemplateChunkElement(chunk) => chunk.template_chunk_token(),
AnyTemplateChunkElement::TsTemplateChunkElement(chunk) => chunk.template_chunk_token(),
}
}
}

impl Format<JsFormatContext> for AnyTemplateChunkElement {
fn fmt(&self, f: &mut Formatter<JsFormatContext>) -> FormatResult<()> {
// Per https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#sec-static-semantics-trv:
// In template literals, the '\r' and '\r\n' line terminators are normalized to '\n'
MichaReiser marked this conversation as resolved.
Show resolved Hide resolved

let chunk = self.template_chunk_token()?;

let chunk = template_chunk_token?;
format_template_chunk(chunk, formatter)
write!(
f,
[format_replaced(
&chunk,
&syntax_token_cow_slice(
normalize_newlines(chunk.text_trimmed(), ['\r']),
&chunk,
chunk.text_trimmed_range().start(),
)
)]
)
}
}
Loading