blots_core/
formatter.rs

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