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