1use crate::ast_impl::*;
6use std::collections::HashMap;
7
8use super::types::{Annotation, AstFormatter, Doc, FormatConfig, LayoutConfig, LayoutEngine};
9
10#[allow(missing_docs)]
12pub fn intersperse(docs: &[Doc], sep: Doc) -> Doc {
13 if docs.is_empty() {
14 return Doc::Nil;
15 }
16 let mut result = docs[0].clone();
17 for d in &docs[1..] {
18 result = result.cat(sep.clone()).cat(d.clone());
19 }
20 result
21}
22#[allow(missing_docs)]
24pub fn hsep(docs: &[Doc]) -> Doc {
25 intersperse(docs, Doc::text(" "))
26}
27#[allow(missing_docs)]
29pub fn vsep(docs: &[Doc]) -> Doc {
30 intersperse(docs, Doc::SoftLine)
31}
32#[allow(missing_docs)]
34pub fn vcat(docs: &[Doc]) -> Doc {
35 intersperse(docs, Doc::HardLine)
36}
37#[allow(missing_docs)]
39pub fn parens(doc: Doc) -> Doc {
40 Doc::text("(").cat(doc).cat(Doc::text(")"))
41}
42#[allow(missing_docs)]
44pub fn brackets(doc: Doc) -> Doc {
45 Doc::text("[").cat(doc).cat(Doc::text("]"))
46}
47#[allow(missing_docs)]
49pub fn braces(doc: Doc) -> Doc {
50 Doc::text("{").cat(doc).cat(Doc::text("}"))
51}
52#[allow(missing_docs)]
54pub fn double_braces(doc: Doc) -> Doc {
55 Doc::text("{{").cat(doc).cat(Doc::text("}}"))
56}
57#[allow(missing_docs)]
59pub fn keyword(s: &str) -> Doc {
60 Doc::text(s).annotate(Annotation::Keyword)
61}
62#[allow(missing_docs)]
64pub fn operator(s: &str) -> Doc {
65 Doc::text(s).annotate(Annotation::Operator)
66}
67#[allow(missing_docs)]
69pub fn ident(s: &str) -> Doc {
70 Doc::text(s).annotate(Annotation::Identifier)
71}
72#[allow(missing_docs)]
74pub fn type_name(s: &str) -> Doc {
75 Doc::text(s).annotate(Annotation::TypeName)
76}
77#[allow(missing_docs)]
79pub fn format_expr(expr: &SurfaceExpr) -> String {
80 let formatter = AstFormatter::new();
81 let doc = formatter.format_expr(expr);
82 let engine = LayoutEngine::default_engine();
83 engine.layout(&doc)
84}
85#[allow(missing_docs)]
87pub fn format_expr_with_config(expr: &SurfaceExpr, config: FormatConfig) -> String {
88 let formatter = AstFormatter::with_config(config.clone());
89 let doc = formatter.format_expr(expr);
90 let engine = LayoutEngine::new(LayoutConfig {
91 max_width: config.max_width,
92 indent_size: config.indent_size,
93 ..LayoutConfig::default()
94 });
95 engine.layout(&doc)
96}
97#[allow(missing_docs)]
99pub fn format_decl(decl: &Decl) -> String {
100 let formatter = AstFormatter::new();
101 let doc = formatter.format_decl(decl);
102 let engine = LayoutEngine::default_engine();
103 engine.layout(&doc)
104}
105#[allow(missing_docs)]
107pub fn format_module(decls: &[Located<Decl>]) -> String {
108 let formatter = AstFormatter::new();
109 let doc = formatter.format_module(decls);
110 let engine = LayoutEngine::default_engine();
111 engine.layout(&doc)
112}
113#[allow(missing_docs)]
115pub fn extract_comments(source: &str) -> HashMap<usize, String> {
116 let mut comments = HashMap::new();
117 let mut offset = 0;
118 for line in source.lines() {
119 let trimmed = line.trim();
120 if let Some(comment) = trimmed.strip_prefix("--") {
121 comments.insert(offset, comment.to_string());
122 }
123 offset += line.len() + 1;
124 }
125 let bytes = source.as_bytes();
126 let mut i = 0;
127 while i + 1 < bytes.len() {
128 if bytes[i] == b'/' && bytes[i + 1] == b'-' {
129 let start = i;
130 i += 2;
131 let mut depth = 1;
132 while i + 1 < bytes.len() && depth > 0 {
133 if bytes[i] == b'/' && bytes[i + 1] == b'-' {
134 depth += 1;
135 i += 1;
136 } else if bytes[i] == b'-' && bytes[i + 1] == b'/' {
137 depth -= 1;
138 i += 1;
139 }
140 i += 1;
141 }
142 let end = i.min(source.len());
143 comments.insert(start, source[start..end].to_string());
144 } else {
145 i += 1;
146 }
147 }
148 comments
149}
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::formatter_adv::*;
154 #[test]
155 fn test_doc_text() {
156 let doc = Doc::text("hello");
157 let engine = LayoutEngine::default_engine();
158 assert_eq!(engine.layout(&doc), "hello");
159 }
160 #[test]
161 fn test_doc_cat() {
162 let doc = Doc::text("hello").space(Doc::text("world"));
163 let engine = LayoutEngine::default_engine();
164 assert_eq!(engine.layout(&doc), "hello world");
165 }
166 #[test]
167 fn test_doc_hardline() {
168 let doc = Doc::text("line1")
169 .cat(Doc::HardLine)
170 .cat(Doc::text("line2"));
171 let engine = LayoutEngine::default_engine();
172 assert_eq!(engine.layout(&doc), "line1\nline2");
173 }
174 #[test]
175 fn test_doc_nest() {
176 let doc = Doc::text("outer")
177 .cat(Doc::HardLine)
178 .cat(Doc::text("inner").nest(4));
179 let engine = LayoutEngine::default_engine();
180 let result = engine.layout(&doc);
181 assert!(result.contains("outer\n"));
182 }
183 #[test]
184 fn test_doc_group_fits() {
185 let doc = Doc::text("a").line(Doc::text("b")).group();
186 let engine = LayoutEngine::new(LayoutConfig {
187 max_width: 80,
188 ..LayoutConfig::default()
189 });
190 let result = engine.layout(&doc);
191 assert_eq!(result, "a b");
192 }
193 #[test]
194 fn test_format_var() {
195 let expr = SurfaceExpr::Var("x".to_string());
196 assert_eq!(format_expr(&expr), "x");
197 }
198 #[test]
199 fn test_format_literal() {
200 let expr = SurfaceExpr::Lit(Literal::Nat(42));
201 assert_eq!(format_expr(&expr), "42");
202 }
203 #[test]
204 fn test_format_hole() {
205 let expr = SurfaceExpr::Hole;
206 assert_eq!(format_expr(&expr), "_");
207 }
208 #[test]
209 fn test_parens_builder() {
210 let doc = parens(Doc::text("x"));
211 let engine = LayoutEngine::default_engine();
212 assert_eq!(engine.layout(&doc), "(x)");
213 }
214 #[test]
215 fn test_brackets_builder() {
216 let doc = brackets(Doc::text("x"));
217 let engine = LayoutEngine::default_engine();
218 assert_eq!(engine.layout(&doc), "[x]");
219 }
220 #[test]
221 fn test_extract_comments() {
222 let source = "-- comment\ndef x := 1\n-- another";
223 let comments = extract_comments(source);
224 assert!(!comments.is_empty());
225 }
226}
227#[allow(dead_code)]
229#[allow(missing_docs)]
230pub fn format_dissimilarity(a: &str, b: &str) -> f64 {
231 if a == b {
232 return 0.0;
233 }
234 let lines_a: std::collections::HashSet<_> = a.lines().collect();
235 let lines_b: std::collections::HashSet<_> = b.lines().collect();
236 let union = lines_a.union(&lines_b).count();
237 let intersection = lines_a.intersection(&lines_b).count();
238 if union == 0 {
239 0.0
240 } else {
241 1.0 - intersection as f64 / union as f64
242 }
243}
244#[allow(dead_code)]
246#[allow(missing_docs)]
247pub fn format_with_precedence(expr: &str, prec: u8) -> String {
248 if prec > 5 {
249 format!("({})", expr)
250 } else {
251 expr.to_string()
252 }
253}
254#[allow(dead_code)]
256#[allow(missing_docs)]
257pub fn escape_for_display(s: &str) -> String {
258 s.replace('\\', "\\\\")
259 .replace('"', "\\\"")
260 .replace('\n', "\\n")
261 .replace('\t', "\\t")
262}
263#[allow(dead_code)]
265#[allow(missing_docs)]
266pub fn unescape_display(s: &str) -> String {
267 let mut out = String::new();
268 let mut chars = s.chars().peekable();
269 while let Some(c) = chars.next() {
270 if c == '\\' {
271 match chars.next() {
272 Some('\\') => out.push('\\'),
273 Some('"') => out.push('"'),
274 Some('n') => out.push('\n'),
275 Some('t') => out.push('\t'),
276 Some(x) => {
277 out.push('\\');
278 out.push(x);
279 }
280 None => out.push('\\'),
281 }
282 } else {
283 out.push(c);
284 }
285 }
286 out
287}
288#[allow(dead_code)]
290#[allow(missing_docs)]
291pub fn box_string(s: &str) -> String {
292 let lines: Vec<_> = s.lines().collect();
293 let max_w = lines.iter().map(|l| l.len()).max().unwrap_or(0);
294 let border = format!("+{}+", "-".repeat(max_w + 2));
295 let mut out = border.clone();
296 for line in &lines {
297 out.push('\n');
298 out.push_str(&format!("| {}{} |", line, " ".repeat(max_w - line.len())));
299 }
300 out.push('\n');
301 out.push_str(&border);
302 out
303}
304#[allow(dead_code)]
306#[allow(missing_docs)]
307pub fn format_kv_table(pairs: &[(&str, &str)]) -> String {
308 let max_k = pairs.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
309 pairs
310 .iter()
311 .map(|(k, v)| format!("{}{} : {}", k, " ".repeat(max_k - k.len()), v))
312 .collect::<Vec<_>>()
313 .join("\n")
314}
315#[allow(dead_code)]
317#[allow(missing_docs)]
318pub fn split_into_sections(text: &str) -> Vec<String> {
319 let mut sections = Vec::new();
320 let mut current = String::new();
321 for line in text.lines() {
322 if line.trim().is_empty() && !current.is_empty() {
323 sections.push(current.trim().to_string());
324 current = String::new();
325 } else {
326 if !current.is_empty() {
327 current.push('\n');
328 }
329 current.push_str(line);
330 }
331 }
332 if !current.trim().is_empty() {
333 sections.push(current.trim().to_string());
334 }
335 sections
336}
337#[allow(dead_code)]
339#[allow(missing_docs)]
340pub fn is_simple_expr(s: &str) -> bool {
341 s.len() < 30 && !s.contains('\n') && s.chars().filter(|&c| c == '(').count() <= 2
342}
343#[allow(dead_code)]
345#[allow(missing_docs)]
346pub fn separator_line(width: usize, char_: char) -> String {
347 std::iter::repeat(char_).take(width).collect()
348}
349#[cfg(test)]
350mod extended_formatter_adv_tests_2 {
351 use super::*;
352 use crate::formatter_adv::*;
353 #[test]
354 fn test_formatter_indent_stack() {
355 let mut stack = FormatterIndentStack::new();
356 stack.push(2);
357 assert_eq!(stack.current(), 2);
358 stack.push(4);
359 assert_eq!(stack.current(), 6);
360 stack.pop();
361 assert_eq!(stack.current(), 2);
362 }
363 #[test]
364 fn test_ribbon_formatter() {
365 let rf = RibbonFormatter::new(100, 0.6);
366 assert_eq!(rf.ribbon_width(), 60);
367 assert!(rf.fits(10, 40));
368 assert!(!rf.fits(10, 80));
369 }
370 #[test]
371 fn test_annotated_output() {
372 let mut ao = AnnotatedOutput::new("hello world");
373 ao.annotate(0, 5, 1);
374 ao.annotate(6, 11, 2);
375 assert_eq!(ao.node_at(0), Some(1));
376 assert_eq!(ao.node_at(7), Some(2));
377 assert_eq!(ao.node_at(5), None);
378 assert_eq!(ao.annotation_count(), 2);
379 }
380 #[test]
381 fn test_line_length_distribution() {
382 let text = "hello\nworld!\nhi";
383 let dist = LineLengthDistribution::compute(text);
384 assert_eq!(dist.total_lines, 3);
385 assert_eq!(dist.max_length(), 6);
386 assert!(dist.mean_length() > 3.0);
387 }
388 #[test]
389 fn test_line_breaker() {
390 let lb = LineBreaker::new(10, 0, " ");
391 let tokens = ["hello", "world", "foo", "bar"];
392 let broken = lb.break_tokens(&tokens);
393 for line in broken.lines() {
394 assert!(line.len() <= 12);
395 }
396 }
397 #[test]
398 fn test_formatter_config() {
399 let cfg = FormatterConfig::default_config();
400 assert_eq!(cfg.line_width, 100);
401 assert_eq!(cfg.indent_str(), " ");
402 let compact = FormatterConfig::compact();
403 assert_eq!(compact.indent_size, 4);
404 }
405 #[test]
406 fn test_format_dissimilarity() {
407 assert_eq!(format_dissimilarity("a\nb", "a\nb"), 0.0);
408 let d = format_dissimilarity("a\nb\nc", "a\nX\nc");
409 assert!(d > 0.0 && d < 1.0);
410 }
411 #[test]
412 fn test_format_context() {
413 let ctx = FormatContext::new(80);
414 let ctx2 = ctx.indented(4);
415 assert_eq!(ctx2.indent, 4);
416 assert_eq!(ctx2.remaining_width(), 76);
417 let ctx3 = ctx2.in_type_mode();
418 assert!(ctx3.in_type);
419 }
420 #[test]
421 fn test_format_decision_log() {
422 let mut log = FormatDecisionLog::new();
423 log.record("expr1", true, 20, 80);
424 log.record("expr2", false, 90, 80);
425 assert_eq!(log.count(), 2);
426 assert!((log.flat_fraction() - 0.5).abs() < 1e-9);
427 }
428 #[test]
429 fn test_escape_unescape() {
430 let original = "hello\nworld\t\"quote\"";
431 let escaped = escape_for_display(original);
432 let restored = unescape_display(&escaped);
433 assert_eq!(restored, original);
434 }
435 #[test]
436 fn test_box_string() {
437 let boxed = box_string("hello\nworld");
438 assert!(boxed.contains("+"));
439 assert!(boxed.contains("| hello |") || boxed.contains("hello"));
440 }
441 #[test]
442 fn test_format_kv_table() {
443 let pairs = [("name", "Alice"), ("age", "30")];
444 let table = format_kv_table(&pairs);
445 assert!(table.contains("name"));
446 assert!(table.contains("Alice"));
447 }
448 #[test]
449 fn test_split_into_sections() {
450 let text = "a\nb\n\nc\nd";
451 let sections = split_into_sections(text);
452 assert_eq!(sections.len(), 2);
453 assert_eq!(sections[0], "a\nb");
454 assert_eq!(sections[1], "c\nd");
455 }
456 #[test]
457 fn test_is_simple_expr() {
458 assert!(is_simple_expr("x + y"));
459 assert!(!is_simple_expr(&"x".repeat(50)));
460 }
461 #[test]
462 fn test_separator_line() {
463 let sep = separator_line(10, '-');
464 assert_eq!(sep, "----------");
465 assert_eq!(sep.len(), 10);
466 }
467 #[test]
468 fn test_format_with_precedence() {
469 assert_eq!(format_with_precedence("x + y", 6), "(x + y)");
470 assert_eq!(format_with_precedence("x", 3), "x");
471 }
472}
473#[allow(dead_code)]
475#[allow(missing_docs)]
476pub fn word_wrap2(text: &str, width: usize) -> String {
477 let mut out = String::new();
478 let mut line_len = 0usize;
479 for word in text.split_whitespace() {
480 if line_len > 0 && line_len + word.len() + 1 > width {
481 out.push('\n');
482 line_len = 0;
483 } else if line_len > 0 {
484 out.push(' ');
485 line_len += 1;
486 }
487 out.push_str(word);
488 line_len += word.len();
489 }
490 out
491}
492#[allow(dead_code)]
494#[allow(missing_docs)]
495pub fn truncate_ellipsis(s: &str, max_len: usize) -> String {
496 if s.len() <= max_len {
497 s.to_string()
498 } else {
499 format!("{}...", &s[..max_len.saturating_sub(3)])
500 }
501}
502#[allow(dead_code)]
504#[allow(missing_docs)]
505pub fn numbered_lines(s: &str) -> String {
506 s.lines()
507 .enumerate()
508 .map(|(i, l)| format!("{:4} | {}", i + 1, l))
509 .collect::<Vec<_>>()
510 .join("\n")
511}
512#[allow(dead_code)]
514#[allow(missing_docs)]
515pub fn fmt_type_ann(name: &str, ty: &str) -> String {
516 format!("({} : {})", name, ty)
517}
518#[allow(dead_code)]
520#[allow(missing_docs)]
521pub fn fmt_lambda(params: &[&str], body: &str) -> String {
522 format!("fun {} -> {}", params.join(" "), body)
523}
524#[allow(dead_code)]
526#[allow(missing_docs)]
527pub fn fmt_forall(binders: &[(&str, &str)], body: &str) -> String {
528 let bs: Vec<_> = binders
529 .iter()
530 .map(|(n, t)| format!("({} : {})", n, t))
531 .collect();
532 format!("forall {}, {}", bs.join(" "), body)
533}
534#[allow(dead_code)]
536#[allow(missing_docs)]
537pub fn fmt_match(scrutinee: &str, arms: &[(&str, &str)]) -> String {
538 let mut out = format!("match {} with\n", scrutinee);
539 for (pat, body) in arms {
540 out.push_str(&format!(" | {} -> {}\n", pat, body));
541 }
542 out
543}
544#[allow(dead_code)]
546#[allow(missing_docs)]
547pub fn check_canonical(s: &str) -> bool {
548 if s.lines().any(|l| l.ends_with(' ')) {
549 return false;
550 }
551 let mut prev_blank = false;
552 for line in s.lines() {
553 let blank = line.trim().is_empty();
554 if blank && prev_blank {
555 return false;
556 }
557 prev_blank = blank;
558 }
559 true
560}
561#[allow(dead_code)]
563#[allow(missing_docs)]
564pub fn canonicalise(s: &str) -> String {
565 let mut out = String::new();
566 let mut prev_blank = false;
567 for line in s.lines() {
568 let trimmed = line.trim_end();
569 let blank = trimmed.is_empty();
570 if blank && prev_blank {
571 continue;
572 }
573 if !out.is_empty() {
574 out.push('\n');
575 }
576 out.push_str(trimmed);
577 prev_blank = blank;
578 }
579 out
580}
581#[allow(dead_code)]
583#[allow(missing_docs)]
584pub fn fmt_list(items: &[&str]) -> String {
585 format!("[{}]", items.join(", "))
586}
587#[allow(dead_code)]
589#[allow(missing_docs)]
590pub fn fmt_tuple(items: &[&str]) -> String {
591 format!("({})", items.join(", "))
592}
593#[allow(dead_code)]
595#[allow(missing_docs)]
596pub fn fmt_record(fields: &[(&str, &str)]) -> String {
597 let body: Vec<_> = fields
598 .iter()
599 .map(|(k, v)| format!("{} := {}", k, v))
600 .collect();
601 format!("{{ {} }}", body.join(", "))
602}
603#[allow(dead_code)]
605#[allow(missing_docs)]
606pub fn fmt_ite(cond: &str, then_: &str, else_: &str) -> String {
607 format!("if {} then {} else {}", cond, then_, else_)
608}
609#[allow(dead_code)]
611#[allow(missing_docs)]
612pub fn fmt_fn_sig(name: &str, args: &[(&str, &str)], ret: &str) -> String {
613 let args_str: Vec<_> = args.iter().map(|(n, t)| fmt_type_ann(n, t)).collect();
614 format!("def {} {} : {}", name, args_str.join(" "), ret)
615}
616#[allow(dead_code)]
618#[allow(missing_docs)]
619pub fn escape_display(s: &str) -> String {
620 s.replace('\\', "\\\\")
621 .replace('"', "\\\"")
622 .replace('\n', "\\n")
623 .replace('\t', "\\t")
624}
625#[allow(dead_code)]
627#[allow(missing_docs)]
628pub fn line_diff(a: &str, b: &str) -> Vec<String> {
629 let la: Vec<_> = a.lines().collect();
630 let lb: Vec<_> = b.lines().collect();
631 let mut diff = Vec::new();
632 let max = la.len().max(lb.len());
633 for i in 0..max {
634 match (la.get(i), lb.get(i)) {
635 (Some(x), Some(y)) if x == y => diff.push(format!(" {}", x)),
636 (Some(x), Some(y)) => {
637 diff.push(format!("- {}", x));
638 diff.push(format!("+ {}", y));
639 }
640 (Some(x), None) => diff.push(format!("- {}", x)),
641 (None, Some(y)) => diff.push(format!("+ {}", y)),
642 (None, None) => {}
643 }
644 }
645 diff
646}
647#[allow(dead_code)]
649#[allow(missing_docs)]
650pub fn count_parens_added(original: &str, formatted: &str) -> usize {
651 let orig = original.chars().filter(|&c| c == '(').count();
652 let fmt = formatted.chars().filter(|&c| c == '(').count();
653 fmt.saturating_sub(orig)
654}
655#[allow(dead_code)]
657#[allow(missing_docs)]
658pub fn expansion_ratio(original: &str, formatted: &str) -> f64 {
659 if original.is_empty() {
660 1.0
661 } else {
662 formatted.len() as f64 / original.len() as f64
663 }
664}
665#[allow(dead_code)]
667#[allow(missing_docs)]
668pub fn ascii_box(s: &str) -> String {
669 let lines: Vec<_> = s.lines().collect();
670 let max_w = lines.iter().map(|l| l.len()).max().unwrap_or(0);
671 let border = format!("+{}+", "-".repeat(max_w + 2));
672 let mut out = border.clone();
673 for line in &lines {
674 out.push_str(&format!("\n| {}{} |", line, " ".repeat(max_w - line.len())));
675 }
676 out.push('\n');
677 out.push_str(&border);
678 out
679}
680#[allow(dead_code)]
682#[allow(missing_docs)]
683pub fn kv_table(pairs: &[(&str, &str)]) -> String {
684 let max_k = pairs.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
685 pairs
686 .iter()
687 .map(|(k, v)| format!("{}{} : {}", k, " ".repeat(max_k - k.len()), v))
688 .collect::<Vec<_>>()
689 .join("\n")
690}
691#[allow(dead_code)]
693#[allow(missing_docs)]
694pub fn simple_expr(s: &str) -> bool {
695 s.len() < 30 && !s.contains('\n') && s.chars().filter(|&c| c == '(').count() <= 2
696}
697#[allow(dead_code)]
699#[allow(missing_docs)]
700pub fn sep_line(width: usize, ch: char) -> String {
701 std::iter::repeat(ch).take(width).collect()
702}
703#[allow(dead_code)]
705#[allow(missing_docs)]
706pub fn stabilise<F: Fn(&str) -> String>(input: &str, f: F, max: usize) -> (String, usize) {
707 let mut cur = input.to_string();
708 for i in 0..max {
709 let next = f(&cur);
710 if next == cur {
711 return (cur, i);
712 }
713 cur = next;
714 }
715 (cur, max)
716}
717#[cfg(test)]
718mod extended_formatter_adv_tests_3 {
719 use super::*;
720 use crate::formatter_adv::*;
721 #[test]
722 fn test_format_token2() {
723 let t = FormatToken::simple("hello").with_priority(FormatPriority::High);
724 assert_eq!(t.priority, FormatPriority::High);
725 assert_eq!(t.len(), 5);
726 }
727 #[test]
728 fn test_token_stream2_render() {
729 let mut ts = TokenStream2::new(80);
730 ts.push(FormatToken::simple("a"));
731 ts.push(FormatToken::simple("b"));
732 let r = ts.render();
733 assert!(r.contains("a"));
734 assert!(r.contains("b"));
735 assert_eq!(ts.token_count(), 2);
736 }
737 #[test]
738 fn test_infix_expr_builder() {
739 let b = InfixExprBuilder::new("x").add("+", "y").add("+", "z");
740 assert_eq!(b.build_flat(), "x + y + z");
741 let broken = b.build_broken(2);
742 assert!(broken.contains('\n'));
743 }
744 #[test]
745 fn test_let_binding_aligner2() {
746 let mut a = LetBindingAligner2::new();
747 a.add("x", "1");
748 a.add("foo", "2");
749 let r = a.render_aligned();
750 assert!(r.contains("let x"));
751 assert!(r.contains("let foo"));
752 }
753 #[test]
754 fn test_flat_or_broken2() {
755 let fb = FlatOrBroken2::new("short", "long\nversion");
756 assert_eq!(fb.choose(100, 0), "short");
757 assert_eq!(fb.choose(3, 0), "long\nversion");
758 }
759 #[test]
760 fn test_word_wrap2() {
761 let w = word_wrap2("the quick brown fox", 8);
762 for line in w.lines() {
763 assert!(line.len() <= 9);
764 }
765 }
766 #[test]
767 fn test_truncate_ellipsis() {
768 assert_eq!(truncate_ellipsis("hello world", 8), "hello...");
769 assert_eq!(truncate_ellipsis("hi", 10), "hi");
770 }
771 #[test]
772 fn test_numbered_lines() {
773 let n = numbered_lines("a\nb");
774 assert!(n.contains("1 | a") || n.contains(" 1 | a"));
775 }
776 #[test]
777 fn test_fmt_helpers() {
778 assert_eq!(fmt_type_ann("x", "Nat"), "(x : Nat)");
779 assert_eq!(fmt_lambda(&["x", "y"], "x"), "fun x y -> x");
780 assert_eq!(fmt_list(&["1", "2"]), "[1, 2]");
781 assert_eq!(fmt_tuple(&["a", "b"]), "(a, b)");
782 assert_eq!(fmt_ite("b", "t", "f"), "if b then t else f");
783 assert_eq!(fmt_record(&[("k", "v")]), "{ k := v }");
784 }
785 #[test]
786 fn test_check_canonical() {
787 assert!(check_canonical("hello\nworld"));
788 assert!(!check_canonical("hello \n"));
789 assert!(!check_canonical("a\n\n\nb"));
790 }
791 #[test]
792 fn test_canonicalise() {
793 let r = canonicalise("a \n\n\nb ");
794 assert!(check_canonical(&r));
795 }
796 #[test]
797 fn test_fmt_match() {
798 let m = fmt_match("n", &[("0", "Z"), ("k", "S k")]);
799 assert!(m.contains("match n with"));
800 assert!(m.contains("| 0 -> Z"));
801 }
802 #[test]
803 fn test_fmt_forall() {
804 let s = fmt_forall(&[("x", "Nat")], "x > 0");
805 assert!(s.starts_with("forall"));
806 assert!(s.contains("(x : Nat)"));
807 }
808 #[test]
809 fn test_escape_display() {
810 let s = escape_display("a\nb");
811 assert!(s.contains("\\n"));
812 }
813 #[test]
814 fn test_line_diff() {
815 let d = line_diff("a\nb", "a\nX");
816 assert!(d.iter().any(|l| l.starts_with("- b")));
817 assert!(d.iter().any(|l| l.starts_with("+ X")));
818 }
819 #[test]
820 fn test_count_parens_added() {
821 assert_eq!(count_parens_added("x + y", "(x + y)"), 1);
822 }
823 #[test]
824 fn test_expansion_ratio() {
825 assert!(expansion_ratio("x", "x + 0") > 1.0);
826 assert_eq!(expansion_ratio("", "abc"), 1.0);
827 }
828 #[test]
829 fn test_ascii_box() {
830 let b = ascii_box("hi");
831 assert!(b.contains('+'));
832 assert!(b.contains("hi"));
833 }
834 #[test]
835 fn test_kv_table() {
836 let t = kv_table(&[("name", "Alice"), ("age", "30")]);
837 assert!(t.contains("name"));
838 assert!(t.contains("Alice"));
839 }
840 #[test]
841 fn test_simple_expr() {
842 assert!(simple_expr("x + y"));
843 assert!(!simple_expr(&"x".repeat(50)));
844 }
845 #[test]
846 fn test_sep_line() {
847 assert_eq!(sep_line(5, '-'), "-----");
848 }
849 #[test]
850 fn test_stabilise() {
851 let (r, _) = stabilise(" hello ", |s| s.trim().to_string(), 10);
852 assert_eq!(r, "hello");
853 }
854 #[test]
855 fn test_fmt_fn_sig() {
856 let s = fmt_fn_sig("foo", &[("x", "Nat")], "Bool");
857 assert!(s.contains("def foo"));
858 assert!(s.contains(": Bool"));
859 }
860}
861#[allow(dead_code)]
863#[allow(missing_docs)]
864pub fn format_comma_list(items: &[String], trailing_comma: bool) -> String {
865 if items.is_empty() {
866 return String::new();
867 }
868 let mut out = items.join(", ");
869 if trailing_comma {
870 out.push(',');
871 }
872 out
873}
874#[allow(dead_code)]
876#[allow(missing_docs)]
877pub fn format_doc_comment(header: Option<&str>, lines: &[&str]) -> String {
878 let mut out = String::from("/--\n");
879 if let Some(h) = header {
880 out.push_str(&format!(" {}\n\n", h));
881 }
882 for line in lines {
883 out.push_str(&format!(" {}\n", line));
884 }
885 out.push_str("--/");
886 out
887}
888#[allow(dead_code)]
890#[allow(missing_docs)]
891pub fn normalise_whitespace(s: &str) -> String {
892 s.lines()
893 .map(|l| l.split_whitespace().collect::<Vec<_>>().join(" "))
894 .collect::<Vec<_>>()
895 .join("\n")
896}
897#[allow(dead_code)]
899#[allow(missing_docs)]
900pub fn strip_comments(s: &str) -> String {
901 s.lines()
902 .filter(|l| !l.trim_start().starts_with("--"))
903 .collect::<Vec<_>>()
904 .join("\n")
905}
906#[allow(dead_code)]
908#[allow(missing_docs)]
909pub fn count_identifiers(s: &str) -> usize {
910 let mut count = 0;
911 let mut in_word = false;
912 let mut word_start = true;
913 for ch in s.chars() {
914 if ch.is_alphanumeric() || ch == '_' {
915 if !in_word {
916 in_word = true;
917 word_start = !ch.is_ascii_digit();
918 }
919 } else {
920 if in_word && word_start {
921 count += 1;
922 }
923 in_word = false;
924 word_start = false;
925 }
926 }
927 if in_word && word_start {
928 count += 1;
929 }
930 count
931}
932#[allow(dead_code)]
934#[allow(missing_docs)]
935pub fn has_consistent_indentation(s: &str, unit: usize) -> bool {
936 if unit == 0 {
937 return true;
938 }
939 s.lines().all(|l| {
940 let indent = l.len() - l.trim_start().len();
941 indent % unit == 0
942 })
943}
944#[allow(dead_code)]
946#[allow(missing_docs)]
947pub fn format_bulleted_list(items: &[&str], bullet: &str) -> String {
948 items
949 .iter()
950 .map(|item| format!("{} {}", bullet, item))
951 .collect::<Vec<_>>()
952 .join("\n")
953}
954#[allow(dead_code)]
956#[allow(missing_docs)]
957pub fn ends_with_single_newline(s: &str) -> bool {
958 s.ends_with('\n') && !s.ends_with("\n\n")
959}
960#[allow(dead_code)]
962#[allow(missing_docs)]
963pub fn ensure_trailing_newline(s: &str) -> String {
964 let trimmed = s.trim_end_matches('\n');
965 format!("{}\n", trimmed)
966}
967#[allow(dead_code)]
969#[allow(missing_docs)]
970pub fn format_file_header(module_name: &str, imports: &[&str]) -> String {
971 let mut out = format!("-- Module: {}\n\n", module_name);
972 for import in imports {
973 out.push_str(&format!("import {}\n", import));
974 }
975 if !imports.is_empty() {
976 out.push('\n');
977 }
978 out
979}
980#[cfg(test)]
981mod extended_formatter_adv_tests_4 {
982 use super::*;
983 use crate::formatter_adv::*;
984 #[test]
985 fn test_labeled_section_formatter() {
986 let mut f = LabeledSectionFormatter::new();
987 f.add_section("Header", "content here");
988 assert_eq!(f.section_count(), 1);
989 let r = f.render(20);
990 assert!(r.contains("Header"));
991 assert!(r.contains("content here"));
992 }
993 #[test]
994 fn test_format_comma_list() {
995 assert_eq!(
996 format_comma_list(&["a".into(), "b".into(), "c".into()], false),
997 "a, b, c"
998 );
999 assert_eq!(format_comma_list(&["x".into()], true), "x,");
1000 assert_eq!(format_comma_list(&[], false), "");
1001 }
1002 #[test]
1003 fn test_format_doc_comment() {
1004 let doc = format_doc_comment(Some("A header"), &["line 1", "line 2"]);
1005 assert!(doc.contains("/--"));
1006 assert!(doc.contains("A header"));
1007 assert!(doc.contains("line 1"));
1008 assert!(doc.contains("--/"));
1009 }
1010 #[test]
1011 fn test_doc_width() {
1012 let a = DocWidth::atom(5);
1013 let b = DocWidth::atom(3);
1014 let c = DocWidth::concat(a, b);
1015 assert_eq!(c.flat, 8);
1016 assert!(a.fits_in(10));
1017 assert!(!a.fits_in(4));
1018 }
1019 #[test]
1020 fn test_syntax_highlight_formatter() {
1021 let mut f = SyntaxHighlightFormatter::new();
1022 f.add_span(0, 3, HighlightKind::Keyword);
1023 f.add_span(4, 7, HighlightKind::Identifier);
1024 assert_eq!(f.spans_of_kind(HighlightKind::Keyword).len(), 1);
1025 assert_eq!(f.total_highlighted_chars(), 6);
1026 }
1027 #[test]
1028 fn test_decl_format() {
1029 let df = DeclFormat::OneLiner("def x := 1".into());
1030 assert!(df.is_one_liner());
1031 assert_eq!(df.line_count(), 1);
1032 let multi = DeclFormat::MultiLine(vec!["def x :=".into(), " 1".into()]);
1033 assert!(!multi.is_one_liner());
1034 assert_eq!(multi.line_count(), 2);
1035 assert!(multi.render().contains('\n'));
1036 }
1037 #[test]
1038 fn test_format_queue() {
1039 let mut q = FormatQueue::new();
1040 q.enqueue("task1");
1041 q.enqueue("task2");
1042 assert_eq!(q.len(), 2);
1043 assert_eq!(q.dequeue(), Some("task1".into()));
1044 assert_eq!(q.len(), 1);
1045 }
1046 #[test]
1047 fn test_normalise_whitespace() {
1048 let r = normalise_whitespace(" hello world \n foo ");
1049 assert_eq!(r, "hello world\nfoo");
1050 }
1051 #[test]
1052 fn test_strip_comments() {
1053 let src = "def x := 1\n-- this is a comment\ndef y := 2";
1054 let stripped = strip_comments(src);
1055 assert!(!stripped.contains("this is a comment"));
1056 assert!(stripped.contains("def x := 1"));
1057 }
1058 #[test]
1059 fn test_count_identifiers() {
1060 let count = count_identifiers("fun x y -> x + y");
1061 assert!(count >= 3);
1062 }
1063 #[test]
1064 fn test_has_consistent_indentation() {
1065 assert!(has_consistent_indentation("a\n b\n c", 2));
1066 assert!(!has_consistent_indentation("a\n b", 2));
1067 }
1068 #[test]
1069 fn test_format_bulleted_list() {
1070 let r = format_bulleted_list(&["apple", "banana"], "-");
1071 assert_eq!(r, "- apple\n- banana");
1072 }
1073 #[test]
1074 fn test_ensure_trailing_newline() {
1075 assert_eq!(ensure_trailing_newline("hello"), "hello\n");
1076 assert_eq!(ensure_trailing_newline("hello\n"), "hello\n");
1077 assert_eq!(ensure_trailing_newline("hello\n\n"), "hello\n");
1078 }
1079 #[test]
1080 fn test_format_file_header() {
1081 let h = format_file_header("Foo.Bar", &["Foo.Baz", "Foo.Quux"]);
1082 assert!(h.contains("Module: Foo.Bar"));
1083 assert!(h.contains("import Foo.Baz"));
1084 }
1085 #[test]
1086 fn test_tree_renderer() {
1087 let mut tr = TreeRenderer::new(2);
1088 tr.add(0, "root");
1089 tr.add(1, "child");
1090 tr.add(2, "grandchild");
1091 assert_eq!(tr.node_count(), 3);
1092 let r = tr.render();
1093 assert!(r.contains("|- root"));
1094 assert!(r.contains(" |- child"));
1095 assert!(r.contains(" |- grandchild"));
1096 }
1097 #[test]
1098 fn test_ends_with_single_newline() {
1099 assert!(ends_with_single_newline("hello\n"));
1100 assert!(!ends_with_single_newline("hello\n\n"));
1101 assert!(!ends_with_single_newline("hello"));
1102 }
1103}