Skip to main content

scheme_edit/
pretty.rs

1use crate::cst::{Item, ListKind, Node};
2
3const MAX_WIDTH: usize = 78;
4
5/// Emacs-style lead counts: the first N arguments of a form are plain
6/// arguments (rendered flat), everything after is body and breaks to
7/// its own line.
8fn leading_args(head: &str) -> usize {
9    match head {
10        "make-channel-introduction" => 1,
11        _ => 0,
12    }
13}
14
15fn delims(kind: &ListKind) -> (&str, &'static str) {
16    match kind {
17        ListKind::Paren => ("(", ")"),
18        ListKind::Bracket => ("[", "]"),
19        ListKind::Vector => ("#(", ")"),
20        ListKind::TaggedVector(s) => (s.as_str(), ")"),
21    }
22}
23
24/// The prefix token itself, with any trivia folded in by the parser removed.
25fn prefix_core(prefix: &str) -> &str {
26    for cand in [
27        "#,@", "#$@", ",@", "#'", "#`", "#,", "#~", "#$", "#+", "'", "`", ",",
28    ] {
29        if prefix.starts_with(cand) {
30            return cand;
31        }
32    }
33    prefix
34}
35
36fn has_comment(node: &Node) -> bool {
37    match node {
38        Node::List { items, .. } => items.iter().any(|i| match i {
39            Item::LineComment(_) | Item::BlockComment(_) | Item::DatumComment(_) => true,
40            Item::Node(n) => has_comment(n),
41            Item::Ws(_) => false,
42        }),
43        Node::Prefixed { inner, .. } => has_comment(inner),
44        _ => false,
45    }
46}
47
48/// Normalized single-line rendering: single spaces, trivia dropped.
49fn flat_source(node: &Node) -> String {
50    match node {
51        Node::Atom(s) | Node::Str(s) => s.clone(),
52        Node::Prefixed { prefix, inner } => {
53            format!("{}{}", prefix_core(prefix), flat_source(inner))
54        }
55        Node::List { kind, items } => {
56            let parts: Vec<String> = items
57                .iter()
58                .filter_map(|i| match i {
59                    Item::Node(n) => Some(flat_source(n)),
60                    _ => None,
61                })
62                .collect();
63            let (open, close) = delims(kind);
64            format!("{open}{}{close}", parts.join(" "))
65        }
66    }
67}
68
69enum El<'a> {
70    Data(&'a Node),
71    Comment(&'a str),
72}
73
74fn break_list(kind: &ListKind, items: &[Item], indent: usize) -> String {
75    let (open, close) = delims(kind);
76    let els: Vec<El> = items
77        .iter()
78        .filter_map(|i| match i {
79            Item::Node(n) => Some(El::Data(n)),
80            Item::LineComment(s) | Item::BlockComment(s) | Item::DatumComment(s) => {
81                Some(El::Comment(s))
82            }
83            Item::Ws(_) => None,
84        })
85        .collect();
86
87    let mut out = String::from(open);
88    let mut rest = els.as_slice();
89    let mut lead = 0;
90    if let Some(El::Data(head)) = els.first() {
91        out.push_str(&pretty(head, indent + open.len()));
92        if let Node::Atom(a) = head {
93            lead = leading_args(a);
94        }
95        rest = &els[1..];
96    }
97
98    let child_indent = indent + 2;
99    let pad = format!("\n{}", " ".repeat(child_indent));
100    let mut args_seen = 0usize;
101    for el in rest {
102        out.push_str(&pad);
103        match el {
104            El::Comment(s) => out.push_str(s),
105            El::Data(n) => {
106                args_seen += 1;
107                if args_seen <= lead {
108                    out.push_str(&flat_source(n));
109                } else {
110                    out.push_str(&pretty(n, child_indent));
111                }
112            }
113        }
114    }
115    out.push_str(close);
116    out
117}
118
119/// Render NODE in guix house style starting at column INDENT.
120pub fn pretty(node: &Node, indent: usize) -> String {
121    let flat = flat_source(node);
122    if !has_comment(node) && indent + flat.len() <= MAX_WIDTH {
123        return flat;
124    }
125    match node {
126        Node::List { kind, items } => break_list(kind, items, indent),
127        Node::Prefixed { prefix, inner } => {
128            let p = prefix_core(prefix);
129            format!("{p}{}", pretty(inner, indent + p.len()))
130        }
131        // Atoms and strings never split.
132        _ => flat,
133    }
134}
135
136impl Node {
137    pub fn to_pretty(&self, indent: usize) -> String {
138        pretty(self, indent)
139    }
140}