use super::content_tracking::{ContentEntry, content_signature, last_content_line_idx};
#[derive(Debug, Clone)]
pub(super) struct Comment {
pub(super) line: usize,
pub(super) text: String,
}
pub(super) fn find_comment_on_line(line: &str) -> Option<(usize, String)> {
let mut in_single = false;
let mut in_double = false;
let mut chars = line.char_indices();
while let Some((byte_pos, c)) = chars.next() {
match c {
'\'' if !in_double => {
in_single = !in_single;
}
'"' if !in_single => {
in_double = !in_double;
}
'\\' if in_double => {
chars.next();
}
'#' if !in_single && !in_double => {
let before = &line[..byte_pos];
if before.trim_end().is_empty() || before.ends_with(|c: char| c.is_whitespace()) {
return Some((byte_pos, line[byte_pos..].to_string()));
}
}
_ => {}
}
}
None
}
pub(super) fn extract_doc_prefix_comments(text: &str) -> Vec<Comment> {
let mut comments = Vec::new();
for (line_idx, line) in text.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Some((byte_pos, comment_text)) = find_comment_on_line(line) {
let before = &line[..byte_pos];
if before.trim().is_empty() {
comments.push(Comment {
line: line_idx,
text: comment_text,
});
continue;
}
}
break;
}
comments
}
pub(super) fn attach_comments(
original: &str,
formatted: &str,
comments: &[Comment],
last_content_hint: Option<usize>,
) -> String {
let line_to_comment: std::collections::HashMap<usize, &Comment> =
comments.iter().map(|c| (c.line, c)).collect();
let last_content_idx = last_content_line_idx(original, &line_to_comment)
.map(|t| last_content_hint.map_or(t, |h| t.max(h)))
.or(last_content_hint);
let mut entries: Vec<ContentEntry> = Vec::new();
let mut pending_leading: Vec<String> = Vec::new();
let mut pending_blanks: usize = 0;
let mut first_entry = true;
for (idx, line) in original.lines().enumerate() {
if let Some(comment) = line_to_comment.get(&idx) {
if pending_blanks > 0 {
pending_leading.push(String::new());
}
pending_blanks = 0;
pending_leading.push(comment.text.clone());
} else if line.is_empty() {
pending_blanks += 1;
} else if line.trim_start().starts_with('#')
&& last_content_idx.is_some_and(|last| idx > last)
{
if pending_blanks > 0 {
pending_leading.push(String::new());
}
pending_blanks = 0;
pending_leading.push(line.trim().to_string());
} else {
entries.push(ContentEntry {
signature: content_signature(line),
blank_lines_before: if first_entry {
0
} else {
pending_blanks.min(1)
},
leading: std::mem::take(&mut pending_leading),
});
first_entry = false;
pending_blanks = 0;
}
}
let trailing_leading = pending_leading;
let mut result_lines: Vec<String> = Vec::new();
let mut entry_iter = entries.iter();
let mut next_entry = entry_iter.next();
for fmt_line in formatted.lines() {
let fmt_sig = content_signature(fmt_line);
if fmt_sig.is_empty() {
if matches!(next_entry, Some(e) if e.signature.is_empty()) {
if let Some(e) = next_entry {
if e.blank_lines_before > 0 {
result_lines.push(String::new());
}
}
next_entry = entry_iter.next();
}
result_lines.push(fmt_line.to_string());
continue;
}
let mut carried_blanks = 0usize;
while matches!(next_entry, Some(e) if e.signature.is_empty()) {
if let Some(e) = next_entry {
carried_blanks = carried_blanks.max(e.blank_lines_before);
}
next_entry = entry_iter.next();
}
if let Some(entry) = next_entry {
if entry.signature == fmt_sig {
let indent_len = fmt_line.len() - fmt_line.trim_start().len();
let indent_str = " ".repeat(indent_len);
let last_is_blank = result_lines.last().is_some_and(String::is_empty);
if (entry.blank_lines_before > 0 || carried_blanks > 0) && !last_is_blank {
result_lines.push(String::new());
}
for lc in &entry.leading {
if lc.is_empty() {
result_lines.push(String::new());
} else {
result_lines.push(format!("{indent_str}{lc}"));
}
}
result_lines.push(fmt_line.to_string());
next_entry = entry_iter.next();
continue;
}
}
result_lines.push(fmt_line.to_string());
}
for lc in &trailing_leading {
if lc.is_empty() {
result_lines.push(String::new());
} else {
result_lines.push(lc.clone());
}
}
let mut out = result_lines.join("\n");
if !out.ends_with('\n') {
out.push('\n');
}
out
}