use std::fmt::Write;
use super::Diagnostic;
use crate::fragment::Fragment;
pub trait DiagnosticRenderer {
fn render(&self, diagnostic: &Diagnostic) -> String;
}
pub struct DefaultRenderer;
pub fn get_line(source: &str, line: u32) -> &str {
source.lines().nth((line - 1) as usize).unwrap_or("")
}
impl DiagnosticRenderer for DefaultRenderer {
fn render(&self, diagnostic: &Diagnostic) -> String {
let mut output = String::new();
if diagnostic.cause.is_none() {
self.render_flat(&mut output, diagnostic);
} else {
self.render_nested(&mut output, diagnostic, 0);
}
output
}
}
impl DefaultRenderer {
fn render_flat(&self, output: &mut String, diagnostic: &Diagnostic) {
let _ = writeln!(output, "Error {}", diagnostic.code);
let _ = writeln!(output, " {}", diagnostic.message);
let _ = writeln!(output);
if let Fragment::Statement {
line,
column,
text,
..
} = &diagnostic.fragment
{
let fragment = text;
let line = line.0;
let col = column.0;
let statement = diagnostic.rql.as_deref().unwrap_or("");
let _ = writeln!(output, "LOCATION");
let _ = writeln!(output, " line {}, column {}", line, col);
let _ = writeln!(output);
let line_content = get_line(statement, line);
let _ = writeln!(output, "RQL");
let _ = writeln!(output, " {} │ {}", line, line_content);
let fragment_start = line_content.find(fragment.as_ref()).unwrap_or(col as usize);
let _ = writeln!(output, " │ {}{}", " ".repeat(fragment_start), "~".repeat(fragment.len()));
let _ = writeln!(output, " │");
let label_text = diagnostic.label.as_deref().unwrap_or("");
let fragment_center = fragment_start + fragment.len() / 2;
let label_center_offset = if label_text.len() / 2 > fragment_center {
0
} else {
fragment_center - label_text.len() / 2
};
let _ = writeln!(output, " │ {}{}", " ".repeat(label_center_offset), label_text);
let _ = writeln!(output);
}
if let Some(chain) = &diagnostic.operator_chain
&& !chain.is_empty()
{
let _ = writeln!(output, "OPERATOR CHAIN");
for (i, entry) in chain.iter().enumerate() {
let _ = writeln!(
output,
" {}. {} (node_id={}, version={})",
i + 1,
entry.operator_name,
entry.node_id,
entry.operator_version
);
}
let _ = writeln!(output);
}
if let Some(help) = &diagnostic.help {
let _ = writeln!(output, "HELP");
let _ = writeln!(output, " {}", help);
let _ = writeln!(output);
}
if let Some(col) = &diagnostic.column {
let _ = writeln!(output, "COLUMN");
let _ = writeln!(output, " column `{}` is of type `{}`", col.name, col.r#type);
let _ = writeln!(output);
}
if !diagnostic.notes.is_empty() {
let _ = writeln!(output, "NOTES");
for note in &diagnostic.notes {
let _ = writeln!(output, " • {}", note);
}
}
}
fn render_nested(&self, output: &mut String, diagnostic: &Diagnostic, depth: usize) {
let indent = if depth == 0 {
""
} else {
" "
};
let prefix = if depth == 0 {
""
} else {
"↳ "
};
let _ = writeln!(output, "{}{} Error {}: {}", indent, prefix, diagnostic.code, diagnostic.message);
if let Fragment::Statement {
line,
column,
text,
..
} = &diagnostic.fragment
{
let fragment = text;
let line = line.0;
let col = column.0;
let statement = diagnostic.rql.as_deref().unwrap_or("");
let _ = writeln!(
output,
"{} at {} (line {}, column {})",
indent,
if statement.is_empty() {
"unknown".to_string()
} else {
format!("\"{}\"", fragment)
},
line,
col
);
let _ = writeln!(output);
let line_content = get_line(statement, line);
let _ = writeln!(output, "{} {} │ {}", indent, line, line_content);
let fragment_start = line_content.find(fragment.as_ref()).unwrap_or(col as usize);
let _ = writeln!(
output,
"{} │ {}{}",
indent,
" ".repeat(fragment_start),
"~".repeat(fragment.len())
);
let label_text = diagnostic.label.as_deref().unwrap_or("");
if !label_text.is_empty() {
let fragment_center = fragment_start + fragment.len() / 2;
let label_center_offset = if label_text.len() / 2 > fragment_center {
0
} else {
fragment_center - label_text.len() / 2
};
let _ = writeln!(
output,
"{} │ {}{}",
indent,
" ".repeat(label_center_offset),
label_text
);
}
let _ = writeln!(output);
}
if let Some(cause) = &diagnostic.cause {
self.render_nested(output, cause, depth + 1);
}
if let Some(help) = &diagnostic.help {
let _ = writeln!(output, "{} help: {}", indent, help);
}
if let Some(col) = &diagnostic.column {
let _ = writeln!(output, "{} column `{}` is of type `{}`", indent, col.name, col.r#type);
}
if !diagnostic.notes.is_empty() {
for note in &diagnostic.notes {
let _ = writeln!(output, "{} note: {}", indent, note);
}
}
if depth > 0 {
let _ = writeln!(output);
}
}
}
impl DefaultRenderer {
pub fn render_string(diagnostic: &Diagnostic) -> String {
DefaultRenderer.render(diagnostic)
}
}