use colored::{Colorize, Color};
use pad::PadStr;
use crate::compiling::failing::position_info::PositionInfo;
use crate::compiling::failing::message::MessageType;
use crate::prelude::Position;
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Logger {
kind: MessageType,
trace: Vec<PositionInfo>
}
impl Logger {
pub fn new(kind: MessageType, trace: &[PositionInfo]) -> Self {
Logger {
kind,
trace: trace.to_vec()
}
}
fn kind_to_color(&self) -> Color {
match self.kind {
MessageType::Error => Color::Red,
MessageType::Warning => Color::Yellow,
MessageType::Info => Color::Blue
}
}
pub fn header(self, kind: MessageType) -> Self {
let name = match kind {
MessageType::Error => " ERROR ".to_string(),
MessageType::Warning => " WARN ".to_string(),
MessageType::Info => " INFO ".to_string()
};
let formatted = name
.black()
.bold()
.on_color(self.kind_to_color());
eprint!("{formatted} ");
self
}
pub fn text(self, text: Option<String>) -> Self {
if let Some(text) = text {
eprint!("{}", text.color(self.kind_to_color()));
}
self
}
pub fn line(self, text: Option<String>) -> Self {
if let Some(text) = text {
eprintln!("{}", text.color(self.kind_to_color()));
}
self
}
pub fn padded_line(self, text: Option<String>) -> Self {
if let Some(text) = text {
eprintln!("\n{}", text.color(self.kind_to_color()));
}
self
}
pub fn path(self) -> Self {
let get_row_col = |pos: &PositionInfo| match pos.position {
Position::Pos(row, col) => format!("{}:{}", row, col),
Position::EOF => " end of file".to_string()
};
let path = match self.trace.first() {
Some(pos) => {
[
format!("at {}:{}", pos.get_path(), get_row_col(pos)),
self.trace.iter()
.skip(1)
.map(|pos| format!("in {}:{}", pos.get_path(), get_row_col(pos)))
.collect::<Vec<String>>()
.join("\n")
].join("\n")
},
None => {
"at [unknown]:0:0".to_string()
}
}.trim_end().to_string();
eprintln!("{}", path.color(self.kind_to_color()).dimmed());
self
}
fn get_row_col_len(&self) -> Option<(usize, usize, usize)> {
match self.trace.first() {
Some(pos) => match pos.position {
Position::Pos(row, col) => Some((row, col, pos.len)),
Position::EOF => None
},
None => None
}
}
fn get_max_pad_size(&self, length: usize) -> Option<usize> {
let (row, _, _) = self.get_row_col_len()?;
if row < length - 1 {
Some(format!("{}", row + 1).len())
}
else {
Some(format!("{}", row).len())
}
}
fn get_highlighted_part(&self, line: &str) -> Option<[String;3]> {
let (_row, col, len) = self.get_row_col_len()?;
let begin = col - 1;
let end = begin + len;
let mut results: [String; 3] = Default::default();
for (index, letter) in line.chars().enumerate() {
if index < begin {
results[0].push(letter);
}
else if index >= end {
results[2].push(letter);
}
else {
results[1].push(letter);
}
}
Some(results)
}
fn get_snippet_row(&self, code: &Vec<String>, index: usize, offset: i8, overflow: &mut usize) -> Option<String> {
let (row, col, len) = self.get_row_col_len()?;
let max_pad = self.get_max_pad_size(code.len())?;
let index = index as i32 + offset as i32;
let row = row as i32 + offset as i32;
let code = code.get(index as usize)?.clone();
let line = format!("{row}").pad_to_width(max_pad);
if offset == 0 {
let slices = self.get_highlighted_part(&code)?;
let formatted = format!("{}{}{}", slices[0], slices[1].color(self.kind_to_color()), slices[2]);
let end = col.checked_add(len).unwrap_or(len);
if end - 1 > code.chars().count() {
*overflow = (end - 2).checked_sub(code.chars().count()).unwrap_or(0);
}
Some(format!("{line}| {formatted}"))
}
else {
if *overflow > 0 {
if *overflow > code.chars().count() {
Some(format!("{line}| {}", code.color(self.kind_to_color())).dimmed().to_string())
}
else {
let err = code.get(0..*overflow).unwrap().to_string().color(self.kind_to_color());
let rest = code.get(*overflow..).unwrap().to_string();
Some(format!("{line}| {err}{rest}").dimmed().to_string())
}
}
else {
Some(format!("{line}| {code}").dimmed().to_string())
}
}
}
pub fn snippet<T: AsRef<str>>(self, code: Option<T>) -> Self {
if let Some(pos) = self.trace.first() {
if let Ok(code) = std::fs::read_to_string(pos.get_path()) {
self.snippet_from_code(code);
return self;
}
}
if let Some(code) = code {
self.snippet_from_code(code.as_ref().to_string());
}
self
}
fn snippet_from_code(&self, code: String) -> Option<()> {
let (row, _, _) = self.get_row_col_len()?;
let mut overflow = 0;
let index = row - 1;
let code = code.split('\n')
.map(|item| item.trim_end().to_string())
.collect::<Vec<String>>();
eprintln!();
if let Some(line) = self.get_snippet_row(&code, index, -1, &mut overflow) {
eprintln!("{}", line);
}
eprintln!("{}", self.get_snippet_row(&code, index, 0, &mut overflow)?);
eprintln!("{}", self.get_snippet_row(&code, index, 1, &mut overflow)?);
Some(())
}
}
#[cfg(test)]
mod test {
#![allow(unused_imports)]
use std::time::Duration;
use std::thread::sleep;
use crate::prelude::{DefaultMetadata, Metadata, MessageType, PositionInfo, Token};
#[allow(unused_variables)]
#[test]
fn test_displayer() {
let code = vec![
"let a = 12",
"value = 'this",
"is mutltiline",
"code'"
].join("\n");
sleep(Duration::from_secs(1));
let trace = [
PositionInfo::at_pos(Some("/path/to/bar".to_string()), (3, 4), 10, 10),
PositionInfo::at_pos(Some("/path/to/foo".to_string()), (2, 9), 24, 24),
];
super::Logger::new(MessageType::Error, &trace)
.header(MessageType::Error)
.line(Some(format!("Cannot call function \"foobar\" on a number")))
.path()
.snippet(Some(code));
}
#[test]
fn test_end_of_line_displayer() {
let code = vec![
"hello"
].join("\n");
sleep(Duration::from_secs(1));
let trace = [
PositionInfo::at_pos(Some("/path/to/foo".to_string()), (2, 6), 5, 1)
];
super::Logger::new(MessageType::Error, &trace)
.header(MessageType::Error)
.line(Some(format!("Cannot call function \"foobar\" on a number")))
.path()
.snippet(Some(code));
}
#[test]
fn test_between_tokens() {
let code = vec![
"foo(12 + 24)"
].join("\n");
sleep(Duration::from_secs(1));
let begin = Token { word: "12".to_string(), pos: (1, 5), start: 4 };
let end = Token { word: ")".to_string(), pos: (1, 12), start: 11 };
let mut meta = DefaultMetadata::new(vec![], Some("/path/to/foo".to_string()), Some(code.clone()));
let trace = [
PositionInfo::from_between_tokens(&mut meta, Some(begin), Some(end))
];
super::Logger::new(MessageType::Error, &trace)
.header(MessageType::Error)
.line(Some(format!("Cannot call function \"foobar\" on a number")))
.path()
.snippet(Some(code));
}
}