use std::error::Error as ErrorTrait;
use std::fmt::{self, Display};
use colored::{Color, Colorize};
use unicode_width::UnicodeWidthStr;
use crate::cursor::InputCursor;
use crate::lexer::Lexer;
use crate::span::Span;
use crate::symbol::SymbolGroup;
use crate::token::{Token, TokenKind, TokenValue};
struct LineIter<'a> {
s: &'a str,
eof: bool,
}
impl<'a> LineIter<'a> {
fn new(s: &'a str) -> Self {
Self { s, eof: false }
}
}
impl<'a> Iterator for LineIter<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if self.eof {
return None;
}
let mut iter = self.s.chars().peekable();
let mut line = "";
self.eof = true;
while let Some(c) = iter.next() {
match c {
'\r' if iter.peek() == Some(&'\n') => self.s = &self.s[(line.len() + 2)..],
'\n' if iter.peek() == Some(&'\r') => self.s = &self.s[(line.len() + 2)..],
'\r' | '\n' => self.s = &self.s[(line.len() + 1)..],
_ => line = &self.s[..(line.len() + c.len_utf8())],
}
if c == '\r' || c == '\n' {
self.eof = false;
return Some(line);
}
}
self.s = &self.s[line.len()..];
Some(line)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Severity {
Error,
Warning,
}
impl Display for Severity {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Error => "error",
Self::Warning => "warning",
};
if formatter.alternate() {
let s = match self {
Self::Error => s.bright_red(),
Self::Warning => s.yellow(),
}.bold();
write!(formatter, "{}", s)
} else {
write!(formatter, "{}", s)
}
}
}
impl Default for Severity {
fn default() -> Self {
Self::Error
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Error {
span: Span,
severity: Severity,
message: String,
}
impl Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if !formatter.alternate() {
write!(formatter, "{}", self.message)
} else {
write!(
formatter,
"{:#}: {} ({})",
self.severity, self.message, self.span
)
}
}
}
impl ErrorTrait for Error {}
impl Error {
pub fn new(span: Span, message: impl Display) -> Self {
Self {
span,
severity: Default::default(),
message: message.to_string(),
}
}
pub fn with_severity(self, severity: Severity) -> Self {
Self { severity, ..self }
}
pub fn with_buffer<'a>(&'a self, buf: &'a str) -> ErrorPrinter<'a> {
ErrorPrinter { error: &self, buf }
}
pub fn span(&self) -> Span {
self.span
}
pub fn severity(&self) -> Severity {
self.severity
}
}
#[derive(Debug)]
pub struct ErrorPrinter<'a> {
error: &'a Error,
buf: &'a str,
}
impl Display for ErrorPrinter<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if formatter.alternate() {
writeln!(formatter, "{:#}", self.error)?;
display_source_for_span(formatter, self.error.span, self.buf)
} else {
write!(formatter, "{}", self.error)
}
}
}
fn get_color_for_token(token: &Token<'_>) -> Option<Color> {
match token.kind() {
TokenKind::Number => Some(Color::BrightCyan),
TokenKind::String => Some(Color::Yellow),
TokenKind::Comment => Some(Color::BrightGreen),
TokenKind::Whitespace => None,
TokenKind::Ident => None,
TokenKind::Eof => None,
TokenKind::Symbol(s) => match s.group() {
SymbolGroup::Operator => Some(Color::Cyan),
SymbolGroup::Keyword => Some(Color::Magenta),
SymbolGroup::Value => Some(Color::BrightCyan),
},
}
}
fn line_range_to_show(Span { start, end }: Span) -> (u32, u32) {
let lines = end.line - start.line + 1;
match lines {
1 => (start.line.checked_sub(2).unwrap_or(1).max(1), end.line + 2),
2 => ((start.line - 1).max(1), end.line + 2),
_ => ((start.line - 1).max(1), end.line + 1),
}
}
fn start_new_line(f: &mut fmt::Formatter<'_>, current_line: u32, width: usize) -> fmt::Result {
write!(f, " {:>width$} │ ", current_line, width = width)
}
fn print_line_skip(f: &mut fmt::Formatter<'_>, width: usize) -> fmt::Result {
write!(f, "╶{:╴>width$}┼╴", "─", width = width + 1)
}
fn print_blank_margin(f: &mut fmt::Formatter<'_>, width: usize) -> fmt::Result {
write!(f, " {:>width$}", " ", width = width + 1)
}
fn print_first_line(f: &mut fmt::Formatter<'_>, width: usize) -> fmt::Result {
print_blank_margin(f, width)?;
writeln!(f, "┌")
}
fn print_last_line(f: &mut fmt::Formatter<'_>, width: usize) -> fmt::Result {
print_blank_margin(f, width)?;
writeln!(f, "â””")
}
fn print_line_features(
f: &mut fmt::Formatter<'_>,
current_line: u32,
line: &str,
width: usize,
span: Span,
) -> fmt::Result {
if current_line == span.start.line {
let leading_pos: usize = line
.chars()
.take((span.start.col - 1) as usize)
.map(char::len_utf8)
.sum();
let leading_spaces = line[..leading_pos].width();
print_blank_margin(f, width)?;
write!(f, "│ ")?;
if span.start.line == span.end.line {
let n = span.end.col - span.start.col + 1;
let len: usize = line[leading_pos..]
.chars()
.take(n as usize)
.map(char::len_utf8)
.sum();
let width = line[leading_pos..(leading_pos + len)].width();
match width {
0 => writeln!(f, "{:>col$}", "↖", col = leading_spaces + 1)?,
1 => writeln!(f, "{:>col$}", "─", col = leading_spaces + 1)?,
2 => writeln!(f, "{:>col$}┘", "└", col = leading_spaces + 1)?,
_ => writeln!(
f,
"{:>col$}{:─>width$}┘",
"â””",
"─",
col = leading_spaces + 1,
width = width - 2
)?,
}
} else {
let width = line[leading_pos..].width().checked_sub(1);
match width {
None => writeln!(f, "{:>width$}â•¶â•¶", "â””", width = line.width() + 1)?,
Some(0) => writeln!(f, "{:>col$}â•¶â•¶", "â””", col = leading_spaces + 1)?,
Some(w) => writeln!(
f,
"{:>col$}{:─>width$}╶╶",
"â””",
"─",
col = leading_spaces + 1,
width = w
)?,
}
}
} else if current_line == span.end.line {
print_blank_margin(f, width)?;
write!(f, "│╶╶")?;
let len = line.chars().take(span.end.col as usize).map(char::len_utf8).sum();
let width = line[..len].width();
match width {
0 => writeln!(f, "{:>col$}", "┘", col = width)?,
_ => writeln!(f, "{:─>width$}┘", "─", width = width - 1)?,
}
}
Ok(())
}
fn num_digits(n: u32) -> usize {
((n as f64).log10() + 1f64) as usize
}
fn remove_trailing_newline(s: &str) -> &str {
let tail = match s.get((s.len() - 2)..) {
Some(tail) => tail,
None => return s,
};
if tail == "\r\n" || tail == "\n\r" {
&s[..(s.len() - 2)]
} else if &tail[1..] == "\r" || &tail[1..] == "\n" {
&s[..(s.len() - 1)]
} else {
s
}
}
fn display_source_for_span(f: &mut fmt::Formatter<'_>, span: Span, buf: &str) -> fmt::Result {
let buf = remove_trailing_newline(buf);
let mut lines = LineIter::new(buf);
let (start, end) = line_range_to_show(span);
let width = num_digits(end);
let cursor = InputCursor::new(buf);
let mut pos = cursor.pos();
let mut lexer = Lexer::new(cursor);
let mut current_line = 1;
let mut skip_written = false;
let write_skips = end - start + 1 > 5;
print_first_line(f, width)?;
start_new_line(f, start, width)?;
loop {
let token = match lexer.next() {
Some(Err(_)) => break,
None | Some(Ok(Token { value: TokenValue::Eof, .. })) => {
writeln!(f)?;
print_line_features(f, current_line, lines.next().unwrap(), width, span)?;
print_last_line(f, width)?;
return Ok(());
}
Some(Ok(token)) => token,
};
pos = lexer.cursor().pos();
if token.span.end.line < start {
if token.span.end.line != current_line {
for _ in current_line..token.span.end.line {
lines.next();
}
current_line = token.span.end.line;
}
continue;
}
let color = get_color_for_token(&token);
let mut first_line = true;
for (i, line) in
LineIter::new(lexer.cursor().substr(token.span.start..=token.span.end)).enumerate()
{
if i != 0 {
current_line += 1;
}
if current_line < start {
lines.next();
continue;
}
if current_line > start + 1 && current_line < end - 1 && write_skips {
if !skip_written {
writeln!(f)?;
print_line_skip(f, width)?;
skip_written = true;
}
continue;
}
if !first_line {
writeln!(f)?;
}
if current_line > end {
print_line_features(f, current_line - 1, lines.next().unwrap(), width, span)?;
print_last_line(f, width)?;
return Ok(());
}
if !first_line {
print_line_features(f, current_line - 1, lines.next().unwrap(), width, span)?;
start_new_line(f, current_line, width)?;
}
match color {
Some(color) => write!(f, "{}", line.color(color))?,
None => write!(f, "{}", line)?,
}
first_line = false;
}
}
let remaining = lexer.cursor().substr(pos..);
for (i, line) in LineIter::new(remaining).enumerate() {
if current_line < start {
lines.next();
continue;
}
if current_line > end {
break;
}
if current_line > start + 1 && current_line < end - 1 && write_skips {
if !skip_written {
print_line_skip(f, width)?;
skip_written = true;
}
lines.next();
continue;
}
if i != 0 {
print_line_features(f, current_line - 1, lines.next().unwrap(), width, span)?;
start_new_line(f, current_line, width)?;
}
writeln!(f, "{}", line)?;
current_line += 1;
}
print_line_features(f, current_line - 1, lines.next().unwrap(), width, span)?;
print_last_line(f, width)?;
Ok(())
}