use crate::event::{flatten, theme_style, EventView, Value};
use crate::hyperlink::Hyperlinker;
use crate::layout::{fit_segments, OverflowPolicy};
use crate::stacktrace::StackTrace;
use rich::{Console, ConsoleOptions, Renderable, Segment, Style, Text};
use std::ops::Range;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DiagnosticError {
InvalidSpan,
}
impl std::fmt::Display for DiagnosticError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "source span is outside UTF-8 boundaries")
}
}
impl std::error::Error for DiagnosticError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Level {
Error,
Warning,
Info,
Note,
Help,
}
impl Level {
pub fn name(self) -> &'static str {
match self {
Level::Error => "error",
Level::Warning => "warning",
Level::Info => "info",
Level::Note => "note",
Level::Help => "help",
}
}
pub fn style(self, console: &Console) -> Style {
let fallback = match self {
Level::Error => "bold red",
Level::Warning => "bold yellow",
Level::Info => "bold blue",
Level::Note => "bold green",
Level::Help => "bold cyan",
};
theme_style(console, &format!("diagnostic.{}", self.name()), fallback)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Location {
pub path: String,
pub line: Option<usize>,
pub column: Option<usize>,
}
impl Location {
pub fn new(path: impl Into<String>, line: Option<usize>, column: Option<usize>) -> Self {
Location {
path: path.into(),
line,
column,
}
}
}
#[derive(Clone, Debug)]
struct SpanLabel {
span: Range<usize>,
message: Option<String>,
primary: bool,
}
fn check_span(source: &str, span: &Range<usize>) -> Result<(), DiagnosticError> {
if span.start > span.end
|| span.end > source.len()
|| !source.is_char_boundary(span.start)
|| !source.is_char_boundary(span.end)
{
return Err(DiagnosticError::InvalidSpan);
}
Ok(())
}
fn marker_cells(line: &str, a: usize, b: usize) -> (usize, usize) {
let (mut a, mut b) = (a.min(line.len()), b.min(line.len()));
for (begin, end, _) in rich::cells::split_graphemes(line).0 {
if begin < a && a < end {
a = begin;
}
if begin < b && b < end {
b = end;
}
}
let mut before = Text::new(&line[..a]);
before.expand_tabs(8);
let mut through = Text::new(&line[..b]);
through.expand_tabs(8);
let left = before.cell_len();
(left, through.cell_len().saturating_sub(left).max(1))
}
#[derive(Clone, Debug)]
pub struct SourceSnippet {
name: String,
source: String,
labels: Vec<SpanLabel>,
context_lines: usize,
}
impl SourceSnippet {
pub fn new(
name: String,
source: String,
span: Range<usize>,
context_lines: usize,
) -> Result<Self, DiagnosticError> {
check_span(&source, &span)?;
Ok(Self {
name,
source,
labels: vec![SpanLabel {
span,
message: None,
primary: true,
}],
context_lines,
})
}
pub fn primary_label(mut self, message: impl Into<String>) -> Self {
self.labels[0].message = Some(message.into());
self
}
pub fn primary(
mut self,
span: Range<usize>,
message: impl Into<String>,
) -> Result<Self, DiagnosticError> {
check_span(&self.source, &span)?;
self.labels.push(SpanLabel {
span,
message: Some(message.into()).filter(|m| !m.is_empty()),
primary: true,
});
Ok(self)
}
pub fn secondary(
mut self,
span: Range<usize>,
message: impl Into<String>,
) -> Result<Self, DiagnosticError> {
check_span(&self.source, &span)?;
self.labels.push(SpanLabel {
span,
message: Some(message.into()).filter(|m| !m.is_empty()),
primary: false,
});
Ok(self)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn location(&self) -> Location {
let start = self.labels[0].span.start;
let before = &self.source[..start];
let line = before.matches('\n').count() + 1;
let line_start = before.rfind('\n').map_or(0, |i| i + 1);
let column = before[line_start..].chars().count() + 1;
Location::new(self.name.clone(), Some(line), Some(column))
}
fn rows(
&self,
primary_style: Option<Style>,
secondary_style: Option<Style>,
linker: Option<&Hyperlinker>,
) -> Vec<Vec<Segment>> {
struct Line<'a> {
number: usize,
offset: usize,
end: usize,
text: &'a str,
}
let mut lines = Vec::new();
let mut offset = 0;
for (i, line) in self.source.split('\n').enumerate() {
let end = offset + line.len();
lines.push(Line {
number: i + 1,
offset,
end,
text: line.strip_suffix('\r').unwrap_or(line),
});
offset = end.saturating_add(1);
}
let touches = |label: &SpanLabel, line: &Line<'_>| {
if label.span.is_empty() {
label.span.start >= line.offset && label.span.start <= line.end
} else {
label.span.start <= line.end && label.span.end > line.offset
}
};
let active: Vec<bool> = lines
.iter()
.map(|line| self.labels.iter().any(|label| touches(label, line)))
.collect();
let last_line: Vec<Option<usize>> = self
.labels
.iter()
.map(|label| lines.iter().rposition(|line| touches(label, line)))
.collect();
let first = active.iter().position(|a| *a).unwrap_or(0);
let last = active.iter().rposition(|a| *a).unwrap_or(first);
let digits = (last + 1).to_string().len();
let gutter = format!("{} | ", " ".repeat(digits));
let mut header = vec![Segment::new("--> ", None)];
match linker.and_then(|linker| {
let location = self.location();
linker.file_url(&self.name, location.line, location.column)
}) {
Some(url) => header.push(Segment::new(
self.name.clone(),
Some(Style::new().with_link(url)),
)),
None => header.push(Segment::new(self.name.clone(), None)),
}
let mut out = vec![Segment::simplify(&header)];
let window_start = first.saturating_sub(self.context_lines);
let window_end = last
.saturating_add(self.context_lines)
.saturating_add(1)
.min(lines.len());
for (index, line) in lines.iter().enumerate().take(window_end).skip(window_start) {
let mut text = Text::new(line.text);
text.expand_tabs(8);
out.push(vec![Segment::new(
format!("{:>digits$} | {}", line.number, text.plain()),
None,
)]);
if !active[index] {
continue;
}
let mut cells: Vec<Option<bool>> = Vec::new();
let mut messages: Vec<(usize, &str, bool)> = Vec::new();
let mut order: Vec<usize> = (0..self.labels.len()).collect();
order.sort_by_key(|&i| self.labels[i].primary);
for i in order {
let label = &self.labels[i];
if !touches(label, line) {
continue;
}
let a = label.span.start.saturating_sub(line.offset);
let b = label.span.end.saturating_sub(line.offset);
let (left, count) = marker_cells(line.text, a, b);
if cells.len() < left + count {
cells.resize(left + count, None);
}
for cell in &mut cells[left..left + count] {
*cell = Some(label.primary);
}
if let (Some(message), Some(true)) =
(&label.message, last_line[i].map(|l| l == index))
{
messages.push((left, message, label.primary));
}
}
messages.sort_by_key(|(left, _, _)| *left);
let style_for = |primary: bool| {
if primary {
primary_style.clone()
} else {
secondary_style.clone()
}
};
let mut row = vec![Segment::new(gutter.clone(), None)];
let mut run = String::new();
let mut run_kind: Option<bool> = None;
let flush = |row: &mut Vec<Segment>, run: &mut String, kind: Option<bool>| {
if !run.is_empty() {
let style = kind.and_then(style_for);
row.push(Segment::new(std::mem::take(run), style));
}
};
for cell in &cells {
if *cell != run_kind {
flush(&mut row, &mut run, run_kind);
run_kind = *cell;
}
run.push(match cell {
Some(true) => '^',
Some(false) => '-',
None => ' ',
});
}
flush(&mut row, &mut run, run_kind);
if let Some((_, message, primary)) = messages.pop() {
row.push(Segment::new(" ", None));
row.push(Segment::new(message.to_string(), style_for(primary)));
}
out.push(Segment::simplify(&row));
for (left, message, primary) in messages.into_iter().rev() {
out.push(Segment::simplify(&[
Segment::new(format!("{gutter}{}", " ".repeat(left)), None),
Segment::new(message.to_string(), style_for(primary)),
]));
}
}
out
}
}
#[derive(Clone, Debug)]
pub struct Suggestion {
message: String,
edit: Option<SuggestedEdit>,
}
#[derive(Clone, Debug)]
struct SuggestedEdit {
line_number: usize,
line: String,
left: usize,
count: usize,
}
impl Suggestion {
pub fn new(message: impl Into<String>) -> Self {
Suggestion {
message: message.into(),
edit: None,
}
}
pub fn replace(
message: impl Into<String>,
source: &str,
span: Range<usize>,
replacement: &str,
) -> Result<Self, DiagnosticError> {
check_span(source, &span)?;
let line_start = source[..span.start].rfind('\n').map_or(0, |i| i + 1);
let line_end = source[span.start..]
.find('\n')
.map_or(source.len(), |i| span.start + i);
let line = &source[line_start..line_end];
let line = line.strip_suffix('\r').unwrap_or(line);
let a = (span.start - line_start).min(line.len());
let b = (span.end.min(line_end) - line_start).clamp(a, line.len());
let edited = format!("{}{replacement}{}", &line[..a], &line[b..]);
let (left, count) = marker_cells(&edited, a, a + replacement.len());
let mut shown = Text::new(edited);
shown.expand_tabs(8);
Ok(Suggestion {
message: message.into(),
edit: Some(SuggestedEdit {
line_number: source[..span.start].matches('\n').count() + 1,
line: shown.plain().to_string(),
left,
count,
}),
})
}
pub fn message(&self) -> &str {
&self.message
}
}
#[derive(Clone, Debug)]
pub struct Diagnostic {
message: String,
level: Option<Level>,
code: Option<String>,
code_url: Option<String>,
location: Option<Location>,
causes: Vec<String>,
notes: Vec<String>,
help: Vec<String>,
suggestions: Vec<Suggestion>,
labels: Vec<String>,
snippets: Vec<SourceSnippet>,
metadata: Vec<(String, Value)>,
trace: Option<StackTrace>,
linker: Option<Hyperlinker>,
view: EventView,
overflow: OverflowPolicy,
}
impl Diagnostic {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
level: None,
code: None,
code_url: None,
location: None,
causes: Vec::new(),
notes: Vec::new(),
help: Vec::new(),
suggestions: Vec::new(),
labels: Vec::new(),
snippets: Vec::new(),
metadata: Vec::new(),
trace: None,
linker: None,
view: EventView::Compact,
overflow: OverflowPolicy::Fold,
}
}
pub fn error(message: impl Into<String>) -> Self {
Self::new(message).level(Level::Error)
}
pub fn warning(message: impl Into<String>) -> Self {
Self::new(message).level(Level::Warning)
}
pub fn from_error(error: &(dyn std::error::Error + 'static), max_depth: usize) -> Self {
let mut result = Self::new(error.to_string());
let mut seen = vec![error as *const dyn std::error::Error];
let mut next = error.source();
while let Some(error) = next {
if result.causes.len() >= max_depth {
result.causes.push("[truncated]".into());
break;
}
let ptr = error as *const dyn std::error::Error;
if seen.iter().any(|&p| std::ptr::eq(p, ptr)) {
result.causes.push("[cycle]".into());
break;
}
seen.push(ptr);
result.causes.push(error.to_string());
next = error.source();
}
result
}
pub fn from_info<E: DiagnosticInfo>(error: &E, max_depth: usize) -> Self {
let mut result = Self::from_error(error, max_depth).level(error.level());
result.code = error.code();
result.code_url = error.code_url();
result.location = error.location();
result.help.extend(error.help());
result.notes.extend(error.notes());
result
}
#[cfg(feature = "anyhow")]
pub fn from_anyhow(error: &anyhow::Error, max_depth: usize) -> Self {
let inner: &(dyn std::error::Error + 'static) = error.as_ref();
let mut result = Self::from_error(inner, max_depth).level(Level::Error);
let backtrace = error.backtrace();
if backtrace.status() == std::backtrace::BacktraceStatus::Captured {
use crate::stacktrace::TraceParser;
result.trace = crate::stacktrace::RustParser.parse(&backtrace.to_string());
}
result
}
pub fn level(mut self, level: Level) -> Self {
self.level = Some(level);
self
}
pub fn code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
pub fn code_url(mut self, url: impl Into<String>) -> Self {
self.code_url = Some(url.into());
self
}
pub fn location(mut self, location: Location) -> Self {
self.location = Some(location);
self
}
pub fn hyperlinker(mut self, linker: Hyperlinker) -> Self {
self.linker = Some(linker);
self
}
pub fn cause(mut self, message: impl Into<String>) -> Self {
self.causes.push(message.into());
self
}
pub fn note(mut self, message: impl Into<String>) -> Self {
self.notes.push(message.into());
self
}
pub fn help(mut self, message: impl Into<String>) -> Self {
self.help.push(message.into());
self
}
pub fn suggestion(mut self, suggestion: Suggestion) -> Self {
self.suggestions.push(suggestion);
self
}
pub fn label(mut self, message: impl Into<String>) -> Self {
self.labels.push(message.into());
self
}
pub fn snippet(mut self, snippet: SourceSnippet) -> Self {
self.snippets.push(snippet);
self
}
pub fn metadata(mut self, key: impl Into<String>, value: Value) -> Self {
self.metadata.push((key.into(), value));
self
}
pub fn trace(mut self, trace: StackTrace) -> Self {
self.trace = Some(trace);
self
}
pub fn view(mut self, view: EventView) -> Self {
self.view = view;
self
}
pub fn overflow(mut self, overflow: OverflowPolicy) -> Self {
self.overflow = overflow;
self
}
pub fn message(&self) -> &str {
&self.message
}
pub fn get_level(&self) -> Option<Level> {
self.level
}
pub fn get_code(&self) -> Option<&str> {
self.code.as_deref()
}
pub fn get_location(&self) -> Option<Location> {
self.location
.clone()
.or_else(|| self.snippets.first().map(SourceSnippet::location))
}
pub fn causes(&self) -> &[String] {
&self.causes
}
pub fn get_trace(&self) -> Option<&StackTrace> {
self.trace.as_ref()
}
pub fn get_code_url(&self) -> Option<&str> {
self.code_url.as_deref()
}
pub fn notes(&self) -> &[String] {
&self.notes
}
pub fn help_messages(&self) -> &[String] {
&self.help
}
pub fn suggestions(&self) -> &[Suggestion] {
&self.suggestions
}
pub fn labels(&self) -> &[String] {
&self.labels
}
fn header(&self, c: &Console) -> Vec<Segment> {
let Some(level) = self.level else {
let mut segments = Vec::new();
if let Some(code) = &self.code {
segments.push(Segment::new(format!("[{code}] "), None));
}
segments.push(Segment::new(
&self.message,
Some(theme_style(c, "diagnostic.message", "bold red")),
));
return segments;
};
let style = level.style(c);
let mut segments = vec![Segment::new(level.name(), Some(style.clone()))];
if let Some(code) = &self.code {
let code_style = match &self.code_url {
Some(url) => style.clone().with_link(url.clone()),
None => style.clone(),
};
segments.push(Segment::new(format!("[{code}]"), Some(code_style)));
}
segments.push(Segment::new(
": ",
Some(theme_style(c, "diagnostic.headline", "bold")),
));
segments.push(Segment::new(
&self.message,
Some(theme_style(c, "diagnostic.headline", "bold")),
));
segments
}
}
pub trait DiagnosticInfo: std::error::Error + 'static {
fn level(&self) -> Level {
Level::Error
}
fn code(&self) -> Option<String> {
None
}
fn code_url(&self) -> Option<String> {
None
}
fn help(&self) -> Option<String> {
None
}
fn notes(&self) -> Vec<String> {
Vec::new()
}
fn location(&self) -> Option<Location> {
None
}
fn to_diagnostic(&self) -> Diagnostic
where
Self: Sized,
{
Diagnostic::from_info(self, 16)
}
}
impl Renderable for Diagnostic {
fn rich_render(&self, c: &Console, o: &ConsoleOptions) -> Vec<Segment> {
if o.max_width == 0 || o.height == Some(0) {
return Vec::new();
}
let mut rows = fit_segments(&self.header(c), o.max_width, self.overflow);
if let Some(location) = &self.location {
let gutter = theme_style(c, "diagnostic.gutter", "bold blue");
let linker = self.linker.clone().unwrap_or_else(Hyperlinker::disabled);
let text = linker.location(&location.path, location.line, location.column, "");
let mut segments = vec![Segment::new(" --> ", Some(gutter))];
segments.extend(text.render(c.theme(), &Style::new()));
rows.extend(fit_segments(&segments, o.max_width, OverflowPolicy::Crop));
}
for cause in &self.causes {
rows.extend(fit_segments(
&[Segment::new(format!("caused by: {cause}"), None)],
o.max_width,
self.overflow,
));
}
if self.view == EventView::Expanded {
for label in &self.labels {
rows.extend(fit_segments(
&[Segment::new(label, None)],
o.max_width,
self.overflow,
));
}
let (primary, secondary) = match self.level {
Some(level) => (
Some(level.style(c)),
Some(theme_style(c, "diagnostic.secondary", "bold blue")),
),
None => (None, None),
};
for (index, snippet) in self.snippets.iter().enumerate() {
let repeated = index == 0
&& self
.location
.as_ref()
.is_some_and(|location| location.path == snippet.name);
let lines = snippet.rows(primary.clone(), secondary.clone(), self.linker.as_ref());
for line in lines.into_iter().skip(usize::from(repeated)) {
rows.extend(fit_segments(&line, o.max_width, OverflowPolicy::Crop));
}
}
for (key, value) in &self.metadata {
rows.extend(fit_segments(
&[Segment::new(
format!("{key}={}", value.format(true, 0)),
None,
)],
o.max_width,
self.overflow,
));
}
for (prefix, values) in [("note", &self.notes), ("help", &self.help)] {
for value in values {
rows.extend(fit_segments(
&[Segment::new(format!("{prefix}: {value}"), None)],
o.max_width,
self.overflow,
));
}
}
let added = theme_style(c, "diagnostic.suggestion", "green");
for suggestion in &self.suggestions {
rows.extend(fit_segments(
&[Segment::new(format!("help: {}", suggestion.message), None)],
o.max_width,
self.overflow,
));
if let Some(edit) = &suggestion.edit {
let digits = edit.line_number.to_string().len();
let line = format!("{:>digits$} | {}", edit.line_number, edit.line);
rows.extend(fit_segments(
&[Segment::new(line, None)],
o.max_width,
OverflowPolicy::Crop,
));
let marks = [
Segment::new(
format!("{} | {}", " ".repeat(digits), " ".repeat(edit.left)),
None,
),
Segment::new("+".repeat(edit.count), Some(added.clone())),
];
rows.extend(fit_segments(&marks, o.max_width, OverflowPolicy::Crop));
}
}
if let Some(trace) = &self.trace {
let view = trace
.render_options()
.hyperlinker(self.linker.clone().unwrap_or_else(Hyperlinker::disabled));
rows.extend(c.render_lines(&view, &o.update_width(o.max_width), false));
}
}
if let Some(height) = o.height {
rows.truncate(height);
}
flatten(rows)
}
}