use codespan_reporting::{ diagnostic::{Label, LabelStyle, Severity}, files::SimpleFiles, }; use ecow::{EcoString, eco_format}; use termcolor::Buffer; use crate::{lexer::Span, parser::Error as ParseError}; pub enum Error { Parse { src: EcoString, path: EcoString, error: ParseError, }, } macro_rules! wrap_format { ($($tts:tt)*) => { wrap(&format!($($tts)+)) }; } impl Error { pub fn write_to(&self, buffer: &mut Buffer) { self.to_diagnostic().write(buffer); } pub fn to_diagnostic(&self) -> Diagnostic { match self { Error::Parse { src, path, error } => match error { ParseError::LexError(errors) => { let (title, text, location) = errors[0].to_diagnostic_info(); Diagnostic { level: Level::Error, title, text, label: None, location, src: src.clone(), path: path.clone(), } } ParseError::UnexpectedEndOfFile { expected } => Diagnostic { level: Level::Error, title: "Unexpected end of file".into(), text: wrap_format!("Unexpected end of file, {}", one_of(expected)), label: None, location: Span::new(src.len() - 1, src.len()), src: src.clone(), path: path.clone(), }, ParseError::UnexpectedToken { token, expected } => Diagnostic { level: Level::Error, title: "Unexpected token".into(), text: wrap_format!("Unexpected token, {}", one_of(expected)), label: None, location: token.location, src: src.clone(), path: path.clone(), }, ParseError::InvalidEscapeSequence { location } => Diagnostic { level: Level::Error, title: "Invalid escape sequence".into(), text: "".into(), label: None, location: *location, src: src.clone(), path: path.clone(), }, }, } } } fn one_of(expected: &[&str]) -> EcoString { if expected.len() == 1 { eco_format!("expected {}", expected[0]) } else { eco_format!("expected one of:\n {}", expected.join(", ")) } } pub fn wrap(text: &str) -> String { let mut result = String::with_capacity(text.len()); for (i, line) in wrap_text(text, 75).iter().enumerate() { if i > 0 { result.push('\n'); } result.push_str(line); } result } fn wrap_text(text: &str, width: usize) -> Vec { let mut lines = Vec::new(); for line in text.split('\n') { // check if line needs to be broken match line.len() > width { false => lines.push(line.into()), true => { let mut new_lines = break_line(line, width); lines.append(&mut new_lines); } }; } lines } fn break_line(line: &str, width: usize) -> Vec { let mut lines = Vec::new(); let mut newline = String::from(""); // split line by spaces for (i, word) in line.split(' ').enumerate() { let is_new_line = i < 1 || newline.is_empty(); let can_add_word = match is_new_line { true => newline.len() + word.len() <= width, // +1 accounts for space added before word false => newline.len() + (word.len() + 1) <= width, }; if can_add_word { if !is_new_line { newline.push(' '); } newline.push_str(word); } else { // word too big, save existing line if present if !newline.is_empty() { // save current line and reset it lines.push(newline.as_str().into()); newline.clear(); } // then save word to a new line or break it match word.len() > width { false => newline.push_str(word), true => { let (mut newlines, remainder) = break_word(word, width); lines.append(&mut newlines); newline.push_str(remainder); } } } } // save last line after loop finishes if !newline.is_empty() { lines.push(newline.into()); } lines } // breaks word into n lines based on width. Returns list of new lines and remainder fn break_word(word: &str, width: usize) -> (Vec, &str) { let mut new_lines = Vec::new(); let (first, mut remainder) = word.split_at(width); new_lines.push(first.into()); // split remainder until it's small enough while remainder.len() > width { let (first, second) = remainder.split_at(width); new_lines.push(first.into()); remainder = second; } (new_lines, remainder) } pub enum Level { Error, Warning, } pub struct Diagnostic { level: Level, title: String, text: String, label: Option, location: Span, src: EcoString, path: EcoString, } impl Diagnostic { pub fn write(&self, buffer: &mut Buffer) { let mut files = SimpleFiles::new(); let main_file_id = files.add(&self.path, &self.src); let mut label = Label::new( LabelStyle::Primary, main_file_id, (self.location.start)..(self.location.end), ); match &self.label { None => {} Some(text) => label = label.with_message(text.clone()), } let severity = match self.level { Level::Error => Severity::Error, Level::Warning => Severity::Warning, }; let diagnostic = codespan_reporting::diagnostic::Diagnostic::new(severity) .with_message(&self.title) .with_labels(vec![label]); let config = codespan_reporting::term::Config::default(); codespan_reporting::term::emit(buffer, &config, &files, &diagnostic) .expect("write_diagnostic"); if !self.text.is_empty() { use std::io::Write; writeln!(buffer, "{}", self.text).expect("write text"); } } }