From 8b6fc247597838f000b412049b9767a5c44dfbe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20H=C3=B6lting?= <87192362+moritz-hoelting@users.noreply.github.com> Date: Mon, 8 Apr 2024 14:03:26 +0200 Subject: [PATCH] Implement better SourceCodeDisplay output --- src/base/log.rs | 252 +++++++++++++++++++++++++++++++++++++++- src/base/source_file.rs | 18 +-- 2 files changed, 252 insertions(+), 18 deletions(-) diff --git a/src/base/log.rs b/src/base/log.rs index f11eb98..35a9dd7 100644 --- a/src/base/log.rs +++ b/src/base/log.rs @@ -1,9 +1,9 @@ //! Module containing structures and implementations for logging messages to the user. use colored::Colorize; -use std::fmt::Display; +use std::{fmt::Display, sync::Arc}; -use super::source_file::Span; +use super::source_file::{Location, SourceFile, Span}; /// Represent the severity of a log message to be printed to the console. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -45,6 +45,17 @@ impl Display for Message { } } +fn get_digit_count(mut number: usize) -> usize { + let mut digit = 0; + + while number > 0 { + number /= 10; + digit += 1; + } + + digit +} + /// Structure implementing [`Display`] that prints the particular span of the source code. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SourceCodeDisplay<'a, T> { @@ -64,12 +75,243 @@ impl<'a, T> SourceCodeDisplay<'a, T> { impl<'a, T: std::fmt::Display> Display for SourceCodeDisplay<'a, T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.span.str())?; + let start_location = self.span.start_location(); + let end_location = self.span.end_location(); - if let Some(help_display) = &self.help_display { - write!(f, "\n\n{help_display}")?; + let start_line = start_location.line; + let end_line = end_location.map_or_else( + || self.span.source_file().line_amount(), + |end_location| end_location.line, + ); + let is_multiline = start_line != end_line; + + // when printing the source code, show the line before the span and the line after the span + let largest_line_number_digits = get_digit_count(end_line + 1); + + // prints the source location + for _ in 0..largest_line_number_digits { + write!(f, " ")?; + } + + writeln!( + f, + "{} {}", + "-->".cyan().bold(), + format_args!( + "{}:{}:{}", + self.span.source_file().path().display(), + start_location.line, + start_location.column + ) + )?; + + // prints the empty pipe + write_empty_pipe(f, largest_line_number_digits)?; + + // prints previous line + if let Some((line_number, line)) = start_line.checked_sub(1).and_then(|line_number| { + self.span + .source_file() + .get_line(line_number) + .map(|line| (line_number, line)) + }) { + write_line(f, largest_line_number_digits, line_number, line)?; + } + + // prints the line with the error + write_error_line( + f, + start_location, + end_location, + largest_line_number_digits, + is_multiline, + self.span.source_file(), + )?; + + if let Some(message) = &self.help_display { + if !is_multiline { + // prints the empty pipe + { + for _ in 0..=largest_line_number_digits { + write!(f, " ")?; + } + write!(f, "{} ", "┃".cyan().bold())?; + } + + // prints the whitespace until the start's column + { + for (index, ch) in self + .span + .source_file() + .get_line(start_line) + .unwrap() + .chars() + .enumerate() + { + if index + 1 >= start_location.column { + break; + } + + // if the char is tab, print 4 spaces + write!(f, "{}", if ch == '\t' { " " } else { " " })?; + } + } + + // prints the message + writeln!(f, "{}: {message}", "help".bold())?; + } + } + + // prints the post line + if let Some((line_number, line)) = end_line.checked_add(1).and_then(|line_number| { + self.span + .source_file() + .get_line(line_number) + .map(|line| (line_number, line)) + }) { + write_line(f, largest_line_number_digits, line_number, line)?; + } + + // prints the empty pipe + write_empty_pipe(f, largest_line_number_digits)?; + + if is_multiline { + if let Some(help_display) = &self.help_display { + write_help_message_multiline(f, largest_line_number_digits, help_display)?; + } } Ok(()) } } + +fn write_empty_pipe( + f: &mut std::fmt::Formatter<'_>, + largest_line_number_digits: usize, +) -> std::fmt::Result { + for _ in 0..=largest_line_number_digits { + write!(f, " ")?; + } + writeln!(f, "{}", "┃".cyan().bold())?; + + Ok(()) +} + +fn write_line( + f: &mut std::fmt::Formatter<'_>, + largest_line_number_digits: usize, + line_number: usize, + line: &str, +) -> std::fmt::Result { + // prints the line number + write!( + f, + "{}{}{} ", + line_number.to_string().cyan().bold(), + format_args!( + "{:width$}", + "", + width = largest_line_number_digits - get_digit_count(line_number) + 1 + ), + "┃".cyan().bold(), + )?; + + for ch in line.chars() { + // if the char is tab, print 4 spaces + if ch == '\t' { + write!(f, " ")?; + } else if ch != '\n' { + write!(f, "{ch}")?; + } + } + + writeln!(f)?; + + Ok(()) +} + +fn write_help_message_multiline( + f: &mut std::fmt::Formatter<'_>, + largest_line_number_digits: usize, + help_display: T, +) -> std::fmt::Result { + for _ in 0..=largest_line_number_digits { + write!(f, " ")?; + } + write!(f, "{} ", "=".cyan().bold())?; + + // prints the message + writeln!(f, "{}: {help_display}", "help".bold())?; + + Ok(()) +} + +fn write_error_line( + f: &mut std::fmt::Formatter<'_>, + start_location: Location, + end_location: Option, + largest_line_number_digits: usize, + is_multiline: bool, + source_file: &Arc, +) -> std::fmt::Result { + let start_line = start_location.line; + let end_line = end_location.map_or_else( + || source_file.line_amount(), + |end_location| end_location.line, + ); + + for line_number in start_line..=end_line { + // prints the line number + write!( + f, + "{}{}{} ", + line_number.to_string().cyan().bold(), + format_args!( + "{:width$}", + "", + width = largest_line_number_digits - get_digit_count(line_number) + 1 + ), + "┃".cyan().bold(), + )?; + + for (index, ch) in source_file + .get_line(line_number) + .unwrap() + .chars() + .enumerate() + { + // if the char is tab, print 4 spaces + if ch == '\t' { + write!(f, " ")?; + } else if ch != '\n' { + // check if the character is in the span + let is_in_span = { + let index = index + 1; + if is_multiline { + (line_number == start_line && index >= start_location.column) + || (line_number == end_line + && (index + 1) + < end_location + .map_or(usize::MAX, |end_location| end_location.column)) + || (line_number > start_line && line_number < end_line) + } else { + line_number == start_line + && index >= start_location.column + && index + < end_location + .map_or(usize::MAX, |end_location| end_location.column) + } + }; + + if is_in_span { + write!(f, "{}", ch.to_string().red().bold().underline())?; + } else { + write!(f, "{ch}")?; + } + } + } + writeln!(f)?; + } + + Ok(()) +} diff --git a/src/base/source_file.rs b/src/base/source_file.rs index 38a3871..eaf7207 100644 --- a/src/base/source_file.rs +++ b/src/base/source_file.rs @@ -17,9 +17,13 @@ use super::Error; /// Represents a source file that contains the source code. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone)] +#[derive(Clone, Getters)] pub struct SourceFile { + /// Get the path of the source file. + #[get = "pub"] path: PathBuf, + /// Get the content of the source file + #[get = "pub"] content: String, lines: Vec>, } @@ -45,18 +49,6 @@ impl SourceFile { }) } - /// Get the content of the source file - #[must_use] - pub fn content(&self) -> &str { - &self.content - } - - /// Get the path of the source file. - #[must_use] - pub fn path(&self) -> &Path { - &self.path - } - /// Get the line of the source file at the given line number. /// /// Numbering starts at 1.