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_some() {
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 statement = diagnostic.statement.as_ref().map(|x| x.as_str()).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(statement, line);
56
57 let _ = writeln!(output, "CODE");
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 {
77 if !chain.is_empty() {
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
93 if let Some(help) = &diagnostic.help {
94 let _ = writeln!(output, "HELP");
95 let _ = writeln!(output, " {}", help);
96 let _ = writeln!(output);
97 }
98
99 if let Some(col) = &diagnostic.column {
100 let _ = writeln!(output, "COLUMN");
101 let _ = writeln!(output, " column `{}` is of type `{}`", col.name, col.r#type);
102 let _ = writeln!(output);
103 }
104
105 if !diagnostic.notes.is_empty() {
106 let _ = writeln!(output, "NOTES");
107 for note in &diagnostic.notes {
108 let _ = writeln!(output, " • {}", note);
109 }
110 }
111 }
112
113 fn render_nested(&self, output: &mut String, diagnostic: &Diagnostic, depth: usize) {
114 let indent = if depth == 0 {
115 ""
116 } else {
117 " "
118 };
119 let prefix = if depth == 0 {
120 ""
121 } else {
122 "↳ "
123 };
124
125 let _ = writeln!(output, "{}{} Error {}: {}", indent, prefix, diagnostic.code, diagnostic.message);
127
128 if let Fragment::Statement {
130 line,
131 column,
132 text,
133 ..
134 } = &diagnostic.fragment
135 {
136 let fragment = text;
137 let line = line.0;
138 let col = column.0;
139 let statement = diagnostic.statement.as_ref().map(|x| x.as_str()).unwrap_or("");
140
141 let _ = writeln!(
142 output,
143 "{} at {} (line {}, column {})",
144 indent,
145 if statement.is_empty() {
146 "unknown".to_string()
147 } else {
148 format!("\"{}\"", fragment)
149 },
150 line,
151 col
152 );
153 let _ = writeln!(output);
154
155 let line_content = get_line(statement, line);
157
158 let _ = writeln!(output, "{} {} │ {}", indent, line, line_content);
159 let fragment_start = line_content.find(fragment.as_ref()).unwrap_or(col as usize);
160 let _ = writeln!(
161 output,
162 "{} │ {}{}",
163 indent,
164 " ".repeat(fragment_start),
165 "~".repeat(fragment.len())
166 );
167
168 let label_text = diagnostic.label.as_deref().unwrap_or("");
169 if !label_text.is_empty() {
170 let fragment_center = fragment_start + fragment.len() / 2;
171 let label_center_offset = if label_text.len() / 2 > fragment_center {
172 0
173 } else {
174 fragment_center - label_text.len() / 2
175 };
176
177 let _ = writeln!(
178 output,
179 "{} │ {}{}",
180 indent,
181 " ".repeat(label_center_offset),
182 label_text
183 );
184 }
185 let _ = writeln!(output);
186 }
187
188 if let Some(cause) = &diagnostic.cause {
190 self.render_nested(output, cause, depth + 1);
191 }
192
193 if let Some(help) = &diagnostic.help {
195 let _ = writeln!(output, "{} help: {}", indent, help);
196 }
197
198 if let Some(col) = &diagnostic.column {
200 let _ = writeln!(output, "{} column `{}` is of type `{}`", indent, col.name, col.r#type);
201 }
202
203 if !diagnostic.notes.is_empty() {
205 for note in &diagnostic.notes {
206 let _ = writeln!(output, "{} note: {}", indent, note);
207 }
208 }
209
210 if depth > 0 {
212 let _ = writeln!(output);
213 }
214 }
215}
216
217impl DefaultRenderer {
218 pub fn render_string(diagnostic: &Diagnostic) -> String {
219 DefaultRenderer.render(diagnostic)
220 }
221}