use std::fmt::Write as _;
use crate::error::TinyAgentsError;
use crate::language::source::SourceFile;
use crate::language::span::Span;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
Note,
}
impl Severity {
pub fn label(&self) -> &'static str {
match self {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Note => "note",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Label {
pub span: Span,
pub message: String,
}
impl Label {
pub fn new(span: Span, message: impl Into<String>) -> Self {
Self {
span,
message: message.into(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Diagnostic {
pub severity: Severity,
pub code: Option<String>,
pub message: String,
pub primary: Span,
pub primary_label: Option<String>,
pub labels: Vec<Label>,
pub help: Option<String>,
}
impl Diagnostic {
pub fn new(severity: Severity, message: impl Into<String>, primary: Span) -> Self {
Self {
severity,
code: None,
message: message.into(),
primary,
primary_label: None,
labels: Vec::new(),
help: None,
}
}
pub fn error(message: impl Into<String>, primary: Span) -> Self {
Self::new(Severity::Error, message, primary)
}
pub fn warning(message: impl Into<String>, primary: Span) -> Self {
Self::new(Severity::Warning, message, primary)
}
pub fn note(message: impl Into<String>, primary: Span) -> Self {
Self::new(Severity::Note, message, primary)
}
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
pub fn with_primary_label(mut self, label: impl Into<String>) -> Self {
self.primary_label = Some(label.into());
self
}
pub fn with_label(mut self, span: Span, message: impl Into<String>) -> Self {
self.labels.push(Label::new(span, message));
self
}
pub fn with_help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
pub fn render(&self, source: &SourceFile) -> String {
let mut out = String::new();
self.write_header(&mut out);
let gutter = self.gutter_width(source);
render_span_block(
&mut out,
source,
gutter,
self.primary,
self.primary_label.as_deref(),
);
for label in &self.labels {
render_span_block(&mut out, source, gutter, label.span, Some(&label.message));
}
if let Some(help) = &self.help {
let _ = writeln!(out, "{:gutter$} = help: {help}", "");
}
out
}
pub fn render_plain(&self) -> String {
let mut out = String::new();
self.write_header(&mut out);
let _ = writeln!(out, " --> {}:{}", self.primary.line, self.primary.column);
if let Some(help) = &self.help {
let _ = writeln!(out, " = help: {help}");
}
out
}
pub fn into_parse_error(self, source: Option<&SourceFile>) -> TinyAgentsError {
let (line, column, message) = match source {
Some(file) => {
let (line, column) = file.location(self.primary.start);
(line, column, self.render(file))
}
None => (self.primary.line, self.primary.column, self.render_plain()),
};
TinyAgentsError::Parse {
message,
line,
column,
}
}
fn write_header(&self, out: &mut String) {
match &self.code {
Some(code) => {
let _ = writeln!(out, "{}[{code}]: {}", self.severity.label(), self.message);
}
None => {
let _ = writeln!(out, "{}: {}", self.severity.label(), self.message);
}
}
}
fn gutter_width(&self, source: &SourceFile) -> usize {
let mut max_line = source.location(self.primary.start).0;
for label in &self.labels {
max_line = max_line.max(source.location(label.span.start).0);
}
max_line.to_string().len()
}
}
fn render_span_block(
out: &mut String,
source: &SourceFile,
gutter: usize,
span: Span,
label: Option<&str>,
) {
let (line, column) = source.location(span.start);
let line_text = source.line_text(line).unwrap_or("");
let _ = writeln!(out, "{:gutter$}--> {}:{line}:{column}", "", source.name());
let _ = writeln!(out, "{:gutter$} |", "");
let _ = writeln!(out, "{line:>gutter$} | {line_text}");
let (line_start, line_end) = source.line_range(line).unwrap_or((span.start, span.start));
let caret_start = span.start.max(line_start);
let caret_end = span.end.clamp(caret_start, line_end);
let caret_width = source.text()[caret_start..caret_end].chars().count().max(1);
let indent = column.saturating_sub(1);
let caret_line = format!("{:gutter$} | {:indent$}{}", "", "", "^".repeat(caret_width));
match label {
Some(text) if !text.is_empty() => {
let _ = writeln!(out, "{caret_line} {text}");
}
_ => {
let _ = writeln!(out, "{caret_line}");
}
}
let _ = writeln!(out, "{:gutter$} |", "");
}