use super::*;
pub(in crate::print) fn transform_assertion_paragraphs(block_lines: &[&str]) -> String {
let merged_owned: Vec<String>;
let block_lines: Vec<&str> = {
let mut out: Vec<String> = Vec::with_capacity(block_lines.len());
let mut i = 0;
let orig = block_lines;
while i < orig.len() {
let line = orig[i];
let trimmed = line.trim();
if trimmed == "..." && !out.is_empty() {
let mut last_idx = out.len();
while last_idx > 0 && out[last_idx - 1].trim().is_empty() {
last_idx -= 1;
}
if last_idx > 0 {
let last = &out[last_idx - 1];
let last_trimmed = last.trim();
if !last_trimmed.is_empty()
&& !last_trimmed.starts_with("--")
&& !last_trimmed.ends_with(" ...")
&& !last_trimmed.ends_with("->")
&& super::predicates::looks_like_assertion(last_trimmed)
{
out[last_idx - 1] = format!("{} ...", last.trim_end());
while out.len() > last_idx
&& out.last().is_some_and(|l| l.trim().is_empty())
{
out.pop();
}
let mut j = i + 1;
while j < orig.len() && orig[j].trim().is_empty() {
j += 1;
}
i = j;
continue;
}
}
}
out.push(line.to_string());
i += 1;
}
merged_owned = out;
merged_owned.iter().map(|s| s.as_str()).collect()
};
let block_lines: &[&str] = &block_lines;
if block_has_non_assertion_content(block_lines) {
return block_lines.join("\n");
}
if block_has_column_aligned_assertions(block_lines) {
return block_lines.join("\n");
}
let mut out = String::new();
let mut i = 0;
while i < block_lines.len() {
let line = block_lines[i];
if line.trim().is_empty() {
if i > 0 {
out.push('\n');
}
out.push_str(line);
i += 1;
continue;
}
let run_start = i;
let mut run_end = i;
loop {
let next = run_end + 1;
if next >= block_lines.len() {
break;
}
if !block_lines[next].trim().is_empty() {
run_end = next;
continue;
}
let last_trimmed = block_lines[run_end].trim();
if !last_trimmed.ends_with(" ...") {
break;
}
let mut j = next;
while j < block_lines.len() && block_lines[j].trim().is_empty() {
j += 1;
}
if j >= block_lines.len() {
break;
}
let next_trimmed = block_lines[j].trim();
if next_trimmed.starts_with("--") || !looks_like_assertion(next_trimmed) {
break;
}
run_end = j;
}
let first_indent = block_lines[run_start].len() - block_lines[run_start].trim_start().len();
let mut all_valid = true;
let mut assertion_count = 0usize;
let mut has_eq_or_comment_shape = false;
#[allow(clippy::needless_range_loop)]
for k in run_start..=run_end {
let l = block_lines[k];
if l.trim().is_empty() {
continue;
}
let indent = l.len() - l.trim_start().len();
if indent != first_indent {
all_valid = false;
break;
}
let trimmed = l.trim();
if trimmed.starts_with("--") {
} else if looks_like_assertion(trimmed) {
assertion_count += 1;
if trimmed.contains(" == ") || trimmed.contains(" -- ") {
has_eq_or_comment_shape = true;
}
} else {
all_valid = false;
break;
}
}
let last_is_assertion = {
let trimmed = block_lines[run_end].trim();
!trimmed.starts_with("--") && looks_like_assertion(trimmed)
};
let is_assertion_run =
all_valid && assertion_count >= 1 && last_is_assertion && has_eq_or_comment_shape;
let run_last_ends_with_dots = {
let mut idx = run_end;
while idx > run_start && block_lines[idx].trim().is_empty() {
idx -= 1;
}
block_lines[idx].trim().ends_with(" ...")
};
if is_assertion_run && run_last_ends_with_dots {
for (k, idx) in (run_start..=run_end).enumerate() {
if i > 0 || k > 0 {
out.push('\n');
}
out.push_str(block_lines[idx]);
}
i = run_end + 1;
continue;
}
if is_assertion_run {
let mut chains: Vec<(Vec<usize>, Vec<usize>)> = Vec::new();
let mut cur_comments: Vec<usize> = Vec::new();
let mut cur_assertions: Vec<usize> = Vec::new();
#[allow(clippy::needless_range_loop)]
for k in run_start..=run_end {
let trimmed = block_lines[k].trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with("--") {
cur_comments.push(k);
} else {
cur_assertions.push(k);
if !trimmed.ends_with(" ...") {
chains.push((
std::mem::take(&mut cur_comments),
std::mem::take(&mut cur_assertions),
));
}
}
}
if !cur_comments.is_empty() || !cur_assertions.is_empty() {
chains.push((cur_comments, cur_assertions));
}
for (chain_idx, (comments, assertions)) in chains.iter().enumerate() {
if chain_idx == 0 && i > 0 {
out.push('\n');
} else if chain_idx > 0 {
out.push_str("\n\n");
}
for &ci in comments {
out.push_str(block_lines[ci]);
out.push('\n');
}
if assertions.len() == 1 {
let l = block_lines[assertions[0]];
let indent_str = &l[..first_indent];
let content = &l[first_indent..];
let normalized = collapse_spaces_outside_strings(content);
let normalized = space_tight_binary_ops(&normalized);
let normalized = space_tight_tuples_lists(&normalized);
out.push_str(indent_str);
out.push_str(&normalized);
} else if !assertions.is_empty() {
let joined = assertions
.iter()
.map(|&idx| block_lines[idx].trim())
.collect::<Vec<_>>()
.join(" ");
let joined = collapse_spaces_outside_strings(&joined);
let joined = space_tight_binary_ops(&joined);
let joined = space_tight_tuples_lists(&joined);
let segments = split_at_chain_operators(&joined);
let indent_str = &block_lines[assertions[0]][..first_indent];
let cont_indent: String = std::iter::repeat_n(' ', first_indent + 4).collect();
out.push_str(indent_str);
if let Some(first) = segments.first() {
out.push_str(first);
}
for seg in segments.iter().skip(1) {
out.push('\n');
out.push_str(&cont_indent);
out.push_str(seg);
}
}
}
} else {
for (k, idx) in (run_start..=run_end).enumerate() {
if i > 0 || k > 0 {
out.push('\n');
}
out.push_str(block_lines[idx]);
}
}
i = run_end + 1;
}
out
}
pub(in crate::print) fn block_mixes_decls_and_bare_exprs(block_lines: &[&str]) -> bool {
let mut has_decl = false;
let mut has_bare = false;
let mut in_triple = false;
for &line in block_lines {
let was_in_triple = in_triple;
let triple_count = line.matches("\"\"\"").count();
if triple_count % 2 == 1 {
in_triple = !in_triple;
}
if was_in_triple {
continue;
}
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let leading = line.len() - line.trim_start().len();
if leading != 4 {
continue;
}
if trimmed.starts_with("--") {
continue;
}
if trimmed.starts_with("module ")
|| trimmed.starts_with("import ")
|| trimmed.starts_with("port module ")
|| trimmed.starts_with("effect module ")
{
continue;
}
if looks_like_code_block_decl(trimmed) {
has_decl = true;
} else if line_looks_like_bare_expression(trimmed) {
has_bare = true;
}
if has_decl && has_bare {
return true;
}
}
false
}
pub(in crate::print) fn block_looks_decl_only(block_lines: &[&str]) -> bool {
let mut has_decl = false;
let mut has_bare = false;
let mut in_triple = false;
for &line in block_lines {
let was_in_triple = in_triple;
let triple_count = line.matches("\"\"\"").count();
if triple_count % 2 == 1 {
in_triple = !in_triple;
}
if was_in_triple {
continue;
}
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with("--") {
continue;
}
let leading = line.len() - line.trim_start().len();
if leading != 4 {
continue;
}
let is_header = trimmed.starts_with("module ")
|| trimmed.starts_with("import ")
|| trimmed.starts_with("port module ")
|| trimmed.starts_with("effect module ");
if is_header || looks_like_code_block_decl(trimmed) {
has_decl = true;
} else if line_looks_like_bare_expression(trimmed) {
has_bare = true;
}
}
has_decl && !has_bare
}
pub(in crate::print) fn block_looks_bare_only(block_lines: &[&str]) -> bool {
let mut has_decl = false;
let mut has_bare = false;
let mut in_triple = false;
for &line in block_lines {
let was_in_triple = in_triple;
let triple_count = line.matches("\"\"\"").count();
if triple_count % 2 == 1 {
in_triple = !in_triple;
}
if was_in_triple {
continue;
}
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with("--") {
continue;
}
let leading = line.len() - line.trim_start().len();
if leading != 4 {
continue;
}
let is_header = trimmed.starts_with("module ")
|| trimmed.starts_with("import ")
|| trimmed.starts_with("port module ")
|| trimmed.starts_with("effect module ");
if is_header || looks_like_code_block_decl(trimmed) {
has_decl = true;
} else if line_looks_like_bare_expression(trimmed) {
has_bare = true;
}
}
has_bare && !has_decl
}
fn line_looks_like_bare_expression(trimmed: &str) -> bool {
if trimmed.starts_with("{-") {
return false;
}
let first = match trimmed.chars().next() {
Some(c) => c,
None => return false,
};
let starter_ok = first.is_ascii_alphabetic()
|| first.is_ascii_digit()
|| first == '_'
|| first == '('
|| first == '['
|| first == '{'
|| first == '\''
|| first == '"'
|| first == '\\'
|| first == '-';
if !starter_ok {
return false;
}
if first == '-' {
let second = trimmed.chars().nth(1);
match second {
Some(c) if c.is_ascii_digit() || c == '(' => {}
_ => return false,
}
}
let first_word_end = trimmed
.find(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '.')
.unwrap_or(trimmed.len());
let first_word = &trimmed[..first_word_end];
match first_word {
"type" | "port" | "module" | "import" | "exposing" | "effect" | "infix" => {
return false;
}
_ => {}
}
true
}
fn has_same_line_triple_string_rhs(trimmed: &str) -> bool {
if trimmed.matches("\"\"\"").count() < 2 {
return false;
}
let Some(eq) = trimmed.find("= \"\"\"") else {
return false;
};
if eq == 0 {
return false;
}
let prev = trimmed.as_bytes()[eq - 1];
if prev != b' ' {
return false;
}
true
}
pub(in crate::print) fn code_block_needs_reformat(block_lines: &[&str]) -> bool {
if block_mixes_decls_and_bare_exprs(block_lines) {
return false;
}
let mut count_non_4_aligned = 0usize;
let mut has_compact_syntax = false;
let mut has_single_line_decl = false;
let mut has_unsorted_import = false;
for &line in block_lines {
if line.trim().is_empty() {
continue;
}
let leading = line.len() - line.trim_start().len();
if leading > 4 && (leading - 4) % 4 != 0 {
count_non_4_aligned += 1;
}
if leading == 4 && import_has_unsorted_exposing(line.trim()) {
has_unsorted_import = true;
}
let trimmed = line.trim();
if trimmed.contains('[') && trimmed.contains(']') {
if trimmed.contains("[\"")
|| trimmed.contains("[(")
|| trimmed.contains("['")
|| trimmed.contains("[0")
|| trimmed.contains("[1")
|| trimmed.contains("[2")
|| trimmed.contains("[3")
|| trimmed.contains("[4")
|| trimmed.contains("[5")
|| trimmed.contains("[6")
|| trimmed.contains("[7")
|| trimmed.contains("[8")
|| trimmed.contains("[9")
{
has_compact_syntax = true;
}
}
if trimmed.contains('(') && trimmed.contains(',') && trimmed.contains(')') {
if has_compact_tuple(trimmed) {
has_compact_syntax = true;
}
}
if leading == 4
&& is_single_line_value_decl(trimmed)
&& !has_same_line_triple_string_rhs(trimmed)
{
has_single_line_decl = true;
}
if leading == 4
&& (trimmed.starts_with("type alias ") || trimmed.starts_with("type "))
&& trimmed.contains(" = ")
{
has_single_line_decl = true;
}
if leading == 4
&& trimmed.starts_with("{-|")
&& trimmed.ends_with("-}")
&& trimmed.len() > 5
{
has_single_line_decl = true;
}
if has_tight_binary_op(trimmed) {
has_compact_syntax = true;
}
if leading == 4 && is_redundant_paren_expr(trimmed) {
has_compact_syntax = true;
}
if line_has_unpadded_hex(trimmed) {
has_compact_syntax = true;
}
if line_has_sci_float_without_dot(trimmed) {
has_compact_syntax = true;
}
}
let has_indent_issues = count_non_4_aligned > 0;
let has_unseparated_assertions = block_has_unseparated_assertions(block_lines);
let has_single_line_if = block_has_single_line_if(block_lines);
if has_unseparated_assertions && block_has_column_aligned_assertions(block_lines) {
return false;
}
let other_reformat_signal = has_indent_issues
|| has_compact_syntax
|| has_single_line_decl
|| has_unsorted_import
|| has_single_line_if;
other_reformat_signal || has_unseparated_assertions
}
pub(in crate::print) fn code_block_has_narrow_indent(block_lines: &[&str]) -> bool {
for &line in block_lines {
if line.trim().is_empty() {
continue;
}
let leading = line.len() - line.trim_start().len();
if leading > 0 && leading < 4 {
return true;
}
if leading > 4 && leading < 8 && (leading - 4) % 4 != 0 {
return true;
}
}
false
}
pub(in crate::print) fn code_block_has_structural_reformat_signal(block_lines: &[&str]) -> bool {
if block_mixes_decls_and_bare_exprs(block_lines) {
return false;
}
if code_block_has_narrow_indent(block_lines) {
return true;
}
for &line in block_lines {
if line.trim().is_empty() {
continue;
}
let leading = line.len() - line.trim_start().len();
let trimmed = line.trim();
if leading == 4 && import_has_unsorted_exposing(trimmed) {
return true;
}
let is_header_line = trimmed.starts_with("import ")
|| trimmed.starts_with("module ")
|| trimmed.starts_with("port module ")
|| trimmed.starts_with("effect module ");
if is_header_line {
continue;
}
if trimmed.contains('[')
&& trimmed.contains(']')
&& (trimmed.contains("[\"")
|| trimmed.contains("[(")
|| trimmed.contains("['")
|| trimmed.contains("[0")
|| trimmed.contains("[1")
|| trimmed.contains("[2")
|| trimmed.contains("[3")
|| trimmed.contains("[4")
|| trimmed.contains("[5")
|| trimmed.contains("[6")
|| trimmed.contains("[7")
|| trimmed.contains("[8")
|| trimmed.contains("[9"))
{
return true;
}
if trimmed.contains('(')
&& trimmed.contains(',')
&& trimmed.contains(')')
&& has_compact_tuple(trimmed)
{
return true;
}
if leading == 4
&& is_single_line_value_decl(trimmed)
&& !has_same_line_triple_string_rhs(trimmed)
{
return true;
}
if leading == 4
&& (trimmed.starts_with("type alias ") || trimmed.starts_with("type "))
&& trimmed.contains(" = ")
{
return true;
}
if leading == 4
&& trimmed.starts_with("{-|")
&& trimmed.ends_with("-}")
&& trimmed.len() > 5
{
return true;
}
if has_tight_binary_op(trimmed) {
return true;
}
if leading == 4 && is_redundant_paren_expr(trimmed) {
return true;
}
if line_has_unpadded_hex(trimmed) {
return true;
}
if line_has_sci_float_without_dot(trimmed) {
return true;
}
}
if block_has_unseparated_assertions(block_lines) {
return true;
}
if block_has_single_line_if(block_lines) {
return true;
}
false
}
pub(in crate::print) fn try_reformat_code_block(block_lines: &[&str]) -> Option<String> {
let min_leading = block_lines
.iter()
.filter(|l| !l.trim().is_empty())
.map(|l| l.len() - l.trim_start().len())
.min();
if matches!(min_leading, Some(n) if n > 4) {
return None;
}
if block_has_assertion_then_comment_paragraph(block_lines) {
return None;
}
let mut raw_lines: Vec<String> = Vec::new();
for &line in block_lines {
if line.trim().is_empty() {
raw_lines.push(String::new());
} else if let Some(stripped) = line.strip_prefix(" ") {
raw_lines.push(stripped.to_string());
} else {
return None;
}
}
let raw_code = raw_lines.join("\n");
let trimmed_raw = raw_code.trim_start();
if (trimmed_raw.starts_with("module ")
|| trimmed_raw.starts_with("port module ")
|| trimmed_raw.starts_with("effect module "))
&& let Some(result) = try_parse_and_format_full_module(&raw_code)
{
let mut out_lines: Vec<String> = Vec::new();
let mut prev_blank = false;
for l in result.split('\n') {
if l.is_empty() {
if prev_blank {
continue;
}
prev_blank = true;
out_lines.push(String::new());
} else {
prev_blank = false;
out_lines.push(format!(" {}", l));
}
}
return Some(out_lines.join("\n"));
}
let wrapped = format!("module DocTemp__ exposing (..)\n\n\n{}\n", raw_code);
if let Some(result) = try_parse_and_format_module(&wrapped) {
return Some(result);
}
let paragraphs = split_into_paragraphs(&raw_lines);
if paragraphs.iter().any(|p| paragraph_is_all_imports(p)) {
let mut out_lines: Vec<String> = Vec::new();
let mut prev_blank = false;
for l in raw_lines.iter() {
if l.trim().is_empty() {
if prev_blank {
continue;
}
prev_blank = true;
out_lines.push(String::new());
} else {
prev_blank = false;
out_lines.push(format!(" {}", l));
}
}
return Some(out_lines.join("\n"));
}
let mut formatted_paragraphs: Vec<String> = Vec::new();
for para in ¶graphs {
let para_text = para.join("\n");
let wrapped_decl = format!("module DocTemp__ exposing (..)\n\n\n{}\n", para_text);
if let Some(result) = try_parse_and_format_module_raw(&wrapped_decl) {
formatted_paragraphs.push(result);
continue;
}
if paragraph_is_single_expr_with_line_comment(para) {
formatted_paragraphs.push(para_text);
continue;
}
if para.iter().any(|l| l.contains("\"\"\"")) {
formatted_paragraphs.push(para_text);
continue;
}
let try_per_line = is_assertion_only_paragraph(para) && {
let mut per_line_results: Vec<String> = Vec::new();
let mut pending_comments: Vec<String> = Vec::new();
let mut current_accum: Option<String> = None;
let mut all_ok = true;
let flush_accum = |accum: Option<String>,
results: &mut Vec<String>,
pending: &mut Vec<String>|
-> bool {
let Some(text) = accum else {
return true;
};
let wrapped = format!(
"module DocTemp__ exposing (..)\n\n\ndocTemp__ =\n{}\n",
text
);
match try_parse_and_format_expr(&wrapped) {
Some(r) => {
let combined = if pending.is_empty() {
r
} else {
let mut s = pending.join("\n");
s.push('\n');
s.push_str(&r);
pending.clear();
s
};
results.push(combined);
true
}
None => false,
}
};
for line in para {
if line.trim().is_empty() {
continue;
}
let trimmed = line.trim();
if trimmed.starts_with("--") {
pending_comments.push(trimmed.to_string());
continue;
}
let is_minus_cont = trimmed.strip_prefix('-').is_some_and(|r| {
r.chars()
.next()
.is_some_and(|c| c.is_ascii_digit() || c == '(')
});
if let Some(cur) = current_accum.as_mut().filter(|_| is_minus_cont) {
cur.push('\n');
cur.push_str(" ");
cur.push_str(trimmed);
} else {
if !flush_accum(
current_accum.take(),
&mut per_line_results,
&mut pending_comments,
) {
all_ok = false;
break;
}
current_accum = Some(format!(" {}", trimmed));
}
}
if all_ok && !flush_accum(current_accum, &mut per_line_results, &mut pending_comments) {
all_ok = false;
}
if !pending_comments.is_empty() {
per_line_results.push(pending_comments.join("\n"));
}
if all_ok && !per_line_results.is_empty() {
formatted_paragraphs.push(per_line_results.join("\n\n"));
true
} else {
false
}
};
if try_per_line {
continue;
}
let indented: Vec<String> = para
.iter()
.map(|line| {
if line.is_empty() {
String::new()
} else {
format!(" {}", line)
}
})
.collect();
let wrapped_expr = format!(
"module DocTemp__ exposing (..)\n\n\ndocTemp__ =\n{}\n",
indented.join("\n")
);
if let Some(result) = try_parse_and_format_expr(&wrapped_expr) {
formatted_paragraphs.push(result);
continue;
}
if is_assertion_only_paragraph(para) {
let mut per_line_results: Vec<String> = Vec::new();
let mut pending_comments: Vec<String> = Vec::new();
let mut all_ok = true;
for line in para {
if line.trim().is_empty() {
continue;
}
let trimmed = line.trim();
if trimmed.starts_with("--") {
pending_comments.push(trimmed.to_string());
continue;
}
let wrapped_line = format!(
"module DocTemp__ exposing (..)\n\n\ndocTemp__ =\n {}\n",
line
);
match try_parse_and_format_expr(&wrapped_line) {
Some(r) => {
let combined = if pending_comments.is_empty() {
r
} else {
let mut s = pending_comments.join("\n");
s.push('\n');
s.push_str(&r);
pending_comments.clear();
s
};
per_line_results.push(combined);
}
None => {
all_ok = false;
break;
}
}
}
if !pending_comments.is_empty() {
per_line_results.push(pending_comments.join("\n"));
}
if all_ok && !per_line_results.is_empty() {
formatted_paragraphs.push(per_line_results.join("\n\n"));
continue;
}
}
if para.iter().any(|l| l.contains("\"\"\"")) {
formatted_paragraphs.push(para_text);
continue;
}
return None;
}
let mut joined = String::new();
for (idx, para_text) in formatted_paragraphs.iter().enumerate() {
if idx > 0 {
let prev_para = ¶graphs[idx - 1];
let cur_para = ¶graphs[idx];
let sep = if paragraph_is_all_imports(prev_para)
&& paragraph_starts_with_line_comment(cur_para)
{
"\n\n\n"
} else {
"\n\n"
};
joined.push_str(sep);
}
joined.push_str(para_text);
}
let mut output = String::new();
for (idx, line) in joined.split('\n').enumerate() {
if idx > 0 {
output.push('\n');
}
if line.is_empty() {
} else {
output.push_str(" ");
output.push_str(line);
}
}
Some(output)
}