diff --git a/.gitattributes b/.gitattributes index 4a60ba3165678..eaa0aff008d72 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ crates/oxc_formatter/tests/fixtures/** text=auto eol=lf +crates/oxc_formatter/tests/fixtures/js/crlf text=auto eol=crlf apps/oxfmt/test/fixtures/** text=auto eol=lf apps/oxlint/test/fixtures/** text=auto eol=lf diff --git a/crates/oxc_formatter/src/formatter/trivia.rs b/crates/oxc_formatter/src/formatter/trivia.rs index 547186bc0564a..bc376f9d49155 100644 --- a/crates/oxc_formatter/src/formatter/trivia.rs +++ b/crates/oxc_formatter/src/formatter/trivia.rs @@ -56,8 +56,10 @@ //! 2. Applies indentation based on container type (block, soft, none) //! 3. Preserves comment relationships and spacing //! 4. Advances cursor for processed comments +use oxc_allocator::StringBuilder; use oxc_ast::{Comment, CommentContent, CommentKind}; use oxc_span::Span; +use oxc_syntax::line_terminator::LineTerminatorSplitter; use crate::write; @@ -427,20 +429,32 @@ impl<'a> Format<'a> for FormatDanglingComments<'a> { impl<'a> Format<'a> for Comment { fn fmt(&self, f: &mut Formatter<'_, 'a>) { - let source_text = f.source_text().text_for(&self.span).trim_end(); - if is_alignable_comment(source_text) { - let mut lines = source_text.lines(); - - // `is_alignable_comment` only returns `true` for multiline comments - let first_line = lines.next().unwrap(); - write!(f, [text(first_line.trim_end())]); - - // Indent the remaining lines by one space so that all `*` are aligned. - for line in lines { - write!(f, [hard_line_break(), " ", text(line.trim())]); + let content = f.source_text().text_for(&self.span); + if content.bytes().any(|b| b == b'\n' || b == b'\r') { + let mut lines = LineTerminatorSplitter::new(content); + if is_alignable_comment(content) { + // `unwrap` is safe because `content` contains at least one line. + let first_line = lines.next().unwrap(); + write!(f, [text(first_line.trim_end())]); + + // Indent the remaining lines by one space so that all `*` are aligned. + for line in lines { + write!(f, [hard_line_break(), " ", text(line.trim())]); + } + } else { + // Normalize line endings `\r\n` to `\n` + let mut string = StringBuilder::with_capacity_in(content.len(), f.allocator()); + // `unwrap` is safe because `content` contains at least one line. + string.push_str(lines.next().unwrap().trim_end()); + + for str in lines { + string.push('\n'); + string.push_str(str); + } + write!(f, [text(string.into_str())]); } } else { - write!(f, [text(source_text)]); + write!(f, [text(content.trim_end())]); } } } @@ -475,11 +489,6 @@ impl<'a> Format<'a> for Comment { /// */ /// "#))); /// ``` -pub fn is_alignable_comment(source_text: &str) -> bool { - if !source_text.contains('\n') { - return false; - } - source_text.lines().enumerate().all(|(index, line)| { - if index == 0 { line.starts_with("/*") } else { line.trim_start().starts_with('*') } - }) +pub fn is_alignable_comment(lines: &str) -> bool { + LineTerminatorSplitter::new(lines).skip(1).all(|line| line.trim_start().starts_with('*')) } diff --git a/crates/oxc_formatter/tests/fixtures/js/crlf/block-comment.js b/crates/oxc_formatter/tests/fixtures/js/crlf/block-comment.js new file mode 100644 index 0000000000000..855eeb51aa0b7 --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/js/crlf/block-comment.js @@ -0,0 +1,13 @@ +/* +multi +line +block +*/ + +/** + * js doc + * @type {string} + * @description This is a description. + */ + +// single line comment diff --git a/crates/oxc_formatter/tests/fixtures/js/crlf/block-comment.js.snap b/crates/oxc_formatter/tests/fixtures/js/crlf/block-comment.js.snap new file mode 100644 index 0000000000000..6b8536622b0a4 --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/js/crlf/block-comment.js.snap @@ -0,0 +1,88 @@ +--- +source: crates/oxc_formatter/tests/fixtures/mod.rs +--- +==================== Input ==================== +/* +multi +line +block +*/ + +/** + * js doc + * @type {string} + * @description This is a description. + */ + +// single line comment + +==================== Output ==================== +----------------------------------- +{ endOfLine: "lf", printWidth: 80 } +----------------------------------- +/* +multi +line +block +*/ + +/** + * js doc + * @type {string} + * @description This is a description. + */ + +// single line comment + +------------------------------------ +{ endOfLine: "lf", printWidth: 100 } +------------------------------------ +/* +multi +line +block +*/ + +/** + * js doc + * @type {string} + * @description This is a description. + */ + +// single line comment + +------------------------------------- +{ endOfLine: "crlf", printWidth: 80 } +------------------------------------- +/* +multi +line +block +*/ + +/** + * js doc + * @type {string} + * @description This is a description. + */ + +// single line comment + +-------------------------------------- +{ endOfLine: "crlf", printWidth: 100 } +-------------------------------------- +/* +multi +line +block +*/ + +/** + * js doc + * @type {string} + * @description This is a description. + */ + +// single line comment + +===================== End ===================== diff --git a/crates/oxc_formatter/tests/fixtures/js/crlf/options.json b/crates/oxc_formatter/tests/fixtures/js/crlf/options.json new file mode 100644 index 0000000000000..a7fe4e5804fee --- /dev/null +++ b/crates/oxc_formatter/tests/fixtures/js/crlf/options.json @@ -0,0 +1,8 @@ +[ + { + "endOfLine": "lf" + }, + { + "endOfLine": "crlf" + } +] diff --git a/crates/oxc_formatter/tests/fixtures/mod.rs b/crates/oxc_formatter/tests/fixtures/mod.rs index 177adf301416e..3a2e4d4f3637b 100644 --- a/crates/oxc_formatter/tests/fixtures/mod.rs +++ b/crates/oxc_formatter/tests/fixtures/mod.rs @@ -3,7 +3,7 @@ use std::{env::current_dir, fs, path::Path}; use oxc_allocator::Allocator; use oxc_formatter::{ ArrowParentheses, BracketSameLine, BracketSpacing, FormatOptions, Formatter, IndentStyle, - IndentWidth, LineWidth, QuoteStyle, Semicolons, TrailingCommas, get_parse_options, + IndentWidth, LineEnding, LineWidth, QuoteStyle, Semicolons, TrailingCommas, get_parse_options, }; use oxc_parser::Parser; use oxc_span::SourceType; @@ -108,6 +108,16 @@ fn parse_format_options(json: &OptionSet) -> FormatOptions { options.bracket_same_line = BracketSameLine::from(b); } } + "endOfLine" => { + if let Some(s) = value.as_str() { + options.line_ending = match s { + "lf" => LineEnding::Lf, + "crlf" => LineEnding::Crlf, + "cr" => LineEnding::Cr, + _ => LineEnding::default(), + }; + } + } _ => {} } }