use std::sync::Arc;
use anstyle::{AnsiColor, Color, Reset, Style};
use unicode_width::UnicodeWidthChar;
pub use anstyle;
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct File {
path: String,
source: String,
}
impl File {
pub fn new(path: impl Into<String>, source: impl Into<String>) -> Arc<Self> {
Arc::new(Self {
path: path.into(),
source: source.into(),
})
}
pub fn open(path: impl Into<String>) -> Result<Arc<Self>, std::io::Error> {
let path = path.into();
let source = std::fs::read_to_string(&path)?;
Ok(File::new(path, source))
}
#[inline]
pub fn path(&self) -> &str {
&self.path
}
#[inline]
pub fn source(&self) -> &str {
&self.source
}
pub fn line_column(&self, offset: usize) -> Option<(usize, usize)> {
if offset > self.source().len() {
return None;
}
let mut line = 1;
let mut column = 1;
for (idx, char) in self.source().char_indices() {
if idx >= offset {
break;
}
if char == '\n' {
line += 1;
column = 1;
} else {
column += 1;
}
}
Some((line, column))
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct Location {
file: Arc<File>,
offset: usize,
}
impl Location {
pub fn new(file: Arc<File>, offset: usize) -> Self {
Self::try_new(file, offset).expect("Offset should not be out of file's bounds")
}
pub fn try_new(file: Arc<File>, offset: usize) -> Option<Self> {
if offset > file.source().len() {
None
} else {
Some(Location { file, offset })
}
}
#[inline]
pub fn file(&self) -> Arc<File> {
self.file.clone()
}
#[inline]
pub fn offset(&self) -> usize {
self.offset
}
pub fn line_column(&self) -> (usize, usize) {
self.file()
.line_column(self.offset())
.expect("Offset should not be out of file's bounds")
}
}
impl std::fmt::Debug for Location {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}@{}", self.file().path(), self.offset())
}
}
impl std::fmt::Display for Location {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (line, column) = self.line_column();
write!(f, "{}:{}:{}", self.file().path(), line, column)
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Note,
Warning,
Error,
Bug,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Report {
pub location: Option<Location>,
pub severity: Severity,
pub message: String,
}
impl Report {
pub fn new(severity: Severity, message: impl Into<String>) -> Self {
Self {
location: None,
severity,
message: message.into(),
}
}
pub fn bug(message: impl Into<String>) -> Self {
Self::new(Severity::Bug, message)
}
pub fn error(message: impl Into<String>) -> Self {
Self::new(Severity::Error, message)
}
pub fn warning(message: impl Into<String>) -> Self {
Self::new(Severity::Warning, message)
}
pub fn note(message: impl Into<String>) -> Self {
Self::new(Severity::Note, message)
}
pub fn location(mut self, location: impl Into<Option<Location>>) -> Self {
self.location = location.into();
self
}
pub fn render<'a>(&'a self, styles: &'a Styles) -> Renderer<'a> {
Renderer::new(styles, std::slice::from_ref(self))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Styles {
pub location: Style,
pub bug: Style,
pub error: Style,
pub warning: Style,
pub note: Style,
pub colon: Style,
pub message: Style,
pub snippet: Style,
pub cursor: Style,
}
impl Styles {
pub const fn styled() -> Self {
Self {
location: Style::new().fg_color(Some(Color::Ansi(AnsiColor::BrightWhite))),
bug: Style::new().fg_color(Some(Color::Ansi(AnsiColor::BrightRed))),
error: Style::new().fg_color(Some(Color::Ansi(AnsiColor::BrightRed))),
warning: Style::new().fg_color(Some(Color::Ansi(AnsiColor::BrightYellow))),
note: Style::new().fg_color(Some(Color::Ansi(AnsiColor::BrightCyan))),
colon: Style::new().fg_color(Some(Color::Ansi(AnsiColor::BrightBlack))),
message: Style::new().fg_color(Some(Color::Ansi(AnsiColor::BrightWhite))),
snippet: Style::new(),
cursor: Style::new().fg_color(Some(Color::Ansi(AnsiColor::BrightGreen))),
}
}
pub const fn plain() -> Self {
Self {
location: Style::new(),
bug: Style::new(),
error: Style::new(),
warning: Style::new(),
note: Style::new(),
colon: Style::new(),
message: Style::new(),
snippet: Style::new(),
cursor: Style::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Renderer<'a> {
styles: &'a Styles,
reports: &'a [Report],
}
impl<'a> Renderer<'a> {
pub const fn new(styles: &'a Styles, reports: &'a [Report]) -> Self {
Self { styles, reports }
}
}
impl<'a> std::fmt::Display for Renderer<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for report in self.reports {
let line_column = if let Some(location) = &report.location {
let (line, column) = location.file().line_column(location.offset()).unwrap();
write!(
f,
"{}{}:{}:{}:{} ",
&self.styles.location,
location.file.path(),
line,
column,
Reset
)?;
Some((location, line, column))
} else {
None
};
write!(f, "{}", Reset)?;
match report.severity {
Severity::Bug => write!(f, "{}bug", &self.styles.bug)?,
Severity::Error => write!(f, "{}error", &self.styles.error)?,
Severity::Warning => write!(f, "{}warning", &self.styles.warning)?,
Severity::Note => write!(f, "{}note", &self.styles.note)?,
}
write!(f, "{}", Reset)?;
write!(f, "{}: ", &self.styles.colon)?;
write!(f, "{}", Reset)?;
write!(f, "{}{}", &self.styles.message, &report.message)?;
if let Some((location, line, column)) = line_column {
let line = location.file.source().lines().nth(line - 1).unwrap();
writeln!(f, "{}", Reset)?;
writeln!(f, "{}{}", &self.styles.snippet, &line,)?;
let mut offset = 0;
let cursor_width = match line.chars().enumerate().find(|(idx, char)| {
if *idx == column - 1 {
true
} else {
offset += char.width().unwrap_or(1);
false
}
}) {
Some((_, char)) => char.width().unwrap_or(1),
None => 1,
};
write!(f, "{}", Reset)?;
write!(f, "{: <offset$}", "")?;
write!(
f,
"{}{:^<cursor_width$}",
&self.styles.cursor,
""
)?;
};
writeln!(f, "{}", Reset)?;
}
Ok(())
}
}
#[macro_export]
macro_rules! bug {
($($t:tt)*) => {{
$crate::Report::bug(format!($($t)*))
}};
}
#[macro_export]
macro_rules! error {
($($t:tt)*) => {{
$crate::Report::error(format!($($t)*))
}};
}
#[macro_export]
macro_rules! warning {
($($t:tt)*) => {{
$crate::Report::warning(format!($($t)*))
}};
}
#[macro_export]
macro_rules! note {
($($t:tt)*) => {{
$crate::Report::note(format!($($t)*))
}};
}