1use std::fmt::Write;
5
6use super::Diagnostic;
7use crate::fragment::Fragment;
8
9pub trait DiagnosticRenderer {
10 fn render(&self, diagnostic: &Diagnostic) -> String;
11}
12
13pub struct DefaultRenderer;
14
15pub fn get_line(source: &str, line: u32) -> &str {
16 source.lines().nth((line - 1) as usize).unwrap_or("")
17}
18
19impl DiagnosticRenderer for DefaultRenderer {
20 fn render(&self, diagnostic: &Diagnostic) -> String {
21 let mut output = String::new();
22
23 if diagnostic.cause.is_none() {
24 self.render_flat(&mut output, diagnostic);
25 } else {
26 self.render_nested(&mut output, diagnostic, 0);
27 }
28
29 output
30 }
31}
32
33impl DefaultRenderer {
34 fn render_flat(&self, output: &mut String, diagnostic: &Diagnostic) {
35 self.render_flat_header(output, diagnostic);
36 self.render_flat_location(output, diagnostic);
37 self.render_flat_operator_chain(output, diagnostic);
38 self.render_flat_trailing(output, diagnostic);
39 }
40
41 #[inline]
42 fn render_flat_header(&self, output: &mut String, diagnostic: &Diagnostic) {
43 let _ = writeln!(output, "Error {}", diagnostic.code);
44 let _ = writeln!(output, " {}", diagnostic.message);
45 let _ = writeln!(output);
46 }
47
48 #[inline]
49 fn render_flat_location(&self, output: &mut String, diagnostic: &Diagnostic) {
50 if let Fragment::Statement {
51 line,
52 column,
53 text,
54 ..
55 } = &diagnostic.fragment
56 {
57 let fragment = text;
58 let line = line.0;
59 let col = column.0;
60 let rql = diagnostic.rql.as_deref().unwrap_or("");
61
62 let _ = writeln!(output, "LOCATION");
63 let _ = writeln!(output, " line {}, column {}", line, col);
64 let _ = writeln!(output);
65
66 let line_content = get_line(rql, line);
67
68 let _ = writeln!(output, "RQL");
69 let _ = writeln!(output, " {} │ {}", line, line_content);
70 let fragment_start = line_content.find(fragment.as_ref()).unwrap_or(col as usize);
71 let _ = writeln!(output, " │ {}{}", " ".repeat(fragment_start), "~".repeat(fragment.len()));
72 let _ = writeln!(output, " │");
73
74 let label_text = diagnostic.label.as_deref().unwrap_or("");
75 let fragment_center = fragment_start + fragment.len() / 2;
76 let label_center_offset = if label_text.len() / 2 > fragment_center {
77 0
78 } else {
79 fragment_center - label_text.len() / 2
80 };
81
82 let _ = writeln!(output, " │ {}{}", " ".repeat(label_center_offset), label_text);
83 let _ = writeln!(output);
84 }
85 }
86
87 #[inline]
88 fn render_flat_operator_chain(&self, output: &mut String, diagnostic: &Diagnostic) {
89 if let Some(chain) = &diagnostic.operator_chain
90 && !chain.is_empty()
91 {
92 let _ = writeln!(output, "OPERATOR CHAIN");
93 for (i, entry) in chain.iter().enumerate() {
94 let _ = writeln!(
95 output,
96 " {}. {} (node_id={}, version={})",
97 i + 1,
98 entry.operator_name,
99 entry.node_id,
100 entry.operator_version
101 );
102 }
103 let _ = writeln!(output);
104 }
105 }
106
107 #[inline]
108 fn render_flat_trailing(&self, output: &mut String, diagnostic: &Diagnostic) {
109 if let Some(help) = &diagnostic.help {
110 let _ = writeln!(output, "HELP");
111 let _ = writeln!(output, " {}", help);
112 let _ = writeln!(output);
113 }
114
115 if let Some(col) = &diagnostic.column {
116 let _ = writeln!(output, "COLUMN");
117 let _ = writeln!(output, " column `{}` is of type `{}`", col.name, col.r#type);
118 let _ = writeln!(output);
119 }
120
121 if !diagnostic.notes.is_empty() {
122 let _ = writeln!(output, "NOTES");
123 for note in &diagnostic.notes {
124 let _ = writeln!(output, " • {}", note);
125 }
126 }
127 }
128
129 fn render_nested(&self, output: &mut String, diagnostic: &Diagnostic, depth: usize) {
130 let indent = if depth == 0 {
131 ""
132 } else {
133 " "
134 };
135 let prefix = if depth == 0 {
136 ""
137 } else {
138 "↳ "
139 };
140
141 self.render_nested_header(output, diagnostic, indent, prefix);
142 self.render_nested_location(output, diagnostic, indent);
143
144 if let Some(cause) = &diagnostic.cause {
145 self.render_nested(output, cause, depth + 1);
146 }
147
148 self.render_nested_trailing(output, diagnostic, indent, depth);
149 }
150
151 #[inline]
152 fn render_nested_header(&self, output: &mut String, diagnostic: &Diagnostic, indent: &str, prefix: &str) {
153 let _ = writeln!(output, "{}{} Error {}: {}", indent, prefix, diagnostic.code, diagnostic.message);
154 }
155
156 #[inline]
157 fn render_nested_location(&self, output: &mut String, diagnostic: &Diagnostic, indent: &str) {
158 if let Fragment::Statement {
159 line,
160 column,
161 text,
162 ..
163 } = &diagnostic.fragment
164 {
165 let fragment = text;
166 let line = line.0;
167 let col = column.0;
168 let rql = diagnostic.rql.as_deref().unwrap_or("");
169
170 let _ = writeln!(
171 output,
172 "{} at {} (line {}, column {})",
173 indent,
174 if rql.is_empty() {
175 "unknown".to_string()
176 } else {
177 format!("\"{}\"", fragment)
178 },
179 line,
180 col
181 );
182 let _ = writeln!(output);
183
184 let line_content = get_line(rql, line);
185
186 let _ = writeln!(output, "{} {} │ {}", indent, line, line_content);
187 let fragment_start = line_content.find(fragment.as_ref()).unwrap_or(col as usize);
188 let _ = writeln!(
189 output,
190 "{} │ {}{}",
191 indent,
192 " ".repeat(fragment_start),
193 "~".repeat(fragment.len())
194 );
195
196 let label_text = diagnostic.label.as_deref().unwrap_or("");
197 if !label_text.is_empty() {
198 let fragment_center = fragment_start + fragment.len() / 2;
199 let label_center_offset = if label_text.len() / 2 > fragment_center {
200 0
201 } else {
202 fragment_center - label_text.len() / 2
203 };
204
205 let _ = writeln!(
206 output,
207 "{} │ {}{}",
208 indent,
209 " ".repeat(label_center_offset),
210 label_text
211 );
212 }
213 let _ = writeln!(output);
214 }
215 }
216
217 #[inline]
218 fn render_nested_trailing(&self, output: &mut String, diagnostic: &Diagnostic, indent: &str, depth: usize) {
219 if let Some(help) = &diagnostic.help {
220 let _ = writeln!(output, "{} help: {}", indent, help);
221 }
222
223 if let Some(col) = &diagnostic.column {
224 let _ = writeln!(output, "{} column `{}` is of type `{}`", indent, col.name, col.r#type);
225 }
226
227 if !diagnostic.notes.is_empty() {
228 for note in &diagnostic.notes {
229 let _ = writeln!(output, "{} note: {}", indent, note);
230 }
231 }
232
233 if depth > 0 {
234 let _ = writeln!(output);
235 }
236 }
237}
238
239impl DefaultRenderer {
240 pub fn render_string(diagnostic: &Diagnostic) -> String {
241 DefaultRenderer.render(diagnostic)
242 }
243}