#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__doc.snap"))]
#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt_doc_alt.snap"))]
#[cfg(feature = "std")]
mod hook;
use alloc::{
borrow::ToOwned,
collections::VecDeque,
format,
string::{String, ToString},
vec,
vec::Vec,
};
use core::{
fmt::{self, Debug, Display, Formatter},
iter::once,
mem,
};
#[cfg(feature = "std")]
pub use hook::HookContext;
#[cfg(feature = "std")]
pub(crate) use hook::{install_builtin_hooks, Hooks};
#[cfg(feature = "pretty-print")]
use owo_colors::{OwoColorize, Stream, Style as OwOStyle};
use crate::{AttachmentKind, Context, Frame, FrameKind, Report};
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum Symbol {
Vertical,
VerticalRight,
Horizontal,
HorizontalLeft,
HorizontalDown,
ArrowRight,
CurveRight,
Space,
}
macro_rules! sym {
(#char '@') => {
Symbol::Location
};
(#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)),*]
};
}
#[cfg(feature = "pretty-print")]
impl Display for Symbol {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Vertical => f.write_str("│"),
Self::VerticalRight => f.write_str("├"),
Self::Horizontal => f.write_str("─"),
Self::HorizontalLeft => f.write_str("╴"),
Self::HorizontalDown => f.write_str("┬"),
Self::ArrowRight => f.write_str("▶"),
Self::CurveRight => f.write_str("╰"),
Self::Space => f.write_str(" "),
}
}
}
#[cfg(not(feature = "pretty-print"))]
impl Display for Symbol {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Vertical | Self::VerticalRight | Self::CurveRight => f.write_str("|"),
Self::Horizontal | Self::HorizontalDown | Self::HorizontalLeft => f.write_str("-"),
Self::ArrowRight => f.write_str(">"),
Self::Space => f.write_str(" "),
}
}
}
#[derive(Debug, Copy, Clone)]
struct Style {
bold: bool,
}
impl Style {
const fn new() -> Self {
Self { bold: false }
}
const fn bold(mut self) -> Self {
self.bold = true;
self
}
}
#[cfg(feature = "pretty-print")]
impl From<Style> for OwOStyle {
fn from(value: Style) -> Self {
let mut this = Self::new();
if value.bold {
this = this.bold();
}
this
}
}
#[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 {
#[allow(clippy::match_same_arms)]
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()),
}
}
}
#[cfg(feature = "pretty-print")]
impl Display for Instruction {
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
match self.prepare() {
PreparedInstruction::Symbols(symbols) => {
for symbol in symbols {
Display::fmt(
&symbol.if_supports_color(Stream::Stdout, OwoColorize::red),
fmt,
)?;
}
}
PreparedInstruction::Content(value, &style) => Display::fmt(
&value.if_supports_color(Stream::Stdout, |value| value.style(style.into())),
fmt,
)?,
}
Ok(())
}
}
#[cfg(not(feature = "pretty-print"))]
impl Display for Instruction {
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
match self.prepare() {
PreparedInstruction::Symbols(symbols) => {
for symbol in symbols {
Display::fmt(symbol, fmt)?;
}
}
PreparedInstruction::Content(value, _) => fmt.write_str(value)?,
}
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)
}
}
impl Display for Line {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
for instruction in self.0.iter().rev() {
Display::fmt(instruction, f)?;
}
Ok(())
}
}
struct Lines(VecDeque<Line>);
impl Lines {
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 Context) -> Lines {
context
.to_string()
.lines()
.map(ToOwned::to_owned)
.enumerate()
.map(|(idx, value)| {
if idx == 0 {
Line::new().push(Instruction::Value {
value,
style: Style::new().bold(),
})
} else {
Line::new().push(Instruction::Value {
value,
style: Style::new(),
})
}
})
.collect()
}
struct Opaque(usize);
impl Opaque {
const fn new() -> Self {
Self(0)
}
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(),
})),
}
}
}
fn debug_attachments_invoke<'a>(
frames: impl IntoIterator<Item = &'a Frame>,
#[cfg(feature = "std")] context: &mut HookContext<Frame>,
) -> (Opaque, Vec<String>) {
let mut opaque = Opaque::new();
let body = frames
.into_iter()
.map(|frame| match frame.kind() {
#[cfg(feature = "std")]
FrameKind::Context(_) => Some(
Report::invoke_debug_format_hook(|hooks| hooks.call(frame, context))
.then(|| context.take_body())
.unwrap_or_default(),
),
#[cfg(feature = "std")]
FrameKind::Attachment(AttachmentKind::Printable(attachment)) => Some(
Report::invoke_debug_format_hook(|hooks| hooks.call(frame, context))
.then(|| context.take_body())
.unwrap_or_else(|| vec![attachment.to_string()]),
),
#[cfg(feature = "std")]
FrameKind::Attachment(AttachmentKind::Opaque(_)) => {
Report::invoke_debug_format_hook(|hooks| hooks.call(frame, context))
.then(|| context.take_body())
}
#[cfg(not(feature = "std"))]
FrameKind::Context(_) => Some(vec![]),
#[cfg(not(feature = "std"))]
FrameKind::Attachment(AttachmentKind::Printable(attachment)) => {
Some(vec![attachment.to_string()])
}
#[cfg(all(not(feature = "std"), feature = "pretty-print"))]
FrameKind::Attachment(AttachmentKind::Opaque(_)) => frame
.downcast_ref::<core::panic::Location<'static>>()
.map(|location| {
vec![
location
.if_supports_color(Stream::Stdout, OwoColorize::bright_black)
.to_string(),
]
}),
#[cfg(all(not(feature = "std"), not(feature = "pretty-print")))]
FrameKind::Attachment(AttachmentKind::Opaque(_)) => frame
.downcast_ref::<core::panic::Location>()
.map(|location| vec![format!("at {location}")]),
})
.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>,
#[cfg(feature = "std")] context: &mut HookContext<Frame>,
) -> Lines {
let last = matches!(position, Position::Final);
let (opaque, entries) = debug_attachments_invoke(
frames,
#[cfg(feature = "std")]
context,
);
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],
#[cfg(feature = "std")] context: &mut HookContext<Frame>,
) -> 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(c) => c,
FrameKind::Attachment(_) => unreachable!(),
});
body.reverse();
let body = debug_attachments(
if (len == 1 && sources.is_empty()) || idx > 0 {
Position::Final
} else {
Position::Inner
},
once(head).chain(body),
#[cfg(feature = "std")]
context,
);
head_context.then(body)
})
.collect();
let sources = sources
.iter()
.flat_map(
|source| {
debug_frame(
source,
&prefix,
#[cfg(feature = "std")]
context,
)
},
)
.collect::<Vec<_>>();
if contexts.is_empty() {
return sources;
}
let head = contexts.pop_front().unwrap();
vec![debug_render(head, contexts, sources)]
}
impl<C> Debug for Report<C> {
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
#[cfg(feature = "std")]
if let Some(result) = Report::invoke_debug_hook(|hook| hook(self.generalized(), fmt)) {
return result;
}
#[cfg(feature = "std")]
let mut context = HookContext::new(fmt.alternate());
#[cfg_attr(not(feature = "std"), allow(unused_mut))]
let mut lines = self
.current_frames()
.iter()
.flat_map(|frame| {
debug_frame(
frame,
&[],
#[cfg(feature = "std")]
&mut context,
)
})
.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| line.to_string())
.collect::<Vec<_>>()
.join("\n");
#[cfg(feature = "std")]
{
let appendix = context
.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");
#[cfg(feature = "pretty-print")]
{
lines.push_str(&"━".repeat(40));
}
#[cfg(not(feature = "pretty-print"))]
{
lines.push_str(&"=".repeat(40));
}
lines.push_str("\n\n");
lines.push_str(&appendix);
}
}
fmt.write_str(&lines)
}
}
impl<Context> Display for Report<Context> {
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
#[cfg(feature = "std")]
if let Some(result) = Report::invoke_display_hook(|hook| hook(self.generalized(), fmt)) {
return 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(())
}
}