Skip to main content

oxilean_parse/formatter_adv/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use crate::ast_impl::*;
6use std::collections::HashMap;
7
8use super::types::{Annotation, AstFormatter, Doc, FormatConfig, LayoutConfig, LayoutEngine};
9
10/// Concatenate multiple documents with a separator.
11#[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/// Join documents with spaces.
23#[allow(missing_docs)]
24pub fn hsep(docs: &[Doc]) -> Doc {
25    intersperse(docs, Doc::text(" "))
26}
27/// Join documents with soft lines.
28#[allow(missing_docs)]
29pub fn vsep(docs: &[Doc]) -> Doc {
30    intersperse(docs, Doc::SoftLine)
31}
32/// Join documents with hard lines.
33#[allow(missing_docs)]
34pub fn vcat(docs: &[Doc]) -> Doc {
35    intersperse(docs, Doc::HardLine)
36}
37/// Wrap a document in parentheses.
38#[allow(missing_docs)]
39pub fn parens(doc: Doc) -> Doc {
40    Doc::text("(").cat(doc).cat(Doc::text(")"))
41}
42/// Wrap a document in brackets.
43#[allow(missing_docs)]
44pub fn brackets(doc: Doc) -> Doc {
45    Doc::text("[").cat(doc).cat(Doc::text("]"))
46}
47/// Wrap a document in braces.
48#[allow(missing_docs)]
49pub fn braces(doc: Doc) -> Doc {
50    Doc::text("{").cat(doc).cat(Doc::text("}"))
51}
52/// Wrap a document in double braces.
53#[allow(missing_docs)]
54pub fn double_braces(doc: Doc) -> Doc {
55    Doc::text("{{").cat(doc).cat(Doc::text("}}"))
56}
57/// Create a keyword document.
58#[allow(missing_docs)]
59pub fn keyword(s: &str) -> Doc {
60    Doc::text(s).annotate(Annotation::Keyword)
61}
62/// Create an operator document.
63#[allow(missing_docs)]
64pub fn operator(s: &str) -> Doc {
65    Doc::text(s).annotate(Annotation::Operator)
66}
67/// Create an identifier document.
68#[allow(missing_docs)]
69pub fn ident(s: &str) -> Doc {
70    Doc::text(s).annotate(Annotation::Identifier)
71}
72/// Create a type name document.
73#[allow(missing_docs)]
74pub fn type_name(s: &str) -> Doc {
75    Doc::text(s).annotate(Annotation::TypeName)
76}
77/// Format a surface expression to a string.
78#[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/// Format a surface expression with custom configuration.
86#[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/// Format a declaration to a string.
98#[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/// Format an entire module to a string.
106#[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/// Extract comments from source text with their byte offsets.
114#[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/// Measures how similar two formatted outputs are (0.0 = identical, 1.0 = completely different).
228#[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/// Formats an expression with explicit precedence parentheses.
245#[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/// Escapes special characters in a string for display in formatted output.
255#[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/// Unescapes display-escaped string.
264#[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/// Generate an ASCII box around a string.
289#[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/// Format a key-value table as a grid.
305#[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/// Splits a formatted output into sections based on blank-line boundaries.
316#[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/// Heuristic: detect if an expression is "simple" (should be formatted flat).
338#[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/// Generate a separator line for use in formatted output.
344#[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/// Word wrap utility.
474#[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/// Truncate with ellipsis.
493#[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/// Add line numbers.
503#[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/// Format type annotation.
513#[allow(dead_code)]
514#[allow(missing_docs)]
515pub fn fmt_type_ann(name: &str, ty: &str) -> String {
516    format!("({} : {})", name, ty)
517}
518/// Format lambda.
519#[allow(dead_code)]
520#[allow(missing_docs)]
521pub fn fmt_lambda(params: &[&str], body: &str) -> String {
522    format!("fun {} -> {}", params.join(" "), body)
523}
524/// Format forall.
525#[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/// Format match.
535#[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/// Check canonical format.
545#[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/// Canonicalise.
562#[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/// Format a list.
582#[allow(dead_code)]
583#[allow(missing_docs)]
584pub fn fmt_list(items: &[&str]) -> String {
585    format!("[{}]", items.join(", "))
586}
587/// Format a tuple.
588#[allow(dead_code)]
589#[allow(missing_docs)]
590pub fn fmt_tuple(items: &[&str]) -> String {
591    format!("({})", items.join(", "))
592}
593/// Format a record.
594#[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/// Format an if-then-else.
604#[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/// Format fn signature.
610#[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/// Escape for display.
617#[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/// Diff two strings line by line.
626#[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/// Count added parens.
648#[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/// Expansion ratio.
656#[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/// Generate box around string.
666#[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/// KV table.
681#[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/// Heuristic simple expr check.
692#[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/// Separator line.
698#[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/// Stabilise format.
704#[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/// Formats a list of items with optional trailing comma.
862#[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/// Formats a multi-line comment block with optional header.
875#[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/// Normalise whitespace: collapse multiple spaces, trim lines.
889#[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/// Remove all comments from source (lines starting with `--`).
898#[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/// Count identifiers in a formatted string (word characters not starting with digit).
907#[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/// Detect if output has consistent indentation (all indents are multiples of `unit`).
933#[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/// Format a sequence of items one-per-line with a prefix.
945#[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/// Check if a string ends with exactly one newline.
955#[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/// Ensure string ends with exactly one newline.
961#[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/// Format a source file header with metadata.
968#[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}