blots_core/
formatter.rs

1use crate::ast::{BinaryOp, Commented, Expr, RecordEntry, RecordKey, SpannedExpr};
2use crate::ast_to_source::{expr_to_source, format_record_key, needs_parens_in_binop};
3use crate::values::LambdaArg;
4
5const DEFAULT_MAX_COLUMNS: usize = 80;
6const INDENT_SIZE: usize = 2;
7
8/// Format a Blots expression with intelligent line breaking
9pub fn format_expr(expr: &SpannedExpr, max_columns: Option<usize>) -> String {
10    let max_cols = max_columns.unwrap_or(DEFAULT_MAX_COLUMNS);
11    format_expr_impl(expr, max_cols, 0)
12}
13
14/// Internal formatting implementation with indentation tracking
15fn format_expr_impl(expr: &SpannedExpr, max_cols: usize, indent: usize) -> String {
16    // Special handling for lambdas to ensure correct argument formatting
17    if let Expr::Lambda { args, body } = &expr.node {
18        return format_lambda(args, body, max_cols, indent);
19    }
20
21    // Special handling for do blocks - they're inherently multi-line
22    // but we want to check if the opening fits on the current line
23    if let Expr::DoBlock { .. } = &expr.node {
24        return format_multiline(expr, max_cols, indent);
25    }
26
27    // First, try single-line formatting using our custom formatter
28    let single_line = format_single_line(expr);
29
30    // For expressions with do blocks inside, we can't use single_line.len()
31    // because it already contains newlines. Instead, check the first line only.
32    let first_line = single_line.lines().next().unwrap_or(&single_line);
33    let current_line_length = indent + first_line.len();
34
35    // If the first line fits and there are no newlines, use single-line format
36    if !single_line.contains('\n') && current_line_length <= max_cols {
37        return single_line;
38    }
39
40    // Otherwise, apply smart multi-line formatting based on expression type
41    format_multiline(expr, max_cols, indent)
42}
43
44/// Format an expression on a single line (respecting our formatting rules)
45fn format_single_line(expr: &SpannedExpr) -> String {
46    match &expr.node {
47        Expr::Assignment { ident, value } => {
48            format!("{} = {}", ident, format_single_line(value))
49        }
50        Expr::Output { expr: inner_expr } => {
51            format!("output {}", format_single_line(inner_expr))
52        }
53        Expr::Lambda { args, body } => {
54            let args_str: Vec<String> = args.iter().map(lambda_arg_to_str).collect();
55            let args_part = if args.len() == 1 && matches!(args[0], LambdaArg::Required(_)) {
56                args_str[0].clone()
57            } else {
58                format!("({})", args_str.join(", "))
59            };
60            format!("{} => {}", args_part, format_single_line(body))
61        }
62        Expr::Call { func, args } => {
63            let func_str = match &func.node {
64                Expr::Lambda { .. } => format!("({})", format_single_line(func)),
65                _ => format_single_line(func),
66            };
67            let args_str: Vec<String> = args.iter().map(format_single_line).collect();
68            format!("{}({})", func_str, args_str.join(", "))
69        }
70        Expr::List(items) => {
71            // If any items have comments, force multiline by returning string with newline
72            if items.iter().any(|c| c.has_comments()) {
73                return "[\n]".to_string(); // Placeholder that forces multiline
74            }
75            let items_str: Vec<String> =
76                items.iter().map(|c| format_single_line(&c.node)).collect();
77            format!("[{}]", items_str.join(", "))
78        }
79        Expr::Record(entries) => {
80            // If any entries have comments, force multiline by returning string with newline
81            if entries.iter().any(|c| c.has_comments()) {
82                return "{\n}".to_string(); // Placeholder that forces multiline
83            }
84            let entries_str: Vec<String> = entries
85                .iter()
86                .map(|c| format_record_entry_single_line(&c.node))
87                .collect();
88            format!("{{{}}}", entries_str.join(", "))
89        }
90        // For everything else, use the existing expr_to_source
91        _ => expr_to_source(expr),
92    }
93}
94
95/// Format a record entry on a single line
96fn format_record_entry_single_line(entry: &RecordEntry) -> String {
97    match &entry.key {
98        RecordKey::Static(key) => format!(
99            "{}: {}",
100            format_record_key(key),
101            format_single_line(&entry.value)
102        ),
103        RecordKey::Dynamic(key_expr) => {
104            format!(
105                "[{}]: {}",
106                format_single_line(key_expr),
107                format_single_line(&entry.value)
108            )
109        }
110        RecordKey::Shorthand(name) => name.clone(),
111        RecordKey::Spread(expr) => format_single_line(expr),
112    }
113}
114
115/// Format an expression across multiple lines
116fn format_multiline(expr: &SpannedExpr, max_cols: usize, indent: usize) -> String {
117    match &expr.node {
118        Expr::Output { expr: inner_expr } => {
119            // Format as "output " + formatted inner expression
120            let formatted_inner = format_expr_impl(inner_expr, max_cols, indent);
121            format!("output {}", formatted_inner)
122        }
123        Expr::Assignment { ident, value } => {
124            format_assignment_multiline(ident, value, max_cols, indent)
125        }
126        Expr::List(items) => format_list_multiline(items, max_cols, indent),
127        Expr::Record(entries) => format_record_multiline(entries, max_cols, indent),
128        Expr::Conditional {
129            condition,
130            then_expr,
131            else_expr,
132        } => format_conditional_multiline(condition, then_expr, else_expr, max_cols, indent),
133        Expr::Call { func, args } => format_call_multiline(func, args, max_cols, indent),
134        Expr::BinaryOp { op, left, right } => {
135            format_binary_op_multiline(op, left, right, max_cols, indent)
136        }
137        Expr::DoBlock {
138            statements,
139            return_expr,
140        } => format_do_block_multiline(statements, return_expr, max_cols, indent),
141        // For other expression types, fall back to single-line
142        _ => expr_to_source(expr),
143    }
144}
145
146/// Format an assignment with line breaks
147fn format_assignment_multiline(
148    ident: &str,
149    value: &SpannedExpr,
150    max_cols: usize,
151    indent: usize,
152) -> String {
153    // The assignment itself doesn't add indentation, but the value might need it
154    // Format as: ident = <formatted_value>
155    // The value should be formatted at the same indentation level, not pushed over
156
157    let prefix = format!("{} = ", ident);
158
159    // Format the value at the current indentation level
160    // (not at prefix_len which would cause excessive indentation)
161    let formatted_value = format_expr_impl(value, max_cols, indent);
162
163    format!("{}{}", prefix, formatted_value)
164}
165
166/// Format a list with line breaks
167fn format_list_multiline(
168    items: &[Commented<SpannedExpr>],
169    max_cols: usize,
170    indent: usize,
171) -> String {
172    if items.is_empty() {
173        return "[]".to_string();
174    }
175
176    let inner_indent = indent + INDENT_SIZE;
177    let indent_str = make_indent(inner_indent);
178
179    let mut result = "[".to_string();
180
181    for item in items.iter() {
182        // Leading comments
183        for comment in &item.leading {
184            result.push('\n');
185            result.push_str(&indent_str);
186            result.push_str(comment);
187        }
188        // Expression
189        result.push('\n');
190        result.push_str(&indent_str);
191        result.push_str(&format_expr_impl(&item.node, max_cols, inner_indent));
192        result.push(',');
193        // Trailing comment
194        if let Some(trailing) = &item.trailing {
195            result.push_str("  ");
196            result.push_str(trailing);
197        }
198    }
199
200    result.push('\n');
201    result.push_str(&make_indent(indent));
202    result.push(']');
203
204    result
205}
206
207/// Format a record with line breaks
208fn format_record_multiline(
209    entries: &[Commented<RecordEntry>],
210    max_cols: usize,
211    indent: usize,
212) -> String {
213    if entries.is_empty() {
214        return "{}".to_string();
215    }
216
217    let inner_indent = indent + INDENT_SIZE;
218    let indent_str = make_indent(inner_indent);
219
220    let mut result = "{".to_string();
221
222    for entry in entries {
223        // Leading comments
224        for comment in &entry.leading {
225            result.push('\n');
226            result.push_str(&indent_str);
227            result.push_str(comment);
228        }
229        // Entry
230        result.push('\n');
231        result.push_str(&indent_str);
232        result.push_str(&format_record_entry(&entry.node, max_cols, inner_indent));
233        result.push(',');
234        // Trailing comment
235        if let Some(trailing) = &entry.trailing {
236            result.push_str("  ");
237            result.push_str(trailing);
238        }
239    }
240
241    result.push('\n');
242    result.push_str(&make_indent(indent));
243    result.push('}');
244
245    result
246}
247
248/// Format a single record entry
249fn format_record_entry(entry: &RecordEntry, max_cols: usize, indent: usize) -> String {
250    match &entry.key {
251        RecordKey::Static(key) => {
252            format!(
253                "{}: {}",
254                format_record_key(key),
255                format_expr_impl(&entry.value, max_cols, indent)
256            )
257        }
258        RecordKey::Dynamic(key_expr) => {
259            format!(
260                "[{}]: {}",
261                format_expr_impl(key_expr, max_cols, indent),
262                format_expr_impl(&entry.value, max_cols, indent)
263            )
264        }
265        RecordKey::Shorthand(name) => name.clone(),
266        RecordKey::Spread(expr) => format_expr_impl(expr, max_cols, indent),
267    }
268}
269
270/// Format a lambda (handles both single-line and multi-line)
271fn format_lambda(args: &[LambdaArg], body: &SpannedExpr, max_cols: usize, indent: usize) -> String {
272    let args_str: Vec<String> = args.iter().map(lambda_arg_to_str).collect();
273
274    // For single required arguments, omit parentheses
275    let args_part = if args.len() == 1 && matches!(args[0], LambdaArg::Required(_)) {
276        format!("{} =>", args_str[0])
277    } else {
278        format!("({}) =>", args_str.join(", "))
279    };
280
281    // Special handling for do blocks - keep "=> do {" together
282    if let Expr::DoBlock { .. } = &body.node {
283        let body_formatted = format_expr_impl(body, max_cols, indent);
284        return format!("{} {}", args_part, body_formatted);
285    }
286
287    // Try single-line first for other body types
288    let single_line_body = format_expr_impl(body, max_cols, indent);
289    let single_line = format!("{} {}", args_part, single_line_body);
290
291    // Check only if it's actually single-line and fits
292    if !single_line.contains('\n') && indent + single_line.len() <= max_cols {
293        return single_line;
294    }
295
296    // Otherwise, put body on next line with increased indentation
297    let body_indent = indent + INDENT_SIZE;
298    format!(
299        "{}\n{}{}",
300        args_part,
301        make_indent(body_indent),
302        format_expr_impl(body, max_cols, body_indent)
303    )
304}
305
306/// Format a conditional with line breaks
307fn format_conditional_multiline(
308    condition: &SpannedExpr,
309    then_expr: &SpannedExpr,
310    else_expr: &SpannedExpr,
311    max_cols: usize,
312    indent: usize,
313) -> String {
314    let cond_str = format_expr_impl(condition, max_cols, indent);
315
316    // Try to fit "if <condition> then" on one line
317    let if_then_prefix = format!("if {} then", cond_str);
318
319    let inner_indent = indent + INDENT_SIZE;
320
321    if indent + if_then_prefix.len() <= max_cols {
322        // Put then/else clauses on new lines
323        // Check if else_expr is another conditional (else-if chain)
324        if let Expr::Conditional {
325            condition: else_cond,
326            then_expr: else_then,
327            else_expr: else_else,
328        } = &else_expr.node
329        {
330            // Format as "else if" at the same indentation level (not nested)
331            let else_if_part =
332                format_conditional_multiline(else_cond, else_then, else_else, max_cols, indent);
333            format!(
334                "{}\n{}{}\n{}else {}",
335                if_then_prefix,
336                make_indent(inner_indent),
337                format_expr_impl(then_expr, max_cols, inner_indent),
338                make_indent(indent),
339                else_if_part
340            )
341        } else {
342            format!(
343                "{}\n{}{}\n{}else\n{}{}",
344                if_then_prefix,
345                make_indent(inner_indent),
346                format_expr_impl(then_expr, max_cols, inner_indent),
347                make_indent(indent),
348                make_indent(inner_indent),
349                format_expr_impl(else_expr, max_cols, inner_indent)
350            )
351        }
352    } else {
353        // Everything on separate lines
354        // Check if else_expr is another conditional (else-if chain)
355        if let Expr::Conditional {
356            condition: else_cond,
357            then_expr: else_then,
358            else_expr: else_else,
359        } = &else_expr.node
360        {
361            let else_if_part =
362                format_conditional_multiline(else_cond, else_then, else_else, max_cols, indent);
363            format!(
364                "if\n{}{}\n{}then\n{}{}\n{}else {}",
365                make_indent(inner_indent),
366                format_expr_impl(condition, max_cols, inner_indent),
367                make_indent(indent),
368                make_indent(inner_indent),
369                format_expr_impl(then_expr, max_cols, inner_indent),
370                make_indent(indent),
371                else_if_part
372            )
373        } else {
374            format!(
375                "if\n{}{}\n{}then\n{}{}\n{}else\n{}{}",
376                make_indent(inner_indent),
377                format_expr_impl(condition, max_cols, inner_indent),
378                make_indent(indent),
379                make_indent(inner_indent),
380                format_expr_impl(then_expr, max_cols, inner_indent),
381                make_indent(indent),
382                make_indent(inner_indent),
383                format_expr_impl(else_expr, max_cols, inner_indent)
384            )
385        }
386    }
387}
388
389/// Format a function call with line breaks
390fn format_call_multiline(
391    func: &SpannedExpr,
392    args: &[SpannedExpr],
393    max_cols: usize,
394    indent: usize,
395) -> String {
396    let func_str = match &func.node {
397        Expr::Lambda { .. } => format!("({})", format_expr_impl(func, max_cols, indent)),
398        _ => format_expr_impl(func, max_cols, indent),
399    };
400
401    if args.is_empty() {
402        return format!("{}()", func_str);
403    }
404
405    // Try formatting args on separate lines
406    let inner_indent = indent + INDENT_SIZE;
407    let indent_str = make_indent(inner_indent);
408
409    let mut result = format!("{}(", func_str);
410
411    for (i, arg) in args.iter().enumerate() {
412        result.push('\n');
413        result.push_str(&indent_str);
414        result.push_str(&format_expr_impl(arg, max_cols, inner_indent));
415
416        if i < args.len() - 1 {
417            result.push(',');
418        } else {
419            // Trailing comma on last arg for multi-line
420            result.push(',');
421        }
422    }
423
424    result.push('\n');
425    result.push_str(&make_indent(indent));
426    result.push(')');
427
428    result
429}
430
431/// Format a binary operation with line breaks
432fn format_binary_op_multiline(
433    op: &BinaryOp,
434    left: &SpannedExpr,
435    right: &SpannedExpr,
436    max_cols: usize,
437    indent: usize,
438) -> String {
439    let op_str = binary_op_str(op);
440
441    // Check if operands need parentheses based on precedence
442    let left_needs_parens = needs_parens_in_binop(op, left, true);
443    let right_needs_parens = needs_parens_in_binop(op, right, false);
444
445    let left_str = format_expr_impl(left, max_cols, indent);
446    let left_str = if left_needs_parens {
447        format!("({})", left_str)
448    } else {
449        left_str
450    };
451
452    // Special handling for via/into/where with lambda on the right
453    // Try to keep "via lambda" together on the same line
454    if matches!(op, BinaryOp::Via | BinaryOp::Into | BinaryOp::Where)
455        && let Expr::Lambda { .. } = &right.node
456    {
457        // Format the right side (lambda with possible do block)
458        let right_str = format_expr_impl(right, max_cols, indent);
459        let right_str = if right_needs_parens {
460            format!("({})", right_str)
461        } else {
462            right_str
463        };
464
465        // Check if the first line of the whole expression fits
466        // (for lambdas with do blocks, this would be "left via i => do {")
467        let first_line_of_right = right_str.lines().next().unwrap_or(&right_str);
468        let first_line_combined = format!("{} {} {}", left_str, op_str, first_line_of_right);
469
470        if indent + first_line_combined.len() <= max_cols {
471            // The opening line fits! Return the full formatted expression
472            // If right_str is multi-line, this will preserve that structure
473            if right_str.contains('\n') {
474                // Multi-line lambda (like with do block)
475                let remaining_lines = right_str.lines().skip(1).collect::<Vec<_>>().join("\n");
476                return format!(
477                    "{} {} {}\n{}",
478                    left_str, op_str, first_line_of_right, remaining_lines
479                );
480            } else {
481                // Single-line lambda
482                return format!("{} {} {}", left_str, op_str, right_str);
483            }
484        }
485
486        // If it doesn't fit, break before the operator (keep operator with right operand)
487        let continued_indent = indent;
488        let right_formatted = format_expr_impl(right, max_cols, continued_indent);
489        let right_formatted = if right_needs_parens {
490            format!("({})", right_formatted)
491        } else {
492            right_formatted
493        };
494        return format!(
495            "{}\n{}{} {}",
496            left_str,
497            make_indent(continued_indent),
498            op_str,
499            right_formatted
500        );
501    }
502
503    // Default: break before the operator with indentation
504    let right_indent = indent + INDENT_SIZE;
505    let right_str = format_expr_impl(right, max_cols, right_indent);
506    let right_str = if right_needs_parens {
507        format!("({})", right_str)
508    } else {
509        right_str
510    };
511    format!(
512        "{}\n{}{} {}",
513        left_str,
514        make_indent(right_indent),
515        op_str,
516        right_str
517    )
518}
519
520/// Format a do block (always multi-line)
521fn format_do_block_multiline(
522    statements: &[Commented<SpannedExpr>],
523    return_expr: &Commented<SpannedExpr>,
524    max_cols: usize,
525    indent: usize,
526) -> String {
527    let inner_indent = indent + INDENT_SIZE;
528    let indent_str = make_indent(inner_indent);
529
530    let mut result = "do {".to_string();
531
532    for stmt in statements {
533        // Leading comments
534        for comment in &stmt.leading {
535            result.push('\n');
536            result.push_str(&indent_str);
537            result.push_str(comment);
538        }
539        // Expression
540        result.push('\n');
541        result.push_str(&indent_str);
542        result.push_str(&format_expr_impl(&stmt.node, max_cols, inner_indent));
543        // Trailing comment
544        if let Some(trailing) = &stmt.trailing {
545            result.push_str("  ");
546            result.push_str(trailing);
547        }
548    }
549
550    // Return expression with leading comments
551    for comment in &return_expr.leading {
552        result.push('\n');
553        result.push_str(&indent_str);
554        result.push_str(comment);
555    }
556    result.push('\n');
557    result.push_str(&indent_str);
558    result.push_str("return ");
559    result.push_str(&format_expr_impl(&return_expr.node, max_cols, inner_indent));
560    result.push('\n');
561    result.push_str(&make_indent(indent));
562    result.push('}');
563
564    result
565}
566
567/// Convert lambda argument to string
568fn lambda_arg_to_str(arg: &LambdaArg) -> String {
569    match arg {
570        LambdaArg::Required(name) => name.clone(),
571        LambdaArg::Optional(name) => format!("{}?", name),
572        LambdaArg::Rest(name) => format!("...{}", name),
573    }
574}
575
576/// Convert binary operator to string
577fn binary_op_str(op: &BinaryOp) -> &'static str {
578    match op {
579        BinaryOp::Add => "+",
580        BinaryOp::Subtract => "-",
581        BinaryOp::Multiply => "*",
582        BinaryOp::Divide => "/",
583        BinaryOp::Modulo => "%",
584        BinaryOp::Power => "^",
585        BinaryOp::Equal => "==",
586        BinaryOp::NotEqual => "!=",
587        BinaryOp::Less => "<",
588        BinaryOp::LessEq => "<=",
589        BinaryOp::Greater => ">",
590        BinaryOp::GreaterEq => ">=",
591        BinaryOp::DotEqual => ".==",
592        BinaryOp::DotNotEqual => ".!=",
593        BinaryOp::DotLess => ".<",
594        BinaryOp::DotLessEq => ".<=",
595        BinaryOp::DotGreater => ".>",
596        BinaryOp::DotGreaterEq => ".>=",
597        BinaryOp::And => "&&",
598        BinaryOp::NaturalAnd => "and",
599        BinaryOp::Or => "||",
600        BinaryOp::NaturalOr => "or",
601        BinaryOp::Via => "via",
602        BinaryOp::Into => "into",
603        BinaryOp::Where => "where",
604        BinaryOp::Coalesce => "??",
605    }
606}
607
608/// Generate indentation string
609fn make_indent(indent: usize) -> String {
610    " ".repeat(indent)
611}
612
613/// Join formatted statements with appropriate spacing based on their original positions
614/// Preserves up to 2 empty lines between statements
615pub fn join_statements_with_spacing(
616    statements: &[(String, usize, usize)], // (formatted_statement, start_line, end_line)
617) -> String {
618    if statements.is_empty() {
619        return String::new();
620    }
621
622    let mut result = String::new();
623
624    for (i, (stmt, _start_line, end_line)) in statements.iter().enumerate() {
625        result.push_str(stmt);
626
627        // Add newlines between statements
628        if i < statements.len() - 1 {
629            let next_start_line = statements[i + 1].1;
630
631            // Calculate how many lines apart they are
632            // If they're on consecutive lines (end_line = 1, next_start = 2), gap = 0
633            // If there's one empty line (end_line = 1, next_start = 3), gap = 1
634            // If there's two empty lines (end_line = 1, next_start = 4), gap = 2
635            let line_gap = next_start_line.saturating_sub(*end_line).saturating_sub(1);
636
637            // Preserve up to 2 empty lines (which means up to 3 newlines total)
638            // 0 empty lines = 1 newline
639            // 1 empty line = 2 newlines
640            // 2 empty lines = 3 newlines
641            // 3+ empty lines = 3 newlines (capped at 2)
642            let newlines = std::cmp::min(line_gap + 1, 3);
643
644            for _ in 0..newlines {
645                result.push('\n');
646            }
647        }
648    }
649
650    result
651}
652
653#[cfg(test)]
654mod tests {
655    use super::*;
656    use crate::expressions::pairs_to_expr;
657    use crate::parser::get_pairs;
658
659    fn parse_test_expr(source: &str) -> SpannedExpr {
660        use crate::parser::Rule;
661
662        let pairs = get_pairs(source).unwrap();
663
664        // Extract the actual expression from the statement wrapper
665        for pair in pairs {
666            if pair.as_rule() == Rule::statement
667                && let Some(inner_pair) = pair.into_inner().next()
668            {
669                return pairs_to_expr(inner_pair.into_inner()).unwrap();
670            }
671        }
672
673        panic!("No statement found in parsed input");
674    }
675
676    #[test]
677    fn test_format_short_list() {
678        let expr = parse_test_expr("[1, 2, 3]");
679        let formatted = format_expr(&expr, Some(80));
680        assert_eq!(formatted, "[1, 2, 3]");
681    }
682
683    #[test]
684    fn test_format_long_list() {
685        let expr = parse_test_expr("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]");
686        let formatted = format_expr(&expr, Some(40));
687        assert!(formatted.contains("\n"));
688        assert!(formatted.contains("[\n"));
689    }
690
691    #[test]
692    fn test_format_short_record() {
693        let expr = parse_test_expr("{x: 1, y: 2}");
694        let formatted = format_expr(&expr, Some(80));
695        assert_eq!(formatted, "{x: 1, y: 2}");
696    }
697
698    #[test]
699    fn test_format_long_record() {
700        let expr = parse_test_expr(
701            "{name: \"Alice\", age: 30, email: \"alice@example.com\", address: \"123 Main St\"}",
702        );
703        let formatted = format_expr(&expr, Some(40));
704        assert!(formatted.contains("\n"));
705        assert!(formatted.contains("{\n"));
706    }
707
708    #[test]
709    fn test_format_conditional() {
710        let expr =
711            parse_test_expr("if very_long_condition_variable > 100 then \"yes\" else \"no\"");
712        let formatted = format_expr(&expr, Some(30));
713        assert!(formatted.contains("\n"));
714    }
715
716    #[test]
717    fn test_format_binary_op() {
718        let expr = parse_test_expr("very_long_variable_name + another_very_long_variable_name");
719        let formatted = format_expr(&expr, Some(30));
720        assert!(formatted.contains("\n"));
721    }
722
723    #[test]
724    fn test_format_lambda() {
725        let expr = parse_test_expr("(x, y) => x + y");
726        let formatted = format_expr(&expr, Some(80));
727        assert_eq!(formatted, "(x, y) => x + y");
728    }
729
730    #[test]
731    fn test_format_nested_list() {
732        let expr = parse_test_expr("[[1, 2, 3], [4, 5, 6], [7, 8, 9]]");
733        let formatted = format_expr(&expr, Some(20));
734        assert!(formatted.contains("\n"));
735    }
736
737    #[test]
738    fn test_format_function_call() {
739        let expr = parse_test_expr("map([1, 2, 3], x => x * 2)");
740        let formatted = format_expr(&expr, Some(80));
741        assert_eq!(formatted, "map([1, 2, 3], x => x * 2)");
742    }
743
744    #[test]
745    fn test_format_do_block() {
746        let expr = parse_test_expr("do { x = 1\n  return x }");
747        let formatted = format_expr(&expr, Some(80));
748        assert!(formatted.contains("do {"));
749        assert!(formatted.contains("return"));
750    }
751
752    #[test]
753    fn test_multiple_statements() {
754        use crate::parser::Rule;
755
756        let source = "x = [1, 2, 3, 4, 5]\ny = {name: \"Alice\", age: 30}\nz = x + y";
757        let pairs = get_pairs(source).unwrap();
758
759        let mut formatted_statements = Vec::new();
760
761        for pair in pairs {
762            if pair.as_rule() == Rule::statement
763                && let Some(inner_pair) = pair.into_inner().next()
764                && let Ok(expr) = pairs_to_expr(inner_pair.into_inner())
765            {
766                let formatted = format_expr(&expr, Some(80));
767                formatted_statements.push(formatted);
768            }
769        }
770
771        // Should have formatted all 3 statements
772        assert_eq!(formatted_statements.len(), 3);
773        assert_eq!(formatted_statements[0], "x = [1, 2, 3, 4, 5]");
774        assert_eq!(formatted_statements[1], "y = {name: \"Alice\", age: 30}");
775        assert_eq!(formatted_statements[2], "z = x + y");
776    }
777
778    #[test]
779    fn test_comments_are_preserved() {
780        use crate::parser::Rule;
781
782        let source = "// Comment 1\nx = [1, 2, 3]\n// Comment 2\ny = x + 1";
783        let pairs = get_pairs(source).unwrap();
784
785        let mut formatted_statements = Vec::new();
786
787        for pair in pairs {
788            if pair.as_rule() == Rule::statement
789                && let Some(inner_pair) = pair.into_inner().next()
790            {
791                match inner_pair.as_rule() {
792                    Rule::comment => {
793                        // Preserve comment as-is
794                        formatted_statements.push(inner_pair.as_str().to_string());
795                    }
796                    _ => {
797                        // Format as expression
798                        if let Ok(expr) = pairs_to_expr(inner_pair.into_inner()) {
799                            let formatted = format_expr(&expr, Some(80));
800                            formatted_statements.push(formatted);
801                        }
802                    }
803                }
804            }
805        }
806
807        // All 4 items should be preserved
808        assert_eq!(formatted_statements.len(), 4);
809        assert_eq!(formatted_statements[0], "// Comment 1");
810        assert_eq!(formatted_statements[1], "x = [1, 2, 3]");
811        assert_eq!(formatted_statements[2], "// Comment 2");
812        assert_eq!(formatted_statements[3], "y = x + 1");
813    }
814
815    #[test]
816    fn test_comments_with_formatted_code() {
817        use crate::parser::Rule;
818
819        let source = "// Configuration\nconfig = {name: \"test\", debug: true}\n// Process data\nresult = [1, 2, 3]";
820        let pairs = get_pairs(source).unwrap();
821
822        let mut formatted_statements = Vec::new();
823
824        for pair in pairs {
825            if pair.as_rule() == Rule::statement
826                && let Some(inner_pair) = pair.into_inner().next()
827            {
828                match inner_pair.as_rule() {
829                    Rule::comment => {
830                        formatted_statements.push(inner_pair.as_str().to_string());
831                    }
832                    _ => {
833                        if let Ok(expr) = pairs_to_expr(inner_pair.into_inner()) {
834                            let formatted = format_expr(&expr, Some(80));
835                            formatted_statements.push(formatted);
836                        }
837                    }
838                }
839            }
840        }
841
842        let result = formatted_statements.join("\n");
843
844        // Should have 4 statements: 2 comments + 2 expressions
845        assert_eq!(formatted_statements.len(), 4);
846        assert_eq!(formatted_statements[0], "// Configuration");
847        assert!(formatted_statements[1].starts_with("config = "));
848        assert_eq!(formatted_statements[2], "// Process data");
849        assert_eq!(formatted_statements[3], "result = [1, 2, 3]");
850
851        // Verify the full result preserves comments
852        assert!(result.contains("// Configuration"));
853        assert!(result.contains("// Process data"));
854    }
855
856    #[test]
857    fn test_output_declaration_with_assignment() {
858        use crate::parser::Rule;
859
860        let source = "output result = 42";
861        let pairs = get_pairs(source).unwrap();
862
863        let mut formatted_statements = Vec::new();
864
865        for pair in pairs {
866            if pair.as_rule() == Rule::statement
867                && let Some(inner_pair) = pair.into_inner().next()
868            {
869                match inner_pair.as_rule() {
870                    Rule::output_declaration => {
871                        // Should preserve the output keyword
872                        let mut inner = inner_pair.into_inner();
873                        let assignment_or_ident = inner.next().unwrap();
874
875                        let formatted = if assignment_or_ident.as_rule() == Rule::assignment {
876                            // Extract identifier and value from assignment
877                            let mut assignment_inner = assignment_or_ident.into_inner();
878                            let ident = assignment_inner.next().unwrap().as_str();
879                            let value_expr =
880                                pairs_to_expr(assignment_inner.next().unwrap().into_inner())
881                                    .unwrap();
882                            let value_formatted = format_expr(&value_expr, Some(80));
883                            format!("{} = {}", ident, value_formatted)
884                        } else {
885                            assignment_or_ident.as_str().to_string()
886                        };
887
888                        formatted_statements.push(format!("output {}", formatted));
889                    }
890                    _ => {
891                        if let Ok(expr) = pairs_to_expr(inner_pair.into_inner()) {
892                            let formatted = format_expr(&expr, Some(80));
893                            formatted_statements.push(formatted);
894                        }
895                    }
896                }
897            }
898        }
899
900        assert_eq!(formatted_statements.len(), 1);
901        assert_eq!(formatted_statements[0], "output result = 42");
902    }
903
904    #[test]
905    fn test_output_statement_separate() {
906        use crate::parser::Rule;
907
908        let source = "result = 42\noutput result";
909        let pairs = get_pairs(source).unwrap();
910
911        let mut formatted_statements = Vec::new();
912
913        for pair in pairs {
914            if pair.as_rule() == Rule::statement
915                && let Some(inner_pair) = pair.into_inner().next()
916            {
917                match inner_pair.as_rule() {
918                    Rule::output_declaration => {
919                        // Should preserve the output keyword
920                        let mut inner = inner_pair.into_inner();
921                        let assignment_or_ident = inner.next().unwrap();
922
923                        let formatted = if assignment_or_ident.as_rule() == Rule::assignment {
924                            // Extract identifier and value from assignment
925                            let mut assignment_inner = assignment_or_ident.into_inner();
926                            let ident = assignment_inner.next().unwrap().as_str();
927                            let value_expr =
928                                pairs_to_expr(assignment_inner.next().unwrap().into_inner())
929                                    .unwrap();
930                            let value_formatted = format_expr(&value_expr, Some(80));
931                            format!("{} = {}", ident, value_formatted)
932                        } else {
933                            assignment_or_ident.as_str().to_string()
934                        };
935
936                        formatted_statements.push(format!("output {}", formatted));
937                    }
938                    _ => {
939                        if let Ok(expr) = pairs_to_expr(inner_pair.into_inner()) {
940                            let formatted = format_expr(&expr, Some(80));
941                            formatted_statements.push(formatted);
942                        }
943                    }
944                }
945            }
946        }
947
948        assert_eq!(formatted_statements.len(), 2);
949        assert_eq!(formatted_statements[0], "result = 42");
950        assert_eq!(formatted_statements[1], "output result");
951    }
952
953    #[test]
954    fn test_output_with_complex_expression() {
955        use crate::parser::Rule;
956
957        let source = "output total = [1, 2, 3] into sum";
958        let pairs = get_pairs(source).unwrap();
959
960        let mut formatted_statements = Vec::new();
961
962        for pair in pairs {
963            if pair.as_rule() == Rule::statement
964                && let Some(inner_pair) = pair.into_inner().next()
965            {
966                match inner_pair.as_rule() {
967                    Rule::output_declaration => {
968                        // Should preserve the output keyword
969                        let mut inner = inner_pair.into_inner();
970                        let assignment_or_ident = inner.next().unwrap();
971
972                        let formatted = if assignment_or_ident.as_rule() == Rule::assignment {
973                            // Extract identifier and value from assignment
974                            let mut assignment_inner = assignment_or_ident.into_inner();
975                            let ident = assignment_inner.next().unwrap().as_str();
976                            let value_expr =
977                                pairs_to_expr(assignment_inner.next().unwrap().into_inner())
978                                    .unwrap();
979                            let value_formatted = format_expr(&value_expr, Some(80));
980                            format!("{} = {}", ident, value_formatted)
981                        } else {
982                            assignment_or_ident.as_str().to_string()
983                        };
984
985                        formatted_statements.push(format!("output {}", formatted));
986                    }
987                    _ => {
988                        if let Ok(expr) = pairs_to_expr(inner_pair.into_inner()) {
989                            let formatted = format_expr(&expr, Some(80));
990                            formatted_statements.push(formatted);
991                        }
992                    }
993                }
994            }
995        }
996
997        assert_eq!(formatted_statements.len(), 1);
998        assert_eq!(formatted_statements[0], "output total = [1, 2, 3] into sum");
999    }
1000
1001    #[test]
1002    fn test_end_of_line_comments() {
1003        use crate::parser::Rule;
1004
1005        let source = "x = 5  // this is an end-of-line comment\ny = 10";
1006        let pairs = get_pairs(source).unwrap();
1007
1008        let mut formatted_statements = Vec::new();
1009
1010        for pair in pairs {
1011            if pair.as_rule() == Rule::statement {
1012                let mut inner_pairs = pair.into_inner();
1013
1014                if let Some(first_pair) = inner_pairs.next() {
1015                    let formatted = match first_pair.as_rule() {
1016                        Rule::comment => first_pair.as_str().to_string(),
1017                        _ => {
1018                            if let Ok(expr) = pairs_to_expr(first_pair.into_inner()) {
1019                                format_expr(&expr, Some(80))
1020                            } else {
1021                                continue;
1022                            }
1023                        }
1024                    };
1025
1026                    // Check for end-of-line comment (second element in statement)
1027                    if let Some(eol_comment) = inner_pairs.next() {
1028                        if eol_comment.as_rule() == Rule::comment {
1029                            formatted_statements.push(format!(
1030                                "{}  {}",
1031                                formatted,
1032                                eol_comment.as_str()
1033                            ));
1034                        } else {
1035                            formatted_statements.push(formatted);
1036                        }
1037                    } else {
1038                        formatted_statements.push(formatted);
1039                    }
1040                }
1041            }
1042        }
1043
1044        assert_eq!(formatted_statements.len(), 2);
1045        assert_eq!(
1046            formatted_statements[0],
1047            "x = 5  // this is an end-of-line comment"
1048        );
1049        assert_eq!(formatted_statements[1], "y = 10");
1050    }
1051
1052    #[test]
1053    fn test_multiple_end_of_line_comments() {
1054        use crate::parser::Rule;
1055
1056        let source = "a = 1  // comment 1\nb = 2  // comment 2\nc = 3";
1057        let pairs = get_pairs(source).unwrap();
1058
1059        let mut formatted_statements = Vec::new();
1060
1061        for pair in pairs {
1062            if pair.as_rule() == Rule::statement {
1063                let mut inner_pairs = pair.into_inner();
1064
1065                if let Some(first_pair) = inner_pairs.next() {
1066                    let formatted = match first_pair.as_rule() {
1067                        Rule::comment => first_pair.as_str().to_string(),
1068                        _ => {
1069                            if let Ok(expr) = pairs_to_expr(first_pair.into_inner()) {
1070                                format_expr(&expr, Some(80))
1071                            } else {
1072                                continue;
1073                            }
1074                        }
1075                    };
1076
1077                    // Check for end-of-line comment
1078                    if let Some(eol_comment) = inner_pairs.next() {
1079                        if eol_comment.as_rule() == Rule::comment {
1080                            formatted_statements.push(format!(
1081                                "{}  {}",
1082                                formatted,
1083                                eol_comment.as_str()
1084                            ));
1085                        } else {
1086                            formatted_statements.push(formatted);
1087                        }
1088                    } else {
1089                        formatted_statements.push(formatted);
1090                    }
1091                }
1092            }
1093        }
1094
1095        assert_eq!(formatted_statements.len(), 3);
1096        assert_eq!(formatted_statements[0], "a = 1  // comment 1");
1097        assert_eq!(formatted_statements[1], "b = 2  // comment 2");
1098        assert_eq!(formatted_statements[2], "c = 3");
1099    }
1100
1101    #[test]
1102    fn test_eol_comments_not_joined_with_next_line() {
1103        use crate::parser::Rule;
1104
1105        // Ensure statements with EOL comments remain on their own line
1106        let source = "x = 1  // first value\ny = 2  // second value\nz = x + y";
1107        let pairs = get_pairs(source).unwrap();
1108
1109        let mut formatted_statements = Vec::new();
1110
1111        for pair in pairs {
1112            if pair.as_rule() == Rule::statement {
1113                let mut inner_pairs = pair.into_inner();
1114
1115                if let Some(first_pair) = inner_pairs.next() {
1116                    let formatted = match first_pair.as_rule() {
1117                        Rule::comment => first_pair.as_str().to_string(),
1118                        _ => {
1119                            if let Ok(expr) = pairs_to_expr(first_pair.into_inner()) {
1120                                format_expr(&expr, Some(80))
1121                            } else {
1122                                continue;
1123                            }
1124                        }
1125                    };
1126
1127                    // Check for end-of-line comment
1128                    if let Some(eol_comment) = inner_pairs.next() {
1129                        if eol_comment.as_rule() == Rule::comment {
1130                            formatted_statements.push(format!(
1131                                "{}  {}",
1132                                formatted,
1133                                eol_comment.as_str()
1134                            ));
1135                        } else {
1136                            formatted_statements.push(formatted);
1137                        }
1138                    } else {
1139                        formatted_statements.push(formatted);
1140                    }
1141                }
1142            }
1143        }
1144
1145        // Join with newlines - each statement should be on its own line
1146        let result = formatted_statements.join("\n");
1147
1148        assert_eq!(formatted_statements.len(), 3);
1149        // Verify no statement got joined into one line
1150        assert_eq!(result.lines().count(), 3);
1151        assert_eq!(formatted_statements[0], "x = 1  // first value");
1152        assert_eq!(formatted_statements[1], "y = 2  // second value");
1153        assert_eq!(formatted_statements[2], "z = x + y");
1154
1155        // Ensure the result doesn't contain any line with multiple statements
1156        for line in result.lines() {
1157            // Count equals signs - should only be 1 per line
1158            assert_eq!(
1159                line.matches('=').count(),
1160                1,
1161                "Line should not contain multiple statements: {}",
1162                line
1163            );
1164        }
1165    }
1166
1167    #[test]
1168    fn test_eol_comments_with_line_breaking() {
1169        use crate::parser::Rule;
1170
1171        // Test that expressions can still be broken across lines when they have EOL comments
1172        let source = "longRecord = {name: \"Alice\", age: 30, email: \"alice@example.com\", address: \"123 Main St\"}  // user data";
1173        let pairs = get_pairs(source).unwrap();
1174
1175        let mut formatted_statements = Vec::new();
1176
1177        for pair in pairs {
1178            if pair.as_rule() == Rule::statement {
1179                let mut inner_pairs = pair.into_inner();
1180
1181                if let Some(first_pair) = inner_pairs.next() {
1182                    let formatted = match first_pair.as_rule() {
1183                        Rule::comment => first_pair.as_str().to_string(),
1184                        _ => {
1185                            if let Ok(expr) = pairs_to_expr(first_pair.into_inner()) {
1186                                // Use a shorter line limit to force breaking
1187                                format_expr(&expr, Some(40))
1188                            } else {
1189                                continue;
1190                            }
1191                        }
1192                    };
1193
1194                    // Check for end-of-line comment
1195                    if let Some(eol_comment) = inner_pairs.next() {
1196                        if eol_comment.as_rule() == Rule::comment {
1197                            formatted_statements.push(format!(
1198                                "{}  {}",
1199                                formatted,
1200                                eol_comment.as_str()
1201                            ));
1202                        } else {
1203                            formatted_statements.push(formatted);
1204                        }
1205                    } else {
1206                        formatted_statements.push(formatted);
1207                    }
1208                }
1209            }
1210        }
1211
1212        assert_eq!(formatted_statements.len(), 1);
1213        let result = &formatted_statements[0];
1214
1215        // The comment should be at the end
1216        assert!(result.ends_with("// user data"));
1217        // The expression should be formatted (likely multi-line)
1218        assert!(result.contains("longRecord = "));
1219    }
1220
1221    #[test]
1222    fn test_actual_line_breaking_behavior() {
1223        use crate::parser::Rule;
1224
1225        // Test what actually happens with long lines
1226        let source = "x = {name: \"Alice\", age: 30, email: \"alice@example.com\", address: \"123 Main St\", city: \"Springfield\"}";
1227        let pairs = get_pairs(source).unwrap();
1228
1229        for pair in pairs {
1230            if pair.as_rule() == Rule::statement
1231                && let Some(inner_pair) = pair.into_inner().next()
1232                && let Ok(expr) = pairs_to_expr(inner_pair.into_inner())
1233            {
1234                let formatted = format_expr(&expr, Some(40));
1235                println!("Formatted output:\n{}", formatted);
1236                println!("Line count: {}", formatted.lines().count());
1237
1238                // Check if it's actually breaking lines
1239                if formatted.lines().count() > 1 {
1240                    println!("✓ Lines were broken");
1241                } else {
1242                    println!(
1243                        "✗ No line breaking occurred - output is {} chars",
1244                        formatted.len()
1245                    );
1246                }
1247            }
1248        }
1249    }
1250
1251    #[test]
1252    fn test_multiline_input_gets_collapsed() {
1253        use crate::parser::Rule;
1254
1255        // Test if multiline input gets collapsed to a single line
1256        let source = "x = [\n  1,\n  2,\n  3\n]";
1257        let pairs = get_pairs(source).unwrap();
1258
1259        for pair in pairs {
1260            if pair.as_rule() == Rule::statement
1261                && let Some(inner_pair) = pair.into_inner().next()
1262                && let Ok(expr) = pairs_to_expr(inner_pair.into_inner())
1263            {
1264                let formatted = format_expr(&expr, Some(80));
1265                println!("Input:\n{}", source);
1266                println!("Output:\n{}", formatted);
1267
1268                // Currently it collapses to single line
1269                assert_eq!(formatted, "x = [1, 2, 3]");
1270            }
1271        }
1272    }
1273
1274    #[test]
1275    fn test_empty_lines_between_statements() {
1276        use crate::parser::Rule;
1277
1278        // Test that empty lines between statements are preserved (up to 2)
1279        let source = "x = 1\n\ny = 2\n\n\n\nz = 3";
1280        let pairs = get_pairs(source).unwrap();
1281
1282        let mut statements_with_positions = Vec::new();
1283
1284        for pair in pairs {
1285            if pair.as_rule() == Rule::statement {
1286                let start_line = pair.as_span().start_pos().line_col().0;
1287                let end_line = pair.as_span().end_pos().line_col().0;
1288
1289                if let Some(inner_pair) = pair.into_inner().next()
1290                    && let Ok(expr) = pairs_to_expr(inner_pair.into_inner())
1291                {
1292                    let formatted = format_expr(&expr, Some(80));
1293                    statements_with_positions.push((formatted, start_line, end_line));
1294                }
1295            }
1296        }
1297
1298        println!("Statement positions: {:?}", statements_with_positions);
1299
1300        // Use the new helper function to join with spacing
1301        let result = join_statements_with_spacing(&statements_with_positions);
1302        println!("Output with preserved spacing:\n{}", result);
1303
1304        // We want to preserve 1 empty line between x and y, and cap at 2 between y and z
1305        // So it should be:
1306        // x = 1
1307        // <blank>
1308        // y = 2
1309        // <blank>
1310        // <blank>
1311        // z = 3
1312
1313        assert_eq!(result.lines().count(), 6); // 3 statements + 1 blank + 2 blanks
1314        assert_eq!(result, "x = 1\n\ny = 2\n\n\nz = 3");
1315    }
1316
1317    #[test]
1318    fn test_assignment_with_long_list_indentation() {
1319        use crate::parser::Rule;
1320
1321        // Test that assignments with lists don't have excessive indentation
1322        let source = "ingredients = [{name: \"sugar\", amount: 1}, {name: \"flour\", amount: 2}]";
1323        let pairs = get_pairs(source).unwrap();
1324
1325        for pair in pairs {
1326            if pair.as_rule() == Rule::statement
1327                && let Some(inner_pair) = pair.into_inner().next()
1328                && let Ok(expr) = pairs_to_expr(inner_pair.into_inner())
1329            {
1330                let formatted = format_expr(&expr, Some(40));
1331                println!("Formatted:\n{}", formatted);
1332
1333                // The list items should be indented by 2 spaces, not pushed over by the "ingredients = " prefix
1334                assert!(formatted.contains("[\n  {"));
1335                assert!(!formatted.contains("            ")); // Should not have excessive indentation
1336            }
1337        }
1338    }
1339
1340    #[test]
1341    fn test_else_if_chain_stays_flat() {
1342        // Test that else-if chains don't create staircase indentation
1343        let source = "x = if a then 1 else if b then 2 else if c then 3 else 4";
1344        let expr = parse_test_expr(source);
1345        let formatted = format_expr(&expr, Some(40));
1346        println!("Formatted:\n{}", formatted);
1347
1348        // All "else if" should be at the same indentation level (no staircase)
1349        // Count how many times we see "else if" - should be 2
1350        let else_if_count = formatted.matches("else if").count();
1351        assert_eq!(else_if_count, 2, "Should have 2 'else if' clauses");
1352
1353        // Check that there's no increasing indentation (staircase pattern)
1354        // The then branches should be indented by 2 spaces
1355        for line in formatted.lines() {
1356            let leading_spaces = line.len() - line.trim_start().len();
1357            // No line should be indented more than 2 spaces
1358            assert!(
1359                leading_spaces <= 2,
1360                "Line has too much indentation: '{}'",
1361                line
1362            );
1363        }
1364    }
1365
1366    #[test]
1367    fn test_long_else_if_chain() {
1368        // Test with a longer else-if chain similar to the user's tax bracket example
1369        let source = "tax = if income <= 10000 then income * 0.1 else if income <= 50000 then 1000 + (income - 10000) * 0.2 else if income <= 100000 then 9000 + (income - 50000) * 0.3 else 24000 + (income - 100000) * 0.4";
1370        let expr = parse_test_expr(source);
1371        let formatted = format_expr(&expr, Some(60));
1372        println!("Formatted:\n{}", formatted);
1373
1374        // Should have 3 'else if' or 'else' clauses and all at the same level
1375        let else_count = formatted.matches("\nelse").count();
1376        assert!(
1377            else_count >= 3,
1378            "Should have at least 3 else/else-if clauses, found {}",
1379            else_count
1380        );
1381
1382        // Verify no staircase - each "else if" should start at column 0
1383        for line in formatted.lines() {
1384            if line.starts_with("else") {
1385                // "else" and "else if" should start at the beginning of the line
1386                assert!(
1387                    line.starts_with("else"),
1388                    "else clause should start at column 0: '{}'",
1389                    line
1390                );
1391            }
1392        }
1393    }
1394
1395    #[test]
1396    fn test_multiline_preserves_precedence_parentheses() {
1397        // Test that multiline formatting preserves necessary parentheses for precedence
1398        // This is the loan amortization formula: loan * numerator / denominator
1399        // The denominator needs parens because subtraction has lower precedence than division
1400        let source = "result = amount * (rate * (1 + rate) ^ n) / ((1 + rate) ^ n - 1)";
1401        let expr = parse_test_expr(source);
1402        let formatted = format_expr(&expr, Some(40));
1403        println!("Formatted:\n{}", formatted);
1404
1405        // The denominator ((1 + rate) ^ n - 1) must be wrapped in parentheses
1406        // because subtraction has lower precedence than division
1407        assert!(
1408            formatted.contains("/ ((1 + rate) ^ n - 1)"),
1409            "Denominator should be wrapped in parentheses to preserve precedence. Got:\n{}",
1410            formatted
1411        );
1412    }
1413
1414    #[test]
1415    fn test_multiline_division_with_subtraction() {
1416        // Simpler test: a / (b - c) should preserve the parens
1417        let source = "x = a / (b - c)";
1418        let expr = parse_test_expr(source);
1419        let formatted = format_expr(&expr, Some(80));
1420
1421        assert_eq!(formatted, "x = a / (b - c)");
1422    }
1423
1424    #[test]
1425    fn test_multiline_multiplication_with_addition() {
1426        // a * (b + c) should preserve the parens
1427        let source = "x = a * (b + c)";
1428        let expr = parse_test_expr(source);
1429        let formatted = format_expr(&expr, Some(80));
1430
1431        assert_eq!(formatted, "x = a * (b + c)");
1432    }
1433
1434    #[test]
1435    fn test_else_indented_in_lambda_body() {
1436        // When a conditional is inside a lambda body, the else keyword should be
1437        // indented to match the if, not placed at column 0
1438        let source = "is_positive = n => if n > 0 then true else false";
1439        let expr = parse_test_expr(source);
1440        let formatted = format_expr(&expr, Some(30)); // Force multiline
1441
1442        // The else should be indented, not at column 0
1443        for line in formatted.lines() {
1444            if line.trim().starts_with("else") {
1445                let leading_spaces = line.len() - line.trim_start().len();
1446                assert!(
1447                    leading_spaces > 0,
1448                    "else should be indented in lambda body, got: '{}'",
1449                    line
1450                );
1451            }
1452        }
1453
1454        // Verify the formatted output is still valid syntax by re-parsing
1455        let reparsed = parse_test_expr(&formatted);
1456        assert!(matches!(reparsed.node, Expr::Assignment { .. }));
1457    }
1458
1459    #[test]
1460    fn test_via_breaks_before_operator_not_after() {
1461        // When a via expression is too long, it should break BEFORE the operator,
1462        // not after (breaking after would leave 'via' at end of line, which is a parse error)
1463        let source = "result = [1, 2, 3, 4, 5] via (x) => x * 2";
1464        let expr = parse_test_expr(source);
1465        let formatted = format_expr(&expr, Some(30)); // Force multiline
1466
1467        // If it breaks, 'via' should be at the start of a line, not at the end
1468        for line in formatted.lines() {
1469            assert!(
1470                !line.trim_end().ends_with("via"),
1471                "via should not be at end of line (would be parse error), got: '{}'",
1472                line
1473            );
1474        }
1475
1476        // Verify the formatted output is still valid syntax by re-parsing
1477        let reparsed = parse_test_expr(&formatted);
1478        assert!(matches!(reparsed.node, Expr::Assignment { .. }));
1479    }
1480
1481    #[test]
1482    fn test_record_keys_with_spaces_preserve_quotes() {
1483        // Record keys with spaces need to be quoted to be valid syntax
1484        let source = r#"x = {"my key": 1, "another-key": 2}"#;
1485        let expr = parse_test_expr(source);
1486        let formatted = format_expr(&expr, Some(80));
1487
1488        // The formatted output should preserve the quotes around keys with spaces/dashes
1489        assert!(
1490            formatted.contains(r#""my key""#),
1491            "Key with spaces should remain quoted, got: {}",
1492            formatted
1493        );
1494        assert!(
1495            formatted.contains(r#""another-key""#),
1496            "Key with dashes should remain quoted, got: {}",
1497            formatted
1498        );
1499
1500        // Verify the formatted output is still valid syntax by re-parsing
1501        let reparsed = parse_test_expr(&formatted);
1502        assert!(matches!(reparsed.node, Expr::Assignment { .. }));
1503    }
1504
1505    #[test]
1506    fn test_record_keys_valid_identifiers_no_quotes() {
1507        // Record keys that are valid identifiers should NOT have quotes
1508        let source = r#"x = {name: "Alice", age: 30}"#;
1509        let expr = parse_test_expr(source);
1510        let formatted = format_expr(&expr, Some(80));
1511
1512        // Valid identifier keys should not be quoted
1513        assert_eq!(formatted, r#"x = {name: "Alice", age: 30}"#);
1514    }
1515
1516    #[test]
1517    fn test_record_keys_starting_with_number_need_quotes() {
1518        // Record keys starting with a number are not valid identifiers
1519        let source = r#"x = {"123abc": 1}"#;
1520        let expr = parse_test_expr(source);
1521        let formatted = format_expr(&expr, Some(80));
1522
1523        assert!(
1524            formatted.contains(r#""123abc""#),
1525            "Key starting with number should remain quoted, got: {}",
1526            formatted
1527        );
1528
1529        // Verify the formatted output is still valid syntax by re-parsing
1530        let reparsed = parse_test_expr(&formatted);
1531        assert!(matches!(reparsed.node, Expr::Assignment { .. }));
1532    }
1533
1534    #[test]
1535    fn test_list_comments_preserved() {
1536        use crate::expressions::pairs_to_expr_with_comments;
1537        use crate::parser::Rule;
1538
1539        let source = "x = [\n  // first item\n  1,\n  2,  // inline\n  // third\n  3,\n]";
1540        let pairs = get_pairs(source).unwrap();
1541
1542        for pair in pairs {
1543            if pair.as_rule() == Rule::statement
1544                && let Some(inner_pair) = pair.into_inner().next()
1545                && let Ok(expr) = pairs_to_expr_with_comments(inner_pair.into_inner())
1546            {
1547                let formatted = format_expr(&expr, Some(80));
1548
1549                assert!(
1550                    formatted.contains("// first item"),
1551                    "Leading comment should be preserved, got:\n{}",
1552                    formatted
1553                );
1554                assert!(
1555                    formatted.contains("// inline"),
1556                    "Inline comment should be preserved, got:\n{}",
1557                    formatted
1558                );
1559                assert!(
1560                    formatted.contains("// third"),
1561                    "Leading comment on third item should be preserved, got:\n{}",
1562                    formatted
1563                );
1564            }
1565        }
1566    }
1567
1568    #[test]
1569    fn test_record_comments_preserved() {
1570        use crate::expressions::pairs_to_expr_with_comments;
1571        use crate::parser::Rule;
1572
1573        let source = "x = {\n  // name field\n  name: \"Alice\",\n  age: 30,  // years old\n}";
1574        let pairs = get_pairs(source).unwrap();
1575
1576        for pair in pairs {
1577            if pair.as_rule() == Rule::statement
1578                && let Some(inner_pair) = pair.into_inner().next()
1579                && let Ok(expr) = pairs_to_expr_with_comments(inner_pair.into_inner())
1580            {
1581                let formatted = format_expr(&expr, Some(80));
1582
1583                assert!(
1584                    formatted.contains("// name field"),
1585                    "Leading comment should be preserved, got:\n{}",
1586                    formatted
1587                );
1588                assert!(
1589                    formatted.contains("// years old"),
1590                    "Inline comment should be preserved, got:\n{}",
1591                    formatted
1592                );
1593            }
1594        }
1595    }
1596
1597    #[test]
1598    fn test_do_block_comments_preserved() {
1599        use crate::expressions::pairs_to_expr_with_comments;
1600        use crate::parser::Rule;
1601
1602        let source =
1603            "f = n => do {\n  // compute\n  doubled = n * 2\n  // result\n  return doubled\n}";
1604        let pairs = get_pairs(source).unwrap();
1605
1606        for pair in pairs {
1607            if pair.as_rule() == Rule::statement
1608                && let Some(inner_pair) = pair.into_inner().next()
1609                && let Ok(expr) = pairs_to_expr_with_comments(inner_pair.into_inner())
1610            {
1611                let formatted = format_expr(&expr, Some(80));
1612
1613                assert!(
1614                    formatted.contains("// compute"),
1615                    "Statement leading comment should be preserved, got:\n{}",
1616                    formatted
1617                );
1618                assert!(
1619                    formatted.contains("// result"),
1620                    "Return leading comment should be preserved, got:\n{}",
1621                    formatted
1622                );
1623            }
1624        }
1625    }
1626
1627    #[test]
1628    fn test_comments_force_multiline() {
1629        use crate::expressions::pairs_to_expr_with_comments;
1630        use crate::parser::Rule;
1631
1632        // Even a short list with comments should be multiline
1633        let source = "x = [1,  // one\n2]";
1634        let pairs = get_pairs(source).unwrap();
1635
1636        for pair in pairs {
1637            if pair.as_rule() == Rule::statement
1638                && let Some(inner_pair) = pair.into_inner().next()
1639                && let Ok(expr) = pairs_to_expr_with_comments(inner_pair.into_inner())
1640            {
1641                let formatted = format_expr(&expr, Some(80));
1642
1643                // Should contain newlines because of the comment
1644                assert!(
1645                    formatted.contains('\n'),
1646                    "List with comments should be multiline, got:\n{}",
1647                    formatted
1648                );
1649                assert!(
1650                    formatted.contains("// one"),
1651                    "Comment should be preserved, got:\n{}",
1652                    formatted
1653                );
1654            }
1655        }
1656    }
1657}