///|
/// 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()
}