use std::{ fmt::{Debug, Display}, rc::Rc, sync::{atomic::AtomicUsize, Arc}, }; use colored::{ColoredString, Colorize}; use line_span::LineSpans; #[cfg(feature = "parse")] use crate::parsing::Source; #[derive(Clone, Copy, Debug)] pub struct SourcePos(pub(crate) usize); impl SourcePos { pub fn pos(&self) -> usize { self.0 } } #[derive(Clone, Debug)] pub struct SourceRange { in_file: Arc, start: SourcePos, end: SourcePos, } impl From<(SourcePos, SourcePos, &Arc)> for SourceRange { fn from(value: (SourcePos, SourcePos, &Arc)) -> Self { SourceRange { in_file: Arc::clone(value.2), start: value.0, end: value.1, } } } impl SourceRange { pub fn start(&self) -> SourcePos { self.start } pub fn end(&self) -> SourcePos { self.end } pub fn in_file(&self) -> &Arc { &self.in_file } } #[derive(Clone)] pub struct CheckError(pub Vec); #[allow(non_upper_case_globals)] pub mod error_colors { use colored::Color; pub const UnknownVariable: Color = Color::Red; pub const WhitespaceAfterHashtag: Color = Color::Red; pub const HashUnknown: Color = Color::Red; pub const HashIncludeCantLoadFile: Color = Color::Red; pub const HashIncludeNotAString: Color = Color::Red; pub const HashIncludeErrorInIncludedFile: Color = Color::Red; pub const BackslashEscapeUnknown: Color = Color::Red; pub const BackslashEscapeEOF: Color = Color::Red; pub const StringEOF: Color = Color::Red; pub const IfConditionNotBool: Color = Color::Red; pub const ChainWithNonFunction: Color = Color::Yellow; pub const Function: Color = Color::BrightMagenta; pub const FunctionArgument: Color = Color::BrightBlue; pub const InitFrom: Color = Color::BrightCyan; pub const InitTo: Color = Color::Green; pub const AssignFrom: Color = InitFrom; pub const AssignTo: Color = InitTo; pub const AssignTargetNonReference: Color = Color::BrightYellow; pub const AsTypeStatementWithTooBroadType: Color = InitFrom; pub const AsTypeTypeAnnotation: Color = InitTo; pub const BadCharInTupleType: Color = Color::Red; pub const BadTypeFromParsed: Color = Color::Blue; pub const TypeAnnotationNoClosingBracket: Color = Color::Blue; } #[derive(Clone)] pub enum CheckErrorComponent { Message(String), Error(CheckError), ErrorWithDifferentSource(CheckError), Source(Vec<(SourceRange, Option)>), } #[derive(Clone)] pub struct CheckErrorHRConfig { color_index: Rc, indent_start: String, indent_default: String, indent_end: String, /// if true, shows "original" source code, if false, shows source with comments removed (this is what the parser uses internally) show_comments: bool, } #[cfg(feature = "parse")] pub struct CheckErrorDisplay<'a> { e: &'a CheckError, pub show_comments: bool, } #[cfg(feature = "parse")] impl<'a> CheckErrorDisplay<'a> { pub fn show_comments(mut self, show_comments: bool) -> Self { self.show_comments = show_comments; self } } #[cfg(feature = "parse")] impl Display for CheckErrorDisplay<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.e.human_readable( f, &CheckErrorHRConfig { color_index: Rc::new(AtomicUsize::new(0)), indent_start: String::new(), indent_default: String::new(), indent_end: String::new(), show_comments: self.show_comments, }, ) } } impl CheckError { pub fn new() -> Self { CheckError(vec![]) } fn add(mut self, v: CheckErrorComponent) -> Self { self.0.push(v); self } pub(crate) fn msg(self, s: String) -> Self { self.add(CheckErrorComponent::Message(s)) } pub(crate) fn err(self, e: Self) -> Self { self.add(CheckErrorComponent::Error(e)) } pub(crate) fn err_with_diff_src(self, e: CheckError) -> Self { self.add(CheckErrorComponent::ErrorWithDifferentSource(e)) } pub(crate) fn src(self, s: Vec<(SourceRange, Option)>) -> Self { self.add(CheckErrorComponent::Source(s)) } #[cfg(feature = "parse")] pub fn display<'a>(&'a self) -> CheckErrorDisplay<'a> { CheckErrorDisplay { e: self, show_comments: true, } } /// will, unless empty, end in a newline #[cfg(feature = "parse")] fn human_readable( &self, f: &mut std::fmt::Formatter<'_>, cfg: &CheckErrorHRConfig, ) -> std::fmt::Result { use crate::parsing::SourceFrom; let len = self.0.len(); for (i, component) in self.0.iter().enumerate() { macro_rules! indent { ($s:expr, $e:expr) => { if $e && i + 1 == len { &cfg.indent_end } else if $s && i == 0 { &cfg.indent_start } else { &cfg.indent_default } }; } match component { CheckErrorComponent::Message(msg) => { let lines = msg.lines().collect::>(); let lc = lines.len(); for (i, line) in lines.into_iter().enumerate() { writeln!(f, "{}{line}", indent!(i == 0, i + 1 == lc))? } } CheckErrorComponent::Error(err) => { let clr = Self::get_color(&cfg.color_index); let mut cfg = cfg.clone(); cfg.indent_start.push_str(&clr("│").to_string()); cfg.indent_default.push_str(&clr("│").to_string()); cfg.indent_end.push_str(&clr("└").to_string()); err.human_readable(f, &cfg)?; } CheckErrorComponent::ErrorWithDifferentSource(err) => { let clr = Self::get_color(&cfg.color_index); let mut cfg = cfg.clone(); cfg.indent_start.push_str(&clr("┃").bold().to_string()); cfg.indent_default.push_str(&clr("┃").bold().to_string()); cfg.indent_end.push_str(&clr("┗").bold().to_string()); err.human_readable(f, &cfg)?; } CheckErrorComponent::Source(highlights) => { if let Some(src) = highlights.first().map(|v| v.0.in_file.as_ref()) { let start = highlights.iter().map(|v| v.0.start.pos()).min(); let end = highlights.iter().map(|v| v.0.end.pos()).max(); if let (Some(start_in_line), Some(end_in_line)) = (start, end) { let start = src.get_line_start(start_in_line); let end = src.get_line_end(end_in_line); let (start_with_comments, end_with_comments) = ( src.pos_in_og(start_in_line, true), src.pos_in_og(end_in_line, false), ); let (mut start, mut end) = if cfg.show_comments { (src.pos_in_og(start, true), src.pos_in_og(end, false)) } else { (start, end) }; let mut first_line_start = 0; let first_line_nr = src .src_og() .line_spans() .take_while(|l| { if l.start() <= start_with_comments { first_line_start = l.start(); true } else { false } }) .count(); if cfg.show_comments && first_line_start < start { start = first_line_start; } let mut last_line_start = 0; let last_line_nr = src .src_og() .line_spans() .take_while(|l| { if l.start() <= end_with_comments { last_line_start = l.start(); if cfg.show_comments && l.end() > end { end = l.end(); } true } else { false } }) .count(); let src_from = match src.src_from() { SourceFrom::File(path) => format!(" [{}]", path.to_string_lossy()), SourceFrom::Unspecified => String::with_capacity(0), }; if first_line_nr == last_line_nr { writeln!( f, "{}Line {first_line_nr} ({}..{}){}", indent!(true, false), start_with_comments + 1 - first_line_start, end_with_comments - last_line_start, src_from, )?; } else { writeln!( f, "{}Lines {first_line_nr}-{last_line_nr} ({}..{}){}", indent!(true, false), start_with_comments + 1 - first_line_start, end_with_comments - last_line_start, src_from, )?; } let lines = if cfg.show_comments { src.src_og()[start..end].line_spans().collect::>() } else { src.src()[start..end].line_spans().collect::>() }; let lines_count = lines.len(); for (line_nr_rel, line) in lines.into_iter().enumerate() { let last_line = line_nr_rel + 1 == lines_count; let line_start = line.start(); let line_end = line.end(); let line = line.as_str(); let mut line_printed = false; let mut right = 0; for (pos, color) in highlights { if let Some(color) = color { let (highlight_start, highlight_end) = if cfg.show_comments { ( src.pos_in_og(pos.start.pos(), true), src.pos_in_og(pos.end.pos(), false), ) } else { (pos.start.pos(), pos.end.pos()) }; let highlight_start = highlight_start - start; let highlight_end = highlight_end - start; if highlight_start < line_end && highlight_end > line_start { if !line_printed { // this isn't the last line (important for indent) writeln!(f, "{} {line}", indent!(false, false))?; line_printed = true; } // where the highlight starts in this line let hl_start = highlight_start.saturating_sub(line_start); // highlight would be further left than cursor, so we need a new line if hl_start < right { right = 0; writeln!(f)?; } // length of the highlight let hl_len = highlight_end .saturating_sub(line_start) .saturating_sub(hl_start); let hl_space = hl_start - right; let print_indent = right == 0; let hl_len = hl_len.min(line.len() - right); right += hl_space + hl_len; if print_indent && right != 0 { write!(f, "{} ", indent!(false, false))?; } write!( f, "{}{}", " ".repeat(hl_space), "~".repeat(hl_len).color(*color) )?; } } } if !line_printed { // may be last line (important for indent) writeln!(f, "{} {line}", indent!(false, last_line))?; } if right != 0 { writeln!(f)?; } } } } } } } Ok(()) } fn get_color(i: &AtomicUsize) -> impl Fn(&str) -> ColoredString { let i = i.fetch_add(1, std::sync::atomic::Ordering::Relaxed); move |s| match i % 8 { 0 => s.bright_white(), 1 => s.bright_green(), 2 => s.bright_purple(), 3 => s.bright_cyan(), 4 => s.bright_red(), 5 => s.bright_yellow(), 6 => s.bright_magenta(), _ => s.bright_blue(), } } } impl From for CheckError { fn from(value: String) -> Self { Self::new().msg(value) } } impl Debug for CheckError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{self}") } } impl Display for CheckError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.display()) } }