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

Commit

Permalink
feat(rome_js_formatter): Template formatting
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
MichaReiser committed Aug 16, 2022
1 parent 24e83be commit f0cd774
Show file tree
Hide file tree
Showing 38 changed files with 904 additions and 1,269 deletions.
5 changes: 1 addition & 4 deletions crates/rome_formatter/src/builders.rs
Expand Up @@ -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(),
Expand All @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion crates/rome_formatter/src/format_element.rs
Expand Up @@ -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),
}
Expand Down
2 changes: 1 addition & 1 deletion crates/rome_formatter/src/lib.rs
Expand Up @@ -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())
}
}

Expand Down
10 changes: 5 additions & 5 deletions crates/rome_formatter/src/printer/mod.rs
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()
};

Expand Down Expand Up @@ -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!(
Expand Down
38 changes: 35 additions & 3 deletions crates/rome_formatter/src/printer/printer_options/mod.rs
Expand Up @@ -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,
Expand All @@ -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.
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
}
Expand Down Expand Up @@ -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,
}
Expand Down
7 changes: 5 additions & 2 deletions crates/rome_js_formatter/src/context.rs
Expand Up @@ -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())
}
}

Expand Down Expand Up @@ -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
)
}

Expand Down
Expand Up @@ -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)]
Expand Down Expand Up @@ -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),
},
};
Expand All @@ -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 {
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
@@ -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(),
}
}
}
@@ -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;
Expand All @@ -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'

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(),
)
)]
)
}
}

0 comments on commit f0cd774

Please sign in to comment.