Skip to content

Commit

Permalink
feat(rome_js_formatter): Template formatting (rome#3063)
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaReiser authored and IWANABETHATGUY committed Aug 22, 2022
1 parent 21d5f54 commit e7d05bc
Show file tree
Hide file tree
Showing 38 changed files with 956 additions and 1,272 deletions.
5 changes: 1 addition & 4 deletions crates/rome_formatter/src/builders.rs
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
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
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,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
}
Expand Down Expand Up @@ -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,
}
Expand Down
7 changes: 5 additions & 2 deletions crates/rome_js_formatter/src/context.rs
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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,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 {
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;
Expand All @@ -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

0 comments on commit e7d05bc

Please sign in to comment.