Skip to main content

lykn_cli/
formatter.rs

1//! lykn formatter
2//!
3//! Pretty-prints s-expressions with consistent indentation.
4//! Follows common Lisp/Scheme formatting conventions.
5
6use crate::reader::SExpr;
7
8pub fn format_exprs(exprs: &[SExpr], indent: usize) -> String {
9    let mut out = String::new();
10    for (i, expr) in exprs.iter().enumerate() {
11        out.push_str(&format_expr(expr, indent));
12        if i + 1 < exprs.len() {
13            out.push('\n');
14            // Add blank line between top-level forms
15            if indent == 0 {
16                out.push('\n');
17            }
18        }
19    }
20    out.push('\n');
21    out
22}
23
24fn format_expr(expr: &SExpr, indent: usize) -> String {
25    match expr {
26        SExpr::Atom(s) => s.clone(),
27        SExpr::Str(s) => format!("\"{}\"", escape_string(s)),
28        SExpr::Num(n) => {
29            if *n == (*n as i64) as f64 {
30                format!("{}", *n as i64)
31            } else {
32                format!("{}", n)
33            }
34        }
35        SExpr::List(values) => format_list(values, indent),
36    }
37}
38
39fn format_list(values: &[SExpr], indent: usize) -> String {
40    if values.is_empty() {
41        return "()".to_string();
42    }
43
44    // Try single-line first
45    let single = format_single_line(values);
46    if single.len() + indent <= 80 && !single.contains('\n') {
47        return format!("({})", single);
48    }
49
50    // Multi-line: head on first line, rest indented
51    let head = format_expr(&values[0], 0);
52    let child_indent = indent + 2;
53    let child_prefix = " ".repeat(child_indent);
54
55    let mut out = format!("({}", head);
56    for val in &values[1..] {
57        let formatted = format_expr(val, child_indent);
58        out.push('\n');
59        out.push_str(&child_prefix);
60        out.push_str(&formatted);
61    }
62    out.push(')');
63    out
64}
65
66fn format_single_line(values: &[SExpr]) -> String {
67    values
68        .iter()
69        .map(|v| format_expr(v, 0))
70        .collect::<Vec<_>>()
71        .join(" ")
72}
73
74fn escape_string(s: &str) -> String {
75    s.replace('\\', "\\\\")
76        .replace('"', "\\\"")
77        .replace('\n', "\\n")
78        .replace('\t', "\\t")
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::reader::SExpr;
85
86    #[test]
87    fn format_single_atom() {
88        let exprs = vec![SExpr::Atom("hello".into())];
89        assert_eq!(format_exprs(&exprs, 0), "hello\n");
90    }
91
92    #[test]
93    fn format_integer_number() {
94        let exprs = vec![SExpr::Num(42.0)];
95        assert_eq!(format_exprs(&exprs, 0), "42\n");
96    }
97
98    #[test]
99    fn format_float_number() {
100        let exprs = vec![SExpr::Num(3.14)];
101        assert_eq!(format_exprs(&exprs, 0), "3.14\n");
102    }
103
104    #[test]
105    fn format_string_simple() {
106        let exprs = vec![SExpr::Str("hello".into())];
107        assert_eq!(format_exprs(&exprs, 0), "\"hello\"\n");
108    }
109
110    #[test]
111    fn format_string_with_escapes() {
112        let exprs = vec![SExpr::Str("a\nb\t\"c\\".into())];
113        assert_eq!(format_exprs(&exprs, 0), "\"a\\nb\\t\\\"c\\\\\"\n");
114    }
115
116    #[test]
117    fn format_empty_list() {
118        let exprs = vec![SExpr::List(vec![])];
119        assert_eq!(format_exprs(&exprs, 0), "()\n");
120    }
121
122    #[test]
123    fn format_short_list() {
124        let exprs = vec![SExpr::List(vec![
125            SExpr::Atom("+".into()),
126            SExpr::Num(1.0),
127            SExpr::Num(2.0),
128        ])];
129        assert_eq!(format_exprs(&exprs, 0), "(+ 1 2)\n");
130    }
131
132    #[test]
133    fn format_long_list_wraps() {
134        // Build a list that exceeds 80 chars
135        let mut vals = vec![SExpr::Atom("function-with-a-very-long-name".into())];
136        for _ in 0..5 {
137            vals.push(SExpr::Str("some-really-long-argument-value".into()));
138        }
139        let exprs = vec![SExpr::List(vals)];
140        let result = format_exprs(&exprs, 0);
141        assert!(result.contains('\n'));
142        assert!(result.starts_with("(function-with-a-very-long-name"));
143    }
144
145    #[test]
146    fn format_multiple_top_level_exprs() {
147        let exprs = vec![SExpr::Atom("a".into()), SExpr::Atom("b".into())];
148        let result = format_exprs(&exprs, 0);
149        // Top-level forms separated by blank line
150        assert_eq!(result, "a\n\nb\n");
151    }
152
153    #[test]
154    fn format_nested_list() {
155        let inner = SExpr::List(vec![
156            SExpr::Atom("+".into()),
157            SExpr::Num(1.0),
158            SExpr::Num(2.0),
159        ]);
160        let outer = SExpr::List(vec![
161            SExpr::Atom("define".into()),
162            SExpr::Atom("x".into()),
163            inner,
164        ]);
165        let result = format_exprs(&vec![outer], 0);
166        assert_eq!(result, "(define x (+ 1 2))\n");
167    }
168
169    #[test]
170    fn format_indented_children() {
171        let exprs = vec![SExpr::List(vec![
172            SExpr::Atom("define".into()),
173            SExpr::Atom("x".into()),
174        ])];
175        // With indent > 0, shouldn't add blank line separator
176        let result = format_exprs(&exprs, 4);
177        assert_eq!(result, "(define x)\n");
178    }
179
180    #[test]
181    fn escape_string_empty() {
182        assert_eq!(escape_string(""), "");
183    }
184
185    #[test]
186    fn escape_string_no_special() {
187        assert_eq!(escape_string("hello"), "hello");
188    }
189
190    #[test]
191    fn escape_string_all_special() {
192        assert_eq!(escape_string("\\\"\n\t"), "\\\\\\\"\\n\\t");
193    }
194}