#![cfg_attr(doc, doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__doc.snap")))]
#![cfg_attr(doc, doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt_doc_alt.snap")))]
mod charset;
mod color;
mod config;
#[cfg(any(feature = "std", feature = "hooks"))]
mod hook;
mod location;
mod r#override;
use alloc::{
borrow::ToOwned,
collections::VecDeque,
format,
string::{String, ToString as _},
vec,
vec::Vec,
};
use core::{
error::Error,
fmt::{self, Debug, Display, Formatter},
iter::once,
mem,
};
pub use charset::Charset;
pub use color::ColorMode;
#[cfg(any(feature = "std", feature = "hooks"))]
pub use hook::HookContext;
#[cfg(any(feature = "std", feature = "hooks"))]
pub(crate) use hook::{Format, Hooks, install_builtin_hooks};
#[cfg(not(any(feature = "std", feature = "hooks")))]
use location::LocationAttachment;
use crate::{
AttachmentKind, Frame, FrameKind, Report,
fmt::{
color::{Color, DisplayStyle, Style},
config::Config,
},
};
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum Symbol {
Vertical,
VerticalRight,
Horizontal,
HorizontalLeft,
HorizontalDown,
ArrowRight,
CurveRight,
Space,
}
macro_rules! sym {
(#char '│') => {
Symbol::Vertical
};
(#char '├') => {
Symbol::VerticalRight
};
(#char '─') => {
Symbol::Horizontal
};
(#char '╴') => {
Symbol::HorizontalLeft
};
(#char '┬') => {
Symbol::HorizontalDown
};
(#char '▶') => {
Symbol::ArrowRight
};
(#char '╰') => {
Symbol::CurveRight
};
(#char ' ') => {
Symbol::Space
};
($($char:tt),+) => {
&[$(sym!(#char $char)),*]
};
}
impl Symbol {
const fn to_str_utf8(self) -> &'static str {
match self {
Self::Vertical => "\u{2502}", Self::VerticalRight => "\u{251c}", Self::Horizontal => "\u{2500}", Self::HorizontalLeft => "\u{2574}", Self::HorizontalDown => "\u{252c}", Self::ArrowRight => "\u{25b6}", Self::CurveRight => "\u{2570}", Self::Space => " ",
}
}
const fn to_str_ascii(self) -> &'static str {
match self {
Self::Vertical | Self::VerticalRight | Self::CurveRight => "|",
Self::Horizontal | Self::HorizontalDown | Self::HorizontalLeft => "-",
Self::ArrowRight => ">",
Self::Space => " ",
}
}
const fn to_str(self, charset: Charset) -> &'static str {
match charset {
Charset::Utf8 => self.to_str_utf8(),
Charset::Ascii => self.to_str_ascii(),
}
}
}
struct SymbolDisplay<'a> {
inner: &'a [Symbol],
charset: Charset,
}
impl Display for SymbolDisplay<'_> {
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
for symbol in self.inner {
fmt.write_str(symbol.to_str(self.charset))?;
}
Ok(())
}
}
#[derive(Debug, Copy, Clone)]
enum Position {
First,
Inner,
Final,
}
#[derive(Debug, Copy, Clone)]
enum Spacing {
Full,
Minimal,
}
#[derive(Debug, Copy, Clone)]
struct Indent {
group: bool,
visible: bool,
spacing: Option<Spacing>,
}
impl Indent {
const fn new(group: bool) -> Self {
Self {
group,
visible: true,
spacing: Some(Spacing::Full),
}
}
fn spacing(mut self, spacing: impl Into<Option<Spacing>>) -> Self {
self.spacing = spacing.into();
self
}
const fn visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
const fn group() -> Self {
Self::new(true)
}
const fn no_group() -> Self {
Self::new(false)
}
const fn prepare(self) -> &'static [Symbol] {
match self {
Self {
group: true,
visible: true,
spacing: Some(_),
} => sym!(' ', '│', ' ', ' '),
Self {
group: true,
visible: true,
spacing: None,
} => sym!(' ', '│'),
Self {
group: false,
visible: true,
spacing: Some(Spacing::Full),
} => sym!('│', ' ', ' ', ' '),
Self {
group: false,
visible: true,
spacing: Some(Spacing::Minimal),
} => sym!('│', ' '),
Self {
group: false,
visible: true,
spacing: None,
} => sym!('│'),
Self {
visible: false,
spacing: Some(Spacing::Full),
..
} => sym!(' ', ' ', ' ', ' '),
Self {
visible: false,
spacing: Some(Spacing::Minimal),
..
} => sym!(' ', ' '),
Self {
visible: false,
spacing: None,
..
} => &[],
}
}
}
impl From<Indent> for Instruction {
fn from(indent: Indent) -> Self {
Self::Indent(indent)
}
}
#[derive(Debug)]
enum Instruction {
Value {
value: String,
style: Style,
},
Group {
position: Position,
},
Context {
position: Position,
},
Attachment {
position: Position,
},
Indent(Indent),
}
enum PreparedInstruction<'a> {
Symbols(&'a [Symbol]),
Content(&'a str, &'a Style),
}
impl Instruction {
#[expect(
clippy::match_same_arms,
reason = "the match arms are the same intentionally, this makes it more clean which \
variant emits which and also keeps it nicely formatted."
)]
fn prepare(&self) -> PreparedInstruction<'_> {
match self {
Self::Value { value, style } => PreparedInstruction::Content(value, style),
Self::Group { position } => match position {
Position::First => PreparedInstruction::Symbols(sym!('╰', '┬', '▶', ' ')),
Position::Inner => PreparedInstruction::Symbols(sym!(' ', '├', '▶', ' ')),
Position::Final => PreparedInstruction::Symbols(sym!(' ', '╰', '▶', ' ')),
},
Self::Context { position } => match position {
Position::First => PreparedInstruction::Symbols(sym!('├', '─', '▶', ' ')),
Position::Inner => PreparedInstruction::Symbols(sym!('├', '─', '▶', ' ')),
Position::Final => PreparedInstruction::Symbols(sym!('╰', '─', '▶', ' ')),
},
Self::Attachment { position } => match position {
Position::First => PreparedInstruction::Symbols(sym!('├', '╴')),
Position::Inner => PreparedInstruction::Symbols(sym!('├', '╴')),
Position::Final => PreparedInstruction::Symbols(sym!('╰', '╴')),
},
Self::Indent(indent) => PreparedInstruction::Symbols(indent.prepare()),
}
}
}
struct InstructionDisplay<'a> {
color: ColorMode,
charset: Charset,
instruction: &'a Instruction,
}
impl Display for InstructionDisplay<'_> {
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
match self.instruction.prepare() {
PreparedInstruction::Symbols(symbols) => {
let display = SymbolDisplay {
inner: symbols,
charset: self.charset,
};
let mut style = Style::new();
if self.color == ColorMode::Color {
style.set_foreground(Color::Red, false);
}
Display::fmt(&style.apply(&display), fmt)?;
}
PreparedInstruction::Content(value, &style) => Display::fmt(&style.apply(&value), fmt)?,
}
Ok(())
}
}
struct Line(Vec<Instruction>);
impl Line {
const fn new() -> Self {
Self(Vec::new())
}
fn push(mut self, instruction: Instruction) -> Self {
self.0.push(instruction);
self
}
fn into_lines(self) -> Lines {
let lines = Lines::new();
lines.after(self)
}
}
struct LineDisplay<'a> {
color: ColorMode,
charset: Charset,
line: &'a Line,
}
impl Display for LineDisplay<'_> {
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
for instruction in self.line.0.iter().rev() {
Display::fmt(
&InstructionDisplay {
color: self.color,
charset: self.charset,
instruction,
},
fmt,
)?;
}
Ok(())
}
}
struct Lines(VecDeque<Line>);
impl Lines {
const fn new() -> Self {
Self(VecDeque::new())
}
fn into_iter(self) -> alloc::collections::vec_deque::IntoIter<Line> {
self.0.into_iter()
}
fn then(mut self, other: Self) -> Self {
self.0.extend(other.0);
self
}
fn before(mut self, line: Line) -> Self {
self.0.push_front(line);
self
}
fn after(mut self, line: Line) -> Self {
self.0.push_back(line);
self
}
fn into_vec(self) -> Vec<Line> {
self.0.into_iter().collect()
}
}
impl FromIterator<Line> for Lines {
fn from_iter<T: IntoIterator<Item = Line>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
fn collect<'a>(root: &'a Frame, prefix: &'a [&Frame]) -> (Vec<&'a Frame>, &'a [Frame]) {
let mut stack = vec![];
stack.extend(prefix);
stack.push(root);
let mut ptr = Some(root);
let mut next: &'a [_] = &[];
while let Some(current) = ptr.take() {
let sources = current.sources();
match sources {
[parent] => {
stack.push(parent);
ptr = Some(parent);
}
sources => {
next = sources;
}
}
}
(stack, next)
}
fn partition<'a>(stack: &'a [&'a Frame]) -> (Vec<(&'a Frame, Vec<&'a Frame>)>, Vec<&'a Frame>) {
let mut result = vec![];
let mut queue = vec![];
for frame in stack {
if matches!(frame.kind(), FrameKind::Context(_)) {
let frames = mem::take(&mut queue);
result.push((*frame, frames));
} else {
queue.push(*frame);
}
}
(result, queue)
}
fn debug_context(context: &(dyn Error + Send + Sync + 'static), mode: ColorMode) -> Lines {
context
.to_string()
.lines()
.map(ToOwned::to_owned)
.enumerate()
.map(|(idx, value)| {
if idx == 0 {
let mut style = Style::new();
if mode == ColorMode::Color || mode == ColorMode::Emphasis {
style.set_display(DisplayStyle::new().with_bold(true));
}
Line::new().push(Instruction::Value { value, style })
} else {
Line::new().push(Instruction::Value {
value,
style: Style::new(),
})
}
})
.collect()
}
struct Opaque(usize);
impl Opaque {
const fn new() -> Self {
Self(0)
}
const fn increase(&mut self) {
self.0 += 1;
}
fn render(self) -> Option<Line> {
match self.0 {
0 => None,
1 => Some(Line::new().push(Instruction::Value {
value: "1 additional opaque attachment".to_owned(),
style: Style::new(),
})),
n => Some(Line::new().push(Instruction::Value {
value: format!("{n} additional opaque attachments"),
style: Style::new(),
})),
}
}
}
#[cfg_attr(
not(any(feature = "std", feature = "hooks")),
expect(clippy::needless_pass_by_ref_mut)
)]
fn debug_attachments_invoke<'a>(
frames: impl IntoIterator<Item = &'a Frame>,
config: &mut Config,
) -> (Opaque, Vec<String>) {
let mut opaque = Opaque::new();
#[cfg(any(feature = "std", feature = "hooks"))]
let context = config.context();
let body = frames
.into_iter()
.map(|frame| match frame.kind() {
#[cfg(any(feature = "std", feature = "hooks"))]
FrameKind::Context(_) => Some(
if Report::invoke_debug_format_hook(|hooks| hooks.call(frame, context)) {
context.take_body()
} else {
Vec::new()
},
),
#[cfg(any(feature = "std", feature = "hooks"))]
FrameKind::Attachment(AttachmentKind::Printable(attachment)) => Some(
if Report::invoke_debug_format_hook(|hooks| hooks.call(frame, context)) {
context.take_body()
} else {
vec![attachment.to_string()]
},
),
#[cfg(any(feature = "std", feature = "hooks"))]
FrameKind::Attachment(AttachmentKind::Opaque(_)) => {
Report::invoke_debug_format_hook(|hooks| hooks.call(frame, context))
.then(|| context.take_body())
}
#[cfg(not(any(feature = "std", feature = "hooks")))]
FrameKind::Context(_) => Some(vec![]),
#[cfg(not(any(feature = "std", feature = "hooks")))]
FrameKind::Attachment(AttachmentKind::Printable(attachment)) => {
Some(vec![attachment.to_string()])
}
#[cfg(not(any(feature = "std", feature = "hooks")))]
FrameKind::Attachment(AttachmentKind::Opaque(_)) => frame
.downcast_ref::<core::panic::Location<'static>>()
.map(|location| {
vec![LocationAttachment::new(location, config.color_mode()).to_string()]
}),
})
.flat_map(|body| {
body.unwrap_or_else(|| {
opaque.increase();
Vec::new()
})
})
.collect();
(opaque, body)
}
fn debug_attachments<'a>(
position: Position,
frames: impl IntoIterator<Item = &'a Frame>,
config: &mut Config,
) -> Lines {
let last = matches!(position, Position::Final);
let (opaque, entries) = debug_attachments_invoke(frames, config);
let opaque = opaque.render();
let len = entries.len() + opaque.as_ref().map_or(0, |_| 1);
let lines = entries.into_iter().map(|value| {
value
.lines()
.map(ToOwned::to_owned)
.map(|line| {
Line::new().push(Instruction::Value {
value: line,
style: Style::new(),
})
})
.collect::<Vec<_>>()
});
lines
.chain(opaque.into_iter().map(|line| line.into_lines().into_vec()))
.enumerate()
.flat_map(|(idx, lines)| {
let position = match idx {
pos if pos + 1 == len && last => Position::Final,
0 => Position::First,
_ => Position::Inner,
};
lines.into_iter().enumerate().map(move |(idx, line)| {
if idx == 0 {
line.push(Instruction::Attachment { position })
} else {
line.push(
Indent::no_group()
.visible(!matches!(position, Position::Final))
.spacing(Spacing::Minimal)
.into(),
)
}
})
})
.collect()
}
fn debug_render(head: Lines, contexts: VecDeque<Lines>, sources: Vec<Lines>) -> Lines {
let len = sources.len();
let sources = sources
.into_iter()
.enumerate()
.map(|(idx, lines)| {
let position = match idx {
pos if pos + 1 == len => Position::Final,
0 => Position::First,
_ => Position::Inner,
};
lines
.into_iter()
.enumerate()
.map(|(idx, line)| {
if idx == 0 {
line.push(Instruction::Group { position })
} else {
line.push(
Indent::group()
.visible(!matches!(position, Position::Final))
.into(),
)
}
})
.collect::<Lines>()
.before(
Line::new().push(Indent::new(idx != 0).spacing(None).into()),
)
})
.collect::<Vec<_>>();
let tail = !sources.is_empty();
let len = contexts.len();
let contexts = contexts.into_iter().enumerate().flat_map(|(idx, lines)| {
let position = match idx {
pos if pos + 1 == len && !tail => Position::Final,
0 => Position::First,
_ => Position::Inner,
};
let mut lines = lines
.into_iter()
.enumerate()
.map(|(idx, line)| {
if idx == 0 {
line.push(Instruction::Context { position })
} else {
line.push(
Indent::no_group()
.visible(!matches!(position, Position::Final))
.into(),
)
}
})
.collect::<Vec<_>>();
lines.insert(0, Line::new().push(Indent::no_group().spacing(None).into()));
lines
});
head.into_iter()
.chain(contexts)
.chain(sources.into_iter().flat_map(Lines::into_vec))
.collect()
}
fn debug_frame(root: &Frame, prefix: &[&Frame], config: &mut Config) -> Vec<Lines> {
let (stack, sources) = collect(root, prefix);
let (stack, prefix) = partition(&stack);
let len = stack.len();
let mut contexts: VecDeque<_> = stack
.into_iter()
.enumerate()
.map(|(idx, (head, mut body))| {
let head_context = debug_context(
match head.kind() {
FrameKind::Context(context) => context,
FrameKind::Attachment(_) => unreachable!(),
},
config.color_mode(),
);
body.reverse();
let body = debug_attachments(
if (len == 1 && sources.is_empty()) || idx > 0 {
Position::Final
} else {
Position::Inner
},
once(head).chain(body),
config,
);
head_context.then(body)
})
.collect();
let sources = sources
.iter()
.flat_map(
|source| debug_frame(source, &prefix, config),
)
.collect::<Vec<_>>();
if contexts.is_empty() {
return sources;
}
let head = contexts
.pop_front()
.expect("should always have single context");
vec![debug_render(head, contexts, sources)]
}
impl<C: ?Sized> Debug for Report<C> {
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
let mut config = Config::load(fmt.alternate());
let color = config.color_mode();
let charset = config.charset();
#[cfg_attr(not(any(feature = "std", feature = "hooks")), expect(unused_mut))]
let mut lines = self
.current_frames_unchecked()
.iter()
.flat_map(|frame| debug_frame(frame, &[], &mut config))
.enumerate()
.flat_map(|(idx, lines)| {
if idx == 0 {
lines.into_vec()
} else {
lines
.before(
Line::new()
.push(Indent::no_group().visible(false).spacing(None).into()),
)
.into_vec()
}
})
.map(|line| {
LineDisplay {
color,
charset,
line: &line,
}
.to_string()
})
.collect::<Vec<_>>()
.join("\n");
#[cfg(any(feature = "std", feature = "hooks"))]
{
let appendix = config
.context::<Frame>()
.appendix()
.iter()
.map(
|snippet| snippet.trim_end_matches('\n').to_owned(),
)
.collect::<Vec<_>>()
.join("\n\n");
if !appendix.is_empty() {
lines.reserve(44 + appendix.len());
lines.push_str("\n\n");
if charset == Charset::Utf8 {
lines.push_str(&"\u{2501}".repeat(40)); } else {
lines.push_str(&"=".repeat(40));
}
lines.push_str("\n\n");
lines.push_str(&appendix);
}
}
fmt.write_str(&lines)
}
}
impl<C: ?Sized> Display for Report<C> {
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
for (index, frame) in self
.frames()
.filter_map(|frame| match frame.kind() {
FrameKind::Context(context) => Some(context.to_string()),
FrameKind::Attachment(_) => None,
})
.enumerate()
{
if index == 0 {
fmt::Display::fmt(&frame, fmt)?;
if !fmt.alternate() {
break;
}
} else {
write!(fmt, ": {frame}")?;
}
}
Ok(())
}
}