use core::fmt; use thiserror::Error; use crate::{ lexing::{error::SpannedLexingError, TokenSpan}, parsing::checked::error::SpannedParsingError, }; #[derive(Error, Debug)] pub enum TrixyError { #[error(transparent)] Lexing(#[from] SpannedLexingError), #[error(transparent)] Parsing(#[from] SpannedParsingError), } /// The context of an Error. #[derive(Debug, Clone)] pub struct ErrorContext { /// The span of the error in the source file pub span: TokenSpan, /// The span of the error in the context line relative to the context line pub contexted_span: TokenSpan, /// The line above the error pub line_above: String, /// The line below the error pub line_below: String, /// The line in which the error occurred pub line: String, /// The line number of the main error line pub line_number: usize, } impl ErrorContext { pub fn from_span(span: TokenSpan, original_file: &str) -> Self { let line_number = original_file .chars() .take(span.start) .filter(|a| a == &'\n') .count() // This is here, as we are missing one newline with the method above + 1; let lines: Vec<_> = original_file.lines().collect(); let line = (*lines .get(line_number - 1) .expect("This should work, as have *at least* one (index = 0) line")) .to_owned(); let contexted_span = { let matched_line: Vec<_> = original_file.match_indices(&line).collect(); let (index, matched_line) = matched_line.first().expect("This first index should always match, as we took the line from the string in the first place"); debug_assert_eq!(matched_line, &&line); TokenSpan { start: span.start - index, end: span.end - index, } }; let line_above = if line_number == 1 { // We only have one line, so no line above "".to_owned() } else { (*lines .get((line_number - 1) - 1) .expect("We checked that this should work")) .to_owned() }; let line_below = if lines.len() - 1 > line_number { // We have a line after the current line (*lines .get((line_number + 1) - 1) .expect("We checked that this should work")) .to_owned() } else { "".to_owned() }; Self { span, contexted_span, line_above, line_below, line, line_number, } } pub fn from_index(start: usize, orginal_file: &str) -> Self { let span = TokenSpan { start, end: start + 1, }; Self::from_span(span, orginal_file) } pub fn get_error_line(&self, source_error: &str) -> String { // deconstruct the structure let ErrorContext { contexted_span, line_number, .. } = self; let mut output = String::new(); output.push_str("\x1b[92;1m"); // pad to accommodate the line number printing. // 32 -> needs two spaces padding to print it line_number.to_string().chars().for_each(|_| { output.push(' '); }); // pad to the beginning of the error for _ in 0..contexted_span.start { output.push(' '); } // push the error markers for _ in contexted_span.start..contexted_span.end { output.push('^'); } // // pad until end of line // for _ in contexted_span.end..(line.len() - 1) { // output.push('-'); // } // // additional space to avoid having to end with a '-' output.push(' '); output.push_str("help: "); output.push_str(source_error); output.push_str("\x1b[0m"); output } } pub trait AdditionalHelp { fn additional_help(&self) -> String; } pub trait ErrorContextDisplay: fmt::Display { type Error; fn error_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result where ::Error: std::fmt::Display + AdditionalHelp, { let error_line = self .context() .get_error_line(&self.source().additional_help()); writeln!(f, "\x1b[31;1merror: \x1b[37;1m{}\x1b[0m", self.source())?; if !self.line_above().is_empty() { writeln!( f, "\x1b[32;1m{} |\x1b[0m {}", self.line_number() - 1, self.line_above() )?; } writeln!( f, "\x1b[36;1m{} |\x1b[0m {}", self.line_number(), self.line() )?; writeln!(f, " {}", error_line)?; if !self.line_below().is_empty() { writeln!( f, "\x1b[32;1m{} |\x1b[0m {}", self.line_number() + 1, self.line_below() ) } else { write!(f, "") } } // getters fn context(&self) -> &ErrorContext; fn source(&self) -> &Self::Error; fn line_number(&self) -> usize; fn line_above(&self) -> &str; fn line_below(&self) -> &str; fn line(&self) -> &str; }