use crate::error::ParseError;
pub fn format_diagnostic(err: &ParseError, source: &str) -> String {
let mut buf = String::new();
buf.push_str(&format!(
"error[{}] at line {}, col {}: {}\n",
err.code, err.line, err.col, err.message
));
if let Some(ref help) = err.help {
buf.push_str(" help: ");
buf.push_str(help);
buf.push('\n');
}
if let Some(ref suggestion) = err.suggestion {
buf.push_str(" suggestion: ");
buf.push_str(suggestion);
buf.push('\n');
}
let (src_line, underline_col) = source_line_at(source, err.line, err.col);
if !src_line.is_empty() {
buf.push_str(&format!(" {:>4} │ {}\n", err.line, src_line));
let pad = underline_col.saturating_sub(1);
buf.push_str(" │ ");
for _ in 0..pad {
buf.push(' ');
}
buf.push_str("^\n");
}
buf
}
fn source_line_at(source: &str, line: usize, col: usize) -> (String, usize) {
if source.is_empty() || line == 0 {
return (String::new(), col);
}
let mut line_starts = Vec::new();
line_starts.push(0);
for (idx, ch) in source.char_indices() {
if ch == '\n' {
line_starts.push(idx + 1);
}
}
if line > line_starts.len() {
return (String::new(), col);
}
let line_start = line_starts[line - 1];
let rest = &source[line_start..];
let line_end = rest.find('\n').map(|n| line_start + n).unwrap_or(source.len());
let src_line = &source[line_start..line_end];
let effective_col = col.min(src_line.chars().count()).max(1);
(src_line.to_string(), effective_col)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::{ErrorCode, ParseError};
fn make_err() -> ParseError {
ParseError {
code: ErrorCode::E0010,
line: 2,
col: 10,
message: "unexpected token 'x'; expected 'bar'".into(),
help: None,
suggestion: None,
}
}
#[test]
fn format_diagnostic_basic() {
let src = "func Foo():\n steps:\n x = =\n";
let err = make_err();
let out = format_diagnostic(&err, src);
assert!(out.contains("error[E0010]"));
assert!(out.contains("at line 2, col 10"));
assert!(out.contains("2 │"));
}
#[test]
fn format_diagnostic_with_help() {
let mut err = make_err();
err.help = Some("use a single '=' for assignment".into());
let src = "func Foo():\n steps:\n x = =\n";
let out = format_diagnostic(&err, src);
assert!(out.contains("help: use a single '=' for assignment"));
}
#[test]
fn format_diagnostic_with_suggestion() {
let mut err = make_err();
err.suggestion = Some("bar".into());
let src = "func Foo():\n steps:\n x = =\n";
let out = format_diagnostic(&err, src);
assert!(out.contains("suggestion: bar"));
}
#[test]
fn format_diagnostic_empty_source() {
let err = make_err();
let out = format_diagnostic(&err, "");
assert!(out.contains("error[E0010]"));
assert!(!out.contains("│"));
}
#[test]
fn format_diagnostic_col_clamped() {
let err = ParseError {
code: ErrorCode::E0010,
line: 1,
col: 999,
message: "test".into(),
help: None,
suggestion: None,
};
let src = "abc\n";
let out = format_diagnostic(&err, src);
assert!(out.contains("1 │ abc"));
}
}