use crate::span::{LineCol, SourceFile, Span};
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
}
#[derive(Debug, Clone, Serialize)]
pub struct Label {
pub span: Span,
pub message: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct Diagnostic {
pub severity: Severity,
pub code: &'static str,
pub message: String,
pub labels: Vec<Label>,
pub help: Option<String>,
pub notes: Vec<String>,
}
impl Diagnostic {
pub fn error(code: &'static str, message: impl Into<String>) -> Self {
Diagnostic {
severity: Severity::Error,
code,
message: message.into(),
labels: Vec::new(),
help: None,
notes: Vec::new(),
}
}
pub fn warning(code: &'static str, message: impl Into<String>) -> Self {
Diagnostic {
severity: Severity::Warning,
..Self::error(code, message)
}
}
pub fn with_label(mut self, span: Span, message: impl Into<String>) -> Self {
self.labels.push(Label {
span,
message: message.into(),
});
self
}
pub fn with_help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.notes.push(note.into());
self
}
pub fn primary_span(&self) -> Option<Span> {
self.labels.first().map(|l| l.span)
}
}
#[derive(Debug, Serialize)]
pub struct ResolvedDiagnostic<'a> {
pub severity: Severity,
pub code: &'static str,
pub message: &'a str,
pub file: &'a str,
pub labels: Vec<ResolvedLabel<'a>>,
pub help: Option<&'a str>,
pub notes: &'a [String],
}
#[derive(Debug, Serialize)]
pub struct ResolvedLabel<'a> {
pub message: &'a str,
pub span: Span,
pub start: LineCol,
pub end: LineCol,
pub text: &'a str,
}
pub fn resolve<'a>(d: &'a Diagnostic, src: &'a SourceFile) -> ResolvedDiagnostic<'a> {
ResolvedDiagnostic {
severity: d.severity,
code: d.code,
message: &d.message,
file: &src.name,
labels: d
.labels
.iter()
.map(|l| ResolvedLabel {
message: &l.message,
span: l.span,
start: src.line_col(l.span.start),
end: src.line_col(l.span.end),
text: src.snippet(l.span),
})
.collect(),
help: d.help.as_deref(),
notes: &d.notes,
}
}
pub struct Styles {
bold: &'static str,
red: &'static str,
yellow: &'static str,
blue: &'static str,
green: &'static str,
reset: &'static str,
}
impl Styles {
pub fn colored() -> Self {
Styles {
bold: "\x1b[1m",
red: "\x1b[1;31m",
yellow: "\x1b[1;33m",
blue: "\x1b[1;34m",
green: "\x1b[1;32m",
reset: "\x1b[0m",
}
}
pub fn plain() -> Self {
Styles {
bold: "",
red: "",
yellow: "",
blue: "",
green: "",
reset: "",
}
}
}
pub fn render(d: &Diagnostic, src: &SourceFile, st: &Styles) -> String {
let mut out = String::new();
let (sev_color, sev_name) = match d.severity {
Severity::Error => (st.red, "error"),
Severity::Warning => (st.yellow, "warning"),
};
out.push_str(&format!(
"{sev_color}{sev_name}[{code}]{reset}{bold}: {msg}{reset}\n",
code = d.code,
msg = d.message,
sev_color = sev_color,
reset = st.reset,
bold = st.bold,
));
if let Some(primary) = d.primary_span() {
let lc = src.line_col(primary.start);
let max_line = d
.labels
.iter()
.map(|l| src.line_col(l.span.start).line)
.max()
.unwrap_or(lc.line);
let gw = max_line.to_string().len();
out.push_str(&format!(
"{:gw$}{blue}-->{reset} {}:{}:{}\n",
"",
src.name,
lc.line,
lc.col,
gw = gw + 1,
blue = st.blue,
reset = st.reset,
));
out.push_str(&format!(
"{:gw$} {blue}|{reset}\n",
"",
gw = gw,
blue = st.blue,
reset = st.reset
));
for (i, label) in d.labels.iter().enumerate() {
let is_primary = i == 0;
let l_start = src.line_col(label.span.start);
let l_end = src.line_col(label.span.end);
let line_text = src.line_text(l_start.line);
out.push_str(&format!(
"{blue}{:>gw$} |{reset} {}\n",
l_start.line,
line_text,
gw = gw,
blue = st.blue,
reset = st.reset,
));
let pad = l_start.col - 1;
let width = if l_end.line == l_start.line {
(l_end.col - l_start.col).max(1)
} else {
line_text.chars().count().saturating_sub(pad).max(1)
};
let (mark, color) = if is_primary {
("^", sev_color)
} else {
("-", st.blue)
};
out.push_str(&format!(
"{blue}{:gw$} |{reset} {:pad$}{color}{marks} {msg}{reset}\n",
"",
"",
gw = gw,
pad = pad,
color = color,
marks = mark.repeat(width),
msg = label.message,
blue = st.blue,
reset = st.reset,
));
}
out.push_str(&format!(
"{:gw$} {blue}|{reset}\n",
"",
gw = gw,
blue = st.blue,
reset = st.reset
));
}
if let Some(help) = &d.help {
out.push_str(&format!(
"{green}help{reset}{bold}:{reset} {help}\n",
green = st.green,
reset = st.reset,
bold = st.bold,
help = help
));
}
for note in &d.notes {
out.push_str(&format!(
"{blue}note{reset}{bold}:{reset} {note}\n",
blue = st.blue,
reset = st.reset,
bold = st.bold,
note = note
));
}
out
}
pub fn render_all(diags: &[Diagnostic], src: &SourceFile, st: &Styles) -> String {
let mut out = String::new();
for d in diags {
out.push_str(&render(d, src, st));
out.push('\n');
}
let errors = diags
.iter()
.filter(|d| d.severity == Severity::Error)
.count();
let warnings = diags
.iter()
.filter(|d| d.severity == Severity::Warning)
.count();
if errors > 0 || warnings > 0 {
let mut parts = Vec::new();
if errors > 0 {
parts.push(format!(
"{} error{}",
errors,
if errors == 1 { "" } else { "s" }
));
}
if warnings > 0 {
parts.push(format!(
"{} warning{}",
warnings,
if warnings == 1 { "" } else { "s" }
));
}
let (color, word) = if errors > 0 {
(st.red, "error")
} else {
(st.yellow, "warning")
};
out.push_str(&format!(
"{color}{word}{reset}{bold}: `{file}` emitted {summary}{reset}\n",
color = color,
word = word,
file = src.name,
summary = parts.join(", "),
reset = st.reset,
bold = st.bold,
));
}
out
}
pub fn edit_distance(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
let mut prev: Vec<usize> = (0..=b.len()).collect();
let mut cur = vec![0; b.len() + 1];
for i in 1..=a.len() {
cur[0] = i;
for j in 1..=b.len() {
let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
cur[j] = (prev[j] + 1).min(cur[j - 1] + 1).min(prev[j - 1] + cost);
}
std::mem::swap(&mut prev, &mut cur);
}
prev[b.len()]
}
pub fn suggest<'a>(input: &str, candidates: impl IntoIterator<Item = &'a str>) -> Option<&'a str> {
let max = (input.chars().count() / 3).max(1) + 1;
candidates
.into_iter()
.map(|c| (edit_distance(input, c), c))
.filter(|(d, _)| *d <= max)
.min_by_key(|(d, _)| *d)
.map(|(_, c)| c)
}