diff --git a/crates/rome_formatter/src/builders.rs b/crates/rome_formatter/src/builders.rs index 2d262d7a739..d4158fcca85 100644 --- a/crates/rome_formatter/src/builders.rs +++ b/crates/rome_formatter/src/builders.rs @@ -1,15 +1,11 @@ use crate::prelude::*; -use crate::{ - format_element, write, Argument, Arguments, BufferSnapshot, FormatState, GroupId, TextRange, - TextSize, -}; +use crate::{format_element, write, Argument, Arguments, GroupId, TextRange, TextSize}; use crate::{Buffer, VecBuffer}; use rome_rowan::{Language, SyntaxNode, SyntaxToken, SyntaxTokenText, TextLen}; use std::borrow::Cow; use std::cell::Cell; use std::marker::PhantomData; use std::num::NonZeroU8; -use std::ops::Deref; /// A line break that only gets printed if the enclosing `Group` doesn't fit on a single line. /// It's omitted if the enclosing `Group` fits on a single line. @@ -1085,6 +1081,7 @@ pub struct BlockIndent<'a, Context> { enum IndentMode { Soft, Block, + SoftSpace, SoftLineOrSpace, } @@ -1095,7 +1092,9 @@ impl Format for BlockIndent<'_, Context> { match self.mode { IndentMode::Soft => write!(buffer, [soft_line_break()])?, IndentMode::Block => write!(buffer, [hard_line_break()])?, - IndentMode::SoftLineOrSpace => write!(buffer, [soft_line_break_or_space()])?, + IndentMode::SoftLineOrSpace | IndentMode::SoftSpace => { + write!(buffer, [soft_line_break_or_space()])? + } }; buffer.write_fmt(Arguments::from(&self.content))?; @@ -1112,6 +1111,7 @@ impl Format for BlockIndent<'_, Context> { match self.mode { IndentMode::Soft => write!(f, [soft_line_break()])?, IndentMode::Block => write!(f, [hard_line_break()])?, + IndentMode::SoftSpace => write!(f, [soft_line_break_or_space()])?, IndentMode::SoftLineOrSpace => {} } @@ -1125,12 +1125,77 @@ impl std::fmt::Debug for BlockIndent<'_, Context> { IndentMode::Soft => "SoftBlockIndent", IndentMode::Block => "HardBlockIndent", IndentMode::SoftLineOrSpace => "SoftLineIndentOrSpace", + IndentMode::SoftSpace => "SoftSpaceBlockIndent", }; f.debug_tuple(name).field(&"{{content}}").finish() } } +/// Adds spaces around the content if its enclosing group fits on a line, otherwise indents the content and separates it by line breaks. +/// +/// # Examples +/// +/// Adds line breaks and indents the content if the enclosing group doesn't fit on the line. +/// +/// ``` +/// use rome_formatter::{format, format_args, LineWidth, SimpleFormatOptions}; +/// use rome_formatter::prelude::*; +/// +/// let context = SimpleFormatContext::new(SimpleFormatOptions { +/// line_width: LineWidth::try_from(10).unwrap(), +/// ..SimpleFormatOptions::default() +/// }); +/// +/// let elements = format!(context, [ +/// group(&format_args![ +/// text("{"), +/// soft_space_or_block_indent(&format_args![ +/// text("aPropertyThatExceeds"), +/// text(":"), +/// space(), +/// text("'line width'"), +/// ]), +/// text("}") +/// ]) +/// ]).unwrap(); +/// +/// assert_eq!( +/// "{\n\taPropertyThatExceeds: 'line width'\n}", +/// elements.print().as_code() +/// ); +/// ``` +/// +/// Adds spaces around the content if the group fits on the line +/// ``` +/// use rome_formatter::{format, format_args}; +/// use rome_formatter::prelude::*; +/// +/// let elements = format!(SimpleFormatContext::default(), [ +/// group(&format_args![ +/// text("{"), +/// soft_space_or_block_indent(&format_args![ +/// text("a"), +/// text(":"), +/// space(), +/// text("5"), +/// ]), +/// text("}") +/// ]) +/// ]).unwrap(); +/// +/// assert_eq!( +/// "{ a: 5 }", +/// elements.print().as_code() +/// ); +/// ``` +pub fn soft_space_or_block_indent(content: &impl Format) -> BlockIndent { + BlockIndent { + content: Argument::new(content), + mode: IndentMode::SoftSpace, + } +} + /// Creates a logical `Group` around the content that should either consistently be printed on a single line /// or broken across multiple lines. /// @@ -1240,7 +1305,7 @@ impl Format for Group<'_, Context> { return f.write_fmt(Arguments::from(&self.content)); } - let mut buffer = GroupBuffer::new(f); + let mut buffer = VecBuffer::new(f.state_mut()); buffer.write_fmt(Arguments::from(&self.content))?; @@ -1255,9 +1320,7 @@ impl Format for Group<'_, Context> { let group = format_element::Group::new(content).with_id(self.group_id); - f.write_element(FormatElement::Group(group))?; - - Ok(()) + f.write_element(FormatElement::Group(group)) } } @@ -1271,154 +1334,6 @@ impl std::fmt::Debug for Group<'_, Context> { } } -/// Custom buffer implementation for `GroupElements` that moves the leading comments out of the group -/// to prevent that a leading line comment expands the token's enclosing group. -/// -/// # Examples -/// -/// ```javascript -/// /* a comment */ -/// [1] -/// ``` -/// -/// The `/* a comment */` belongs to the `[` group token that is part of a group wrapping the whole -/// `[1]` expression. It's important that the comment `/* a comment */` gets moved out of the group element -/// to avoid that the `[1]` group expands because of the line break inserted by the comment. -struct GroupBuffer<'inner, Context> { - inner: &'inner mut dyn Buffer, - - /// The group inner content - content: Vec, -} - -impl<'inner, Context> GroupBuffer<'inner, Context> { - fn new(inner: &'inner mut dyn Buffer) -> Self { - Self { - inner, - content: Vec::new(), - } - } - - fn into_vec(self) -> Vec { - self.content - } - - fn write_interned(&mut self, interned: Interned) -> FormatResult<()> { - debug_assert!(self.content.is_empty()); - - match interned.deref() { - FormatElement::Comment(_) => { - self.inner.write_element(FormatElement::Interned(interned)) - } - FormatElement::List(list) => { - let mut content_start = 0; - - for element in list.iter() { - match element { - element @ FormatElement::Comment(_) => { - content_start += 1; - // Cloning comments should be alright as they are rarely nested - // and the case where all elements of an interned data structure are comments - // are rare - self.inner.write_element(element.clone())?; - } - FormatElement::Interned(interned) => { - self.write_interned(interned.clone())?; - content_start += 1; - - if !self.content.is_empty() { - // Interned struct contained non-comment - break; - } - } - _ => { - // Found the first non-comment / nested interned element - break; - } - } - } - - // No leading comments, this group has no comments - if content_start == 0 { - self.content.push(FormatElement::Interned(interned)); - return Ok(()); - } - - let content = &list[content_start..]; - - // It is necessary to mutate the interned elements, write cloned elements - self.write_elements(content.iter().cloned()) - } - FormatElement::Interned(interned) => self.write_interned(interned.clone()), - _ => { - self.content.push(FormatElement::Interned(interned)); - Ok(()) - } - } - } -} - -impl Buffer for GroupBuffer<'_, Context> { - type Context = Context; - - fn write_element(&mut self, element: FormatElement) -> FormatResult<()> { - if self.content.is_empty() { - match element { - FormatElement::List(list) => { - self.write_elements(list.into_vec())?; - } - FormatElement::Interned(interned) => match Interned::try_unwrap(interned) { - Ok(owned) => self.write_element(owned)?, - Err(interned) => self.write_interned(interned)?, - }, - comment @ FormatElement::Comment { .. } => { - self.inner.write_element(comment)?; - } - element => self.content.push(element), - } - } else { - match element { - FormatElement::List(list) => { - self.content.extend(list.into_vec()); - } - element => self.content.push(element), - } - } - - Ok(()) - } - - fn elements(&self) -> &[FormatElement] { - &self.content - } - - fn state(&self) -> &FormatState { - self.inner.state() - } - - fn state_mut(&mut self) -> &mut FormatState { - self.inner.state_mut() - } - - fn snapshot(&self) -> BufferSnapshot { - BufferSnapshot::Any(Box::new(GroupElementsBufferSnapshot { - inner: self.inner.snapshot(), - content_len: self.content.len(), - })) - } - - fn restore_snapshot(&mut self, snapshot: BufferSnapshot) { - let snapshot = snapshot.unwrap_any::(); - self.inner.restore_snapshot(snapshot.inner); - self.content.truncate(snapshot.content_len); - } -} - -struct GroupElementsBufferSnapshot { - inner: BufferSnapshot, - content_len: usize, -} - /// IR element that forces the parent group to print in expanded mode. /// /// Has no effect if used outside of a group or element that introduce implicit groups (fill element). @@ -2146,9 +2061,9 @@ pub fn get_lines_before(next_node: &SyntaxNode) -> usize { leading_trivia .pieces() .take_while(|piece| { - // Stop at the first comment piece, the comment printer + // Stop at the first comment or skipped piece, the comment printer // will handle newlines between the comment and the node - !piece.is_comments() + !(piece.is_comments() || piece.is_skipped()) }) .filter(|piece| piece.is_newline()) .count() diff --git a/crates/rome_formatter/src/format_element.rs b/crates/rome_formatter/src/format_element.rs index 79c3c57cc0e..128c6a70271 100644 --- a/crates/rome_formatter/src/format_element.rs +++ b/crates/rome_formatter/src/format_element.rs @@ -398,10 +398,6 @@ impl Interned { pub(crate) fn new(element: FormatElement) -> Self { Self(Rc::new(element)) } - - pub(crate) fn try_unwrap(this: Interned) -> Result { - Rc::try_unwrap(this.0).map_err(Interned) - } } impl PartialEq for Interned { diff --git a/crates/rome_formatter/src/formatter.rs b/crates/rome_formatter/src/formatter.rs index a2b816f0acd..df248c3d029 100644 --- a/crates/rome_formatter/src/formatter.rs +++ b/crates/rome_formatter/src/formatter.rs @@ -2,7 +2,8 @@ use crate::buffer::BufferSnapshot; use crate::builders::{FillBuilder, JoinBuilder, JoinNodesBuilder, Line}; use crate::prelude::*; use crate::{ - Arguments, Buffer, FormatContext, FormatState, FormatStateSnapshot, GroupId, VecBuffer, + Arguments, Buffer, Comments, CstFormatContext, FormatContext, FormatState, FormatStateSnapshot, + GroupId, VecBuffer, }; /// Handles the formatting of a CST and stores the context how the CST should be formatted (user preferences). @@ -213,6 +214,16 @@ where } } +impl Formatter<'_, Context> +where + Context: CstFormatContext, +{ + /// Returns the comments from the context. + pub fn comments(&self) -> &Comments { + self.context().comments() + } +} + impl Buffer for Formatter<'_, Context> { type Context = Context; diff --git a/crates/rome_formatter/src/lib.rs b/crates/rome_formatter/src/lib.rs index f8258fdad59..61709ed8aa3 100644 --- a/crates/rome_formatter/src/lib.rs +++ b/crates/rome_formatter/src/lib.rs @@ -36,12 +36,12 @@ pub mod prelude; pub mod printed_tokens; pub mod printer; mod source_map; +pub mod trivia; mod verbatim; use crate::formatter::Formatter; use crate::group_id::UniqueGroupIdBuilder; use crate::prelude::syntax_token_cow_slice; -use std::any::TypeId; #[cfg(debug_assertions)] use crate::printed_tokens::PrintedTokens; @@ -57,10 +57,9 @@ pub use builders::{ use crate::comments::{CommentStyle, Comments, SourceComment}; pub use format_element::{normalize_newlines, FormatElement, Text, Verbatim, LINE_TERMINATORS}; pub use group_id::GroupId; -use indexmap::IndexSet; use rome_rowan::{ - Language, RawSyntaxKind, SyntaxElement, SyntaxError, SyntaxKind, SyntaxNode, SyntaxResult, - SyntaxToken, SyntaxTriviaPieceComments, TextRange, TextSize, TokenAtOffset, + Language, SyntaxElement, SyntaxError, SyntaxNode, SyntaxResult, SyntaxToken, SyntaxTriviaPiece, + TextRange, TextSize, TokenAtOffset, }; pub use source_map::{TransformSourceMap, TransformSourceMapBuilder}; use std::error::Error; @@ -1237,7 +1236,7 @@ pub fn format_sub_tree( )) } -impl Format for SyntaxTriviaPieceComments { +impl Format for SyntaxTriviaPiece { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { let range = self.text_range(); @@ -1245,7 +1244,7 @@ impl Format for SyntaxTriviaPieceComments { f, [syntax_token_cow_slice( normalize_newlines(self.text().trim(), LINE_TERMINATORS), - &self.as_piece().token(), + &self.token(), range.start() )] ) @@ -1262,21 +1261,6 @@ pub struct FormatState { group_id_builder: UniqueGroupIdBuilder, - /// `true` if the last formatted output is an inline comment that may need a space between the next token or comment. - last_content_inline_comment: bool, - - /// The kind of the last formatted token - last_token_kind: Option, - - /// Tracks comments that have been formatted manually and shouldn't be emitted again - /// when formatting the token the comments belong to. - /// - /// The map stores the absolute position of the manually formatted comments. - /// Storing the position is sufficient because comments are guaranteed to not be empty - /// (all start with a specific comment sequence) and thus, no two comments can have the same - /// absolute position. - manually_formatted_comments: IndexSet, - // This is using a RefCell as it only exists in debug mode, // the Formatter is still completely immutable in release builds #[cfg(debug_assertions)] @@ -1290,11 +1274,6 @@ where fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_struct("FormatState") .field("context", &self.context) - .field( - "has_trailing_inline_comment", - &self.last_content_inline_comment, - ) - .field("last_token_kind", &self.last_token_kind) .finish() } } @@ -1305,9 +1284,7 @@ impl FormatState { Self { context, group_id_builder: Default::default(), - last_content_inline_comment: false, - last_token_kind: None, - manually_formatted_comments: IndexSet::default(), + #[cfg(debug_assertions)] printed_tokens: Default::default(), } @@ -1317,74 +1294,6 @@ impl FormatState { self.context } - /// Returns `true` if the last written content is an inline comment with no trailing whitespace. - /// - /// The formatting of the next content may need to insert a whitespace to separate the - /// inline comment from the next content. - pub fn is_last_content_inline_comment(&self) -> bool { - self.last_content_inline_comment - } - - /// Sets whether the last written content is an inline comment that has no trailing whitespace. - pub fn set_last_content_inline_comment(&mut self, has_comment: bool) { - self.last_content_inline_comment = has_comment; - } - - /// Returns the kind of the last formatted token. - pub fn last_token_kind(&self) -> Option { - self.last_token_kind - } - - /// Sets the kind of the last formatted token and sets `last_content_inline_comment` to `false`. - pub fn set_last_token_kind(&mut self, kind: Kind) { - self.set_last_token_kind_raw(Some(LastTokenKind { - kind_type: TypeId::of::(), - kind: kind.to_raw(), - })); - } - - pub fn set_last_token_kind_raw(&mut self, kind: Option) { - self.last_token_kind = kind; - } - - /// Mark the passed comment as formatted. This is necessary if a comment from a token is formatted - /// to avoid that the comment gets emitted again when formatting that token. - /// - /// # Examples - /// This can be useful when you want to move comments from one token to another. - /// For example, when parenthesising an expression: - /// - /// ```javascript - /// console.log("test"); - /// /* leading */ "string" /* trailing */; - /// ``` - /// - /// It is then desired that the leading and trailing comments are outside of the parentheses. - /// - /// ```javascript - /// /* leading */ ("string") /* trailing */; - /// ``` - /// - /// This can be accomplished by manually formatting the leading/trailing trivia of the string literal expression - /// before/after the close parentheses and then mark the comments as handled. - pub fn mark_comment_as_formatted( - &mut self, - comment: &SyntaxTriviaPieceComments, - ) { - self.manually_formatted_comments - .insert(comment.text_range().start()); - } - - /// Returns `true` if this comment has already been formatted manually - /// and shouldn't be formatted again when formatting the token to which the comment belongs. - pub fn is_comment_formatted( - &self, - comment: &SyntaxTriviaPieceComments, - ) -> bool { - self.manually_formatted_comments - .contains(&comment.text_range().start()) - } - /// Returns the context specifying how to format the current CST pub fn context(&self) -> &Context { &self.context @@ -1432,9 +1341,6 @@ where { pub fn snapshot(&self) -> FormatStateSnapshot { FormatStateSnapshot { - last_content_inline_comment: self.last_content_inline_comment, - last_token_kind: self.last_token_kind, - manual_handled_comments_len: self.manually_formatted_comments.len(), #[cfg(debug_assertions)] printed_tokens: self.printed_tokens.clone(), } @@ -1442,17 +1348,10 @@ where pub fn restore_snapshot(&mut self, snapshot: FormatStateSnapshot) { let FormatStateSnapshot { - last_content_inline_comment, - last_token_kind, - manual_handled_comments_len, #[cfg(debug_assertions)] printed_tokens, } = snapshot; - self.last_content_inline_comment = last_content_inline_comment; - self.last_token_kind = last_token_kind; - self.manually_formatted_comments - .truncate(manual_handled_comments_len); cfg_if::cfg_if! { if #[cfg(debug_assertions)] { self.printed_tokens = printed_tokens; @@ -1461,26 +1360,7 @@ where } } -#[derive(Eq, PartialEq, Debug, Copy, Clone)] -pub struct LastTokenKind { - kind_type: TypeId, - kind: RawSyntaxKind, -} - -impl LastTokenKind { - pub fn as_language(&self) -> Option { - if self.kind_type == TypeId::of::() { - Some(L::Kind::from_raw(self.kind)) - } else { - None - } - } -} - pub struct FormatStateSnapshot { - last_content_inline_comment: bool, - last_token_kind: Option, - manual_handled_comments_len: usize, #[cfg(debug_assertions)] printed_tokens: PrintedTokens, } diff --git a/crates/rome_formatter/src/prelude.rs b/crates/rome_formatter/src/prelude.rs index 7e796d60179..cfaf2c02a3c 100644 --- a/crates/rome_formatter/src/prelude.rs +++ b/crates/rome_formatter/src/prelude.rs @@ -3,11 +3,13 @@ pub use crate::format_element::*; pub use crate::format_extensions::{FormatOptional as _, MemoizeFormat, Memoized}; pub use crate::formatter::Formatter; pub use crate::printer::PrinterOptions; -pub use crate::token::{ - format_leading_trivia, format_only_if_breaks, format_removed, format_replaced, - format_trailing_trivia, format_trimmed_token, +pub use crate::trivia::{ + format_dangling_comments, format_leading_comments, format_only_if_breaks, format_removed, + format_replaced, format_trailing_comments, format_trimmed_token, }; +pub use crate::verbatim::{format_suppressed_node, format_unknown_node, format_verbatim_node}; + pub use crate::{ best_fitting, dbg_write, format, format_args, write, Buffer as _, BufferExtensions, Format, Format as _, FormatError, FormatResult, FormatRule, FormatWithRule as _, SimpleFormatContext, diff --git a/crates/rome_formatter/src/printer/mod.rs b/crates/rome_formatter/src/printer/mod.rs index 9bd693a3b02..45a612c102f 100644 --- a/crates/rome_formatter/src/printer/mod.rs +++ b/crates/rome_formatter/src/printer/mod.rs @@ -264,10 +264,6 @@ impl<'a> Printer<'a> { self.queue_line_suffixes(HARD_BREAK, args, queue); } - FormatElement::Comment(content) => { - queue.extend_with_args(content.iter(), args); - } - FormatElement::Verbatim(verbatim) => { if let VerbatimKind::Verbatim { length } = &verbatim.kind { self.state.verbatim_markers.push(TextRange::at( @@ -355,28 +351,10 @@ impl<'a> Printer<'a> { return; } - // If the indentation level has changed since these line suffixes were queued, - // insert a line break before to push the comments into the new indent block - // SAFETY: Indexing into line_suffixes is guarded by the above call to is_empty - let has_line_break = self.state.line_suffixes[0].args.indent.level() < args.indent.level(); - // Print this line break element again once all the line suffixes have been flushed let call_self = PrintElementCall::new(line_break, args); - let line_break = if has_line_break { - // Duplicate this line break element before the line - // suffixes if a line break is required - Some(call_self.clone()) - } else { - None - }; - - queue.extend( - line_break - .into_iter() - .chain(self.state.line_suffixes.drain(..)) - .chain(once(call_self)), - ); + queue.extend(self.state.line_suffixes.drain(..).chain(once(call_self))); } /// Tries to fit as much content as possible on a single line. @@ -606,9 +584,9 @@ impl GroupModes { } fn unwrap_print_mode(&self, group_id: GroupId, next_element: &FormatElement) -> PrintMode { - self.get_print_mode(group_id).unwrap_or_else(|| + self.get_print_mode(group_id).unwrap_or_else(|| { panic!("Expected group with id {group_id:?} to exist but it wasn't present in the document. Ensure that a group with such a document appears in the document before the element {next_element:?}.") - ) + }) } } @@ -1031,8 +1009,6 @@ fn fits_element_on_line<'a, 'rest>( } } - FormatElement::Comment(content) => queue.extend(content.iter(), args), - FormatElement::Verbatim(verbatim) => queue.extend(verbatim.content.iter(), args), FormatElement::BestFitting(best_fitting) => { let content = match args.mode { @@ -1438,11 +1414,7 @@ two lines`, text("]") ]), text(";"), - comment(&line_suffix(&format_args![ - space(), - text("// trailing"), - space() - ]),) + &line_suffix(&format_args![space(), text("// trailing"), space()]) ]); assert_eq!(printed.as_code(), "[1, 2, 3]; // trailing") diff --git a/crates/rome_formatter/src/source_map.rs b/crates/rome_formatter/src/source_map.rs index 2c2c2167b6e..786f606ec0f 100644 --- a/crates/rome_formatter/src/source_map.rs +++ b/crates/rome_formatter/src/source_map.rs @@ -382,12 +382,18 @@ pub struct TransformSourceMapBuilder { } impl TransformSourceMapBuilder { - /// Creates a new builder for a source map that maps positions back to the passed `root` tree. + /// Creates a new builder. pub fn new() -> Self { Self { - source_text: String::new(), - deleted_ranges: Vec::new(), - mapped_node_ranges: FxHashMap::default(), + ..Default::default() + } + } + + /// Creates a new builder for a document with the given source. + pub fn with_source(source: String) -> Self { + Self { + source_text: source, + ..Default::default() } } diff --git a/crates/rome_formatter/src/token.rs b/crates/rome_formatter/src/token.rs deleted file mode 100644 index 04d8fdfb3d9..00000000000 --- a/crates/rome_formatter/src/token.rs +++ /dev/null @@ -1,850 +0,0 @@ -use crate::comments::{CommentKind, CommentStyle, SourceComment}; -use crate::prelude::*; -use crate::{ - format_args, write, Argument, Arguments, CstFormatContext, FormatRefWithRule, GroupId, - LastTokenKind, -}; -use rome_rowan::{Language, SyntaxToken, SyntaxTriviaPiece}; - -///! Provides builders for working with tokens and the tokens trivia - -/// Formats a token without its leading or trailing trivia -/// -/// ## Warning -/// It's your responsibility to format leading or trailing comments and skipped trivia. -pub const fn format_trimmed_token(token: &SyntaxToken) -> FormatTrimmedToken { - FormatTrimmedToken { token } -} - -#[derive(Debug, Eq, PartialEq, Copy, Clone)] -pub struct FormatTrimmedToken<'a, L: Language> { - token: &'a SyntaxToken, -} - -impl Format for FormatTrimmedToken<'_, L> -where - C: CstFormatContext, -{ - fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { - write_space_between_comment_and_token(self.token.kind(), f)?; - - f.state_mut().set_last_token_kind(self.token.kind()); - - let trimmed_range = self.token.text_trimmed_range(); - syntax_token_text_slice(self.token, trimmed_range).fmt(f) - } -} - -/// Formats a token that has been inserted by the formatter and isn't present in the source text. -/// Takes care of correctly handling spacing to the previous token's trailing trivia. -pub struct FormatInserted -where - L: Language, -{ - kind: L::Kind, - text: &'static str, -} - -impl FormatInserted -where - L: Language, -{ - pub fn new(kind: L::Kind, text: &'static str) -> Self { - Self { kind, text } - } -} - -impl Format for FormatInserted -where - L: Language + 'static, - C: CstFormatContext, -{ - fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { - write_space_between_comment_and_token(self.kind, f)?; - - f.state_mut().set_last_token_kind(self.kind); - text(self.text).fmt(f) - } -} - -fn write_space_between_comment_and_token( - token_kind: ::Kind, - f: &mut Formatter, -) -> FormatResult<()> -where - Context: CstFormatContext, -{ - let is_last_content_inline_content = f.state().is_last_content_inline_comment(); - - // Insert a space if the previous token has any trailing comments and this is not a group - // end token - #[allow(deprecated)] - if is_last_content_inline_content && !f.context().comment_style().is_group_end_token(token_kind) - { - space().fmt(f)?; - } - - f.state_mut().set_last_content_inline_comment(false); - - Ok(()) -} - -/// Inserts a open parentheses before the specified token and ensures -/// that any leading trivia of the token is formatted **before** the inserted parentheses. -/// -/// # Example -/// -/// ```javascript -/// /* leading */ "string"; -/// ``` -/// -/// Becomes -/// -/// ```javascript -/// /* leading */ ("string"; -/// ``` -/// -/// when inserting the "(" before the "string" token. -#[derive(Clone, Debug)] -pub struct FormatInsertedOpenParen<'a, L> -where - L: Language, -{ - /// The token before which the open paren must be inserted - before_token: Option<&'a SyntaxToken>, - - /// The token text of the open paren - text: &'static str, - - /// The kind of the open paren - kind: L::Kind, -} - -impl<'a, L> FormatInsertedOpenParen<'a, L> -where - L: Language, -{ - pub fn new( - before_token: Option<&'a SyntaxToken>, - kind: L::Kind, - text: &'static str, - ) -> Self { - Self { - before_token, - kind, - text, - } - } -} - -impl Format for FormatInsertedOpenParen<'_, L> -where - L: Language + 'static, - Context: CstFormatContext, -{ - fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { - let mut comments = Vec::new(); - - if let Some(before_token) = &self.before_token { - // Format the leading trivia of the next token as the leading trivia of the open paren. - let leading_pieces = before_token.leading_trivia().pieces(); - - let mut lines_before = 0; - - for piece in leading_pieces { - if let Some(comment) = piece.as_comments() { - comments.push(SourceComment::leading(comment, lines_before)); - lines_before = 0; - } else if piece.is_newline() { - lines_before += 1; - } else if piece.is_skipped() { - // Keep the skipped trivia inside of the parens. Handled by the - // formatting of the `before_token`. - break; - } - } - - write!( - f, - [FormatLeadingComments { - comments: &comments, - trim_mode: TriviaPrintMode::Full, - lines_before_token: lines_before, - }] - )?; - } - - write!( - f, - [FormatInserted { - kind: self.kind, - text: self.text, - }] - )?; - - // Prevent that the comments get formatted again when formatting the - // `before_token` - for comment in comments { - f.state_mut().mark_comment_as_formatted(comment.piece()); - } - - Ok(()) - } -} - -/// Inserts a closing parentheses before another token and moves that tokens -/// trailing trivia after the closing parentheses. -/// -/// # Example -/// -/// ```javascript -/// "string" /* trailing */; -/// ``` -/// -/// Becomes -/// -/// ```javascript -/// "string") /* trailing */ -/// ``` -#[derive(Clone, Debug)] -pub struct FormatInsertedCloseParen -where - L: Language, -{ - /// The token after which the close paren must be inserted - comments: Vec>, - - /// The token text of the close paren - text: &'static str, - - /// The kind of the close paren - kind: L::Kind, -} - -impl FormatInsertedCloseParen -where - L: Language, -{ - pub fn after_token( - after_token: Option<&SyntaxToken>, - kind: L::Kind, - text: &'static str, - f: &mut Formatter, - ) -> Self { - let mut comments = Vec::new(); - - if let Some(after_token) = after_token { - // "Steal" the trailing comments and mark them as handled. - // Must be done eagerly before formatting because the `after_token` - // gets formatted **before** formatting the inserted paren. - for piece in after_token.trailing_trivia().pieces() { - if let Some(comment) = piece.as_comments() { - f.state_mut().mark_comment_as_formatted(&comment); - comments.push(SourceComment::trailing(comment)); - } - } - } - - Self { - comments, - kind, - text, - } - } -} - -impl Format for FormatInsertedCloseParen -where - L: Language + 'static, - Context: CstFormatContext, -{ - fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { - write!( - f, - [ - FormatInserted { - kind: self.kind, - text: self.text, - }, - FormatTrailingTrivia::new(self.comments.iter().cloned(), self.kind,) - .skip_formatted_check() - ] - ) - } -} - -/// Formats the leading and trailing trivia of a removed token. -/// -/// Formats all leading and trailing comments up to the first line break or skipped token trivia as a trailing -/// comment of the previous token. The remaining trivia is then printed as leading trivia of the next token. -pub const fn format_removed(token: &SyntaxToken) -> FormatRemoved -where - L: Language, -{ - FormatRemoved { token } -} - -/// Formats the trivia of a token that is present in the source text but should be omitted in the -/// formatted output. -pub struct FormatRemoved<'a, L> -where - L: Language, -{ - token: &'a SyntaxToken, -} - -impl Format for FormatRemoved<'_, L> -where - L: Language + 'static, - C: CstFormatContext, -{ - fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { - f.state_mut().track_token(self.token); - - let last = f.state().last_token_kind(); - - write_removed_token_trivia(self.token, last, f) - } -} - -/// Writes the trivia of a removed token -fn write_removed_token_trivia( - token: &SyntaxToken, - last_token: Option, - f: &mut Formatter, -) -> FormatResult<()> -where - L: Language + 'static, - C: CstFormatContext, -{ - let mut pieces = token - .leading_trivia() - .pieces() - .chain(token.trailing_trivia().pieces()) - .peekable(); - - let last_token = last_token.and_then(|token| token.as_language::()); - - // If this isn't the first token than format all comments that are before the first skipped token - // trivia or line break as the trailing trivia of the previous token (which these comments will - // become if the document gets formatted a second time). - if let Some(last_token) = last_token.as_ref() { - let mut trailing_comments = vec![]; - - while let Some(piece) = pieces.peek() { - if let Some(comment) = piece.as_comments() { - if !f.state().is_comment_formatted(&comment) { - trailing_comments.push(SourceComment::trailing(comment)); - } - } else if piece.is_newline() || piece.is_skipped() { - break; - } - - pieces.next(); - } - - FormatTrailingTrivia::new(trailing_comments.into_iter(), *last_token).fmt(f)?; - } - - write_leading_trivia(pieces, token, TriviaPrintMode::Full, f, || { - token.prev_token() - })?; - - Ok(()) -} - -/// Print out a `token` from the original source with a different `content`. -/// -/// This will print the trivia that belong to `token` to `content`; -/// `token` is then marked as consumed by the formatter. -pub fn format_replaced<'a, 'content, L, Context>( - token: &'a SyntaxToken, - content: &'content impl Format, -) -> FormatReplaced<'a, 'content, L, Context> -where - L: Language, -{ - FormatReplaced { - token, - content: Argument::new(content), - } -} - -/// Formats a token's leading and trailing trivia but uses the provided content instead -/// of the token in the formatted output. -#[derive(Copy, Clone)] -pub struct FormatReplaced<'a, 'content, L, C> -where - L: Language, -{ - token: &'a SyntaxToken, - content: Argument<'content, C>, -} - -impl Format for FormatReplaced<'_, '_, L, C> -where - L: Language + 'static, - C: CstFormatContext, -{ - fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { - f.state_mut().track_token(self.token); - - format_leading_trivia(self.token).fmt(f)?; - - write_space_between_comment_and_token(self.token.kind(), f)?; - - f.state_mut().set_last_token_kind(self.token.kind()); - - f.write_fmt(Arguments::from(&self.content))?; - format_trailing_trivia(self.token).fmt(f) - } -} - -/// Formats the given token only if the group does break and otherwise retains the token's trivia. -pub fn format_only_if_breaks<'a, 'content, L, Content, Context>( - token: &'a SyntaxToken, - content: &'content Content, -) -> FormatOnlyIfBreaks<'a, 'content, L, Context> -where - L: Language, - Content: Format, -{ - FormatOnlyIfBreaks { - token, - content: Argument::new(content), - group_id: None, - } -} - -/// Formats a token with its leading and trailing trivia that only gets printed if its enclosing -/// group does break but otherwise gets omitted from the formatted output. -pub struct FormatOnlyIfBreaks<'a, 'content, L, C> -where - L: Language, -{ - token: &'a SyntaxToken, - content: Argument<'content, C>, - group_id: Option, -} - -impl<'a, 'content, L, C> FormatOnlyIfBreaks<'a, 'content, L, C> -where - L: Language, -{ - pub fn with_group_id(mut self, group_id: Option) -> Self { - self.group_id = group_id; - self - } -} - -impl Format for FormatOnlyIfBreaks<'_, '_, L, C> -where - L: Language + 'static, - C: CstFormatContext, -{ - fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { - // Store the last token and last trailing comment before formatting the content which will override the state. - // Is it safe to set `last_trailing_comment` only in the format removed because format removed may set it to true - // but it's false for the "break" case. Ignorable, because it's after a new line break in that case? - let last_token = f.state().last_token_kind(); - let is_last_content_inline_comment = f.state().is_last_content_inline_comment(); - - write!( - f, - [ - if_group_breaks(&Arguments::from(&self.content)).with_group_id(self.group_id), - // Print the trivia otherwise - if_group_fits_on_line(&format_with(|f| { - // Restore state to how it was before formatting the "breaks" variant - f.state_mut().set_last_token_kind_raw(last_token); - f.state_mut() - .set_last_content_inline_comment(is_last_content_inline_comment); - - write_removed_token_trivia(self.token, last_token, f) - })) - .with_group_id(self.group_id), - ] - ) - } -} - -/// Determines if the whitespace separating comment trivia -/// from their associated tokens should be printed or trimmed -#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] -pub enum TriviaPrintMode { - #[default] - Full, - Trim, -} - -/// Formats the leading trivia (comments, skipped token trivia) of a token -pub fn format_leading_trivia(token: &SyntaxToken) -> FormatLeadingTrivia { - FormatLeadingTrivia { - trim_mode: TriviaPrintMode::Full, - token, - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct FormatLeadingTrivia<'a, L> -where - L: Language, -{ - trim_mode: TriviaPrintMode, - token: &'a SyntaxToken, -} - -impl<'a, L> FormatLeadingTrivia<'a, L> -where - L: Language, -{ - pub fn with_trim_mode(mut self, mode: TriviaPrintMode) -> Self { - self.trim_mode = mode; - self - } -} - -impl Format for FormatLeadingTrivia<'_, L> -where - L: Language, - C: CstFormatContext, -{ - fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { - write_leading_trivia( - self.token.leading_trivia().pieces(), - self.token, - self.trim_mode, - f, - || self.token.prev_token(), - ) - } -} - -fn write_leading_trivia( - pieces: I, - token: &SyntaxToken, - trim_mode: TriviaPrintMode, - f: &mut Formatter, - previous_token: F, -) -> FormatResult<()> -where - I: IntoIterator>, - L: Language, - C: CstFormatContext, - F: FnOnce() -> Option>, -{ - let mut lines_before = 0; - let mut comments = Vec::new(); - let mut pieces = pieces.into_iter(); - - while let Some(piece) = pieces.next() { - if let Some(comment) = piece.as_comments() { - if !f.state().is_comment_formatted(&comment) { - comments.push(SourceComment::leading(comment, lines_before)); - } - - lines_before = 0; - } else if piece.is_newline() { - lines_before += 1; - } else if piece.is_skipped() { - // Special handling for tokens that have skipped trivia: - // - // ``` - // class { - // // comment - // @test(/* inner */) // trailing - // /* token leading */ - // method() {} - // } - // ``` - // If `@test(/*inner)` are skipped trivia that are part of the `method` tokens leading trivia, then the - // following code splits the trivia into for parts: - // 1. The first skipped trivia's leading comments: Comments that come before the first skipped trivia `@`: The `// comment` - // 2. Skipped trivia: All trivia pieces between the first and last skipped trivia: `@test(/* inner *)`. Gets formatted as verbatim - // 3. Trailing comments of the last skipped token: All comments that are on the same line as the last skipped trivia. The `// trailing` comment - // 4. The token's leading trivia: All comments that are not on the same line as the last skipped token trivia: `/* token leading */` - - // Format the 1. part, the skipped trivia's leading comments - FormatLeadingComments { - comments: &comments, - trim_mode: TriviaPrintMode::Full, - lines_before_token: lines_before, - } - .fmt(f)?; - - let has_preceding_whitespace = previous_token() - .and_then(|token| token.trailing_trivia().pieces().last()) - .map_or(false, |piece| piece.is_newline() || piece.is_whitespace()); - - // Maintain a leading whitespace in front of the skipped token trivia - // if the previous token has a trailing whitespace (and there's no comment between the two tokens). - if comments.is_empty() && has_preceding_whitespace { - write!(f, [space()])?; - } - - comments.clear(); - lines_before = 0; - - // Count the whitespace between the last skipped token trivia and the token - let mut spaces = 0; - // The range that covers from the first to the last skipped token trivia - let mut skipped_trivia_range = piece.text_range(); - - for piece in pieces { - if piece.is_whitespace() { - spaces += 1; - continue; - } - - spaces = 0; - - // If this is another skipped trivia, then extend the skipped range and - // clear all accumulated comments because they are formatted as verbatim as part of the - // skipped token trivia - if piece.is_skipped() { - skipped_trivia_range = skipped_trivia_range.cover(piece.text_range()); - comments.clear(); - lines_before = 0; - } else if let Some(comment) = piece.as_comments() { - comments.push(SourceComment::leading(comment, lines_before)); - lines_before = 0; - } else if piece.is_newline() { - lines_before += 1; - } - } - - // Format the skipped token trivia range - syntax_token_text_slice(token, skipped_trivia_range).fmt(f)?; - - // Find the start position of the next token's leading comments. - // The start is the first comment that is preceded by a line break. - let first_token_leading_comment = comments - .iter() - .position(|comment| comment.lines_before() > 0) - .unwrap_or(comments.len()); - - // Everything before the start position are trailing comments of the last skipped token - let token_leading_comments = comments.split_off(first_token_leading_comment); - let skipped_trailing_comments = comments; - - // Format the trailing comments of the last skipped token trivia - FormatTrailingTrivia::skipped(skipped_trailing_comments.into_iter()).fmt(f)?; - - // Ensure that there's some whitespace between the last skipped token trivia and the - // next token except if there was no whitespace present in the source. - if lines_before > 0 { - write!(f, [hard_line_break()])?; - } else if spaces > 0 { - write!(f, [space()])?; - }; - - // Write leading comments of the next token - FormatLeadingComments { - comments: &token_leading_comments, - lines_before_token: lines_before, - trim_mode, - } - .fmt(f)?; - - return Ok(()); - } - } - - FormatLeadingComments { - comments: &comments, - trim_mode, - lines_before_token: lines_before, - } - .fmt(f) -} - -struct FormatLeadingComments<'a, L> -where - L: Language, -{ - comments: &'a [SourceComment], - trim_mode: TriviaPrintMode, - lines_before_token: u32, -} - -impl Format for FormatLeadingComments<'_, L> -where - L: Language, - C: CstFormatContext, -{ - fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { - let mut first = true; - let mut last_inline_comment = f.state().is_last_content_inline_comment(); - - for (index, comment) in self.comments.iter().enumerate() { - if f.state().is_comment_formatted(comment.piece()) { - continue; - } - - let lines_after = self - .comments - .get(index + 1) - .map(|comment| comment.lines_before()) - .unwrap_or_else(|| match self.trim_mode { - TriviaPrintMode::Full => self.lines_before_token, - TriviaPrintMode::Trim => 0, - }); - - #[allow(deprecated)] - let comment_kind = f - .context() - .comment_style() - .get_comment_kind(comment.piece()); - - last_inline_comment = comment_kind.is_inline() && lines_after == 0; - - let format_content = format_with(|f| { - if comment.lines_before() > 0 && first { - write!(f, [hard_line_break()])?; - } else if !first { - write!(f, [space()])?; - }; - - let format_comment = - FormatRefWithRule::new(comment, C::LeadingCommentRule::default()); - - write!(f, [format_comment])?; - - match comment_kind { - CommentKind::Line => match lines_after { - 0 | 1 => write!(f, [hard_line_break()])?, - _ => write!(f, [empty_line()])?, - }, - CommentKind::InlineBlock | CommentKind::Block => { - match lines_after { - 0 => { - // space between last comment and token handled at the end. - // space between comments is inserted before each comment - } - 1 => write!(f, [hard_line_break()])?, - _ => write!(f, [empty_line()])?, - } - } - } - - Ok(()) - }); - - write!(f, [crate::comment(&format_content)])?; - first = false; - } - - f.state_mut() - .set_last_content_inline_comment(last_inline_comment); - - Ok(()) - } -} - -/// Formats the trailing trivia (comments) of a token -pub fn format_trailing_trivia( - token: &SyntaxToken, -) -> FormatTrailingTrivia> + Clone, L> { - let comments = token - .trailing_trivia() - .pieces() - .filter_map(|piece| piece.as_comments().map(SourceComment::trailing)); - - FormatTrailingTrivia::new(comments, token.kind()) -} - -#[derive(Debug, Copy, Clone)] -pub struct FormatTrailingTrivia -where - I: Iterator> + Clone, -{ - /// The comments to format - comments: I, - - /// The kind of the token of which the comments are the trailing trivia. - /// `Some(kind)` for a regular token. `None` for a skipped token trivia OR - token_kind: Option<::Kind>, - - skip_formatted_check: bool, -} - -impl FormatTrailingTrivia -where - I: Iterator> + Clone, -{ - pub fn new(comments: I, token_kind: L::Kind) -> Self { - Self { - comments, - token_kind: Some(token_kind), - skip_formatted_check: false, - } - } - - pub fn skipped(comments: I) -> Self { - Self { - comments, - token_kind: None, - skip_formatted_check: false, - } - } - - pub fn skip_formatted_check(mut self) -> Self { - self.skip_formatted_check = true; - self - } -} - -impl Format for FormatTrailingTrivia -where - I: Iterator> + Clone, - C: CstFormatContext, -{ - fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { - let comments = self.comments.clone(); - let mut last_inline_comment = f.state().is_last_content_inline_comment(); - - for (index, comment) in comments.enumerate() { - if !self.skip_formatted_check && f.state().is_comment_formatted(comment.piece()) { - continue; - } - - #[allow(deprecated)] - let kind = f - .context() - .comment_style() - .get_comment_kind(comment.piece()); - last_inline_comment = kind.is_inline(); - let is_single_line = kind.is_line(); - - let content = format_with(|f: &mut Formatter| { - if !is_single_line { - match self.token_kind { - // Don't write a space if this is a group start token and it isn't the first trailing comment - #[allow(deprecated)] - Some(token) - if f.context().comment_style().is_group_start_token(token) - && index == 0 => {} - // Write a space for all other cases - _ => space().fmt(f)?, - } - comment.piece().fmt(f) - } else { - write![ - f, - [ - line_suffix(&format_args![space(), comment.piece()]), - expand_parent() - ] - ] - } - }); - - crate::comment(&content).fmt(f)?; - } - - f.state_mut() - .set_last_content_inline_comment(last_inline_comment); - - Ok(()) - } -} diff --git a/crates/rome_formatter/src/trivia.rs b/crates/rome_formatter/src/trivia.rs new file mode 100644 index 00000000000..1a51bb41414 --- /dev/null +++ b/crates/rome_formatter/src/trivia.rs @@ -0,0 +1,582 @@ +//! Provides builders for comments and skipped token trivia. + +use crate::prelude::*; +use crate::{ + comments::{CommentKind, CommentStyle}, + write, Argument, Arguments, CstFormatContext, FormatRefWithRule, GroupId, SourceComment, + TextRange, VecBuffer, +}; +use rome_rowan::{Language, SyntaxNode, SyntaxToken}; + +/// Formats the leading comments of `node` +pub const fn format_leading_comments( + node: &SyntaxNode, +) -> FormatLeadingComments { + FormatLeadingComments::Node(node) +} + +/// Formats the leading comments of a node. +#[derive(Debug, Copy, Clone)] +pub enum FormatLeadingComments<'a, L: Language> { + Node(&'a SyntaxNode), + Comments(&'a [SourceComment]), +} + +impl Format for FormatLeadingComments<'_, Context::Language> +where + Context: CstFormatContext, +{ + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let comments = f.context().comments().clone(); + + let leading_comments = match self { + FormatLeadingComments::Node(node) => comments.leading_comments(node), + FormatLeadingComments::Comments(comments) => comments, + }; + + for comment in leading_comments { + let format_comment = FormatRefWithRule::new(comment, Context::CommentRule::default()); + write!(f, [format_comment])?; + + match comment.kind() { + CommentKind::Block | CommentKind::InlineBlock => { + match comment.lines_after() { + 0 => write!(f, [space()])?, + 1 => { + if comment.lines_before() == 0 { + write!(f, [soft_line_break_or_space()])?; + } else { + write!(f, [hard_line_break()])?; + } + } + _ => write!(f, [empty_line()])?, + }; + } + CommentKind::Line => match comment.lines_after() { + 0 | 1 => write!(f, [hard_line_break()])?, + _ => write!(f, [empty_line()])?, + }, + } + } + + Ok(()) + } +} + +/// Formats the trailing comments of `node`. +pub const fn format_trailing_comments( + node: &SyntaxNode, +) -> FormatTrailingComments { + FormatTrailingComments::Node(node) +} + +/// Formats the trailing comments of `node` +#[derive(Debug, Clone, Copy)] +pub enum FormatTrailingComments<'a, L: Language> { + Node(&'a SyntaxNode), + Comments(&'a [SourceComment]), +} + +impl Format for FormatTrailingComments<'_, Context::Language> +where + Context: CstFormatContext, +{ + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let comments = f.context().comments().clone(); + let trailing_comments = match self { + FormatTrailingComments::Node(node) => comments.trailing_comments(node), + FormatTrailingComments::Comments(comments) => comments, + }; + + let mut total_lines_before = 0; + + for comment in trailing_comments { + total_lines_before += comment.lines_before(); + + let format_comment = FormatRefWithRule::new(comment, Context::CommentRule::default()); + + // This allows comments at the end of nested structures: + // { + // x: 1, + // y: 2 + // // A comment + // } + // Those kinds of comments are almost always leading comments, but + // here it doesn't go "outside" the block and turns it into a + // trailing comment for `2`. We can simulate the above by checking + // if this a comment on its own line; normal trailing comments are + // always at the end of another expression. + if total_lines_before > 0 { + write!( + f, + [ + line_suffix(&format_with(|f| { + match comment.lines_before() { + 0 | 1 => write!(f, [hard_line_break()])?, + _ => write!(f, [empty_line()])?, + }; + + write!(f, [format_comment]) + })), + expand_parent() + ] + )?; + } else { + let content = format_with(|f| write!(f, [space(), format_comment])); + if comment.kind().is_line() { + write!(f, [line_suffix(&content), expand_parent()])?; + } else { + write!(f, [content])?; + } + } + } + + Ok(()) + } +} + +/// Formats the dangling comments of `node`. +pub const fn format_dangling_comments( + node: &SyntaxNode, +) -> FormatDanglingComments { + FormatDanglingComments::Node { + node, + indent: DanglingIndentMode::None, + } +} + +/// Formats the dangling trivia of `token`. +pub enum FormatDanglingComments<'a, L: Language> { + Node { + node: &'a SyntaxNode, + indent: DanglingIndentMode, + }, + Comments { + comments: &'a [SourceComment], + indent: DanglingIndentMode, + }, +} + +#[derive(Copy, Clone, Debug)] +pub enum DanglingIndentMode { + /// Writes every comment on its own line and indents them with a block indent. + /// + /// # Examples + /// ```ignore + /// [ + /// /* comment */ + /// ] + /// + /// [ + /// /* comment */ + /// /* multiple */ + /// ] + /// ``` + Block, + + /// Writes every comment on its own line and indents them with a soft line indent. + /// Guarantees to write a line break if the last formatted comment is a [line](CommentKind::Line) comment. + /// + /// # Examples + /// + /// ```ignore + /// [/* comment */] + /// + /// [ + /// /* comment */ + /// /* other */ + /// ] + /// + /// [ + /// // line + /// ] + /// ``` + Soft, + + /// Writes every comment on its own line. + None, +} + +impl FormatDanglingComments<'_, L> { + /// Indents the comments with a [block](DanglingIndentMode::Block) indent. + pub fn with_block_indent(self) -> Self { + self.with_indent_mode(DanglingIndentMode::Block) + } + + /// Indents the comments with a [soft block](DanglingIndentMode::Soft) indent. + pub fn with_soft_block_indent(self) -> Self { + self.with_indent_mode(DanglingIndentMode::Soft) + } + + fn with_indent_mode(mut self, mode: DanglingIndentMode) -> Self { + match &mut self { + FormatDanglingComments::Node { indent, .. } => *indent = mode, + FormatDanglingComments::Comments { indent, .. } => *indent = mode, + } + self + } + + const fn indent(&self) -> DanglingIndentMode { + match self { + FormatDanglingComments::Node { indent, .. } => *indent, + FormatDanglingComments::Comments { indent, .. } => *indent, + } + } +} + +impl Format for FormatDanglingComments<'_, Context::Language> +where + Context: CstFormatContext, +{ + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let comments = f.context().comments().clone(); + let dangling_comments = match self { + FormatDanglingComments::Node { node, .. } => comments.dangling_comments(node), + FormatDanglingComments::Comments { comments, .. } => *comments, + }; + + if dangling_comments.is_empty() { + return Ok(()); + } + + let format_dangling_comments = format_with(|f| { + // Write all comments up to the first skipped token trivia or the token + let mut join = f.join_with(hard_line_break()); + + for comment in dangling_comments { + let format_comment = + FormatRefWithRule::new(comment, Context::CommentRule::default()); + join.entry(&format_comment); + } + + join.finish()?; + + if matches!(self.indent(), DanglingIndentMode::Soft) + && dangling_comments + .last() + .map_or(false, |comment| comment.kind().is_line()) + { + write!(f, [hard_line_break()])?; + } + + Ok(()) + }); + + match self.indent() { + DanglingIndentMode::Block => { + write!(f, [block_indent(&format_dangling_comments)]) + } + DanglingIndentMode::Soft => { + write!(f, [group(&soft_block_indent(&format_dangling_comments))]) + } + DanglingIndentMode::None => { + write!(f, [format_dangling_comments]) + } + } + } +} + +/// Formats a token without its skipped token trivia +/// +/// ## Warning +/// It's your responsibility to format any skipped trivia. +pub const fn format_trimmed_token(token: &SyntaxToken) -> FormatTrimmedToken { + FormatTrimmedToken { token } +} + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub struct FormatTrimmedToken<'a, L: Language> { + token: &'a SyntaxToken, +} + +impl Format for FormatTrimmedToken<'_, L> +where + C: CstFormatContext, +{ + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let trimmed_range = self.token.text_trimmed_range(); + syntax_token_text_slice(self.token, trimmed_range).fmt(f) + } +} +/// Formats the skipped token trivia of a removed token and marks the token as tracked. +pub const fn format_removed(token: &SyntaxToken) -> FormatRemoved +where + L: Language, +{ + FormatRemoved { token } +} + +/// Formats the trivia of a token that is present in the source text but should be omitted in the +/// formatted output. +pub struct FormatRemoved<'a, L> +where + L: Language, +{ + token: &'a SyntaxToken, +} + +impl Format for FormatRemoved<'_, L> +where + L: Language + 'static, + C: CstFormatContext, +{ + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + f.state_mut().track_token(self.token); + + write!(f, [format_skipped_token_trivia(self.token)]) + } +} + +/// Print out a `token` from the original source with a different `content`. +/// +/// This will print the skipped token trivia that belong to `token` to `content`; +/// `token` is then marked as consumed by the formatter. +pub fn format_replaced<'a, 'content, L, Context>( + token: &'a SyntaxToken, + content: &'content impl Format, +) -> FormatReplaced<'a, 'content, L, Context> +where + L: Language, +{ + FormatReplaced { + token, + content: Argument::new(content), + } +} + +/// Formats a token's skipped token trivia but uses the provided content instead +/// of the token in the formatted output. +#[derive(Copy, Clone)] +pub struct FormatReplaced<'a, 'content, L, C> +where + L: Language, +{ + token: &'a SyntaxToken, + content: Argument<'content, C>, +} + +impl Format for FormatReplaced<'_, '_, L, C> +where + L: Language + 'static, + C: CstFormatContext, +{ + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + f.state_mut().track_token(self.token); + + write!(f, [format_skipped_token_trivia(self.token)])?; + + f.write_fmt(Arguments::from(&self.content)) + } +} + +/// Formats the given token only if the group does break and otherwise retains the token's skipped token trivia. +pub fn format_only_if_breaks<'a, 'content, L, Content, Context>( + token: &'a SyntaxToken, + content: &'content Content, +) -> FormatOnlyIfBreaks<'a, 'content, L, Context> +where + L: Language, + Content: Format, +{ + FormatOnlyIfBreaks { + token, + content: Argument::new(content), + group_id: None, + } +} + +/// Formats a token with its skipped token trivia that only gets printed if its enclosing +/// group does break but otherwise gets omitted from the formatted output. +pub struct FormatOnlyIfBreaks<'a, 'content, L, C> +where + L: Language, +{ + token: &'a SyntaxToken, + content: Argument<'content, C>, + group_id: Option, +} + +impl<'a, 'content, L, C> FormatOnlyIfBreaks<'a, 'content, L, C> +where + L: Language, +{ + pub fn with_group_id(mut self, group_id: Option) -> Self { + self.group_id = group_id; + self + } +} + +impl Format for FormatOnlyIfBreaks<'_, '_, L, C> +where + L: Language + 'static, + C: CstFormatContext, +{ + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + write!( + f, + [ + if_group_breaks(&Arguments::from(&self.content)).with_group_id(self.group_id), + // Print the trivia otherwise + if_group_fits_on_line(&format_skipped_token_trivia(self.token)) + .with_group_id(self.group_id), + ] + ) + } +} + +/// Formats the skipped token trivia of `token`. +pub const fn format_skipped_token_trivia( + token: &SyntaxToken, +) -> FormatSkippedTokenTrivia { + FormatSkippedTokenTrivia { token } +} + +/// Formats the skipped token trivia of `token`. +pub struct FormatSkippedTokenTrivia<'a, L: Language> { + token: &'a SyntaxToken, +} + +impl FormatSkippedTokenTrivia<'_, L> { + #[cold] + fn fmt_skipped(&self, f: &mut Formatter) -> FormatResult<()> + where + Context: CstFormatContext, + { + // Lines/spaces before the next token/comment + let (mut lines, mut spaces) = match self.token.prev_token() { + Some(token) => { + let mut lines = 0u32; + let mut spaces = 0u32; + for piece in token.trailing_trivia().pieces().rev() { + if piece.is_whitespace() { + spaces += 1; + } else if piece.is_newline() { + spaces = 0; + lines += 1; + } else { + break; + } + } + + (lines, spaces) + } + None => (0, 0), + }; + + // The comments between the last skipped token trivia and the token + let mut dangling_comments = Vec::new(); + let mut skipped_range: Option = None; + + // Iterate over the remaining pieces to find the full range from the first to the last skipped token trivia. + // Extract the comments between the last skipped token trivia and the token. + for piece in self.token.leading_trivia().pieces() { + if piece.is_whitespace() { + spaces += 1; + continue; + } + + if piece.is_newline() { + lines += 1; + spaces = 0; + } else if let Some(comment) = piece.as_comments() { + let source_comment = SourceComment { + kind: Context::Style::get_comment_kind(&comment), + lines_before: lines, + lines_after: 0, + piece: comment, + }; + + dangling_comments.push(source_comment); + + lines = 0; + spaces = 0; + } else if piece.is_skipped() { + skipped_range = Some(match skipped_range { + Some(range) => range.cover(piece.text_range()), + None => { + if dangling_comments.is_empty() { + match lines { + 0 if spaces == 0 => { + // Token had no space to previous token nor any preceding comment. Keep it that way + } + 0 => write!(f, [space()])?, + _ => write!(f, [hard_line_break()])?, + }; + } else { + match lines { + 0 => write!(f, [space()])?, + 1 => write!(f, [hard_line_break()])?, + _ => write!(f, [empty_line()])?, + }; + } + + piece.text_range() + } + }); + + lines = 0; + spaces = 0; + dangling_comments.clear(); + } + } + + let skipped_range = + skipped_range.unwrap_or_else(|| TextRange::empty(self.token.text_range().start())); + + let verbatim = { + let mut buffer = VecBuffer::new(f.state_mut()); + write!(buffer, [syntax_token_text_slice(self.token, skipped_range)])?; + + FormatElement::Verbatim(Verbatim::new_verbatim( + buffer.into_vec().into_boxed_slice(), + skipped_range.len(), + )) + }; + + f.write_element(verbatim)?; + + // Write whitespace separator between skipped/last comment and token + if dangling_comments.is_empty() { + match lines { + 0 if spaces == 0 => { + // Don't write a space if there was non in the source document + Ok(()) + } + 0 => write!(f, [space()]), + _ => write!(f, [hard_line_break()]), + } + } else { + match dangling_comments.first().unwrap().lines_before { + 0 => write!(f, [space()])?, + 1 => write!(f, [hard_line_break()])?, + _ => write!(f, [empty_line()])?, + } + + write!( + f, + [FormatDanglingComments::Comments { + comments: &dangling_comments, + indent: DanglingIndentMode::None + }] + )?; + + match lines { + 0 => write!(f, [space()]), + _ => write!(f, [hard_line_break()]), + } + } + } +} + +impl Format for FormatSkippedTokenTrivia<'_, Context::Language> +where + Context: CstFormatContext, +{ + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + if f.comments().has_skipped(self.token) { + self.fmt_skipped(f) + } else { + Ok(()) + } + } +} diff --git a/crates/rome_formatter/src/verbatim.rs b/crates/rome_formatter/src/verbatim.rs index dcf35785335..782503d22c1 100644 --- a/crates/rome_formatter/src/verbatim.rs +++ b/crates/rome_formatter/src/verbatim.rs @@ -1,5 +1,5 @@ -use crate::cst::{FormatLeadingComments, FormatTrailingComments}; use crate::prelude::*; +use crate::trivia::{FormatLeadingComments, FormatTrailingComments}; use crate::VecBuffer; use crate::{write, CstFormatContext}; use rome_rowan::{Direction, Language, SyntaxElement, SyntaxNode, TextRange};