tan_formatting/
layout.rs

1use std::collections::HashMap;
2
3use tan::{
4    expr::Expr,
5    util::{fmt::format_float, put_back_iterator::PutBackIterator},
6};
7
8use crate::{types::Dialect, util::escape_string};
9
10// #todo use source-code annotations to control formatting
11
12// #todo add some explanation about the design, e.g. what does Layout do.
13
14// #todo somehow extract the force_vertical computation to include all parameters.
15
16// #todo conds get corrupted
17// #todo remove empty lines from beginning of blocks!
18// #todo implement `html` and `css` dialects
19
20// #todo refine this enum, potentially split into 2 enums?
21// #todo could name this layout 'Cell' or Fragment
22/// A Layout is an abstract representation (model) of formatted source.
23#[derive(Clone, Debug)]
24pub enum Layout {
25    /// Indentation block, supports both indentation and alignment.
26    Indent(Vec<Layout>, Option<usize>), // #todo no need for Indent, add option to stack
27    /// Vertical arrangement
28    Stack(Vec<Layout>),
29    /// Horizontal arrangement
30    Row(Vec<Layout>, String),
31    // #todo wtf is this?
32    Apply(Box<Layout>),
33    Item(String),
34    Ann(HashMap<String, Expr>, Box<Layout>),
35    Separator,
36}
37
38impl Layout {
39    pub fn indent(list: Vec<Layout>) -> Self {
40        Self::Indent(list, None)
41    }
42
43    pub fn align(list: Vec<Layout>, indent_size: usize) -> Self {
44        Self::Indent(list, Some(indent_size))
45    }
46
47    pub fn row(list: impl Into<Vec<Layout>>) -> Self {
48        Self::Row(list.into(), " ".to_string())
49    }
50
51    pub fn join(list: impl Into<Vec<Layout>>) -> Self {
52        Self::Row(list.into(), "".to_string())
53    }
54
55    pub fn apply(l: Layout) -> Self {
56        Self::Apply(Box::new(l))
57    }
58
59    pub fn item(s: impl Into<String>) -> Self {
60        Self::Item(s.into())
61    }
62
63    pub fn space() -> Self {
64        Self::Item(" ".into())
65    }
66}
67
68// #todo should allow for multiple concurrent modes?
69/// An arranger mode to allow for formatting specializations.
70#[derive(Clone, Copy, Eq, PartialEq, Debug)]
71enum ArrangerMode {
72    Default,
73    // #todo document what this does.
74    // #todo find a better name, more encompassing.
75    Let,
76    // Forces inline alignment, for example for function arguments.
77    Inline,
78}
79
80// #todo should allow for multiple modes, make a Set.
81// #todo maybe Dialects should just get expanded to modes?
82
83// #todo find a better name.
84/// The Arranger organizes the input expressions into an abstract Layout. The
85/// Formatter renders the Layout model into the formatted output string.
86pub struct Arranger<'a> {
87    // #todo consider different names, e.g. `flavor`?
88    // #todo use a builder pattern.
89    pub dialect: Dialect,
90    exprs: PutBackIterator<'a, Expr>,
91    mode: ArrangerMode,
92}
93
94impl<'a> Arranger<'a> {
95    pub fn new(exprs: &'a [Expr], dialect: Dialect) -> Self {
96        Self {
97            dialect,
98            exprs: PutBackIterator::new(exprs),
99            mode: ArrangerMode::Default,
100        }
101    }
102
103    fn arrange_next(&mut self) -> Option<Layout> {
104        let expr0 = self.exprs.next()?;
105
106        let layout = self.layout_from_expr(expr0);
107
108        // #insight
109        // Fetch the next expression and try to detect an inline comment.
110        // If an inline comment is found, force vertical layout.
111        if let Some(expr1) = self.exprs.next() {
112            match expr1.unpack() {
113                Expr::Comment(..) => {
114                    if expr1.range().unwrap().start.line == expr0.range().unwrap().start.line {
115                        let comment = self.layout_from_expr(expr1);
116                        return Some(Layout::row(vec![layout, comment]));
117                    } else {
118                        self.exprs.put_back(expr1);
119                    }
120                }
121                _ => {
122                    self.exprs.put_back(expr1);
123                }
124            }
125        };
126
127        Some(layout)
128    }
129
130    fn arrange_all(&mut self) -> (Vec<Layout>, bool) {
131        let mut layouts = Vec::new();
132
133        let mut force_vertical = false;
134
135        while let Some(layout) = self.arrange_next() {
136            // force vertical if there is an inline comment.
137            if let Layout::Row(v, ..) = &layout {
138                if let Some(Layout::Item(t)) = &v.last() {
139                    force_vertical = force_vertical || t.starts_with(';'); // is comment?
140                }
141            };
142
143            // #todo make a constant, find a good threshold value.
144            // #todo compute from max_line_len?
145            // let item_length_vertical_arrange_threshold = 8;
146
147            // force vertical if there is a full-line comment.
148            // force vertical if an item length exceeds a threshold.
149            if let Layout::Item(item) = &layout {
150                force_vertical = force_vertical || item.starts_with(';') // is comment?
151                                                                         // force_vertical = force_vertical
152                                                                         // || item.starts_with(';') // is comment?
153                                                                         // || item.len() > item_length_vertical_arrange_threshold; // is long item?
154            }
155
156            layouts.push(layout);
157        }
158
159        (layouts, force_vertical)
160    }
161
162    // #insight this is problematic for function parameter arrays!
163    // #todo temp specialize arrange_all for arrays.
164    fn arrange_all_array(&mut self) -> (Vec<Layout>, bool) {
165        let mut layouts = Vec::new();
166
167        let mut force_vertical = false;
168
169        let mut items_cumulative_length = 0;
170
171        while let Some(layout) = self.arrange_next() {
172            // force vertical if there is an inline comment.
173            if let Layout::Row(v, ..) = &layout {
174                if let Some(Layout::Item(t)) = &v.last() {
175                    force_vertical = force_vertical || t.starts_with(';'); // is comment?
176                }
177            };
178
179            // #todo make a constant, find a good threshold value.
180            // #todo compute from max_line_len?
181            let item_length_vertical_arrange_threshold = 8;
182
183            // force vertical if there is a full-line comment.
184            // force vertical if an item length exceeds a threshold.
185            if let Layout::Item(item) = &layout {
186                items_cumulative_length += item.len();
187
188                force_vertical = force_vertical
189                    || item.starts_with(';') // is comment?
190                    || (self.mode != ArrangerMode::Inline && item.len() > item_length_vertical_arrange_threshold);
191                // is long item?
192            }
193
194            layouts.push(layout);
195        }
196
197        if self.mode != ArrangerMode::Inline {
198            // #todo find a good threshold!
199            // #todo also support wrapping to next line.
200            force_vertical = force_vertical || items_cumulative_length > 32;
201        }
202
203        (layouts, force_vertical)
204    }
205
206    // #todo Find good name.
207    // Try to bundle all annotations into one Row (span).
208    fn maybe_annotated_layout_from_expr(&mut self, expr: &Expr) -> Option<Layout> {
209        let mut expr = expr;
210
211        let mut annotated = Vec::new();
212
213        while let Expr::Annotation(..) = expr.unpack() {
214            annotated.push(self.layout_from_expr(expr));
215            expr = self.exprs.next()?;
216        }
217
218        // #todo Pretty-print the value/payload of the annotation.
219
220        if annotated.is_empty() {
221            Some(self.layout_from_expr(expr))
222        } else {
223            annotated.push(self.layout_from_expr(expr));
224            Some(Layout::row(annotated))
225        }
226    }
227
228    // #todo add doc-comment.
229    fn arrange_next_pair(&mut self) -> Option<Layout> {
230        // #todo Add unit-test just for this method.
231
232        let expr = self.exprs.next()?;
233
234        // #insight Handles (skips) full line comments.
235        // #todo Needs more elegant solution.
236        if let Expr::Comment(..) = expr.unpack() {
237            return Some(self.layout_from_expr(expr));
238        }
239
240        let mut tuple = Vec::new();
241
242        tuple.push(self.maybe_annotated_layout_from_expr(expr)?);
243
244        let expr = self.exprs.next()?;
245        tuple.push(self.maybe_annotated_layout_from_expr(expr)?);
246
247        // Try to skip trailing comments.
248        if let Some(expr) = self.exprs.next() {
249            match expr.unpack() {
250                Expr::Comment(..) => {
251                    if expr.range().unwrap().start.line == expr.range().unwrap().start.line {
252                        tuple.push(self.layout_from_expr(expr));
253                    } else {
254                        self.exprs.put_back(expr);
255                    }
256                }
257                _ => {
258                    self.exprs.put_back(expr);
259                }
260            }
261        };
262
263        Some(Layout::row(tuple))
264    }
265
266    fn arrange_all_pairs(&mut self) -> (Vec<Layout>, bool) {
267        let mut layouts = Vec::new();
268
269        let mut should_force_vertical = false;
270
271        while let Some(layout) = self.arrange_next_pair() {
272            if let Layout::Row(items, ..) = &layout {
273                if items.len() > 2 {
274                    // If a pair has an inline comments, force vertical layout
275                    should_force_vertical = true;
276                }
277            };
278
279            layouts.push(layout);
280        }
281
282        (layouts, should_force_vertical)
283    }
284
285    fn arrange_list(&mut self) -> Layout {
286        // #insight not need to check here.
287        let expr = self.exprs.next().unwrap();
288
289        let mut layouts = Vec::new();
290
291        let head = expr.unpack();
292
293        // #todo should decide between (h)list/vlist.
294        // #todo special formatting for `if`.
295
296        // #todo #warning (Func [...] ...) generate an Expr::Type("Func") !!
297
298        match head {
299            Expr::Symbol(name) if name == "quot" => {
300                // #todo this is a temp solution, ideally it should recourse into arrange_list again.
301                // Always arrange a `quot` block horizontally.
302                let (exprs, _) = self.arrange_all();
303                layouts.push(Layout::item("'"));
304                layouts.push(Layout::row(exprs));
305                Layout::join(layouts)
306            }
307            Expr::Symbol(name) if name == "unquot" => {
308                // Always arrange a `unquot` block horizontally.
309                let (exprs, _) = self.arrange_all();
310                layouts.push(Layout::item("$"));
311                layouts.push(Layout::row(exprs));
312                Layout::join(layouts)
313            }
314            Expr::Symbol(name) if name == "do" => {
315                // Always arrange a `do` block vertically.
316                let (exprs, _) = self.arrange_all();
317                layouts.push(Layout::item("(do"));
318                layouts.push(Layout::indent(exprs));
319                layouts.push(Layout::apply(Layout::item(")")));
320                Layout::Stack(layouts)
321            }
322            // #todo #hack super nasty way to handle both Symbol and Type.
323            // #todo #warning (Func [...] ...) generate an Expr::Type("Func") !!
324            Expr::Symbol(name) | Expr::Type(name)
325                if name == "if" || name == "for" || name == "Func" =>
326            {
327                // The first expr is rendered inline, the rest are rendered vertically.
328                layouts.push(Layout::row(vec![
329                    Layout::item(format!("({name}")),
330                    // #todo special handling for `for` also needed, separate from Func.
331                    // #todo could set mode here!
332                    // #todo #hack nasty, refactor!
333                    if name == "Func" || name == "for" {
334                        let old_mode = self.mode;
335                        self.mode = ArrangerMode::Inline;
336                        let layout = self.arrange_next().unwrap();
337                        self.mode = old_mode;
338                        layout
339                    } else {
340                        self.arrange_next().unwrap()
341                    },
342                ]));
343                let (block, should_force_vertical) = self.arrange_all();
344
345                // #todo consider making `if` always multiline? no.
346
347                let should_force_vertical = should_force_vertical || block.len() > 1;
348
349                // #todo reconsider forced-multiline for `for`.
350                // #insight
351                // force `for`, to always be multiline, as it doesn't return a
352                // useful value.
353                let should_force_vertical = should_force_vertical || name == "for";
354
355                let should_force_vertical = should_force_vertical || self.mode == ArrangerMode::Let;
356
357                if should_force_vertical {
358                    layouts.push(Layout::indent(block));
359                    layouts.push(Layout::apply(Layout::item(")")));
360                    Layout::Stack(layouts)
361                } else {
362                    layouts.push(Layout::item(" "));
363                    layouts.push(block[0].clone());
364                    layouts.push(Layout::item(")"));
365                    Layout::join(layouts)
366                }
367            }
368            Expr::Symbol(name) if name == "Range" => {
369                // #todo support open-ended ranges.
370                // safe to unwrap, it's already parsed.
371                let start = self.exprs.next().unwrap();
372                let end = self.exprs.next().unwrap();
373                let mut range = format!("{start}..{end}");
374                if let Some(step) = self.exprs.next() {
375                    range = format!("{range}|{step}");
376                }
377                Layout::Item(range)
378            }
379            Expr::Symbol(name) if name == "Array" => {
380                // #todo more sophisticated Array formatting needed.
381                // Try to format the array horizontally.
382                layouts.push(Layout::item("["));
383                // let (items, should_force_vertical) = self.arrange_all();
384                let (items, should_force_vertical) = self.arrange_all_array();
385
386                // #todo consider allowing horizontal for only one element.
387                // For `data` dialect always force vertical.
388                let should_force_vertical = should_force_vertical || self.dialect == Dialect::Data;
389
390                if !items.is_empty() {
391                    if should_force_vertical {
392                        layouts.push(Layout::indent(items));
393                        layouts.push(Layout::apply(Layout::item("]")));
394                        Layout::Stack(layouts)
395                    } else {
396                        match &items[0] {
397                            // Heuristic: if the array includes stacks, arrange
398                            // vertically.
399                            Layout::Stack(..) | Layout::Indent(..) => {
400                                layouts.push(Layout::indent(items));
401                                layouts.push(Layout::apply(Layout::item("]")));
402                                Layout::Stack(layouts)
403                            }
404                            _ => {
405                                layouts.push(Layout::row(items));
406                                layouts.push(Layout::item("]"));
407                                Layout::join(layouts)
408                            }
409                        }
410                    }
411                } else {
412                    layouts.push(Layout::item("]"));
413                    Layout::join(layouts)
414                }
415            }
416            Expr::Symbol(name) if name == "Map" => {
417                // #todo in data mode consider formatting empty Map like this: {}
418                let (bindings, should_force_vertical) = self.arrange_all_pairs();
419
420                // If more than 2 bindings force vertical.
421                let should_force_vertical = should_force_vertical || bindings.len() > 2;
422
423                // For `data` dialect always force vertical.
424                let should_force_vertical = should_force_vertical || self.dialect == Dialect::Data;
425
426                if should_force_vertical {
427                    layouts.push(Layout::item("{"));
428                    layouts.push(Layout::indent(bindings));
429                    layouts.push(Layout::apply(Layout::item("}")));
430                    Layout::Stack(layouts)
431                } else {
432                    layouts.push(Layout::item("{"));
433                    layouts.push(Layout::row(bindings));
434                    layouts.push(Layout::item('}'));
435                    Layout::join(layouts)
436                }
437            }
438            Expr::Symbol(name) if name == "let" => {
439                // #todo add a more intuitive mechanism for mode, maybe a stack?
440                let old_mode = self.mode;
441                self.mode = ArrangerMode::Let;
442                let (mut bindings, should_force_vertical) = self.arrange_all_pairs();
443
444                self.mode = old_mode;
445
446                if should_force_vertical {
447                    // Special case: one binding with inline comment, arrange vertically.
448                    layouts.push(Layout::item("(let"));
449                    layouts.push(Layout::indent(bindings));
450                    layouts.push(Layout::apply(Layout::item(')')));
451                    Layout::Stack(layouts)
452                } else if bindings.len() > 1 {
453                    // More than one binding, arrange vertically.
454                    layouts.push(Layout::row(vec![Layout::item("(let"), bindings.remove(0)]));
455                    if !bindings.is_empty() {
456                        layouts.push(Layout::align(bindings, 5 /* "(let " */));
457                    }
458                    layouts.push(Layout::apply(Layout::item(')')));
459                    Layout::Stack(layouts)
460                } else {
461                    // One binding, arrange horizontally.
462                    layouts.push(Layout::item("(let "));
463                    layouts.push(Layout::row(bindings));
464                    layouts.push(Layout::item(')'));
465                    Layout::join(layouts)
466                }
467            }
468            // #todo currently this is exactly the same code as for `let`, extract.
469            // #todo hmm not exactly the same, always forces multiline!
470            Expr::Symbol(name) if name == "cond" => {
471                let (clauses, should_force_vertical) = self.arrange_all_pairs();
472
473                if should_force_vertical {
474                    // #todo not relevant for `cond`, remove!
475                    // Special case: one clause with inline comment, arrange vertically.
476                    layouts.push(Layout::item("(cond"));
477                    layouts.push(Layout::indent(clauses));
478                    layouts.push(Layout::apply(Layout::item(')')));
479                    Layout::Stack(layouts)
480                } else if clauses.len() > 1 {
481                    // More than one clause, arrange vertically.
482                    layouts.push(Layout::item("(cond"));
483                    // layouts.push(Layout::row(vec![Layout::item("(cond"), bindings.remove(0)]));
484                    if !clauses.is_empty() {
485                        layouts.push(Layout::align(clauses, 4 /* "(cond " */));
486                    }
487                    layouts.push(Layout::apply(Layout::item(')')));
488                    Layout::Stack(layouts)
489                } else {
490                    // #todo there should never be one clause, remove!
491                    // One clause, arrange horizontally.
492                    layouts.push(Layout::item("(cond "));
493                    layouts.push(Layout::row(clauses));
494                    layouts.push(Layout::item(')'));
495                    Layout::join(layouts)
496                }
497            }
498            _ => {
499                // Function call.
500                layouts.push(Layout::item(format!("({head}")));
501                let (args, should_force_vertical) = self.arrange_all();
502                if !args.is_empty() {
503                    if should_force_vertical {
504                        layouts.push(Layout::indent(args));
505                        layouts.push(Layout::apply(Layout::item(")")));
506                        Layout::Stack(layouts)
507                    } else {
508                        layouts.push(Layout::item(" "));
509                        layouts.push(Layout::row(args));
510                        layouts.push(Layout::item(")"));
511                        Layout::join(layouts)
512                    }
513                } else {
514                    layouts.push(Layout::item(")"));
515                    Layout::join(layouts)
516                }
517            }
518        }
519    }
520
521    fn layout_from_expr(&self, expr: &Expr) -> Layout {
522        let (expr, _ann) = expr.extract();
523
524        let layout = match expr {
525            Expr::Comment(s, _) => Layout::Item(s.clone()),
526            Expr::TextSeparator => Layout::Separator, // #todo different impl!
527            Expr::String(s) => Layout::Item(format!("\"{}\"", escape_string(s))),
528            Expr::Symbol(s) => Layout::Item(s.clone()),
529            Expr::Int(n) => Layout::Item(n.to_string()),
530            // #insight `()` is the single instance of the Unit type `Nil`.
531            Expr::None => Layout::Item("()".to_string()),
532            Expr::Bool(b) => Layout::Item(b.to_string()),
533            Expr::Float(n) => Layout::Item(format_float(*n)),
534            // #todo Handle keypaths, don't desugar.
535            Expr::KeySymbol(s) => Layout::Item(format!(":{s}")),
536            Expr::Char(c) => Layout::Item(format!(r#"(Char "{c}")"#)),
537            // #todo should handle Array?!
538            Expr::List(exprs) => {
539                if exprs.is_empty() {
540                    return Layout::Item("()".to_owned());
541                }
542
543                // #insight Recursive data structure, we recurse.
544
545                let mut list_arranger = Arranger::new(exprs, self.dialect);
546                list_arranger.mode = self.mode;
547                list_arranger.arrange_list()
548            }
549            _ => Layout::Item(expr.to_string()),
550        };
551
552        // if let Some(ann) = ann {
553        //     if ann.len() > 1 {
554        //         // #todo give special key to implicit range annotation.
555        //         // Remove the range annotation.
556        //         let mut ann = ann.clone();
557        //         ann.remove("range");
558        //         return Layout::Ann(ann, Box::new(layout));
559        //     }
560        // }
561
562        layout
563    }
564
565    pub fn arrange(&mut self) -> Layout {
566        let (rows, _) = self.arrange_all();
567        Layout::Stack(rows)
568    }
569}