1use crate::parse::LANGUAGE;
9use tree_sitter::{Language, Node, Parser};
10
11struct Leaf {
12 text: String,
13 kind: String,
14 parent: String,
15 row: usize,
16 end_row: usize,
17 col: usize,
18}
19
20const ATOMS: [&str; 7] = [
22 "string",
23 "template_string",
24 "pattern",
25 "unit_literal",
26 "doc_comment",
27 "line_comment",
28 "block_comment",
29];
30const BIN_OPS: [&str; 24] = [
31 "=", "==", "!=", "<=", ">=", "+", "*", "/", "%", "&&", "||", "??", "|>", "=>", "<<", ">>",
32 "in", "matches", "with", "then", "else", "for", "if", "as",
33];
34const BIN_OPS_EXTRA: [&str; 1] = ["from"];
35const CONT_STARTERS: [&str; 22] = [
36 "else", "=", "for", "if", "&&", "||", "|>", "??", ".", "?.", "+", "-", "*", "/", "==", "!=",
37 "<=", ">=", "<", ">", "=>", "then",
38];
39const CONT_ENDERS: [&str; 29] = [
42 "=", "=>", "&&", "||", "|>", "??", "+", "-", "*", "/", "%", "==", "!=", "<=", ">=", "<", ">",
43 "&", "|", "^", "<<", ">>", "..", "..<", "then", "else", "in", "with", "matches",
44];
45const KEYWORDS: [&str; 27] = [
46 "type",
47 "const",
48 "func",
49 "output",
50 "input",
51 "export",
52 "import",
53 "diagnostic",
54 "dimension",
55 "unit",
56 "assert",
57 "when",
58 "if",
59 "then",
60 "else",
61 "match",
62 "for",
63 "in",
64 "with",
65 "as",
66 "from",
67 "true",
68 "false",
69 "null",
70 "error",
71 "warn",
72 "info",
73];
74const KEYWORDS_EXTRA: [&str; 1] = ["matches"];
75
76pub fn u16len(s: &str) -> usize {
78 s.encode_utf16().count()
79}
80fn is_atom(kind: &str) -> bool {
81 ATOMS.contains(&kind)
82}
83fn keywordy(t: &str) -> bool {
85 let mut cs = t.chars();
86 let Some(c0) = cs.next() else { return false };
87 if !(c0.is_ascii_alphabetic() || c0 == '_' || c0 == '$') {
88 return false;
89 }
90 let rest: Vec<char> = cs.collect();
91 let body: &[char] = match rest.split_last() {
92 Some(('$', b)) => b,
93 _ => &rest[..],
94 };
95 body.iter().all(|c| c.is_ascii_alphanumeric() || *c == '_')
96}
97fn is_keyword(t: &str) -> bool {
98 KEYWORDS.contains(&t) || KEYWORDS_EXTRA.contains(&t)
99}
100fn is_bin_op(t: &str) -> bool {
101 BIN_OPS.contains(&t) || BIN_OPS_EXTRA.contains(&t)
102}
103
104fn collect(n: Node, src: &str, lines: &[&str], out: &mut Vec<Leaf>) {
105 if is_atom(n.kind()) || n.child_count() == 0 {
106 let text = n.utf8_text(src.as_bytes()).unwrap_or("");
107 if text.is_empty() {
108 return; }
110 let row = n.start_position().row;
111 let col = lines
112 .get(row)
113 .map(|l| u16len(l.get(..n.start_position().column).unwrap_or("")))
114 .unwrap_or(0);
115 out.push(Leaf {
116 text: text.to_string(),
117 kind: n.kind().to_string(),
118 parent: n.parent().map(|p| p.kind().to_string()).unwrap_or_default(),
119 row,
120 end_row: n.end_position().row,
121 col,
122 });
123 return;
124 }
125 let mut cur = n.walk();
126 for c in n.children(&mut cur) {
127 collect(c, src, lines, out);
128 }
129}
130
131fn is_type_angle(l: &Leaf) -> bool {
132 (l.text == "<" || l.text == ">")
133 && (l.parent == "type_arguments" || l.parent == "type_parameters")
134}
135
136fn spaced(a: &Leaf, b: &Leaf, prev: Option<&Leaf>) -> bool {
138 let (at, bt) = (a.text.as_str(), b.text.as_str());
139 if b.kind.ends_with("comment") {
141 return true;
142 }
143 if is_type_angle(a) && at == "<" {
144 return false;
145 }
146 if is_type_angle(b) {
147 return false; }
149 if at == "(" || at == "[" {
150 return false;
151 }
152 if bt == ")" || bt == "]" || bt == "," || bt == ":" {
153 return false;
154 }
155 if bt == "?" || at == "?" {
156 return false; }
158 if at == "." || bt == "." || at == "?." || bt == "?." {
159 return false;
160 }
161 if bt == ";" {
162 return false;
163 }
164 if at == ".." || at == "..<" || bt == ".." || bt == "..<" {
165 return false;
166 }
167 if bt == "(" {
168 return !(keywordy(at) && !is_keyword(at)) && at != ")" && at != "]" && !is_type_angle(a);
170 }
171 if bt == "[" {
172 return !((keywordy(at) && !is_keyword(at))
175 || at == ")"
176 || at == "]"
177 || at == "}"
178 || is_type_angle(a));
179 }
180 if at == "{" || bt == "}" {
181 return true; }
183 if bt == "{" || at == "}" {
184 return true;
185 }
186 if at == "!" || at == "~" {
187 return false; }
189 if at == "-" || at == "+" {
190 let unary = match prev {
192 None => true,
193 Some(p) => {
194 let pt = p.text.as_str();
195 is_bin_op(pt)
196 || [
197 "(", "[", "{", ",", ":", "<", "..", "..<", "-", "+", "!", "~",
198 ]
199 .contains(&pt)
200 || (keywordy(pt) && is_keyword(pt))
201 }
202 };
203 if unary {
204 return false;
205 }
206 }
207 true
208}
209
210pub fn format(src: &str) -> Result<String, String> {
211 let mut parser = Parser::new();
212 let lang: Language = LANGUAGE.into();
213 parser.set_language(&lang).map_err(|e| e.to_string())?;
214 let tree = parser.parse(src, None).ok_or("parse failed")?;
215 if tree.root_node().has_error() {
216 return Err("cannot format: file has parse errors".into());
217 }
218 let src_lines: Vec<&str> = src.split('\n').collect();
219 let mut leaves: Vec<Leaf> = vec![];
220 collect(tree.root_node(), src, &src_lines, &mut leaves);
221
222 let mut lines: Vec<Vec<Leaf>> = vec![];
224 for l in leaves {
225 match lines.last_mut() {
226 Some(bucket) if bucket[0].row == l.row => bucket.push(l),
227 _ => lines.push(vec![l]),
228 }
229 }
230
231 let mut out: Vec<String> = vec![];
232 let mut depth: usize = 0;
233 let mut last_row_end: i64 = -1; let mut last_code: Option<&Leaf> = None; for line in &lines {
236 let first = &line[0];
237 if (first.row as i64) <= last_row_end {
238 continue; }
240 if !out.is_empty() && (first.row as i64) > last_row_end + 1 {
242 out.push(String::new());
243 }
244 let closers = line
246 .iter()
247 .take_while(|l| l.text == ")" || l.text == "]" || l.text == "}")
248 .count();
249 let mut indent = depth.saturating_sub(closers);
250 let after_open = last_code
254 .map(|l| {
255 !is_atom(&l.kind) && CONT_ENDERS.contains(&l.text.as_str()) && !is_type_angle(l)
256 })
257 .unwrap_or(false);
258 if closers == 0 && (CONT_STARTERS.contains(&first.text.as_str()) || after_open) {
259 indent = depth + 1;
260 }
261 let mut text = " ".repeat(indent);
262 let mut prev: Option<&Leaf> = None;
263 let mut prev2: Option<&Leaf> = None;
264 for l in line {
265 if let Some(p) = prev {
266 if l.kind.ends_with("comment") {
267 let gap = (l.col as i64) - ((p.col + u16len(&p.text)) as i64);
269 text.push_str(&" ".repeat(gap.max(1) as usize));
270 } else if spaced(p, l, prev2) {
271 text.push(' ');
272 }
273 }
274 text.push_str(&l.text);
275 if !is_atom(&l.kind) {
276 for ch in l.text.chars() {
277 match ch {
278 '{' | '[' | '(' => depth += 1,
279 '}' | ']' | ')' => depth = depth.saturating_sub(1),
280 _ => {}
281 }
282 }
283 }
284 prev2 = prev;
285 prev = Some(l);
286 if !l.kind.ends_with("comment") {
287 last_code = Some(l);
288 }
289 last_row_end = last_row_end.max(l.end_row as i64);
290 }
291 out.push(text.trim_end_matches([' ', '\t']).to_string());
292 }
293 Ok(out.join("\n") + "\n")
294}