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