From b20c8eac6854e7e3600313ac8972c513fa8ad921 Mon Sep 17 00:00:00 2001 From: Sofia R Date: Sat, 29 Apr 2023 22:05:48 -0300 Subject: [PATCH] refactor: a little bit of refactor in order to make it easier to add new types of error messages --- Cargo.lock | 30 +- crates/kind-cli/src/main.rs | 14 +- crates/kind-driver/src/lib.rs | 2 +- crates/kind-report/src/data.rs | 39 +- crates/kind-report/src/lib.rs | 4 + crates/kind-report/src/report.rs | 552 ------------------ crates/kind-report/src/report/code.rs | 107 ++++ crates/kind-report/src/report/mod.rs | 5 + crates/kind-report/src/report/mode/classic.rs | 504 ++++++++++++++++ crates/kind-report/src/report/mode/mod.rs | 47 ++ crates/kind-tests/tests/mod.rs | 4 +- 11 files changed, 730 insertions(+), 578 deletions(-) delete mode 100644 crates/kind-report/src/report.rs create mode 100644 crates/kind-report/src/report/code.rs create mode 100644 crates/kind-report/src/report/mod.rs create mode 100644 crates/kind-report/src/report/mode/classic.rs create mode 100644 crates/kind-report/src/report/mode/mod.rs diff --git a/Cargo.lock b/Cargo.lock index e2966437..a1d557d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -500,9 +500,9 @@ checksum = "809e18805660d7b6b2e2b9f316a5099521b5998d5cba4dda11b5157a21aaef03" [[package]] name = "hvm" -version = "1.0.3" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1fc0af0e1b783f072678cd4b557bee147e5c8653930778f5c0c333e6ac0849" +checksum = "35672d6ee046e8ebd6373a48aad5b926b4920cee27cb4f45e74643e7388c7a9e" dependencies = [ "HOPA", "backtrace", @@ -631,7 +631,7 @@ dependencies = [ [[package]] name = "kind-checker" -version = "0.1.1" +version = "0.1.2" dependencies = [ "fxhash", "hvm", @@ -643,7 +643,7 @@ dependencies = [ [[package]] name = "kind-derive" -version = "0.1.1" +version = "0.1.2" dependencies = [ "fxhash", "im-rc", @@ -654,7 +654,7 @@ dependencies = [ [[package]] name = "kind-driver" -version = "0.1.1" +version = "0.1.2" dependencies = [ "anyhow", "dashmap", @@ -673,7 +673,7 @@ dependencies = [ [[package]] name = "kind-parser" -version = "0.1.1" +version = "0.1.2" dependencies = [ "fxhash", "kind-report", @@ -683,7 +683,7 @@ dependencies = [ [[package]] name = "kind-pass" -version = "0.1.1" +version = "0.1.2" dependencies = [ "anyhow", "fxhash", @@ -697,7 +697,7 @@ dependencies = [ [[package]] name = "kind-query" -version = "0.1.1" +version = "0.1.2" dependencies = [ "fxhash", "kind-checker", @@ -712,7 +712,7 @@ dependencies = [ [[package]] name = "kind-report" -version = "0.1.1" +version = "0.1.2" dependencies = [ "fxhash", "kind-span", @@ -724,11 +724,11 @@ dependencies = [ [[package]] name = "kind-span" -version = "0.1.1" +version = "0.1.2" [[package]] name = "kind-target-hvm" -version = "0.1.1" +version = "0.1.2" dependencies = [ "hvm", "kind-derive", @@ -739,7 +739,7 @@ dependencies = [ [[package]] name = "kind-target-kdl" -version = "0.1.1" +version = "0.1.2" dependencies = [ "fxhash", "im-rc", @@ -754,7 +754,7 @@ dependencies = [ [[package]] name = "kind-tests" -version = "0.1.1" +version = "0.1.2" dependencies = [ "kind-checker", "kind-driver", @@ -772,7 +772,7 @@ dependencies = [ [[package]] name = "kind-tree" -version = "0.1.1" +version = "0.1.2" dependencies = [ "fxhash", "hvm", @@ -782,7 +782,7 @@ dependencies = [ [[package]] name = "kind2" -version = "0.3.7" +version = "0.3.9" dependencies = [ "anyhow", "clap 4.0.29", diff --git a/crates/kind-cli/src/main.rs b/crates/kind-cli/src/main.rs index ed3703a6..fd2957ef 100644 --- a/crates/kind-cli/src/main.rs +++ b/crates/kind-cli/src/main.rs @@ -6,11 +6,11 @@ use clap::{Parser, Subcommand}; use driver::resolution::ResolutionError; use kind_driver::session::Session; -use kind_report::data::{Diagnostic, Log, Severity}; -use kind_report::report::{FileCache, Report}; +use kind_report::data::{Diagnostic, Log, Severity, FileCache}; use kind_report::RenderConfig; use kind_driver as driver; +use kind_report::report::mode::{Renderable, Classic}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -122,16 +122,16 @@ where } } -pub fn render_to_stderr(render_config: &RenderConfig, session: &T, err: &E) +pub fn render_to_stderr(render_config: &RenderConfig, session: &T, err: &E) where T: FileCache, - E: Report, + E: Renderable, { - Report::render( + E::render( err, + &mut ToWriteFmt(std::io::stderr()), session, render_config, - &mut ToWriteFmt(std::io::stderr()), ) .unwrap(); } @@ -168,7 +168,7 @@ pub fn compile_in_session( contains_error = true; } - render_to_stderr(render_config, &session, &diagnostic) + render_to_stderr::(render_config, &session, &diagnostic) } if !contains_error { diff --git a/crates/kind-driver/src/lib.rs b/crates/kind-driver/src/lib.rs index edc8fd8d..0efb3d81 100644 --- a/crates/kind-driver/src/lib.rs +++ b/crates/kind-driver/src/lib.rs @@ -1,7 +1,7 @@ use checker::eval; use diagnostic::{DriverDiagnostic, GenericDriverError}; use kind_pass::{desugar, erasure, inline::inline_book}; -use kind_report::report::FileCache; +use kind_report::data::FileCache; use kind_span::SyntaxCtxIndex; use hvm::language::{syntax as backend}; diff --git a/crates/kind-report/src/data.rs b/crates/kind-report/src/data.rs index 1e8298cd..99a180c5 100644 --- a/crates/kind-report/src/data.rs +++ b/crates/kind-report/src/data.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::{time::Duration, path::PathBuf, ops::Sub}; use kind_span::{Range, SyntaxCtxIndex}; @@ -52,6 +52,18 @@ pub struct DiagnosticFrame { pub hints: Vec, pub positions: Vec, } + +pub struct Hints<'a>(pub &'a Vec); + +pub struct Subtitles<'a>(pub &'a Vec); + +pub struct Markers<'a>(pub &'a Vec); + +pub struct Header<'a> { + pub severity: &'a Severity, + pub title: &'a String +} + pub enum Log { Checking(String), Checked(Duration), @@ -64,3 +76,28 @@ pub trait Diagnostic { fn get_severity(&self) -> Severity; fn to_diagnostic_frame(&self) -> DiagnosticFrame; } + +pub trait FileCache { + fn fetch(&self, ctx: SyntaxCtxIndex) -> Option<(PathBuf, &String)>; +} + +impl DiagnosticFrame { + pub fn subtitles(&self) -> Subtitles { + Subtitles(&self.subtitles) + } + + pub fn hints(&self) -> Hints { + Hints(&self.hints) + } + + pub fn header(&self) -> Header { + Header { + severity: &self.severity, + title: &self.title + } + } + + pub fn markers(&self) -> Markers { + Markers(&self.positions) + } +} \ No newline at end of file diff --git a/crates/kind-report/src/lib.rs b/crates/kind-report/src/lib.rs index 73ed7da5..62dea92c 100644 --- a/crates/kind-report/src/lib.rs +++ b/crates/kind-report/src/lib.rs @@ -2,6 +2,7 @@ use yansi::Paint; /// Data structures pub mod data; + /// Render pub mod report; @@ -30,6 +31,7 @@ impl Chars { bullet: '•', } } + pub fn ascii() -> &'static Chars { &Chars { vbar: '|', @@ -48,6 +50,7 @@ impl Chars { pub struct RenderConfig<'a> { pub chars: &'a Chars, pub indent: usize, + } impl<'a> RenderConfig<'a> { @@ -57,6 +60,7 @@ impl<'a> RenderConfig<'a> { indent, } } + pub fn ascii(indent: usize) -> RenderConfig<'a> { RenderConfig { chars: Chars::ascii(), diff --git a/crates/kind-report/src/report.rs b/crates/kind-report/src/report.rs deleted file mode 100644 index 1bf12eb4..00000000 --- a/crates/kind-report/src/report.rs +++ /dev/null @@ -1,552 +0,0 @@ -//! Renders error messages. - -// The code is not so good .. -// pretty printers are always a disaster to write. expect -// that in the future i can rewrite it in a better way. - -use std::fmt::{Display, Write}; -use std::path::{Path, PathBuf}; - -use std::str; - -use fxhash::{FxHashMap, FxHashSet}; -use kind_span::{Pos, SyntaxCtxIndex}; -use unicode_width::UnicodeWidthStr; -use yansi::Paint; - -use crate::{data::*, RenderConfig}; - -type SortedMarkers = FxHashMap>; - -#[derive(Debug, Clone)] -struct Point { - pub line: usize, - pub column: usize, -} - -pub trait FileCache { - fn fetch(&self, ctx: SyntaxCtxIndex) -> Option<(PathBuf, &String)>; -} - -impl Display for Point { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{}", self.line + 1, self.column + 1) - } -} - -fn count_width(str: &str) -> (usize, usize) { - (UnicodeWidthStr::width(str), str.chars().filter(|x| *x == '\t').count()) -} - -fn group_markers(markers: &[Marker]) -> SortedMarkers { - let mut file_group = SortedMarkers::default(); - for marker in markers { - let group = file_group - .entry(marker.position.ctx) - .or_insert_with(Vec::new); - group.push(marker.clone()) - } - for group in file_group.values_mut() { - group.sort_by(|x, y| x.position.start.cmp(&y.position.end)); - } - file_group -} - -fn get_code_line_guide(code: &str) -> Vec { - let mut guide = Vec::new(); - let mut size = 0; - for chr in code.chars() { - size += chr.len_utf8(); - if chr == '\n' { - guide.push(size); - } - } - guide.push(code.len()); - guide -} - -fn find_in_line_guide(pos: Pos, guide: &Vec) -> Point { - for i in 0..guide.len() { - if guide[i] > pos.index as usize { - return Point { - line: i, - column: pos.index as usize - (if i == 0 { 0 } else { guide[i - 1] }), - }; - } - } - let line = guide.len() - 1; - Point { - line, - column: pos.index as usize - (if line == 0 { 0 } else { guide[line - 1] }), - } -} - -// Get color -fn get_colorizer(color: &Color) -> &dyn Fn(T) -> Paint { - match color { - Color::Fst => &|str| yansi::Paint::red(str).bold(), - Color::Snd => &|str| yansi::Paint::blue(str).bold(), - Color::Thr => &|str| yansi::Paint::green(str).bold(), - Color::For => &|str| yansi::Paint::yellow(str).bold(), - Color::Fft => &|str| yansi::Paint::cyan(str).bold(), - } -} - -// TODO: Remove common indentation. -// TODO: Prioritize inline marcations. -fn colorize_code( - markers: &mut [&(Point, Point, &Marker)], - code_line: &str, - modify: &dyn Fn(&str) -> String, - fmt: &mut T, -) -> std::fmt::Result { - markers.sort_by(|x, y| x.0.column.cmp(&y.0.column)); - let mut start = 0; - for marker in markers { - if start < marker.0.column { - write!(fmt, "{}", modify(&code_line[start..marker.0.column]))?; - start = marker.0.column; - } - - let end = if marker.0.line == marker.1.line { - marker.1.column - } else { - code_line.len() - }; - - if start < end { - let colorizer = get_colorizer(&marker.2.color); - write!(fmt, "{}", colorizer(&code_line[start..end]).bold())?; - start = end; - } - } - - if start < code_line.len() { - write!(fmt, "{}", modify(&code_line[start..code_line.len()]))?; - } - writeln!(fmt)?; - Ok(()) -} - -fn paint_line(data: T) -> Paint { - Paint::new(data).fg(yansi::Color::Cyan).dimmed() -} - -fn mark_inlined( - prefix: &str, - code: &str, - config: &RenderConfig, - inline_markers: &mut [&(Point, Point, &Marker)], - fmt: &mut T, -) -> std::fmt::Result { - inline_markers.sort_by(|x, y| x.0.column.cmp(&y.0.column)); - let mut start = 0; - - write!( - fmt, - "{:>5} {} {}", - "", - paint_line(config.chars.vbar), - prefix - )?; - - for marker in inline_markers.iter_mut() { - if start < marker.0.column { - let (pad, tab_pad) = count_width(&code[start..marker.0.column]); - write!(fmt, "{:pad$}{}", "", "\t".repeat(tab_pad), pad = pad)?; - start = marker.0.column; - } - if start < marker.1.column { - let (pad, tab_pad) = count_width(&code[start..marker.1.column]); - let colorizer = get_colorizer(&marker.2.color); - write!(fmt, "{}", colorizer(config.chars.bxline.to_string()))?; - write!( - fmt, - "{}", - colorizer(config.chars.hbar.to_string().repeat((pad + tab_pad).saturating_sub(1))) - )?; - start = marker.1.column; - } - } - writeln!(fmt)?; - - // Pretty print the marker - for i in 0..inline_markers.len() { - write!( - fmt, - "{:>5} {} {}", - "", - paint_line(config.chars.vbar), - prefix - )?; - let mut start = 0; - for j in 0..(inline_markers.len() - i) { - let marker = inline_markers[j]; - if start < marker.0.column { - let (pad, tab_pad) = count_width(&code[start..marker.0.column]); - write!(fmt, "{:pad$}{}", "", "\t".repeat(tab_pad), pad = pad)?; - start = marker.0.column; - } - if start < marker.1.column { - let colorizer = get_colorizer(&marker.2.color); - if j == (inline_markers.len() - i).saturating_sub(1) { - write!( - fmt, - "{}", - colorizer(format!("{}{}", config.chars.trline, marker.2.text)) - )?; - } else { - write!(fmt, "{}", colorizer(config.chars.vbar.to_string()))?; - } - start += 1; - } - } - writeln!(fmt)?; - } - Ok(()) -} - -fn write_code_block<'a, T: Write + Sized>( - file_name: &Path, - config: &RenderConfig, - markers: &[Marker], - group_code: &'a str, - fmt: &mut T, -) -> std::fmt::Result { - let guide = get_code_line_guide(group_code); - - let point = find_in_line_guide(markers[0].position.start, &guide); - - let no_code = markers.iter().all(|x| x.no_code); - - let header = format!( - "{:>5} {}{}[{}:{}]", - "", - if no_code { - config.chars.hbar - } else { - config.chars.brline - }, - config.chars.hbar.to_string().repeat(2), - file_name.to_str().unwrap(), - point - ); - - writeln!(fmt, "{}", paint_line(header))?; - - if no_code { - return Ok(()); - } - - writeln!(fmt, "{:>5} {}", "", paint_line(config.chars.vbar))?; - - let mut lines_set = FxHashSet::default(); - - let mut markers_by_line: FxHashMap> = FxHashMap::default(); - - let mut multi_line_markers: Vec<(Point, Point, &Marker)> = Vec::new(); - - for marker in markers { - let start = find_in_line_guide(marker.position.start, &guide); - let end = find_in_line_guide(marker.position.end, &guide); - - if let Some(row) = markers_by_line.get_mut(&start.line) { - row.push((start.clone(), end.clone(), marker)) - } else { - markers_by_line.insert(start.line, vec![(start.clone(), end.clone(), marker)]); - } - - if end.line != start.line { - multi_line_markers.push((start.clone(), end.clone(), marker)); - } else if marker.main { - // Just to make errors a little bit better - let start = start.line.saturating_sub(1); - let end = if start + 2 >= guide.len() { - guide.len() - 1 - } else { - start + 2 - }; - for i in start..=end { - lines_set.insert(i); - } - } - - if end.line - start.line <= 3 { - for i in start.line..=end.line { - lines_set.insert(i); - } - } else { - lines_set.insert(start.line); - lines_set.insert(end.line); - } - } - - let code_lines: Vec<&'a str> = group_code.lines().collect(); - - let mut lines = lines_set - .iter() - .filter(|x| **x < code_lines.len()) - .collect::>(); - lines.sort(); - - for i in 0..lines.len() { - let line = lines[i]; - let mut prefix = " ".to_string(); - let mut empty_vec = Vec::new(); - - let row = markers_by_line.get_mut(line).unwrap_or(&mut empty_vec); - - let mut inline_markers: Vec<&(Point, Point, &Marker)> = - row.iter().filter(|x| x.0.line == x.1.line).collect(); - - let mut current = None; - - for marker in &multi_line_markers { - if marker.0.line == *line { - writeln!( - fmt, - "{:>5} {} {} ", - "", - paint_line(config.chars.vbar), - get_colorizer(&marker.2.color)(config.chars.brline) - )?; - } - if *line >= marker.0.line && *line <= marker.1.line { - prefix = format!(" {} ", get_colorizer(&marker.2.color)(config.chars.vbar)); - current = Some(marker); - break; - } - } - - write!( - fmt, - "{:>5} {} {}", - line + 1, - paint_line(config.chars.vbar), - prefix, - )?; - - let modify: Box String> = if let Some(marker) = current { - prefix = format!(" {} ", get_colorizer(&marker.2.color)(config.chars.vbar)); - Box::new(|str: &str| get_colorizer(&marker.2.color)(str).to_string()) - } else { - Box::new(|str: &str| str.to_string()) - }; - - if !inline_markers.is_empty() { - colorize_code(&mut inline_markers, code_lines[*line], &modify, fmt)?; - mark_inlined(&prefix, code_lines[*line], config, &mut inline_markers, fmt)?; - if markers_by_line.contains_key(&(line + 1)) { - writeln!( - fmt, - "{:>5} {} {} ", - "", - paint_line(config.chars.dbar), - prefix - )?; - } - } else { - writeln!(fmt, "{}", modify(code_lines[*line]))?; - } - - if let Some(marker) = current { - if marker.1.line == *line { - let col = get_colorizer(&marker.2.color); - writeln!( - fmt, - "{:>5} {} {} ", - "", - paint_line(config.chars.dbar), - prefix - )?; - writeln!( - fmt, - "{:>5} {} {} ", - "", - paint_line(config.chars.dbar), - col(format!(" {} {}", config.chars.trline, marker.2.text)) - )?; - prefix = " ".to_string(); - } - } - - if i < lines.len() - 1 && lines[i + 1] - line > 1 { - writeln!( - fmt, - "{:>5} {} {} ", - "", - paint_line(config.chars.dbar), - prefix - )?; - } - } - - Ok(()) -} - -fn render_tag(severity: &Severity, fmt: &mut T) -> std::fmt::Result { - write!( - fmt, - " {} ", - match severity { - Severity::Error => Paint::new(" ERROR ").bg(yansi::Color::Red).bold(), - Severity::Warning => Paint::new(" WARN ").bg(yansi::Color::Yellow).bold(), - Severity::Info => Paint::new(" INFO ").bg(yansi::Color::Blue).bold(), - } - ) -} - -pub trait Report { - fn render( - &self, - cache: &C, - config: &RenderConfig, - fmt: &mut T, - ) -> std::fmt::Result; -} - -impl Report for Box { - fn render( - &self, - cache: &C, - config: &RenderConfig, - fmt: &mut T, - ) -> std::fmt::Result { - write!(fmt, " ")?; - - let frame = self.to_diagnostic_frame(); - - render_tag(&frame.severity, fmt)?; - writeln!(fmt, "{}", Paint::new(&frame.title).bold())?; - - if !frame.subtitles.is_empty() { - writeln!(fmt)?; - } - - for subtitle in &frame.subtitles { - match subtitle { - Subtitle::Normal(color, phr) => { - let colorizer = get_colorizer(color); - writeln!( - fmt, - "{:>5} {} {}", - "", - colorizer(config.chars.bullet), - Paint::new(phr) - )?; - } - Subtitle::Bold(color, phr) => { - let colorizer = get_colorizer(color); - writeln!( - fmt, - "{:>5} {} {}", - "", - colorizer(config.chars.bullet), - Paint::new(phr).bold() - )?; - } - Subtitle::Phrase(color, words) => { - let colorizer = get_colorizer(color); - write!(fmt, "{:>5} {} ", "", colorizer(config.chars.bullet))?; - for word in words { - match word { - Word::Normal(str) => write!(fmt, "{} ", Paint::new(str))?, - Word::Dimmed(str) => write!(fmt, "{} ", Paint::new(str).dimmed())?, - Word::White(str) => write!(fmt, "{} ", Paint::new(str).bold())?, - Word::Painted(color, str) => { - let colorizer = get_colorizer(color); - write!(fmt, "{} ", colorizer(str))? - } - } - } - writeln!(fmt)?; - } - Subtitle::LineBreak => { - writeln!(fmt)?; - } - } - } - - let groups = group_markers(&frame.positions); - let is_empty = groups.is_empty(); - - for (ctx, group) in groups { - writeln!(fmt)?; - let (file, code) = cache.fetch(ctx).unwrap(); - let diff = - pathdiff::diff_paths(&file.clone(), PathBuf::from(".").canonicalize().unwrap()) - .unwrap_or(file); - write_code_block(&diff, config, &group, code, fmt)?; - } - - if !is_empty { - writeln!(fmt)?; - } - - for hint in &frame.hints { - writeln!( - fmt, - "{:>5} {} {}", - "", - Paint::new("Hint:").fg(yansi::Color::Cyan).bold(), - Paint::new(hint).fg(yansi::Color::Cyan) - )?; - } - - writeln!(fmt)?; - - Ok(()) - } -} - -impl Report for Log { - fn render( - &self, - _cache: &C, - _config: &RenderConfig, - fmt: &mut T, - ) -> std::fmt::Result { - match self { - Log::Checking(file) => { - writeln!( - fmt, - " {} {}", - Paint::new(" CHECKING ").bg(yansi::Color::Green).bold(), - file - ) - } - Log::Compiled(duration) => { - writeln!( - fmt, - " {} All relevant terms compiled. took {:.2}s", - Paint::new(" COMPILED ").bg(yansi::Color::Green).bold(), - duration.as_secs_f32() - ) - } - Log::Checked(duration) => { - writeln!( - fmt, - " {} All terms checked. took {:.2}s", - Paint::new(" CHECKED ").bg(yansi::Color::Green).bold(), - duration.as_secs_f32() - ) - } - Log::Failed(duration) => { - writeln!( - fmt, - " {} Took {}s", - Paint::new(" FAILED ").bg(yansi::Color::Red).bold(), - duration.as_secs() - ) - } - Log::Rewrites(u64) => { - writeln!( - fmt, - " {} Rewrites: {}", - Paint::new(" STATS ").bg(yansi::Color::Green).bold(), - u64 - ) - } - } - } -} diff --git a/crates/kind-report/src/report/code.rs b/crates/kind-report/src/report/code.rs new file mode 100644 index 00000000..5ec6b29b --- /dev/null +++ b/crates/kind-report/src/report/code.rs @@ -0,0 +1,107 @@ +use fxhash::FxHashMap; +use kind_span::{Pos, SyntaxCtxIndex}; +use std::{collections::hash_map::Iter, fmt::Display}; +use unicode_width::UnicodeWidthStr; + +use crate::data::Marker; + +/// The line guide is useful to locate some positions inside the source +/// code by using the index instead of line and column information. +pub struct LineGuide(Vec); + +pub struct FileMarkers(pub Vec); + +/// This structure contains all markers sorted by lines and column for each +/// one of the files. +pub struct SortedMarkers(FxHashMap); + +impl SortedMarkers { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn iter(&self) -> Iter { + self.0.iter() + } +} + +#[derive(Clone, Copy)] +pub struct Point { + pub line: usize, + pub column: usize, +} + +impl Display for Point { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.line + 1, self.column + 1) + } +} + +pub struct Spaces { + pub width: usize, + pub tabs: usize, +} + +impl LineGuide { + pub fn get(code: &str) -> LineGuide { + let mut guide = Vec::new(); + let mut size = 0; + for chr in code.chars() { + size += chr.len_utf8(); + if chr == '\n' { + guide.push(size); + } + } + guide.push(code.len()); + LineGuide(guide) + } + + pub fn find(&self, pos: Pos) -> Point { + for i in 0..self.0.len() { + if self.0[i] > pos.index as usize { + return Point { + line: i, + column: pos.index as usize - (if i == 0 { 0 } else { self.0[i - 1] }), + }; + } + } + let line = self.0.len() - 1; + Point { + line, + column: pos.index as usize - (if line == 0 { 0 } else { self.0[line - 1] }), + } + } + + pub fn len(&self) -> usize { + self.0.len() + } +} + +pub fn count_width(str: &str) -> Spaces { + Spaces { + width: UnicodeWidthStr::width(str), + tabs: str.chars().filter(|x| *x == '\t').count(), + } +} + +pub fn group_markers(markers: &[Marker]) -> SortedMarkers { + let mut file_group = FxHashMap::default(); + + for marker in markers { + let group = file_group + .entry(marker.position.ctx) + .or_insert_with(Vec::new); + group.push(marker.clone()) + } + + for group in file_group.values_mut() { + group.sort_by(|x, y| x.position.start.cmp(&y.position.end)); + } + + SortedMarkers( + file_group + .into_iter() + .map(|(x, y)| (x, FileMarkers(y))) + .collect(), + ) +} diff --git a/crates/kind-report/src/report/mod.rs b/crates/kind-report/src/report/mod.rs new file mode 100644 index 00000000..93970c41 --- /dev/null +++ b/crates/kind-report/src/report/mod.rs @@ -0,0 +1,5 @@ +pub mod code; +pub mod mode; + +pub use code::*; +pub use mode::*; \ No newline at end of file diff --git a/crates/kind-report/src/report/mode/classic.rs b/crates/kind-report/src/report/mode/classic.rs new file mode 100644 index 00000000..79c6c0e5 --- /dev/null +++ b/crates/kind-report/src/report/mode/classic.rs @@ -0,0 +1,504 @@ +use super::CodeBlock; +use super::{Classic, Renderable, Res}; +use crate::data::*; +use crate::report::code::{count_width, group_markers, LineGuide, Spaces}; +use crate::report::code::{FileMarkers, Point}; +use crate::RenderConfig; + +use fxhash::{FxHashMap, FxHashSet}; +use pathdiff::diff_paths; +use std::fmt::Write; +use std::path::PathBuf; +use yansi::Paint; + +fn colorize_code( + markers: &mut [&(Point, Point, &Marker)], + code_line: &str, + modify: &dyn Fn(&str) -> String, + fmt: &mut T, +) -> std::fmt::Result { + markers.sort_by(|x, y| x.0.column.cmp(&y.0.column)); + let mut start = 0; + + for marker in markers { + if start < marker.0.column { + write!(fmt, "{}", modify(&code_line[start..marker.0.column]))?; + start = marker.0.column; + } + + let end = if marker.0.line == marker.1.line { + marker.1.column + } else { + code_line.len() + }; + + if start < end { + let colorizer = &marker.2.color.colorizer(); + write!(fmt, "{}", colorizer(&code_line[start..end]).bold())?; + start = end; + } + } + + if start < code_line.len() { + write!(fmt, "{}", modify(&code_line[start..code_line.len()]))?; + } + + writeln!(fmt)?; + Ok(()) +} + +fn mark_inlined( + prefix: &str, + code: &str, + config: &RenderConfig, + inline_markers: &mut [&(Point, Point, &Marker)], + fmt: &mut T, +) -> std::fmt::Result { + inline_markers.sort_by(|x, y| x.0.column.cmp(&y.0.column)); + let mut start = 0; + + write!( + fmt, + "{:>5} {} {}", + "", + paint_line(config.chars.vbar), + prefix + )?; + + for marker in inline_markers.iter_mut() { + if start < marker.0.column { + let Spaces { width, tabs } = count_width(&code[start..marker.0.column]); + write!(fmt, "{:pad$}{}", "", "\t".repeat(tabs), pad = width)?; + start = marker.0.column; + } + if start < marker.1.column { + let Spaces { width, tabs } = count_width(&code[start..marker.1.column]); + let colorizer = marker.2.color.colorizer(); + write!(fmt, "{}", colorizer(config.chars.bxline.to_string()))?; + write!( + fmt, + "{}", + colorizer( + config + .chars + .hbar + .to_string() + .repeat((width + tabs).saturating_sub(1)) + ) + )?; + start = marker.1.column; + } + } + writeln!(fmt)?; + + // Pretty print the marker + for i in 0..inline_markers.len() { + write!( + fmt, + "{:>5} {} {}", + "", + paint_line(config.chars.vbar), + prefix + )?; + let mut start = 0; + for j in 0..(inline_markers.len() - i) { + let marker = inline_markers[j]; + if start < marker.0.column { + let Spaces { width, tabs } = count_width(&code[start..marker.0.column]); + write!(fmt, "{:pad$}{}", "", "\t".repeat(tabs), pad = width)?; + start = marker.0.column; + } + if start < marker.1.column { + let colorizer = marker.2.color.colorizer(); + if j == (inline_markers.len() - i).saturating_sub(1) { + write!( + fmt, + "{}", + colorizer(format!("{}{}", config.chars.trline, marker.2.text)) + )?; + } else { + write!(fmt, "{}", colorizer(config.chars.vbar.to_string()))?; + } + start += 1; + } + } + writeln!(fmt)?; + } + Ok(()) +} + +fn group_marker_lines<'a>( + guide: &'a LineGuide, + markers: &'a FileMarkers, +) -> ( + FxHashSet, + FxHashMap>, + Vec<(Point, Point, &'a Marker)>, +) { + let mut lines_set = FxHashSet::default(); + let mut markers_by_line: FxHashMap> = FxHashMap::default(); + let mut multi_line_markers: Vec<(Point, Point, &Marker)> = Vec::new(); + + for marker in &markers.0 { + let start = guide.find(marker.position.start); + let end = guide.find(marker.position.end); + + if let Some(row) = markers_by_line.get_mut(&start.line) { + row.push((start.clone(), end.clone(), &marker)) + } else { + markers_by_line.insert(start.line, vec![(start.clone(), end.clone(), &marker)]); + } + + if end.line != start.line { + multi_line_markers.push((start.clone(), end.clone(), &marker)); + } else if marker.main { + // Just to make errors a little bit better + let start = start.line.saturating_sub(1); + let end = if start + 2 >= guide.len() { + guide.len() - 1 + } else { + start + 2 + }; + for i in start..=end { + lines_set.insert(i); + } + } + + if end.line - start.line <= 3 { + for i in start.line..=end.line { + lines_set.insert(i); + } + } else { + lines_set.insert(start.line); + lines_set.insert(end.line); + } + } + + (lines_set, markers_by_line, multi_line_markers) +} + +fn paint_line(data: T) -> Paint { + Paint::new(data).fg(yansi::Color::Cyan).dimmed() +} + +impl Color { + fn colorizer(&self) -> &dyn Fn(T) -> Paint { + match self { + Color::Fst => &|str| yansi::Paint::red(str).bold(), + Color::Snd => &|str| yansi::Paint::blue(str).bold(), + Color::Thr => &|str| yansi::Paint::green(str).bold(), + Color::For => &|str| yansi::Paint::yellow(str).bold(), + Color::Fft => &|str| yansi::Paint::cyan(str).bold(), + } + } + + fn colorize(&self, data: T) -> Paint { + (self.colorizer())(data) + } +} + +impl Renderable for Severity { + fn render(&self, fmt: &mut U, _: &C, _: &RenderConfig) -> Res { + use Severity::*; + + let painted = match self { + Error => Paint::new(" ERROR ").bg(yansi::Color::Red).bold(), + Warning => Paint::new(" WARN ").bg(yansi::Color::Yellow).bold(), + Info => Paint::new(" INFO ").bg(yansi::Color::Blue).bold(), + }; + + write!(fmt, " {} ", painted) + } +} + +impl<'a> Renderable for Header<'a> { + fn render(&self, fmt: &mut U, cache: &C, config: &RenderConfig) -> Res { + self.severity.render(fmt, cache, config)?; + fmt.write_str(&Paint::new(&self.title).bold().to_string())?; + fmt.write_char('\n') + } +} + +impl Renderable for Subtitle { + fn render(&self, fmt: &mut U, cache: &C, config: &RenderConfig) -> Res { + match self { + Subtitle::Normal(color, phr) => { + let bullet = color.colorize(config.chars.bullet); + writeln!(fmt, "{:>5} {} {}", "", bullet, Paint::new(phr)) + } + Subtitle::Bold(color, phr) => { + let bullet = color.colorize(config.chars.bullet); + writeln!(fmt, "{:>5} {} {}", "", bullet, Paint::new(phr).bold()) + } + Subtitle::Phrase(color, words) => { + let bullet = color.colorize(config.chars.bullet); + write!(fmt, "{:>5} {} ", "", bullet)?; + words.render(fmt, cache, config)?; + writeln!(fmt) + } + Subtitle::LineBreak => { + writeln!(fmt) + } + } + } +} + +impl<'a> Renderable for Word { + fn render(&self, fmt: &mut U, _: &C, _: &RenderConfig) -> Res { + match self { + Word::Normal(str) => write!(fmt, "{} ", Paint::new(str)), + Word::Dimmed(str) => write!(fmt, "{} ", Paint::new(str).dimmed()), + Word::White(str) => write!(fmt, "{} ", Paint::new(str).bold()), + Word::Painted(color, str) => write!(fmt, "{} ", color.colorize(str)), + } + } +} + +impl<'a> Renderable for Subtitles<'a> { + fn render(&self, fmt: &mut U, cache: &C, config: &RenderConfig) -> Res { + if !self.0.is_empty() { + writeln!(fmt)?; + } + + self.0.render(fmt, cache, config) + } +} + +impl<'a> Renderable for CodeBlock<'a> { + fn render(&self, fmt: &mut U, _: &C, config: &RenderConfig) -> Res { + let guide = LineGuide::get(self.code); + let point = guide.find(self.markers.0[0].position.start); + + let chars = config.chars; + + // Header of the code block + + let bars = chars.hbar.to_string().repeat(2); + let file = self.path.to_str().unwrap(); + let header = format!("{:>5} {}{}[{}:{}]", "", chars.brline, bars, file, point); + + writeln!(fmt, "{}", paint_line(header))?; + + if self.markers.0.iter().all(|x| x.no_code) { + return Ok(()); + } + + writeln!(fmt, "{:>5} {}", "", paint_line(chars.vbar))?; + + let (lines_set, mut by_line, multi_line) = group_marker_lines(&guide, self.markers); + + let code_lines: Vec<&'a str> = self.code.lines().collect(); + + let mut lines: Vec = lines_set + .into_iter() + .filter(|x| *x < code_lines.len()) + .collect(); + + lines.sort(); + + for i in 0..lines.len() { + let line = lines[i]; + let mut prefix = " ".to_string(); + let mut empty_vec = Vec::new(); + let row = by_line.get_mut(&line).unwrap_or(&mut empty_vec); + + let mut inline_markers: Vec<&(Point, Point, &Marker)> = + row.iter().filter(|x| x.0.line == x.1.line).collect(); + + let mut current = None; + + for marker in &multi_line { + if marker.0.line == line { + writeln!( + fmt, + "{:>5} {} {} ", + "", + paint_line(config.chars.vbar), + marker.2.color.colorize(config.chars.brline) + )?; + } + if line >= marker.0.line && line <= marker.1.line { + prefix = format!(" {} ", marker.2.color.colorize(config.chars.vbar)); + current = Some(marker); + break; + } + } + + write!( + fmt, + "{:>5} {} {}", + line + 1, + paint_line(config.chars.vbar), + prefix, + )?; + + let modify: Box String> = if let Some(marker) = current { + prefix = format!(" {} ", marker.2.color.colorize(config.chars.vbar)); + Box::new(|str: &str| marker.2.color.colorize(str).to_string()) + } else { + Box::new(|str: &str| str.to_string()) + }; + + if !inline_markers.is_empty() { + colorize_code(&mut inline_markers, code_lines[line], &modify, fmt)?; + mark_inlined(&prefix, code_lines[line], config, &mut inline_markers, fmt)?; + if by_line.contains_key(&(line + 1)) { + writeln!( + fmt, + "{:>5} {} {} ", + "", + paint_line(config.chars.dbar), + prefix + )?; + } + } else { + writeln!(fmt, "{}", modify(code_lines[line]))?; + } + + if let Some(marker) = current { + if marker.1.line == line { + let col = marker.2.color.colorizer(); + writeln!( + fmt, + "{:>5} {} {} ", + "", + paint_line(config.chars.dbar), + prefix + )?; + writeln!( + fmt, + "{:>5} {} {} ", + "", + paint_line(config.chars.dbar), + col(format!(" {} {}", config.chars.trline, marker.2.text)) + )?; + prefix = " ".to_string(); + } + } + + if i < lines.len() - 1 && lines[i + 1] - line > 1 { + writeln!( + fmt, + "{:>5} {} {} ", + "", + paint_line(config.chars.dbar), + prefix + )?; + } + } + + Ok(()) + } +} + +impl Renderable for Log { + fn render(&self, fmt: &mut U, _: &C, _: &RenderConfig) -> Res { + match self { + Log::Checking(file) => { + writeln!( + fmt, + " {} {}", + Paint::new(" CHECKING ").bg(yansi::Color::Green).bold(), + file + ) + } + Log::Compiled(duration) => { + writeln!( + fmt, + " {} All relevant terms compiled. took {:.2}s", + Paint::new(" COMPILED ").bg(yansi::Color::Green).bold(), + duration.as_secs_f32() + ) + } + Log::Checked(duration) => { + writeln!( + fmt, + " {} All terms checked. took {:.2}s", + Paint::new(" CHECKED ").bg(yansi::Color::Green).bold(), + duration.as_secs_f32() + ) + } + Log::Failed(duration) => { + writeln!( + fmt, + " {} Took {}s", + Paint::new(" FAILED ").bg(yansi::Color::Red).bold(), + duration.as_secs() + ) + } + Log::Rewrites(u64) => { + writeln!( + fmt, + " {} Rewrites: {}", + Paint::new(" STATS ").bg(yansi::Color::Green).bold(), + u64 + ) + } + } + } +} + +impl<'a> Renderable for Markers<'a> { + fn render(&self, fmt: &mut U, cache: &C, config: &RenderConfig) -> Res { + let groups = group_markers(&self.0); + let is_empty = groups.is_empty(); + let current = PathBuf::from(".").canonicalize().unwrap(); + + for (ctx, markers) in groups.iter() { + writeln!(fmt)?; + + let (file, code) = cache.fetch(*ctx).unwrap(); + let path = diff_paths(&file.clone(), current.clone()).unwrap_or(file); + + let block = CodeBlock { + code, + path: &path, + markers, + }; + + block.render(fmt, cache, config)?; + } + + if !is_empty { + writeln!(fmt)?; + } + + Ok(()) + } +} + +impl<'a> Renderable for Hints<'a> { + fn render(&self, fmt: &mut U, _: &C, _: &RenderConfig) -> Res { + for hint in self.0 { + writeln!( + fmt, + "{:>5} {} {}", + "", + Paint::new("Hint:").fg(yansi::Color::Cyan).bold(), + Paint::new(hint).fg(yansi::Color::Cyan) + )?; + } + + writeln!(fmt) + } +} + +impl Renderable for DiagnosticFrame { + fn render(&self, fmt: &mut U, cache: &C, config: &RenderConfig) -> Res { + write!(fmt, " ")?; + + self.header().render(fmt, cache, config)?; + self.subtitles().render(fmt, cache, config)?; + self.markers().render(fmt, cache, config)?; + + self.hints().render(fmt, cache, config)?; + + Ok(()) + } +} + +impl Renderable for Box { + fn render(&self, fmt: &mut U, cache: &C, config: &RenderConfig) -> Res { + self.to_diagnostic_frame().render(fmt, cache, config) + } +} diff --git a/crates/kind-report/src/report/mode/mod.rs b/crates/kind-report/src/report/mode/mod.rs new file mode 100644 index 00000000..db2a8dbe --- /dev/null +++ b/crates/kind-report/src/report/mode/mod.rs @@ -0,0 +1,47 @@ +use std::{fmt::Write, path::Path}; + +use crate::{data::FileCache, RenderConfig}; + +use super::code::FileMarkers; + +pub mod classic; + +// Just a type synonym to make it easier to read. +pub type Res = std::fmt::Result; + +// ----------------------------------------------------------------- +// Some abstract data types based on Haskell. These types are useful +// for setting some modes on the report. +// ----------------------------------------------------------------- + +/// Classical mode is the default mode for the report. It's made to +/// be easy to read and understand. +pub enum Classic {} + +/// Compact mode is made to be more compact and easy to parse by some +/// LLM. +pub enum Compact {} + +// Utilities + +/// Utility for easier renders +pub(crate) struct CodeBlock<'a> { + pub code: &'a str, + pub path: &'a Path, + pub markers: &'a FileMarkers +} + +/// A type class for renderable error reports and messages. It's useful +/// to change easily things without problems. +pub trait Renderable { + fn render(&self, fmt: &mut U, cache: &C, config: &RenderConfig) -> Res; +} + +impl<'a, T, E> Renderable for Vec where E : Renderable { + fn render(&self, fmt: &mut U, cache: &C, config: &RenderConfig) -> Res { + for elem in self { + elem.render(fmt, cache, config)?; + } + Ok(()) + } +} diff --git a/crates/kind-tests/tests/mod.rs b/crates/kind-tests/tests/mod.rs index 38c49510..0a8b1f76 100644 --- a/crates/kind-tests/tests/mod.rs +++ b/crates/kind-tests/tests/mod.rs @@ -2,7 +2,7 @@ use kind_driver::session::Session; use kind_report::data::Diagnostic; -use kind_report::report::Report; +use kind_report::report::{Renderable, Classic}; use kind_report::RenderConfig; use std::fs::{self, File}; @@ -50,7 +50,7 @@ fn test_kind2(path: &Path, run: fn(&PathBuf, &mut Session) -> Option) -> let mut res_string = String::new(); for diag in diagnostics { - diag.render(&session, &render, &mut res_string).unwrap(); + Renderable::::render(&diag, &mut res_string, &session, &render).unwrap(); } res_string