use rustc_ast::MacCall;
use rustc_ast::token::TokenKind;
use rustc_ast::tokenstream::TokenTree;
use rustc_span::Span;
use rustc_span::source_map::SourceMap;
use crate::common::display_width;
use crate::literal_scan::take_string_escape;
const INDENT_STEP: &str = " ";
pub(super) fn build_fold_suggestion(
source_map: &SourceMap,
mac_call: &MacCall,
template_span: Span,
max_line_width: usize,
) -> Option<(Span, String)> {
let open_span = mac_call.args.dspan.open;
let close_span = mac_call.args.dspan.close;
let call_span = mac_call.path.span.to(mac_call.args.dspan.entire());
let location = source_map.lookup_char_pos(call_span.lo());
let line_index = location.line.checked_sub(1)?;
let line_text = location.file.get_line(line_index)?;
if display_width(&line_text) <= max_line_width {
return None;
}
let indent_len = line_text.len() - line_text.trim_start().len();
let base_indent = line_text[..indent_len].to_owned();
let inner_indent = format!("{base_indent}{INDENT_STEP}");
let template_snippet = source_map.span_to_snippet(template_span).ok()?;
let body = template_snippet
.strip_prefix('"')
.and_then(|rest| rest.strip_suffix('"'))?;
let folded_body = fold_template_body(body, &inner_indent)?;
let folded_literal = format!(r#""{folded_body}""#);
let header = source_map
.span_to_snippet(call_span.with_hi(open_span.hi()))
.ok()?;
let footer = source_map.span_to_snippet(close_span).ok()?;
let inner = source_map
.span_to_snippet(open_span.between(close_span))
.ok()?;
let last_token = mac_call.args.tokens.iter().last()?;
let already_has_trailing_comma =
matches!(last_token, TokenTree::Token(token, _) if token.kind == TokenKind::Comma);
let inner = if already_has_trailing_comma {
inner
} else {
let comma_at = (last_token.span().hi().0.checked_sub(open_span.hi().0)?) as usize;
let (head, tail) = (inner.get(..comma_at)?, inner.get(comma_at..)?);
format!("{head},{tail}")
};
let template_lo = (template_span.lo().0.checked_sub(open_span.hi().0)?) as usize;
let template_hi = (template_span.hi().0.checked_sub(open_span.hi().0)?) as usize;
let before_template = inner.get(..template_lo)?;
let after_template = inner.get(template_hi..)?;
let mut new_inner = format!("{before_template}{folded_literal}{after_template}");
new_inner.truncate(new_inner.trim_end().len());
let replacement = format!("{header}\n{inner_indent}{new_inner}\n{base_indent}{footer}");
Some((call_span, replacement))
}
fn fold_template_body(body: &str, continuation_indent: &str) -> Option<String> {
let mut out = String::with_capacity(body.len());
let mut rest = body;
let mut folded = false;
while !rest.is_empty() {
if let Some((escape, remainder)) = take_string_escape(rest) {
let following_is_whitespace = remainder.chars().next().is_some_and(char::is_whitespace);
if escape == r"\n" && !remainder.is_empty() && !following_is_whitespace {
out.push_str("\\n\\\n");
out.push_str(continuation_indent);
folded = true;
} else {
out.push_str(escape);
}
rest = remainder;
continue;
}
let next = rest.chars().next()?;
let char_len = next.len_utf8();
out.push_str(&rest[..char_len]);
rest = &rest[char_len..];
}
folded.then_some(out)
}