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 all commits
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
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!(
40 changes: 37 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,42 @@ pub struct PrinterOptions {
pub indent_style: IndentStyle,
}

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

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

/// Returns a print width that guarantees that any content, regardless of its width, fits on the line.
///
/// This has the effect that the printer never prints a line break for any soft line break.
pub fn infinite() -> Self {
Self(u32::MAX)
}
}

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

impl From<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 +103,7 @@ impl Default for PrinterOptions {
fn default() -> Self {
PrinterOptions {
tab_width: 2,
print_width: LineWidth::default(),
print_width: PrintWidth::default(),
indent_style: Default::default(),
line_ending: LineEnding::LineFeed,
}
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,59 @@ 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,
})
}

/// Returns `true` for a template that starts on the same line as the previous token and contains a line break.
///
///
/// # Examples
//
/// ```javascript
/// "test" + `
/// some content
/// `;
/// ```
///
/// Returns `true` because the template starts on the same line as the `+` token and its text contains a line break.
///
/// ```javascript
/// "test" + `no line break`
/// ```
///
/// Returns `false` because the template text contains no line break.
///
/// ```javascript
/// "test" +
/// `template
/// with line break`;
/// ```
///
/// Returns `false` because the template isn't on the same line as the '+' token.
fn is_multiline_template_starting_on_same_line(template: &JsTemplate) -> bool {
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,39 @@ 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<()> {
let chunk = self.template_chunk_token()?;

let chunk = template_chunk_token?;
format_template_chunk(chunk, formatter)
write!(
f,
[format_replaced(
&chunk,
&syntax_token_cow_slice(
// Per https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#sec-static-semantics-trv:
// In template literals, the '\r' and '\r\n' line terminators are normalized to '\n'
normalize_newlines(chunk.text_trimmed(), ['\r']),
&chunk,
chunk.text_trimmed_range().start(),
)
)]
)
}
}
Loading