Skip to content
67 changes: 65 additions & 2 deletions src/comment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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()
Expand All @@ -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 {
(
Expand Down Expand Up @@ -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<usize>;
fn find_last_uncommented(&self, pat: &str) -> Option<usize>;
Expand Down
195 changes: 195 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// rustfmt-normalize_comments: true

/*!
* ```
* // foo
* ```
*/

/**
* ```
* // bar
* ```
*/
struct Bar;

/*
* ```
* // baz
* ```
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// rustfmt-normalize_comments: true

/*!
```
// foo
/// BAR
```
*/

/**
// MEOW
```
// bar
```
*/
struct Bar;

/*
```
// baz
/// FOO
```
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// rustfmt-normalize_comments: true

//! ```
//! // foo
//! ```
/// ```
/// // bar
/// ```
struct Bar;

// ```
// // baz
// ```
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// rustfmt-normalize_comments: true

//! ```
//! // foo
//! /// BAR
//! ```
/// MEOW
/// ```
/// // bar
/// ```
struct Bar;

// ```
// // baz
// /// FOO
// ```
Loading