pub fn line_col_at(source: &str, byte_offset: usize) -> (usize, usize) {
let mut line = 1usize;
let mut col = 1usize;
for (idx, ch) in source.char_indices() {
if idx >= byte_offset {
break;
}
if ch == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
fn line_text(source: &str, line_number: usize) -> Option<&str> {
source.lines().nth(line_number.saturating_sub(1))
}
pub fn render_parse_error(
source: &str,
line: usize,
column: usize,
width: usize,
message: &str,
suggestion: Option<&str>,
) -> String {
let caret_width = width.max(1);
let gutter_width = line.to_string().len();
let gutter = " ".repeat(gutter_width);
let mut out = String::new();
out.push_str(&format!("SFC parse error at {line}:{column}: {message}\n"));
match line_text(source, line) {
Some(text) => {
out.push_str(&format!("{gutter} |\n"));
out.push_str(&format!("{line} | {text}\n"));
let pad_chars = column.saturating_sub(1);
let text_chars = text.chars().count();
let underline_len = if pad_chars > text_chars {
(text_chars.saturating_sub(pad_chars)).max(1)
} else {
usize::min(caret_width, text_chars.saturating_sub(pad_chars).max(1))
};
out.push_str(&format!("{gutter} | {}{}", " ".repeat(pad_chars), "^"));
for _ in 1..underline_len {
out.push('^');
}
out.push('\n');
}
None => {
out.push_str(&format!("{gutter} |\n"));
out.push_str(&format!("{line}| <end of input>\n"));
}
}
if let Some(help) = suggestion {
out.push_str(&format!("{gutter} |\n"));
out.push_str(&format!("{gutter} = help: {help}\n"));
}
out
}