blots_core/
formatter.rs

1use crate::ast::{BinaryOp, DoStatement, Expr, RecordEntry, RecordKey, SpannedExpr};
2use crate::ast_to_source::{expr_to_source};
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.iter().map(format_record_entry_single_line).collect();
73            format!("{{{}}}", entries_str.join(", "))
74        }
75        // For everything else, use the existing expr_to_source
76        _ => expr_to_source(expr),
77    }
78}
79
80/// Format a record entry on a single line
81fn format_record_entry_single_line(entry: &RecordEntry) -> String {
82    match &entry.key {
83        RecordKey::Static(key) => format!("{}: {}", key, format_single_line(&entry.value)),
84        RecordKey::Dynamic(key_expr) => {
85            format!("[{}]: {}", format_single_line(key_expr), format_single_line(&entry.value))
86        }
87        RecordKey::Shorthand(name) => name.clone(),
88        RecordKey::Spread(expr) => format_single_line(expr),
89    }
90}
91
92/// Format an expression across multiple lines
93fn format_multiline(expr: &SpannedExpr, max_cols: usize, indent: usize) -> String {
94    match &expr.node {
95        Expr::Output { expr: inner_expr } => {
96            // Format as "output " + formatted inner expression
97            let formatted_inner = format_expr_impl(inner_expr, max_cols, indent);
98            format!("output {}", formatted_inner)
99        }
100        Expr::Assignment { ident, value } => {
101            format_assignment_multiline(ident, value, max_cols, indent)
102        }
103        Expr::List(items) => format_list_multiline(items, max_cols, indent),
104        Expr::Record(entries) => format_record_multiline(entries, max_cols, indent),
105        Expr::Conditional { condition, then_expr, else_expr } => {
106            format_conditional_multiline(condition, then_expr, else_expr, max_cols, indent)
107        }
108        Expr::Call { func, args } => format_call_multiline(func, args, max_cols, indent),
109        Expr::BinaryOp { op, left, right } => {
110            format_binary_op_multiline(op, left, right, max_cols, indent)
111        }
112        Expr::DoBlock { statements, return_expr } => {
113            format_do_block_multiline(statements, return_expr, max_cols, indent)
114        }
115        // For other expression types, fall back to single-line
116        _ => expr_to_source(expr),
117    }
118}
119
120/// Format an assignment with line breaks
121fn format_assignment_multiline(ident: &str, value: &SpannedExpr, max_cols: usize, indent: usize) -> String {
122    // The assignment itself doesn't add indentation, but the value might need it
123    // Format as: ident = <formatted_value>
124    // The value should be formatted at the same indentation level, not pushed over
125
126    let prefix = format!("{} = ", ident);
127
128    // Format the value at the current indentation level
129    // (not at prefix_len which would cause excessive indentation)
130    let formatted_value = format_expr_impl(value, max_cols, indent);
131
132    format!("{}{}", prefix, formatted_value)
133}
134
135/// Format a list with line breaks
136fn format_list_multiline(items: &[SpannedExpr], max_cols: usize, indent: usize) -> String {
137    if items.is_empty() {
138        return "[]".to_string();
139    }
140
141    let inner_indent = indent + INDENT_SIZE;
142    let indent_str = make_indent(inner_indent);
143
144    let mut result = "[".to_string();
145
146    for item in items.iter() {
147        result.push('\n');
148        result.push_str(&indent_str);
149        result.push_str(&format_expr_impl(item, max_cols, inner_indent));
150
151        // Add comma after each item (including last for multi-line)
152        result.push(',');
153    }
154
155    result.push('\n');
156    result.push_str(&make_indent(indent));
157    result.push(']');
158
159    result
160}
161
162/// Format a record with line breaks
163fn format_record_multiline(entries: &[RecordEntry], max_cols: usize, indent: usize) -> String {
164    if entries.is_empty() {
165        return "{}".to_string();
166    }
167
168    let inner_indent = indent + INDENT_SIZE;
169    let indent_str = make_indent(inner_indent);
170
171    let mut result = "{".to_string();
172
173    for entry in entries {
174        result.push('\n');
175        result.push_str(&indent_str);
176        result.push_str(&format_record_entry(entry, max_cols, inner_indent));
177
178        // Add comma after each entry (including last for multi-line)
179        result.push(',');
180    }
181
182    result.push('\n');
183    result.push_str(&make_indent(indent));
184    result.push('}');
185
186    result
187}
188
189/// Format a single record entry
190fn format_record_entry(entry: &RecordEntry, max_cols: usize, indent: usize) -> String {
191    match &entry.key {
192        RecordKey::Static(key) => {
193            format!("{}: {}", key, format_expr_impl(&entry.value, max_cols, indent))
194        }
195        RecordKey::Dynamic(key_expr) => {
196            format!(
197                "[{}]: {}",
198                format_expr_impl(key_expr, max_cols, indent),
199                format_expr_impl(&entry.value, max_cols, indent)
200            )
201        }
202        RecordKey::Shorthand(name) => name.clone(),
203        RecordKey::Spread(expr) => format_expr_impl(expr, max_cols, indent),
204    }
205}
206
207/// Format a lambda (handles both single-line and multi-line)
208fn format_lambda(args: &[LambdaArg], body: &SpannedExpr, max_cols: usize, indent: usize) -> String {
209    let args_str: Vec<String> = args.iter().map(lambda_arg_to_str).collect();
210
211    // For single required arguments, omit parentheses
212    let args_part = if args.len() == 1 && matches!(args[0], LambdaArg::Required(_)) {
213        format!("{} =>", args_str[0])
214    } else {
215        format!("({}) =>", args_str.join(", "))
216    };
217
218    // Special handling for do blocks - keep "=> do {" together
219    if let Expr::DoBlock { .. } = &body.node {
220        let body_formatted = format_expr_impl(body, max_cols, indent);
221        return format!("{} {}", args_part, body_formatted);
222    }
223
224    // Try single-line first for other body types
225    let single_line_body = format_expr_impl(body, max_cols, indent);
226    let single_line = format!("{} {}", args_part, single_line_body);
227
228    // Check only if it's actually single-line and fits
229    if !single_line.contains('\n') && indent + single_line.len() <= max_cols {
230        return single_line;
231    }
232
233    // Otherwise, put body on next line with increased indentation
234    let body_indent = indent + INDENT_SIZE;
235    format!(
236        "{}\n{}{}",
237        args_part,
238        make_indent(body_indent),
239        format_expr_impl(body, max_cols, body_indent)
240    )
241}
242
243/// Format a conditional with line breaks
244fn format_conditional_multiline(
245    condition: &SpannedExpr,
246    then_expr: &SpannedExpr,
247    else_expr: &SpannedExpr,
248    max_cols: usize,
249    indent: usize,
250) -> String {
251    let cond_str = format_expr_impl(condition, max_cols, indent);
252
253    // Try to fit "if <condition> then" on one line
254    let if_then_prefix = format!("if {} then", cond_str);
255
256    if indent + if_then_prefix.len() <= max_cols {
257        // Put then/else clauses on new lines
258        let inner_indent = indent + INDENT_SIZE;
259        format!(
260            "{}\n{}{}\nelse\n{}{}",
261            if_then_prefix,
262            make_indent(inner_indent),
263            format_expr_impl(then_expr, max_cols, inner_indent),
264            make_indent(inner_indent),
265            format_expr_impl(else_expr, max_cols, inner_indent)
266        )
267    } else {
268        // Everything on separate lines
269        let inner_indent = indent + INDENT_SIZE;
270        format!(
271            "if\n{}{}\nthen\n{}{}\nelse\n{}{}",
272            make_indent(inner_indent),
273            format_expr_impl(condition, max_cols, inner_indent),
274            make_indent(inner_indent),
275            format_expr_impl(then_expr, max_cols, inner_indent),
276            make_indent(inner_indent),
277            format_expr_impl(else_expr, max_cols, inner_indent)
278        )
279    }
280}
281
282/// Format a function call with line breaks
283fn format_call_multiline(func: &SpannedExpr, args: &[SpannedExpr], max_cols: usize, indent: usize) -> String {
284    let func_str = match &func.node {
285        Expr::Lambda { .. } => format!("({})", format_expr_impl(func, max_cols, indent)),
286        _ => format_expr_impl(func, max_cols, indent),
287    };
288
289    if args.is_empty() {
290        return format!("{}()", func_str);
291    }
292
293    // Try formatting args on separate lines
294    let inner_indent = indent + INDENT_SIZE;
295    let indent_str = make_indent(inner_indent);
296
297    let mut result = format!("{}(", func_str);
298
299    for (i, arg) in args.iter().enumerate() {
300        result.push('\n');
301        result.push_str(&indent_str);
302        result.push_str(&format_expr_impl(arg, max_cols, inner_indent));
303
304        if i < args.len() - 1 {
305            result.push(',');
306        } else {
307            // Trailing comma on last arg for multi-line
308            result.push(',');
309        }
310    }
311
312    result.push('\n');
313    result.push_str(&make_indent(indent));
314    result.push(')');
315
316    result
317}
318
319/// Format a binary operation with line breaks
320fn format_binary_op_multiline(
321    op: &BinaryOp,
322    left: &SpannedExpr,
323    right: &SpannedExpr,
324    max_cols: usize,
325    indent: usize,
326) -> String {
327    let op_str = binary_op_str(op);
328    let left_str = format_expr_impl(left, max_cols, indent);
329
330    // Special handling for via/into/where with lambda on the right
331    // Try to keep "via lambda" together on the same line
332    if matches!(op, BinaryOp::Via | BinaryOp::Into | BinaryOp::Where)
333        && let Expr::Lambda { .. } = &right.node {
334            // Format the right side (lambda with possible do block)
335            let right_str = format_expr_impl(right, max_cols, indent);
336
337            // Check if the first line of the whole expression fits
338            // (for lambdas with do blocks, this would be "left via i => do {")
339            let first_line_of_right = right_str.lines().next().unwrap_or(&right_str);
340            let first_line_combined = format!("{} {} {}", left_str, op_str, first_line_of_right);
341
342            if indent + first_line_combined.len() <= max_cols {
343                // The opening line fits! Return the full formatted expression
344                // If right_str is multi-line, this will preserve that structure
345                if right_str.contains('\n') {
346                    // Multi-line lambda (like with do block)
347                    let remaining_lines = right_str.lines().skip(1).collect::<Vec<_>>().join("\n");
348                    return format!("{} {} {}\n{}", left_str, op_str, first_line_of_right, remaining_lines);
349                } else {
350                    // Single-line lambda
351                    return format!("{} {} {}", left_str, op_str, right_str);
352                }
353            }
354
355            // If it doesn't fit, break after the operator
356            let continued_indent = indent;
357            let right_formatted = format_expr_impl(right, max_cols, continued_indent);
358            return format!("{} {}\n{}{}", left_str, op_str, make_indent(continued_indent), right_formatted);
359        }
360
361    // Default: break before the operator with indentation
362    let right_indent = indent + INDENT_SIZE;
363    format!(
364        "{}\n{}{} {}",
365        left_str,
366        make_indent(right_indent),
367        op_str,
368        format_expr_impl(right, max_cols, right_indent)
369    )
370}
371
372/// Format a do block (always multi-line)
373fn format_do_block_multiline(statements: &[DoStatement], return_expr: &SpannedExpr, max_cols: usize, indent: usize) -> String {
374    let inner_indent = indent + INDENT_SIZE;
375    let indent_str = make_indent(inner_indent);
376
377    let mut result = "do {".to_string();
378
379    for stmt in statements {
380        match stmt {
381            DoStatement::Expression(e) => {
382                result.push('\n');
383                result.push_str(&indent_str);
384                result.push_str(&format_expr_impl(e, max_cols, inner_indent));
385            }
386            DoStatement::Comment(c) => {
387                result.push('\n');
388                result.push_str(&indent_str);
389                result.push_str(c);
390            }
391        }
392    }
393
394    result.push('\n');
395    result.push_str(&indent_str);
396    result.push_str("return ");
397    result.push_str(&format_expr_impl(return_expr, max_cols, inner_indent));
398    result.push('\n');
399    result.push_str(&make_indent(indent));
400    result.push('}');
401
402    result
403}
404
405/// Convert lambda argument to string
406fn lambda_arg_to_str(arg: &LambdaArg) -> String {
407    match arg {
408        LambdaArg::Required(name) => name.clone(),
409        LambdaArg::Optional(name) => format!("{}?", name),
410        LambdaArg::Rest(name) => format!("...{}", name),
411    }
412}
413
414/// Convert binary operator to string
415fn binary_op_str(op: &BinaryOp) -> &'static str {
416    match op {
417        BinaryOp::Add => "+",
418        BinaryOp::Subtract => "-",
419        BinaryOp::Multiply => "*",
420        BinaryOp::Divide => "/",
421        BinaryOp::Modulo => "%",
422        BinaryOp::Power => "^",
423        BinaryOp::Equal => "==",
424        BinaryOp::NotEqual => "!=",
425        BinaryOp::Less => "<",
426        BinaryOp::LessEq => "<=",
427        BinaryOp::Greater => ">",
428        BinaryOp::GreaterEq => ">=",
429        BinaryOp::DotEqual => ".==",
430        BinaryOp::DotNotEqual => ".!=",
431        BinaryOp::DotLess => ".<",
432        BinaryOp::DotLessEq => ".<=",
433        BinaryOp::DotGreater => ".>",
434        BinaryOp::DotGreaterEq => ".>=",
435        BinaryOp::And => "&&",
436        BinaryOp::NaturalAnd => "and",
437        BinaryOp::Or => "||",
438        BinaryOp::NaturalOr => "or",
439        BinaryOp::Via => "via",
440        BinaryOp::Into => "into",
441        BinaryOp::Where => "where",
442        BinaryOp::Coalesce => "??",
443    }
444}
445
446/// Generate indentation string
447fn make_indent(indent: usize) -> String {
448    " ".repeat(indent)
449}
450
451/// Join formatted statements with appropriate spacing based on their original positions
452/// Preserves up to 2 empty lines between statements
453pub fn join_statements_with_spacing(
454    statements: &[(String, usize, usize)], // (formatted_statement, start_line, end_line)
455) -> String {
456    if statements.is_empty() {
457        return String::new();
458    }
459
460    let mut result = String::new();
461
462    for (i, (stmt, _start_line, end_line)) in statements.iter().enumerate() {
463        result.push_str(stmt);
464
465        // Add newlines between statements
466        if i < statements.len() - 1 {
467            let next_start_line = statements[i + 1].1;
468
469            // Calculate how many lines apart they are
470            // If they're on consecutive lines (end_line = 1, next_start = 2), gap = 0
471            // If there's one empty line (end_line = 1, next_start = 3), gap = 1
472            // If there's two empty lines (end_line = 1, next_start = 4), gap = 2
473            let line_gap = next_start_line.saturating_sub(*end_line).saturating_sub(1);
474
475            // Preserve up to 2 empty lines (which means up to 3 newlines total)
476            // 0 empty lines = 1 newline
477            // 1 empty line = 2 newlines
478            // 2 empty lines = 3 newlines
479            // 3+ empty lines = 3 newlines (capped at 2)
480            let newlines = std::cmp::min(line_gap + 1, 3);
481
482            for _ in 0..newlines {
483                result.push('\n');
484            }
485        }
486    }
487
488    result
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494    use crate::parser::get_pairs;
495    use crate::expressions::pairs_to_expr;
496
497    fn parse_test_expr(source: &str) -> SpannedExpr {
498        use crate::parser::Rule;
499
500        let pairs = get_pairs(source).unwrap();
501
502        // Extract the actual expression from the statement wrapper
503        for pair in pairs {
504            if pair.as_rule() == Rule::statement {
505                if let Some(inner_pair) = pair.into_inner().next() {
506                    return pairs_to_expr(inner_pair.into_inner()).unwrap();
507                }
508            }
509        }
510
511        panic!("No statement found in parsed input");
512    }
513
514    #[test]
515    fn test_format_short_list() {
516        let expr = parse_test_expr("[1, 2, 3]");
517        let formatted = format_expr(&expr, Some(80));
518        assert_eq!(formatted, "[1, 2, 3]");
519    }
520
521    #[test]
522    fn test_format_long_list() {
523        let expr = parse_test_expr("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]");
524        let formatted = format_expr(&expr, Some(40));
525        assert!(formatted.contains("\n"));
526        assert!(formatted.contains("[\n"));
527    }
528
529    #[test]
530    fn test_format_short_record() {
531        let expr = parse_test_expr("{x: 1, y: 2}");
532        let formatted = format_expr(&expr, Some(80));
533        assert_eq!(formatted, "{x: 1, y: 2}");
534    }
535
536    #[test]
537    fn test_format_long_record() {
538        let expr = parse_test_expr("{name: \"Alice\", age: 30, email: \"alice@example.com\", address: \"123 Main St\"}");
539        let formatted = format_expr(&expr, Some(40));
540        assert!(formatted.contains("\n"));
541        assert!(formatted.contains("{\n"));
542    }
543
544    #[test]
545    fn test_format_conditional() {
546        let expr = parse_test_expr("if very_long_condition_variable > 100 then \"yes\" else \"no\"");
547        let formatted = format_expr(&expr, Some(30));
548        assert!(formatted.contains("\n"));
549    }
550
551    #[test]
552    fn test_format_binary_op() {
553        let expr = parse_test_expr("very_long_variable_name + another_very_long_variable_name");
554        let formatted = format_expr(&expr, Some(30));
555        assert!(formatted.contains("\n"));
556    }
557
558    #[test]
559    fn test_format_lambda() {
560        let expr = parse_test_expr("(x, y) => x + y");
561        let formatted = format_expr(&expr, Some(80));
562        assert_eq!(formatted, "(x, y) => x + y");
563    }
564
565    #[test]
566    fn test_format_nested_list() {
567        let expr = parse_test_expr("[[1, 2, 3], [4, 5, 6], [7, 8, 9]]");
568        let formatted = format_expr(&expr, Some(20));
569        assert!(formatted.contains("\n"));
570    }
571
572    #[test]
573    fn test_format_function_call() {
574        let expr = parse_test_expr("map([1, 2, 3], x => x * 2)");
575        let formatted = format_expr(&expr, Some(80));
576        assert_eq!(formatted, "map([1, 2, 3], x => x * 2)");
577    }
578
579    #[test]
580    fn test_format_do_block() {
581        let expr = parse_test_expr("do { x = 1\n  return x }");
582        let formatted = format_expr(&expr, Some(80));
583        assert!(formatted.contains("do {"));
584        assert!(formatted.contains("return"));
585    }
586
587    #[test]
588    fn test_multiple_statements() {
589        use crate::parser::Rule;
590
591        let source = "x = [1, 2, 3, 4, 5]\ny = {name: \"Alice\", age: 30}\nz = x + y";
592        let pairs = get_pairs(source).unwrap();
593
594        let mut formatted_statements = Vec::new();
595
596        for pair in pairs {
597            if pair.as_rule() == Rule::statement {
598                if let Some(inner_pair) = pair.into_inner().next() {
599                    if let Ok(expr) = pairs_to_expr(inner_pair.into_inner()) {
600                        let formatted = format_expr(&expr, Some(80));
601                        formatted_statements.push(formatted);
602                    }
603                }
604            }
605        }
606
607        // Should have formatted all 3 statements
608        assert_eq!(formatted_statements.len(), 3);
609        assert_eq!(formatted_statements[0], "x = [1, 2, 3, 4, 5]");
610        assert_eq!(formatted_statements[1], "y = {name: \"Alice\", age: 30}");
611        assert_eq!(formatted_statements[2], "z = x + y");
612    }
613
614    #[test]
615    fn test_comments_are_preserved() {
616        use crate::parser::Rule;
617
618        let source = "// Comment 1\nx = [1, 2, 3]\n// Comment 2\ny = x + 1";
619        let pairs = get_pairs(source).unwrap();
620
621        let mut formatted_statements = Vec::new();
622
623        for pair in pairs {
624            if pair.as_rule() == Rule::statement {
625                if let Some(inner_pair) = pair.into_inner().next() {
626                    match inner_pair.as_rule() {
627                        Rule::comment => {
628                            // Preserve comment as-is
629                            formatted_statements.push(inner_pair.as_str().to_string());
630                        }
631                        _ => {
632                            // Format as expression
633                            if let Ok(expr) = pairs_to_expr(inner_pair.into_inner()) {
634                                let formatted = format_expr(&expr, Some(80));
635                                formatted_statements.push(formatted);
636                            }
637                        }
638                    }
639                }
640            }
641        }
642
643        // All 4 items should be preserved
644        assert_eq!(formatted_statements.len(), 4);
645        assert_eq!(formatted_statements[0], "// Comment 1");
646        assert_eq!(formatted_statements[1], "x = [1, 2, 3]");
647        assert_eq!(formatted_statements[2], "// Comment 2");
648        assert_eq!(formatted_statements[3], "y = x + 1");
649    }
650
651    #[test]
652    fn test_comments_with_formatted_code() {
653        use crate::parser::Rule;
654
655        let source = "// Configuration\nconfig = {name: \"test\", debug: true}\n// Process data\nresult = [1, 2, 3]";
656        let pairs = get_pairs(source).unwrap();
657
658        let mut formatted_statements = Vec::new();
659
660        for pair in pairs {
661            if pair.as_rule() == Rule::statement {
662                if let Some(inner_pair) = pair.into_inner().next() {
663                    match inner_pair.as_rule() {
664                        Rule::comment => {
665                            formatted_statements.push(inner_pair.as_str().to_string());
666                        }
667                        _ => {
668                            if let Ok(expr) = pairs_to_expr(inner_pair.into_inner()) {
669                                let formatted = format_expr(&expr, Some(80));
670                                formatted_statements.push(formatted);
671                            }
672                        }
673                    }
674                }
675            }
676        }
677
678        let result = formatted_statements.join("\n");
679
680        // Should have 4 statements: 2 comments + 2 expressions
681        assert_eq!(formatted_statements.len(), 4);
682        assert_eq!(formatted_statements[0], "// Configuration");
683        assert!(formatted_statements[1].starts_with("config = "));
684        assert_eq!(formatted_statements[2], "// Process data");
685        assert_eq!(formatted_statements[3], "result = [1, 2, 3]");
686
687        // Verify the full result preserves comments
688        assert!(result.contains("// Configuration"));
689        assert!(result.contains("// Process data"));
690    }
691
692    #[test]
693    fn test_output_declaration_with_assignment() {
694        use crate::parser::Rule;
695
696        let source = "output result = 42";
697        let pairs = get_pairs(source).unwrap();
698
699        let mut formatted_statements = Vec::new();
700
701        for pair in pairs {
702            if pair.as_rule() == Rule::statement {
703                if let Some(inner_pair) = pair.into_inner().next() {
704                    match inner_pair.as_rule() {
705                        Rule::output_declaration => {
706                            // Should preserve the output keyword
707                            let mut inner = inner_pair.into_inner();
708                            let assignment_or_ident = inner.next().unwrap();
709
710                            let formatted = if assignment_or_ident.as_rule() == Rule::assignment {
711                                // Extract identifier and value from assignment
712                                let mut assignment_inner = assignment_or_ident.into_inner();
713                                let ident = assignment_inner.next().unwrap().as_str();
714                                let value_expr = pairs_to_expr(assignment_inner.next().unwrap().into_inner()).unwrap();
715                                let value_formatted = format_expr(&value_expr, Some(80));
716                                format!("{} = {}", ident, value_formatted)
717                            } else {
718                                assignment_or_ident.as_str().to_string()
719                            };
720
721                            formatted_statements.push(format!("output {}", formatted));
722                        }
723                        _ => {
724                            if let Ok(expr) = pairs_to_expr(inner_pair.into_inner()) {
725                                let formatted = format_expr(&expr, Some(80));
726                                formatted_statements.push(formatted);
727                            }
728                        }
729                    }
730                }
731            }
732        }
733
734        assert_eq!(formatted_statements.len(), 1);
735        assert_eq!(formatted_statements[0], "output result = 42");
736    }
737
738    #[test]
739    fn test_output_statement_separate() {
740        use crate::parser::Rule;
741
742        let source = "result = 42\noutput result";
743        let pairs = get_pairs(source).unwrap();
744
745        let mut formatted_statements = Vec::new();
746
747        for pair in pairs {
748            if pair.as_rule() == Rule::statement {
749                if let Some(inner_pair) = pair.into_inner().next() {
750                    match inner_pair.as_rule() {
751                        Rule::output_declaration => {
752                            // Should preserve the output keyword
753                            let mut inner = inner_pair.into_inner();
754                            let assignment_or_ident = inner.next().unwrap();
755
756                            let formatted = if assignment_or_ident.as_rule() == Rule::assignment {
757                                // Extract identifier and value from assignment
758                                let mut assignment_inner = assignment_or_ident.into_inner();
759                                let ident = assignment_inner.next().unwrap().as_str();
760                                let value_expr = pairs_to_expr(assignment_inner.next().unwrap().into_inner()).unwrap();
761                                let value_formatted = format_expr(&value_expr, Some(80));
762                                format!("{} = {}", ident, value_formatted)
763                            } else {
764                                assignment_or_ident.as_str().to_string()
765                            };
766
767                            formatted_statements.push(format!("output {}", formatted));
768                        }
769                        _ => {
770                            if let Ok(expr) = pairs_to_expr(inner_pair.into_inner()) {
771                                let formatted = format_expr(&expr, Some(80));
772                                formatted_statements.push(formatted);
773                            }
774                        }
775                    }
776                }
777            }
778        }
779
780        assert_eq!(formatted_statements.len(), 2);
781        assert_eq!(formatted_statements[0], "result = 42");
782        assert_eq!(formatted_statements[1], "output result");
783    }
784
785    #[test]
786    fn test_output_with_complex_expression() {
787        use crate::parser::Rule;
788
789        let source = "output total = [1, 2, 3] into sum";
790        let pairs = get_pairs(source).unwrap();
791
792        let mut formatted_statements = Vec::new();
793
794        for pair in pairs {
795            if pair.as_rule() == Rule::statement {
796                if let Some(inner_pair) = pair.into_inner().next() {
797                    match inner_pair.as_rule() {
798                        Rule::output_declaration => {
799                            // Should preserve the output keyword
800                            let mut inner = inner_pair.into_inner();
801                            let assignment_or_ident = inner.next().unwrap();
802
803                            let formatted = if assignment_or_ident.as_rule() == Rule::assignment {
804                                // Extract identifier and value from assignment
805                                let mut assignment_inner = assignment_or_ident.into_inner();
806                                let ident = assignment_inner.next().unwrap().as_str();
807                                let value_expr = pairs_to_expr(assignment_inner.next().unwrap().into_inner()).unwrap();
808                                let value_formatted = format_expr(&value_expr, Some(80));
809                                format!("{} = {}", ident, value_formatted)
810                            } else {
811                                assignment_or_ident.as_str().to_string()
812                            };
813
814                            formatted_statements.push(format!("output {}", formatted));
815                        }
816                        _ => {
817                            if let Ok(expr) = pairs_to_expr(inner_pair.into_inner()) {
818                                let formatted = format_expr(&expr, Some(80));
819                                formatted_statements.push(formatted);
820                            }
821                        }
822                    }
823                }
824            }
825        }
826
827        assert_eq!(formatted_statements.len(), 1);
828        assert_eq!(formatted_statements[0], "output total = [1, 2, 3] into sum");
829    }
830
831    #[test]
832    fn test_end_of_line_comments() {
833        use crate::parser::Rule;
834
835        let source = "x = 5  // this is an end-of-line comment\ny = 10";
836        let pairs = get_pairs(source).unwrap();
837
838        let mut formatted_statements = Vec::new();
839
840        for pair in pairs {
841            if pair.as_rule() == Rule::statement {
842                let mut inner_pairs = pair.into_inner();
843
844                if let Some(first_pair) = inner_pairs.next() {
845                    let formatted = match first_pair.as_rule() {
846                        Rule::comment => first_pair.as_str().to_string(),
847                        _ => {
848                            if let Ok(expr) = pairs_to_expr(first_pair.into_inner()) {
849                                format_expr(&expr, Some(80))
850                            } else {
851                                continue;
852                            }
853                        }
854                    };
855
856                    // Check for end-of-line comment (second element in statement)
857                    if let Some(eol_comment) = inner_pairs.next() {
858                        if eol_comment.as_rule() == Rule::comment {
859                            formatted_statements.push(format!("{}  {}", formatted, eol_comment.as_str()));
860                        } else {
861                            formatted_statements.push(formatted);
862                        }
863                    } else {
864                        formatted_statements.push(formatted);
865                    }
866                }
867            }
868        }
869
870        assert_eq!(formatted_statements.len(), 2);
871        assert_eq!(formatted_statements[0], "x = 5  // this is an end-of-line comment");
872        assert_eq!(formatted_statements[1], "y = 10");
873    }
874
875    #[test]
876    fn test_multiple_end_of_line_comments() {
877        use crate::parser::Rule;
878
879        let source = "a = 1  // comment 1\nb = 2  // comment 2\nc = 3";
880        let pairs = get_pairs(source).unwrap();
881
882        let mut formatted_statements = Vec::new();
883
884        for pair in pairs {
885            if pair.as_rule() == Rule::statement {
886                let mut inner_pairs = pair.into_inner();
887
888                if let Some(first_pair) = inner_pairs.next() {
889                    let formatted = match first_pair.as_rule() {
890                        Rule::comment => first_pair.as_str().to_string(),
891                        _ => {
892                            if let Ok(expr) = pairs_to_expr(first_pair.into_inner()) {
893                                format_expr(&expr, Some(80))
894                            } else {
895                                continue;
896                            }
897                        }
898                    };
899
900                    // Check for end-of-line comment
901                    if let Some(eol_comment) = inner_pairs.next() {
902                        if eol_comment.as_rule() == Rule::comment {
903                            formatted_statements.push(format!("{}  {}", formatted, eol_comment.as_str()));
904                        } else {
905                            formatted_statements.push(formatted);
906                        }
907                    } else {
908                        formatted_statements.push(formatted);
909                    }
910                }
911            }
912        }
913
914        assert_eq!(formatted_statements.len(), 3);
915        assert_eq!(formatted_statements[0], "a = 1  // comment 1");
916        assert_eq!(formatted_statements[1], "b = 2  // comment 2");
917        assert_eq!(formatted_statements[2], "c = 3");
918    }
919
920    #[test]
921    fn test_eol_comments_not_joined_with_next_line() {
922        use crate::parser::Rule;
923
924        // Ensure statements with EOL comments remain on their own line
925        let source = "x = 1  // first value\ny = 2  // second value\nz = x + y";
926        let pairs = get_pairs(source).unwrap();
927
928        let mut formatted_statements = Vec::new();
929
930        for pair in pairs {
931            if pair.as_rule() == Rule::statement {
932                let mut inner_pairs = pair.into_inner();
933
934                if let Some(first_pair) = inner_pairs.next() {
935                    let formatted = match first_pair.as_rule() {
936                        Rule::comment => first_pair.as_str().to_string(),
937                        _ => {
938                            if let Ok(expr) = pairs_to_expr(first_pair.into_inner()) {
939                                format_expr(&expr, Some(80))
940                            } else {
941                                continue;
942                            }
943                        }
944                    };
945
946                    // Check for end-of-line comment
947                    if let Some(eol_comment) = inner_pairs.next() {
948                        if eol_comment.as_rule() == Rule::comment {
949                            formatted_statements.push(format!("{}  {}", formatted, eol_comment.as_str()));
950                        } else {
951                            formatted_statements.push(formatted);
952                        }
953                    } else {
954                        formatted_statements.push(formatted);
955                    }
956                }
957            }
958        }
959
960        // Join with newlines - each statement should be on its own line
961        let result = formatted_statements.join("\n");
962
963        assert_eq!(formatted_statements.len(), 3);
964        // Verify no statement got joined into one line
965        assert_eq!(result.lines().count(), 3);
966        assert_eq!(formatted_statements[0], "x = 1  // first value");
967        assert_eq!(formatted_statements[1], "y = 2  // second value");
968        assert_eq!(formatted_statements[2], "z = x + y");
969
970        // Ensure the result doesn't contain any line with multiple statements
971        for line in result.lines() {
972            // Count equals signs - should only be 1 per line
973            assert_eq!(line.matches('=').count(), 1, "Line should not contain multiple statements: {}", line);
974        }
975    }
976
977    #[test]
978    fn test_eol_comments_with_line_breaking() {
979        use crate::parser::Rule;
980
981        // Test that expressions can still be broken across lines when they have EOL comments
982        let source = "longRecord = {name: \"Alice\", age: 30, email: \"alice@example.com\", address: \"123 Main St\"}  // user data";
983        let pairs = get_pairs(source).unwrap();
984
985        let mut formatted_statements = Vec::new();
986
987        for pair in pairs {
988            if pair.as_rule() == Rule::statement {
989                let mut inner_pairs = pair.into_inner();
990
991                if let Some(first_pair) = inner_pairs.next() {
992                    let formatted = match first_pair.as_rule() {
993                        Rule::comment => first_pair.as_str().to_string(),
994                        _ => {
995                            if let Ok(expr) = pairs_to_expr(first_pair.into_inner()) {
996                                // Use a shorter line limit to force breaking
997                                format_expr(&expr, Some(40))
998                            } else {
999                                continue;
1000                            }
1001                        }
1002                    };
1003
1004                    // Check for end-of-line comment
1005                    if let Some(eol_comment) = inner_pairs.next() {
1006                        if eol_comment.as_rule() == Rule::comment {
1007                            formatted_statements.push(format!("{}  {}", formatted, eol_comment.as_str()));
1008                        } else {
1009                            formatted_statements.push(formatted);
1010                        }
1011                    } else {
1012                        formatted_statements.push(formatted);
1013                    }
1014                }
1015            }
1016        }
1017
1018        assert_eq!(formatted_statements.len(), 1);
1019        let result = &formatted_statements[0];
1020
1021        // The comment should be at the end
1022        assert!(result.ends_with("// user data"));
1023        // The expression should be formatted (likely multi-line)
1024        assert!(result.contains("longRecord = "));
1025    }
1026
1027    #[test]
1028    fn test_actual_line_breaking_behavior() {
1029        use crate::parser::Rule;
1030
1031        // Test what actually happens with long lines
1032        let source = "x = {name: \"Alice\", age: 30, email: \"alice@example.com\", address: \"123 Main St\", city: \"Springfield\"}";
1033        let pairs = get_pairs(source).unwrap();
1034
1035        for pair in pairs {
1036            if pair.as_rule() == Rule::statement {
1037                if let Some(inner_pair) = pair.into_inner().next() {
1038                    if let Ok(expr) = pairs_to_expr(inner_pair.into_inner()) {
1039                        let formatted = format_expr(&expr, Some(40));
1040                        println!("Formatted output:\n{}", formatted);
1041                        println!("Line count: {}", formatted.lines().count());
1042
1043                        // Check if it's actually breaking lines
1044                        if formatted.lines().count() > 1 {
1045                            println!("✓ Lines were broken");
1046                        } else {
1047                            println!("✗ No line breaking occurred - output is {} chars", formatted.len());
1048                        }
1049                    }
1050                }
1051            }
1052        }
1053    }
1054
1055    #[test]
1056    fn test_multiline_input_gets_collapsed() {
1057        use crate::parser::Rule;
1058
1059        // Test if multiline input gets collapsed to a single line
1060        let source = "x = [\n  1,\n  2,\n  3\n]";
1061        let pairs = get_pairs(source).unwrap();
1062
1063        for pair in pairs {
1064            if pair.as_rule() == Rule::statement {
1065                if let Some(inner_pair) = pair.into_inner().next() {
1066                    if let Ok(expr) = pairs_to_expr(inner_pair.into_inner()) {
1067                        let formatted = format_expr(&expr, Some(80));
1068                        println!("Input:\n{}", source);
1069                        println!("Output:\n{}", formatted);
1070
1071                        // Currently it collapses to single line
1072                        assert_eq!(formatted, "x = [1, 2, 3]");
1073                    }
1074                }
1075            }
1076        }
1077    }
1078
1079    #[test]
1080    fn test_empty_lines_between_statements() {
1081        use crate::parser::Rule;
1082
1083        // Test that empty lines between statements are preserved (up to 2)
1084        let source = "x = 1\n\ny = 2\n\n\n\nz = 3";
1085        let pairs = get_pairs(source).unwrap();
1086
1087        let mut statements_with_positions = Vec::new();
1088
1089        for pair in pairs {
1090            if pair.as_rule() == Rule::statement {
1091                let start_line = pair.as_span().start_pos().line_col().0;
1092                let end_line = pair.as_span().end_pos().line_col().0;
1093
1094                if let Some(inner_pair) = pair.into_inner().next() {
1095                    if let Ok(expr) = pairs_to_expr(inner_pair.into_inner()) {
1096                        let formatted = format_expr(&expr, Some(80));
1097                        statements_with_positions.push((formatted, start_line, end_line));
1098                    }
1099                }
1100            }
1101        }
1102
1103        println!("Statement positions: {:?}", statements_with_positions);
1104
1105        // Use the new helper function to join with spacing
1106        let result = join_statements_with_spacing(&statements_with_positions);
1107        println!("Output with preserved spacing:\n{}", result);
1108
1109        // We want to preserve 1 empty line between x and y, and cap at 2 between y and z
1110        // So it should be:
1111        // x = 1
1112        // <blank>
1113        // y = 2
1114        // <blank>
1115        // <blank>
1116        // z = 3
1117
1118        assert_eq!(result.lines().count(), 6); // 3 statements + 1 blank + 2 blanks
1119        assert_eq!(result, "x = 1\n\ny = 2\n\n\nz = 3");
1120    }
1121
1122    #[test]
1123    fn test_assignment_with_long_list_indentation() {
1124        use crate::parser::Rule;
1125
1126        // Test that assignments with lists don't have excessive indentation
1127        let source = "ingredients = [{name: \"sugar\", amount: 1}, {name: \"flour\", amount: 2}]";
1128        let pairs = get_pairs(source).unwrap();
1129
1130        for pair in pairs {
1131            if pair.as_rule() == Rule::statement {
1132                if let Some(inner_pair) = pair.into_inner().next() {
1133                    if let Ok(expr) = pairs_to_expr(inner_pair.into_inner()) {
1134                        let formatted = format_expr(&expr, Some(40));
1135                        println!("Formatted:\n{}", formatted);
1136
1137                        // The list items should be indented by 2 spaces, not pushed over by the "ingredients = " prefix
1138                        assert!(formatted.contains("[\n  {"));
1139                        assert!(!formatted.contains("            ")); // Should not have excessive indentation
1140                    }
1141                }
1142            }
1143        }
1144    }
1145}