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 let _ = writeln!(output, "Error {}", diagnostic.code);
36 let _ = writeln!(output, " {}", diagnostic.message);
37 let _ = writeln!(output);
38
39 if let Fragment::Statement {
40 line,
41 column,
42 text,
43 ..
44 } = &diagnostic.fragment
45 {
46 let fragment = text;
47 let line = line.0;
48 let col = column.0;
49 let rql = diagnostic.rql.as_deref().unwrap_or("");
50
51 let _ = writeln!(output, "LOCATION");
52 let _ = writeln!(output, " line {}, column {}", line, col);
53 let _ = writeln!(output);
54
55 let line_content = get_line(rql, line);
56
57 let _ = writeln!(output, "RQL");
58 let _ = writeln!(output, " {} │ {}", line, line_content);
59 let fragment_start = line_content.find(fragment.as_ref()).unwrap_or(col as usize);
60 let _ = writeln!(output, " │ {}{}", " ".repeat(fragment_start), "~".repeat(fragment.len()));
61 let _ = writeln!(output, " │");
62
63 let label_text = diagnostic.label.as_deref().unwrap_or("");
64 let fragment_center = fragment_start + fragment.len() / 2;
65 let label_center_offset = if label_text.len() / 2 > fragment_center {
66 0
67 } else {
68 fragment_center - label_text.len() / 2
69 };
70
71 let _ = writeln!(output, " │ {}{}", " ".repeat(label_center_offset), label_text);
72 let _ = writeln!(output);
73 }
74
75 if let Some(chain) = &diagnostic.operator_chain
76 && !chain.is_empty()
77 {
78 let _ = writeln!(output, "OPERATOR CHAIN");
79 for (i, entry) in chain.iter().enumerate() {
80 let _ = writeln!(
81 output,
82 " {}. {} (node_id={}, version={})",
83 i + 1,
84 entry.operator_name,
85 entry.node_id,
86 entry.operator_version
87 );
88 }
89 let _ = writeln!(output);
90 }
91
92 if let Some(help) = &diagnostic.help {
93 let _ = writeln!(output, "HELP");
94 let _ = writeln!(output, " {}", help);
95 let _ = writeln!(output);
96 }
97
98 if let Some(col) = &diagnostic.column {
99 let _ = writeln!(output, "COLUMN");
100 let _ = writeln!(output, " column `{}` is of type `{}`", col.name, col.r#type);
101 let _ = writeln!(output);
102 }
103
104 if !diagnostic.notes.is_empty() {
105 let _ = writeln!(output, "NOTES");
106 for note in &diagnostic.notes {
107 let _ = writeln!(output, " • {}", note);
108 }
109 }
110 }
111
112 fn render_nested(&self, output: &mut String, diagnostic: &Diagnostic, depth: usize) {
113 let indent = if depth == 0 {
114 ""
115 } else {
116 " "
117 };
118 let prefix = if depth == 0 {
119 ""
120 } else {
121 "↳ "
122 };
123
124 let _ = writeln!(output, "{}{} Error {}: {}", indent, prefix, diagnostic.code, diagnostic.message);
125
126 if let Fragment::Statement {
127 line,
128 column,
129 text,
130 ..
131 } = &diagnostic.fragment
132 {
133 let fragment = text;
134 let line = line.0;
135 let col = column.0;
136 let rql = diagnostic.rql.as_deref().unwrap_or("");
137
138 let _ = writeln!(
139 output,
140 "{} at {} (line {}, column {})",
141 indent,
142 if rql.is_empty() {
143 "unknown".to_string()
144 } else {
145 format!("\"{}\"", fragment)
146 },
147 line,
148 col
149 );
150 let _ = writeln!(output);
151
152 let line_content = get_line(rql, line);
153
154 let _ = writeln!(output, "{} {} │ {}", indent, line, line_content);
155 let fragment_start = line_content.find(fragment.as_ref()).unwrap_or(col as usize);
156 let _ = writeln!(
157 output,
158 "{} │ {}{}",
159 indent,
160 " ".repeat(fragment_start),
161 "~".repeat(fragment.len())
162 );
163
164 let label_text = diagnostic.label.as_deref().unwrap_or("");
165 if !label_text.is_empty() {
166 let fragment_center = fragment_start + fragment.len() / 2;
167 let label_center_offset = if label_text.len() / 2 > fragment_center {
168 0
169 } else {
170 fragment_center - label_text.len() / 2
171 };
172
173 let _ = writeln!(
174 output,
175 "{} │ {}{}",
176 indent,
177 " ".repeat(label_center_offset),
178 label_text
179 );
180 }
181 let _ = writeln!(output);
182 }
183
184 if let Some(cause) = &diagnostic.cause {
185 self.render_nested(output, cause, depth + 1);
186 }
187
188 if let Some(help) = &diagnostic.help {
189 let _ = writeln!(output, "{} help: {}", indent, help);
190 }
191
192 if let Some(col) = &diagnostic.column {
193 let _ = writeln!(output, "{} column `{}` is of type `{}`", indent, col.name, col.r#type);
194 }
195
196 if !diagnostic.notes.is_empty() {
197 for note in &diagnostic.notes {
198 let _ = writeln!(output, "{} note: {}", indent, note);
199 }
200 }
201
202 if depth > 0 {
203 let _ = writeln!(output);
204 }
205 }
206}
207
208impl DefaultRenderer {
209 pub fn render_string(diagnostic: &Diagnostic) -> String {
210 DefaultRenderer.render(diagnostic)
211 }
212}