diff --git a/src/comment.rs b/src/comment.rs index 241934a7d3d..2c37acf9a78 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -11,7 +11,7 @@ use crate::rewrite::{RewriteContext, RewriteErrorExt, RewriteResult}; use crate::shape::{Indent, Shape}; use crate::string::{StringFormat, rewrite_string}; use crate::utils::{ - count_newlines, first_line_width, last_line_width, trim_left_preserve_layout, + CodeBlockTracker, count_newlines, first_line_width, last_line_width, trim_left_preserve_layout, trimmed_last_line_width, unicode_str_width, }; use crate::{ErrorKind, FormattingError}; @@ -908,6 +908,7 @@ fn rewrite_comment_inner( let mut rewriter = CommentRewrite::new(orig, block_style, shape, config); let line_breaks = count_newlines(orig.trim_end()); + let mut code_blocker_tracker = CodeBlockTracker::default(); let lines = orig .lines() .enumerate() @@ -920,7 +921,16 @@ fn rewrite_comment_inner( line }) - .map(|s| left_trim_comment_line(s, &style)) + .map(move |line| { + code_blocker_tracker = code_blocker_tracker.next_line(line); + match code_blocker_tracker { + CodeBlockTracker::Outside + | CodeBlockTracker::Opener + | CodeBlockTracker::Closer + | CodeBlockTracker::SingleLineCodeBlock => left_trim_comment_line(line, &style), + CodeBlockTracker::Inside => left_trim_comment_code_line(line, &style), + } + }) .map(|(line, has_leading_whitespace)| { if orig.starts_with("/*") && line_breaks == 0 { ( @@ -1118,6 +1128,59 @@ fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a s } } +/// Trims the beginning of a comment's opener or line start, leaving the rest untouched. +/// If at least one whitespace is trimmed, the second element of the tuple is true. +/// Will only ever trim one whitespace unless a custom comment style is used. +fn left_trim_comment_code_line<'a>(line: &'a str, style: &CommentStyle<'_>) -> (&'a str, bool) { + enum TrimLeftCodeLine<'a> { + Trimmed(&'a str), + Unmodified(&'a str), + } + fn trim_left_doc_code<'a>(line: &'a str, pat: &'_ str) -> TrimLeftCodeLine<'a> { + if let Some(new_line_segment) = line.strip_prefix(pat) { + TrimLeftCodeLine::Trimmed(new_line_segment) + } else { + TrimLeftCodeLine::Unmodified(line) + } + } + let opener = style.opener(); + match style { + CommentStyle::DoubleSlash | CommentStyle::TripleSlash | CommentStyle::Doc => { + match trim_left_doc_code(line, opener) { + TrimLeftCodeLine::Trimmed(line) => (line, true), + TrimLeftCodeLine::Unmodified(line) => { + match trim_left_doc_code(line, opener.trim_end()) { + TrimLeftCodeLine::Trimmed(line) | TrimLeftCodeLine::Unmodified(line) => { + (line, false) + } + } + } + } + } + CommentStyle::SingleBullet | CommentStyle::DoubleBullet | CommentStyle::Exclamation => { + match trim_left_doc_code(line, opener) { + TrimLeftCodeLine::Trimmed(line) => (line, true), + TrimLeftCodeLine::Unmodified(line) => { + match trim_left_doc_code(line, style.line_start().trim_start()) { + TrimLeftCodeLine::Trimmed(line) => (line, true), + TrimLeftCodeLine::Unmodified(line) => (line, false), + } + } + } + } + CommentStyle::Custom(_) => match trim_left_doc_code(line, opener) { + TrimLeftCodeLine::Trimmed(line) => (line, opener.ends_with(' ')), + TrimLeftCodeLine::Unmodified(line) => { + match trim_left_doc_code(line, opener.trim_end()) { + TrimLeftCodeLine::Trimmed(line) | TrimLeftCodeLine::Unmodified(line) => { + (line, false) + } + } + } + }, + } +} + pub(crate) trait FindUncommented { fn find_uncommented(&self, pat: &str) -> Option; fn find_last_uncommented(&self, pat: &str) -> Option; diff --git a/src/utils.rs b/src/utils.rs index b676803379f..34ae14ad288 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -711,6 +711,106 @@ pub(crate) fn unicode_str_width(s: &str) -> usize { s.width() } +/// Checks whether we are in a code block, +/// and if we are, where inside the code block. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CodeBlockTracker { + /// Not inside a code block. + #[default] + Outside, + /// Code block opener and closer are on the same line. + /// + /// Ex. + /// ```text + /// // ```type SomeType = usize;``` + /// ``` + SingleLineCodeBlock, + /// Line opener to a code block. + Opener, + /// Inside a code block (excluding opener and closer). + Inside, + /// Line closer to a code block. + Closer, +} + +impl CodeBlockTracker { + /// Reads the next line of a comment and + /// updates the code block tracker accordingly. + /// + /// This function only cares about the last state of the line, + /// for example: + /// ```text + /// "```let i = 1;``` ```let i = 2;``` ```" + /// ``` + /// The above line will be considered an opener and not single line code block + /// because the last code block opener/closer is an opener to a new code block. + pub(crate) fn next_line(self, line: &str) -> Self { + let code_block_matches = line.matches("```").count(); + // Check if a code block is opened or closed, + // and if opened, not closed on the same line, or vice versa. + if code_block_matches != 0 { + if code_block_matches % 2 == 1 { + match self { + CodeBlockTracker::Outside + | CodeBlockTracker::Closer + | CodeBlockTracker::SingleLineCodeBlock => { + // If we were outside a code block or a code block was previously closed, + // and now we detect another code block opener/closer, then + // this is an opener to a code block. + CodeBlockTracker::Opener + } + CodeBlockTracker::Inside | CodeBlockTracker::Opener => { + // If we were inside a code block or a code block was previously opened, + // and now we detect another code block opener/closer, then + // this is a closer to a code block. + CodeBlockTracker::Closer + } + } + } else { + // Detected a code block opener and closer. + match self { + CodeBlockTracker::Outside + | CodeBlockTracker::Inside + | CodeBlockTracker::SingleLineCodeBlock => { + // If previously detected outside, inside, or a single line code block, + // and now we detect an opener and closer, + // we are in a single line code block. + CodeBlockTracker::SingleLineCodeBlock + } + CodeBlockTracker::Opener => { + // If previously detected a code block opener, + // and now we detect an opener and closer, + // then the last code block opener/closer is an opener. + CodeBlockTracker::Opener + } + CodeBlockTracker::Closer => { + // If previously detected a code block closer, + // and now we detect an opener and closer, + // then this is a single line code block. + CodeBlockTracker::SingleLineCodeBlock + } + } + } + } else { + // No code block opener/closer detected. + match self { + CodeBlockTracker::Opener => { + // If previously a code block opener was detected, + // now we are inside the code block. + CodeBlockTracker::Inside + } + CodeBlockTracker::Closer | CodeBlockTracker::SingleLineCodeBlock => { + // If previously a code block closer was detected, + // now we are outside the code block. + CodeBlockTracker::Outside + } + CodeBlockTracker::Outside => CodeBlockTracker::Outside, + CodeBlockTracker::Inside => CodeBlockTracker::Inside, + } + } + } +} + #[cfg(test)] mod test { use super::*; @@ -731,4 +831,99 @@ mod test { Some("aaa\n bbb\n ccc".to_string()) ); } + + #[test] + fn test_code_block_tracker_default() { + let code_blocker_tracker = CodeBlockTracker::default(); + assert_eq!(code_blocker_tracker, CodeBlockTracker::Outside); + } + + #[test] + fn test_code_block_tracker() { + let mut code_block_tracker = CodeBlockTracker::default(); + + code_block_tracker = + code_block_tracker.next_line("/// This is a comment before a code block!"); + assert_eq!(code_block_tracker, CodeBlockTracker::Outside); + + code_block_tracker = code_block_tracker.next_line("/// ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Opener); + + code_block_tracker = code_block_tracker.next_line("/// type SomeType = usize;"); + assert_eq!(code_block_tracker, CodeBlockTracker::Inside); + + code_block_tracker = code_block_tracker.next_line("/// ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Closer); + + code_block_tracker = + code_block_tracker.next_line("/// This is a comment after a code block!"); + assert_eq!(code_block_tracker, CodeBlockTracker::Outside); + } + + #[test] + fn test_code_block_tracker_single_line_code_block() { + let mut code_block_tracker = CodeBlockTracker::default(); + + code_block_tracker = + code_block_tracker.next_line("/// This is a comment before a code block!"); + assert_eq!(code_block_tracker, CodeBlockTracker::Outside); + + code_block_tracker = code_block_tracker.next_line("/// ```type SomeType = usize;```"); + assert_eq!(code_block_tracker, CodeBlockTracker::SingleLineCodeBlock); + + code_block_tracker = + code_block_tracker.next_line("/// Ex. ``````// ```type SomeType = usize;``` ``````"); + assert_eq!(code_block_tracker, CodeBlockTracker::SingleLineCodeBlock); + + code_block_tracker = + code_block_tracker.next_line("/// This is a comment after a code block!"); + assert_eq!(code_block_tracker, CodeBlockTracker::Outside); + } + + #[test] + fn test_code_block_tracker_multiple_code_blocks() { + let mut code_block_tracker = CodeBlockTracker::default(); + + code_block_tracker = + code_block_tracker.next_line("/// This is a comment before a code block!"); + assert_eq!(code_block_tracker, CodeBlockTracker::Outside); + + code_block_tracker = code_block_tracker.next_line("/// ```type SomeType = usize;```"); + assert_eq!(code_block_tracker, CodeBlockTracker::SingleLineCodeBlock); + + code_block_tracker = code_block_tracker.next_line("/// ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Opener); + + code_block_tracker = code_block_tracker.next_line("/// ``` In between code blocks! ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Opener); + + code_block_tracker = code_block_tracker.next_line("/// type Meow = f32;"); + assert_eq!(code_block_tracker, CodeBlockTracker::Inside); + + code_block_tracker = code_block_tracker.next_line("/// ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Closer); + + code_block_tracker = code_block_tracker.next_line("/// ```type CatsOuttaTheBag = f64;```"); + assert_eq!(code_block_tracker, CodeBlockTracker::SingleLineCodeBlock); + + code_block_tracker = code_block_tracker.next_line("/// ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Opener); + + code_block_tracker = code_block_tracker.next_line("/// ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Closer); + + code_block_tracker = code_block_tracker + .next_line("/// ```type CatsOuttaTheHome = bool;``` ```type DogsInTheHouse = i64"); + assert_eq!(code_block_tracker, CodeBlockTracker::Opener); + + code_block_tracker = code_block_tracker.next_line("/// ``` ```let me = \"YOU\";``` ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Opener); + + code_block_tracker = code_block_tracker.next_line("/// ```"); + assert_eq!(code_block_tracker, CodeBlockTracker::Closer); + + code_block_tracker = + code_block_tracker.next_line("/// This is a comment after a code block!"); + assert_eq!(code_block_tracker, CodeBlockTracker::Outside); + } } diff --git a/tests/source/issue-6631/normalize_preserve_doc_code_comments_with_star.rs b/tests/source/issue-6631/normalize_preserve_doc_code_comments_with_star.rs new file mode 100644 index 00000000000..e23db69f178 --- /dev/null +++ b/tests/source/issue-6631/normalize_preserve_doc_code_comments_with_star.rs @@ -0,0 +1,20 @@ +// rustfmt-normalize_comments: true + +/*! + * ``` + * // foo + * ``` + */ + +/** + * ``` + * // bar + * ``` + */ +struct Bar; + +/* + * ``` + * // baz + * ``` + */ diff --git a/tests/source/issue-6631/normalize_preserve_doc_code_comments_without_star.rs b/tests/source/issue-6631/normalize_preserve_doc_code_comments_without_star.rs new file mode 100644 index 00000000000..c7b2b37b7fe --- /dev/null +++ b/tests/source/issue-6631/normalize_preserve_doc_code_comments_without_star.rs @@ -0,0 +1,23 @@ +// rustfmt-normalize_comments: true + +/*! +``` +// foo +/// BAR +``` +*/ + +/** +// MEOW +``` +// bar +``` +*/ +struct Bar; + +/* +``` +// baz +/// FOO +``` +*/ diff --git a/tests/target/issue-6631/normalize_preserve_doc_code_comments_with_star.rs b/tests/target/issue-6631/normalize_preserve_doc_code_comments_with_star.rs new file mode 100644 index 00000000000..cabfc401be3 --- /dev/null +++ b/tests/target/issue-6631/normalize_preserve_doc_code_comments_with_star.rs @@ -0,0 +1,14 @@ +// rustfmt-normalize_comments: true + +//! ``` +//! // foo +//! ``` + +/// ``` +/// // bar +/// ``` +struct Bar; + +// ``` +// // baz +// ``` diff --git a/tests/target/issue-6631/normalize_preserve_doc_code_comments_without_star.rs b/tests/target/issue-6631/normalize_preserve_doc_code_comments_without_star.rs new file mode 100644 index 00000000000..4da7da2bcca --- /dev/null +++ b/tests/target/issue-6631/normalize_preserve_doc_code_comments_without_star.rs @@ -0,0 +1,17 @@ +// rustfmt-normalize_comments: true + +//! ``` +//! // foo +//! /// BAR +//! ``` + +/// MEOW +/// ``` +/// // bar +/// ``` +struct Bar; + +// ``` +// // baz +// /// FOO +// ```