///|
/// Formats a parse error with a source snippet and caret marker.
///
/// Returns a multi-line message like:
///
/// ```text
/// parse error at 1:2: expected '\n'
///
/// 1 | hello world
/// | ^
/// ```
pub fn format_parse_error(err : @talc.ParseError, source : String) -> String {
let line_no = err.line()
let col = err.column()
let detail = error_detail(err)
let lines = source.split("\n").to_array()
let source_line = {
guard line_no >= 1 && line_no <= lines.length() else { "" }
lines[line_no - 1].to_owned()
}
let label = line_no.to_string()
let gutter = " ".repeat(label.length())
let sb = StringBuilder()
sb <+ "parse error at \{line_no}:\{col}: \{detail}"
if !source_line.is_empty() {
sb.write_string("\n\n")
sb <+ "\{gutter} |"
sb.write_string("\n")
sb <+ "\{label} | \{source_line}"
sb.write_string("\n")
sb <+ "\{gutter} | \{caret_marker(source_line, col)}"
}
sb.to_string()
}
///|
fn error_detail(err : @talc.ParseError) -> String {
let message = err.message_text()
if !message.is_empty() {
return message
}
let expected = err.expected()
if expected.is_empty() {
return "unexpected token"
}
"expected \{expected.join(" or ")}"
}
///|
/// One space per column before the error, then `^`.
fn caret_marker(line : String, col : Int) -> String {
let sb = StringBuilder()
let mut count = 0
for _ in line.iter() {
if count >= col - 1 {
break
}
sb.write_char(' ')
count = count + 1
}
sb.write_char('^')
sb.to_string()
}