Skip to content

Commit

Permalink
Leverage itemized blocks to support formatting markdown block quotes
Browse files Browse the repository at this point in the history
Fixes 5157

Doc comments support markdown, but rustfmt didn't previously assign any
semantic value to leading '> ' in comments. This lead to poor formatting
when using ``wrap_comments=true``.

Now, rustfmt treats block quotes as itemized blocks, which greatly
improves how block quotes are formatted when ``wrap_comments=true``.
  • Loading branch information
ytmimi authored and calebcartwright committed Feb 11, 2022
1 parent b05b313 commit 1e78a2b
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 7 deletions.
46 changes: 39 additions & 7 deletions src/comment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,36 +432,49 @@ impl CodeBlockAttribute {

/// Block that is formatted as an item.
///
/// An item starts with either a star `*` or a dash `-`. Different level of indentation are
/// handled by shrinking the shape accordingly.
/// An item starts with either a star `*` a dash `-` or a greater-than `>`.
/// Different level of indentation are handled by shrinking the shape accordingly.
struct ItemizedBlock {
/// the lines that are identified as part of an itemized block
lines: Vec<String>,
/// the number of whitespaces up to the item sigil
/// the number of characters (typically whitespaces) up to the item sigil
indent: usize,
/// the string that marks the start of an item
opener: String,
/// sequence of whitespaces to prefix new lines that are part of the item
/// sequence of characters (typically whitespaces) to prefix new lines that are part of the item
line_start: String,
}

impl ItemizedBlock {
/// Returns `true` if the line is formatted as an item
fn is_itemized_line(line: &str) -> bool {
let trimmed = line.trim_start();
trimmed.starts_with("* ") || trimmed.starts_with("- ")
trimmed.starts_with("* ") || trimmed.starts_with("- ") || trimmed.starts_with("> ")
}

/// Creates a new ItemizedBlock described with the given line.
/// The `is_itemized_line` needs to be called first.
fn new(line: &str) -> ItemizedBlock {
let space_to_sigil = line.chars().take_while(|c| c.is_whitespace()).count();
let indent = space_to_sigil + 2;
// +2 = '* ', which will add the appropriate amount of whitespace to keep itemized
// content formatted correctly.
let mut indent = space_to_sigil + 2;
let mut line_start = " ".repeat(indent);

// Markdown blockquote start with a "> "
if line.trim_start().starts_with(">") {
// remove the original +2 indent because there might be multiple nested block quotes
// and it's easier to reason about the final indent by just taking the length
// of th new line_start. We update the indent because it effects the max width
// of each formatted line.
line_start = itemized_block_quote_start(line, line_start, 2);
indent = line_start.len();
}
ItemizedBlock {
lines: vec![line[indent..].to_string()],
indent,
opener: line[..indent].to_string(),
line_start: " ".repeat(indent),
line_start,
}
}

Expand Down Expand Up @@ -504,6 +517,25 @@ impl ItemizedBlock {
}
}

/// Determine the line_start when formatting markdown block quotes.
/// The original line_start likely contains indentation (whitespaces), which we'd like to
/// replace with '> ' characters.
fn itemized_block_quote_start(line: &str, mut line_start: String, remove_indent: usize) -> String {
let quote_level = line
.chars()
.take_while(|c| !c.is_alphanumeric())
.fold(0, |acc, c| if c == '>' { acc + 1 } else { acc });

for _ in 0..remove_indent {
line_start.pop();
}

for _ in 0..quote_level {
line_start.push_str("> ")
}
line_start
}

struct CommentRewrite<'a> {
result: String,
code_block_buffer: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// rustfmt-wrap_comments: true

/// > For each sample received, the middleware internally maintains a sample_state relative to each DataReader. The sample_state can either be READ or NOT_READ.
fn block_quote() {}
10 changes: 10 additions & 0 deletions tests/source/issue-5157/nested_itemized_markdown_blockquote.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// rustfmt-wrap_comments: true

/// > For each sample received, the middleware internally maintains a sample_state relative to each DataReader. The sample_state can either be READ or NOT_READ.
///
/// > > For each sample received, the middleware internally maintains a sample_state relative to each DataReader. The sample_state can either be READ or NOT_READ.
///
/// > > > For each sample received, the middleware internally maintains a sample_state relative to each DataReader. The sample_state can either be READ or NOT_READ.
///
/// > > > > > > > > For each sample received, the middleware internally maintains a sample_state relative to each DataReader. The sample_state can either be READ or NOT_READ.
fn block_quote() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// rustfmt-wrap_comments: true

/// > For each sample received, the middleware internally maintains a sample_state relative to each DataReader. The sample_state can either be READ or NOT_READ.
fn block_quote() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// rustfmt-wrap_comments: true

/// > For each sample received, the middleware internally maintains a
/// > sample_state relative to each DataReader. The sample_state can
/// > either be READ or NOT_READ.
fn block_quote() {}
18 changes: 18 additions & 0 deletions tests/target/issue-5157/nested_itemized_markdown_blockquote.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// rustfmt-wrap_comments: true

/// > For each sample received, the middleware internally maintains a
/// > sample_state relative to each DataReader. The sample_state can either be
/// > READ or NOT_READ.
///
/// > > For each sample received, the middleware internally maintains a
/// > > sample_state relative to each DataReader. The sample_state can either be
/// > > READ or NOT_READ.
///
/// > > > For each sample received, the middleware internally maintains a
/// > > > sample_state relative to each DataReader. The sample_state can either
/// > > > be READ or NOT_READ.
///
/// > > > > > > > > For each sample received, the middleware internally
/// > > > > > > > > maintains a sample_state relative to each DataReader. The
/// > > > > > > > > sample_state can either be READ or NOT_READ.
fn block_quote() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// rustfmt-wrap_comments: true

/// > For each sample received, the middleware internally maintains a
/// > sample_state relative to each DataReader. The sample_state can either be
/// > READ or NOT_READ.
fn block_quote() {}

0 comments on commit 1e78a2b

Please sign in to comment.