#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
mod display;
mod draw;
mod source;
mod write;
pub use crate::{
draw::{ColorGenerator, Fmt},
source::{sources, Cache, FileCache, FnCache, Line, Source},
};
pub use yansi::Color;
#[cfg(any(feature = "concolor", doc))]
pub use crate::draw::StdoutFmt;
use crate::display::*;
use std::{
cmp::{Eq, PartialEq},
fmt,
hash::Hash,
io::{self, Write},
ops::Range,
ops::RangeInclusive,
};
use unicode_width::UnicodeWidthChar;
pub trait Span {
type SourceId: PartialEq + ToOwned + ?Sized;
fn source(&self) -> &Self::SourceId;
fn start(&self) -> usize;
fn end(&self) -> usize;
fn len(&self) -> usize {
self.end().saturating_sub(self.start())
}
fn is_empty(&self) -> bool {
self.len() == 0
}
fn contains(&self, offset: usize) -> bool {
(self.start()..self.end()).contains(&offset)
}
}
impl Span for Range<usize> {
type SourceId = ();
fn source(&self) -> &Self::SourceId {
&()
}
fn start(&self) -> usize {
self.start
}
fn end(&self) -> usize {
self.end
}
}
impl<Id: fmt::Debug + Hash + PartialEq + Eq + ToOwned> Span for (Id, Range<usize>) {
type SourceId = Id;
fn source(&self) -> &Self::SourceId {
&self.0
}
fn start(&self) -> usize {
self.1.start
}
fn end(&self) -> usize {
self.1.end
}
}
impl Span for RangeInclusive<usize> {
type SourceId = ();
fn source(&self) -> &Self::SourceId {
&()
}
fn start(&self) -> usize {
*self.start()
}
fn end(&self) -> usize {
*self.end() + 1
}
}
impl<Id: fmt::Debug + Hash + PartialEq + Eq + ToOwned> Span for (Id, RangeInclusive<usize>) {
type SourceId = Id;
fn source(&self) -> &Self::SourceId {
&self.0
}
fn start(&self) -> usize {
*self.1.start()
}
fn end(&self) -> usize {
*self.1.end() + 1
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
struct LabelDisplay {
msg: Option<String>,
color: Option<Color>,
order: i32,
priority: i32,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Label<S = Range<usize>> {
span: S,
display_info: LabelDisplay,
}
impl<S: Span> Label<S> {
pub fn new(span: S) -> Self {
assert!(span.start() <= span.end(), "Label start is after its end");
Self {
span,
display_info: LabelDisplay {
msg: None,
color: None,
order: 0,
priority: 0,
},
}
}
pub fn with_message<M: ToString>(mut self, msg: M) -> Self {
self.display_info.msg = Some(msg.to_string());
self
}
pub fn with_color(mut self, color: Color) -> Self {
self.display_info.color = Some(color);
self
}
pub fn with_order(mut self, order: i32) -> Self {
self.display_info.order = order;
self
}
pub fn with_priority(mut self, priority: i32) -> Self {
self.display_info.priority = priority;
self
}
}
pub struct Report<'a, S: Span = Range<usize>> {
kind: ReportKind<'a>,
code: Option<String>,
msg: Option<String>,
notes: Vec<String>,
help: Vec<String>,
span: S,
labels: Vec<Label<S>>,
config: Config,
}
impl<S: Span> Report<'_, S> {
pub fn build(kind: ReportKind, span: S) -> ReportBuilder<S> {
ReportBuilder {
kind,
code: None,
msg: None,
notes: vec![],
help: vec![],
span,
labels: Vec::new(),
config: Config::default(),
}
}
pub fn eprint<C: Cache<S::SourceId>>(&self, cache: C) -> io::Result<()> {
self.write(cache, io::stderr())
}
pub fn print<C: Cache<S::SourceId>>(&self, cache: C) -> io::Result<()> {
self.write_for_stdout(cache, io::stdout())
}
}
impl<'a, S: Span> fmt::Debug for Report<'a, S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Report")
.field("kind", &self.kind)
.field("code", &self.code)
.field("msg", &self.msg)
.field("notes", &self.notes)
.field("help", &self.help)
.field("config", &self.config)
.finish()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ReportKind<'a> {
Error,
Warning,
Advice,
Custom(&'a str, Color),
}
impl fmt::Display for ReportKind<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ReportKind::Error => write!(f, "Error"),
ReportKind::Warning => write!(f, "Warning"),
ReportKind::Advice => write!(f, "Advice"),
ReportKind::Custom(s, _) => write!(f, "{}", s),
}
}
}
pub struct ReportBuilder<'a, S: Span> {
kind: ReportKind<'a>,
code: Option<String>,
msg: Option<String>,
notes: Vec<String>,
help: Vec<String>,
span: S,
labels: Vec<Label<S>>,
config: Config,
}
impl<'a, S: Span> ReportBuilder<'a, S> {
pub fn with_code<C: fmt::Display>(mut self, code: C) -> Self {
self.code = Some(format!("{:02}", code));
self
}
pub fn set_message<M: ToString>(&mut self, msg: M) {
self.msg = Some(msg.to_string());
}
pub fn with_message<M: ToString>(mut self, msg: M) -> Self {
self.msg = Some(msg.to_string());
self
}
pub fn set_note<N: ToString>(&mut self, note: N) {
self.notes = vec![note.to_string()];
}
pub fn add_note<N: ToString>(&mut self, note: N) {
self.notes.push(note.to_string());
}
pub fn with_notes<N: IntoIterator<Item = impl ToString>>(&mut self, notes: N) {
for note in notes {
self.add_note(note)
}
}
pub fn with_note<N: ToString>(mut self, note: N) -> Self {
self.add_note(note);
self
}
pub fn set_help<N: ToString>(&mut self, note: N) {
self.help = vec![note.to_string()];
}
pub fn add_help<N: ToString>(&mut self, note: N) {
self.help.push(note.to_string());
}
pub fn with_helps<N: IntoIterator<Item = impl ToString>>(&mut self, helps: N) {
for help in helps {
self.add_help(help)
}
}
pub fn with_help<N: ToString>(mut self, note: N) -> Self {
self.add_help(note);
self
}
pub fn add_label(&mut self, label: Label<S>) {
self.add_labels(std::iter::once(label));
}
pub fn add_labels<L: IntoIterator<Item = Label<S>>>(&mut self, labels: L) {
let config = &self.config; self.labels.extend(labels.into_iter().map(|mut label| {
label.display_info.color = config.filter_color(label.display_info.color);
label
}));
}
pub fn with_label(mut self, label: Label<S>) -> Self {
self.add_label(label);
self
}
pub fn with_labels<L: IntoIterator<Item = Label<S>>>(mut self, labels: L) -> Self {
self.add_labels(labels);
self
}
pub fn with_config(mut self, config: Config) -> Self {
self.config = config;
self
}
pub fn finish(self) -> Report<'a, S> {
Report {
kind: self.kind,
code: self.code,
msg: self.msg,
notes: self.notes,
help: self.help,
span: self.span,
labels: self.labels,
config: self.config,
}
}
}
impl<'a, S: Span> fmt::Debug for ReportBuilder<'a, S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ReportBuilder")
.field("kind", &self.kind)
.field("code", &self.code)
.field("msg", &self.msg)
.field("notes", &self.notes)
.field("help", &self.help)
.field("config", &self.config)
.finish()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum LabelAttach {
Start,
Middle,
End,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CharSet {
Unicode,
Ascii,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum IndexType {
Byte,
Char,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Config {
cross_gap: bool,
label_attach: LabelAttach,
compact: bool,
underlines: bool,
multiline_arrows: bool,
color: bool,
tab_width: usize,
char_set: CharSet,
index_type: IndexType,
}
impl Config {
pub const fn with_cross_gap(mut self, cross_gap: bool) -> Self {
self.cross_gap = cross_gap;
self
}
pub const fn with_label_attach(mut self, label_attach: LabelAttach) -> Self {
self.label_attach = label_attach;
self
}
pub const fn with_compact(mut self, compact: bool) -> Self {
self.compact = compact;
self
}
pub const fn with_underlines(mut self, underlines: bool) -> Self {
self.underlines = underlines;
self
}
pub const fn with_multiline_arrows(mut self, multiline_arrows: bool) -> Self {
self.multiline_arrows = multiline_arrows;
self
}
pub const fn with_color(mut self, color: bool) -> Self {
self.color = color;
self
}
pub const fn with_tab_width(mut self, tab_width: usize) -> Self {
self.tab_width = tab_width;
self
}
pub const fn with_char_set(mut self, char_set: CharSet) -> Self {
self.char_set = char_set;
self
}
pub const fn with_index_type(mut self, index_type: IndexType) -> Self {
self.index_type = index_type;
self
}
fn error_color(&self) -> Option<Color> {
Some(Color::Red).filter(|_| self.color)
}
fn warning_color(&self) -> Option<Color> {
Some(Color::Yellow).filter(|_| self.color)
}
fn advice_color(&self) -> Option<Color> {
Some(Color::Fixed(147)).filter(|_| self.color)
}
fn margin_color(&self) -> Option<Color> {
Some(Color::Fixed(246)).filter(|_| self.color)
}
fn skipped_margin_color(&self) -> Option<Color> {
Some(Color::Fixed(240)).filter(|_| self.color)
}
fn unimportant_color(&self) -> Option<Color> {
Some(Color::Fixed(249)).filter(|_| self.color)
}
fn note_color(&self) -> Option<Color> {
Some(Color::Fixed(115)).filter(|_| self.color)
}
fn filter_color(&self, color: Option<Color>) -> Option<Color> {
color.filter(|_| self.color)
}
fn char_width(&self, c: char, col: usize) -> (char, usize) {
match c {
'\t' => {
let tab_end = (col / self.tab_width + 1) * self.tab_width;
(' ', tab_end - col)
}
c if c.is_whitespace() => (' ', 1),
_ => (c, c.width().unwrap_or(1)),
}
}
pub const fn new() -> Self {
Self {
cross_gap: true,
label_attach: LabelAttach::Middle,
compact: false,
underlines: true,
multiline_arrows: true,
color: true,
tab_width: 4,
char_set: CharSet::Unicode,
index_type: IndexType::Char,
}
}
}
impl Default for Config {
fn default() -> Self {
Self::new()
}
}
#[test]
#[should_panic]
#[allow(clippy::reversed_empty_ranges)]
fn backwards_label_should_panic() {
Label::new(1..0);
}