error_stack/fmt/
mod.rs

1//! Implementation of formatting, to enable colors and the use of box-drawing characters use the
2//! `pretty-print` feature.
3//!
4//! > **Note:** `error-stack` does not provide any stability guarantees for the [`Debug`] output.
5//!
6//! # Hooks
7//!
8//! The [`Debug`] implementation can be easily extended using hooks. Hooks are functions of the
9//! signature `Fn(&T, &mut HookContext<T>)`, they provide an ergonomic way to partially modify the
10//! output format and enable custom output for types that are not necessarily added via
11//! [`Report::attach_printable`] or are unable to implement [`Display`].
12//!
13//! Hooks can be attached through the central hooking mechanism which `error-stack`
14//! provides via [`Report::install_debug_hook`].
15//!
16//! Hooks are called for contexts which provide additional values through [`Context::provide`] and
17//! attachments which are added via [`Report::attach`] or [`Report::attach_printable`]. The order of
18//! [`Report::install_debug_hook`] calls determines the order of the rendered output. Note, that
19//! Hooks get called on all values provided by [`Context::provide`], but not on the [`Context`]
20//! object itself. Therefore if you want to call a hook on a [`Context`] to print in addition to its
21//! [`Display`] implementation, you may want to call [`request.provide_ref(self)`] inside of
22//! [`Context::provide`].
23//!
24//! [`request.provide_ref(self)`]: core::error::Request::provide_ref
25//!
26//! Hook functions need to be [`Fn`] and **not** [`FnMut`], which means they are unable to directly
27//! mutate state outside of the closure.
28//! You can still achieve mutable state outside of the scope of your closure through interior
29//! mutability, e.g. by using the [`std::sync`] module like [`Mutex`], [`RwLock`], and [`atomic`]s.
30//!
31//! The type, a hook will be called for, is determined by the type of the first argument to the
32//! closure. This type can either be specified at the closure level or when calling
33//! [`Report::install_debug_hook`].
34//! This type needs to be `'static`, [`Send`], and [`Sync`].
35//!
36//! You can then add additional entries to the body with [`HookContext::push_body`], and entries to
37//! the appendix with [`HookContext::push_appendix`], refer to the documentation of [`HookContext`]
38//! for further information.
39//!
40//! ## Example
41//!
42//! ```rust
43//! # // we only test with nightly, which means that `render()` is unused on earlier version
44//! # #![cfg_attr(not(nightly), allow(dead_code, unused_variables, unused_imports))]
45//! use core::fmt::{Display, Formatter};
46//! use std::io::{Error, ErrorKind};
47//! use error_stack::Report;
48//!
49//! #[derive(Debug)]
50//! struct ErrorCode(u64);
51//!
52//! impl Display for ErrorCode {
53//!   fn fmt(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result {
54//!     write!(fmt, "error: {}", self.0)
55//!   }
56//! }
57//!
58//! struct Suggestion(&'static str);
59//! struct Warning(&'static str);
60//!
61//! // This hook will never be called, because a later invocation of `install_debug_hook` overwrites
62//! // the hook for the type `ErrorCode`.
63//! Report::install_debug_hook::<ErrorCode>(|_, _| {
64//!     unreachable!("will never be called");
65//! });
66//!
67//! // `HookContext` always has a type parameter, which needs to be the same as the type of the
68//! // value, we use `HookContext` here as storage, to store values specific to this hook.
69//! // Here we make use of the auto-incrementing feature.
70//! // The incrementation is type specific, meaning that `context.increment()` for the `Suggestion` hook
71//! // will not influence the counter of the `ErrorCode` or `Warning` hook.
72//! Report::install_debug_hook::<Suggestion>(|Suggestion(value), context| {
73//!     let idx = context.increment_counter() + 1;
74//!     context.push_body(format!("suggestion {idx}: {value}"));
75//! });
76//!
77//! // Even though we used `attach_printable`, we can still use hooks, `Display` of a type attached
78//! // via `attach_printable` is only ever used when no hook was found.
79//! Report::install_debug_hook::<ErrorCode>(|ErrorCode(value), context| {
80//!     context.push_body(format!("error ({value})"));
81//! });
82//!
83//! Report::install_debug_hook::<Warning>(|Warning(value), context| {
84//!     let idx = context.increment_counter() + 1;
85//!
86//!     // we set a value, which will be removed on non-alternate views
87//!     // and is going to be appended to the actual return value.
88//!     if context.alternate() {
89//!         context.push_appendix(format!("warning {idx}:\n  {value}"));
90//!     }
91//!
92//!     context.push_body(format!("warning ({idx}) occurred"));
93//!  });
94//!
95//!
96//! let report = Report::new(Error::from(ErrorKind::InvalidInput))
97//!     .attach_printable(ErrorCode(404))
98//!     .attach(Suggestion("try to be connected to the internet."))
99//!     .attach(Suggestion("try better next time!"))
100//!     .attach(Warning("unable to fetch resource"));
101//!
102//! # Report::set_color_mode(error_stack::fmt::ColorMode::Emphasis);
103//! # fn render(value: String) -> String {
104//! #     let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?:  .*\n)*  .*").unwrap();
105//! #     let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
106//! #
107//! #     let value = backtrace.replace_all(&value, "backtrace no. $1\n  [redacted]");
108//! #     let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
109//! #
110//! #     ansi_to_html::convert(value.as_ref()).unwrap()
111//! # }
112//! #
113//! # #[cfg(nightly)]
114//! # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__doc.snap")].assert_eq(&render(format!("{report:?}")));
115//! #
116//! println!("{report:?}");
117//!
118//! # #[cfg(nightly)]
119//! # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt_doc_alt.snap")].assert_eq(&render(format!("{report:#?}")));
120//! #
121//! println!("{report:#?}");
122//! ```
123//!
124//! The output of `println!("{report:?}")`:
125//!
126//! <pre>
127#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__doc.snap"))]
128//! </pre>
129//!
130//! The output of `println!("{report:#?}")`:
131//!
132//! <pre>
133#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt_doc_alt.snap"))]
134//! </pre>
135//!
136//! ## Implementation Details
137//!
138//! Nothing explained here is under any semver guarantee. This section explains the algorithm used
139//! to produce the [`Debug`] output of a [`Report`].
140//!
141//! During the explanation we will make use of two different [`Report`]s, the overview tree (shown
142//! first) only visualizes contexts, while the second, more detailed tree shows attachments and
143//! contexts.
144//!
145//! In the detailed tree the type of [`Frame`] is distinguished using a superscript letter, `ᵃ` is
146//! used to indicate attachments and `ᶜ` is used to indicate contexts. For clarity the overview tree
147//! uses digits, while the detailed tree uses letters for different [`Frame`]s.
148//!
149//! Overview (Context only) Tree:
150//!
151//! ```text
152//!     0
153//!     |
154//!     1
155//!    / \
156//!   2   6
157//!  / \  |
158//! 3   4 7
159//!     | |
160//!     5 8
161//! ```
162//!
163//!
164//! Detailed (Context + Attachment) Tree:
165//!
166//! ```text
167//!    Aᶜ
168//!    |
169//!    Bᵃ
170//!   / \
171//!  Cᵃ  Eᵃ
172//!  |   |
173//!  Dᶜ  Fᵃ
174//!     / \
175//!    Gᵃ  Iᶜ
176//!    |
177//!    Hᶜ
178//! ```
179//!
180//! During formatting we distinguish between two cases (for contexts):
181//!
182//! * Lists
183//! * Groups
184//!
185//! in this explanation lists are delimited by `[` and `]`, while groups are delimited by `(` and
186//! `)`.
187//!
188//! While formatting we view the [`Report`]s as a tree of [`Frame`]s, therefore the following
189//! explanation will use terminology associated with trees, every [`Frame`] is a node and can have
190//! `0..n` children, a node that has no children (a leaf) is guaranteed to be a [`Context`].
191//!
192//! A list is a list of nodes where each node in the list is the parent of the next node in the list
193//! and only has a single child. The last node in the list is exempt of that rule of that rule and
194//! can have `0..n` children. In the examples above, `[6, 7, 8]` is considered a list, while `[1,
195//! 6]` is not, because while `1` is a parent of `6`, `1` has more than 1 child.
196//!
197//! A group is a list of nodes where each node shares a common immediate context parent that has
198//! more than `1` child, this means that `(2, 6)` is a group (they share `1` as an immediate context
199//! parent), while `(3, 4, 6)` is not. `(3, 4, 6)` share the same parent with more than 1 child
200//! (`1`), but `1` is not the immediate context parent of `3` and `4` (`2`) is. In the more detailed
201//! example `(Dᶜ, Hᶜ, Iᶜ)` is considered a group because they share the same *immediate* context
202//! parent `Aᶜ`, important to note is that we only refer to immediate context parents, `Fᵃ` is the
203//! immediate parent of `Iᶜ`, but is not a [`Context`], therefore to find the immediate context
204//! parent, we travel up the tree until we encounter our first [`Context`] node. Groups always
205//! contain lists, for the sake of clarity this explanation only shows the first element.
206//!
207//! The rules stated above also implies some additional rules:
208//! * lists are never empty
209//! * lists are nested in groups
210//! * groups are always preceded by a single list
211//! * groups are ordered left to right
212//!
213//! Using the aforementioned delimiters for lists and groups the end result would be:
214//!
215//! Overview Tree: `[0, 1] ([2] ([3], [4, 5]), [6, 7, 8])`
216//! Detailed Tree: `[Aᶜ] ([Dᶜ], [Hᶜ], [Iᶜ])`
217//!
218//! Attachments are not ordered by insertion order but by depth in the tree. The depth in the tree
219//! is the inverse of the insertion order, this means that the [`Debug`] output of all
220//! attachments is reversed from the calling order of [`Report::attach`]. Each context uses the
221//! attachments that are it's parents until the next context node. If attachments are shared between
222//! multiple contexts, they are duplicated and output twice.
223//!
224//! Groups are always preceded by a single list, the only case where this is not true is at the top
225//! level, in that case we opt to output separate trees for each member in the group.
226//!
227//! ### Output Formatting
228//!
229//! Lists are guaranteed to be non-empty and have at least a single context. The context is the
230//! heading of the whole list, while all other contexts are intended. The last entry in that
231//! indentation is (if present) the group that follows, taking the detailed example this means that
232//! the following output would be rendered:
233//!
234//! ```text
235//! Aᶜ
236//! │
237//! ╰┬▶ Dᶜ
238//!  │  ├╴Bᵃ
239//!  │  ╰╴Cᵃ
240//!  │
241//!  ├▶ Hᶜ
242//!  │  ├╴Bᵃ
243//!  │  ├╴Eᵃ
244//!  │  ├╴Fᵃ
245//!  │  ╰╴Gᵃ
246//!  │
247//!  ╰▶ Iᶜ
248//!     ├╴Bᵃ
249//!     ├╴Eᵃ
250//!     ╰╴Fᵃ
251//! ```
252//!
253//! Groups are visually represented as an additional distinct indentation for other contexts in the
254//! preceding list, taking the overview tree this means:
255//!
256//! ```text
257//! 0
258//! ├╴Attachment
259//! │
260//! ├─▶ 1
261//! │   ╰╴Attachment
262//! │
263//! ╰┬▶ 2
264//!  │  │
265//!  │  ╰┬▶ 3
266//!  │   │
267//!  │   ╰▶ 4
268//!  │      │
269//!  │      ╰─▶ 5
270//!  ╰▶ 6
271//!     │
272//!     ├─▶ 7
273//!     │
274//!     ╰─▶ 8
275//! ```
276//!
277//! Attachments have been added to various places to simulate a real use-case with attachments and
278//! to visualise their placement.
279//!
280//! The spacing and characters used are chosen carefully, to reduce indentation and increase visual
281//! legibility in large trees. The indentation of the group following the last entry in the
282//! preceding list is the same. To indicate that the last entry in the preceding list is the parent
283//! a new indentation of the connecting line is used.
284//!
285//! [`Display`]: core::fmt::Display
286//! [`Debug`]: core::fmt::Debug
287//! [`Mutex`]: std::sync::Mutex
288//! [`RwLock`]: std::sync::RwLock
289//! [`Backtrace`]: std::backtrace::Backtrace
290//! [`SpanTrace`]: tracing_error::SpanTrace
291//! [`atomic`]: std::sync::atomic
292//! [`Error::provide`]: core::error::Error::provide
293
294mod charset;
295mod color;
296mod config;
297#[cfg(any(feature = "std", feature = "hooks"))]
298mod hook;
299mod location;
300mod r#override;
301
302use alloc::collections::VecDeque;
303#[cfg_attr(feature = "std", allow(unused_imports))]
304use alloc::{
305    borrow::ToOwned,
306    format,
307    string::{String, ToString},
308    vec,
309    vec::Vec,
310};
311use core::{
312    fmt::{self, Debug, Display, Formatter},
313    iter::once,
314    mem,
315};
316
317pub use charset::Charset;
318pub use color::ColorMode;
319#[cfg(any(feature = "std", feature = "hooks"))]
320pub use hook::HookContext;
321#[cfg(any(feature = "std", feature = "hooks"))]
322pub(crate) use hook::{install_builtin_hooks, Format, Hooks};
323#[cfg(not(any(feature = "std", feature = "hooks")))]
324use location::LocationAttachment;
325
326use crate::{
327    fmt::{
328        color::{Color, DisplayStyle, Style},
329        config::Config,
330    },
331    AttachmentKind, Context, Frame, FrameKind, Report,
332};
333
334#[derive(Debug, Copy, Clone, Eq, PartialEq)]
335enum Symbol {
336    Vertical,
337    VerticalRight,
338    Horizontal,
339    HorizontalLeft,
340    HorizontalDown,
341    ArrowRight,
342    CurveRight,
343
344    Space,
345}
346
347/// We use symbols during resolution, which is efficient and versatile, but makes the conversion
348/// between [`Instruction`] to [`Symbol`] harder to comprehend for a user.
349///
350/// This macro fixes this by creating a compile-time lookup table to easily map every character in
351/// the [`Display`] of [`Symbol`] to it's corresponding symbol.
352///
353/// # Example
354///
355/// ```ignore
356/// assert_eq!(sym!('├', '┬'), &[
357///     Symbol::VerticalRight,
358///     Symbol::HorizontalDown
359/// ])
360/// ```
361macro_rules! sym {
362    (#char '│') => {
363        Symbol::Vertical
364    };
365
366    (#char '├') => {
367        Symbol::VerticalRight
368    };
369
370    (#char '─') => {
371        Symbol::Horizontal
372    };
373
374    (#char '╴') => {
375        Symbol::HorizontalLeft
376    };
377
378    (#char '┬') => {
379        Symbol::HorizontalDown
380    };
381
382    (#char '▶') => {
383        Symbol::ArrowRight
384    };
385
386    (#char '╰') => {
387        Symbol::CurveRight
388    };
389
390    (#char ' ') => {
391        Symbol::Space
392    };
393
394    ($($char:tt),+) => {
395        &[$(sym!(#char $char)),*]
396    };
397}
398
399impl Symbol {
400    const fn to_str_utf8(self) -> &'static str {
401        match self {
402            Self::Vertical => "\u{2502}",       // │
403            Self::VerticalRight => "\u{251c}",  // ├
404            Self::Horizontal => "\u{2500}",     // ─
405            Self::HorizontalLeft => "\u{2574}", // ╴
406            Self::HorizontalDown => "\u{252c}", // ┬
407            Self::ArrowRight => "\u{25b6}",     // ▶
408            Self::CurveRight => "\u{2570}",     // ╰
409            Self::Space => " ",
410        }
411    }
412
413    const fn to_str_ascii(self) -> &'static str {
414        match self {
415            Self::Vertical | Self::VerticalRight | Self::CurveRight => "|",
416            Self::Horizontal | Self::HorizontalDown | Self::HorizontalLeft => "-",
417            Self::ArrowRight => ">",
418            Self::Space => " ",
419        }
420    }
421
422    const fn to_str(self, charset: Charset) -> &'static str {
423        match charset {
424            Charset::Utf8 => self.to_str_utf8(),
425            Charset::Ascii => self.to_str_ascii(),
426        }
427    }
428}
429
430struct SymbolDisplay<'a> {
431    inner: &'a [Symbol],
432    charset: Charset,
433}
434
435impl Display for SymbolDisplay<'_> {
436    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
437        for symbol in self.inner {
438            f.write_str(symbol.to_str(self.charset))?;
439        }
440
441        Ok(())
442    }
443}
444
445#[derive(Debug, Copy, Clone)]
446enum Position {
447    First,
448    Inner,
449    Final,
450}
451
452#[derive(Debug, Copy, Clone)]
453enum Spacing {
454    // Standard to create a width of 4 characters
455    Full,
456    // Minimal width to create a width of 2/3 characters
457    Minimal,
458}
459
460#[derive(Debug, Copy, Clone)]
461struct Indent {
462    /// Is this used in a group context, if that is the case, then add a single leading space
463    group: bool,
464    /// Should the indent be visible, if that isn't the case it will render a space instead of
465    /// `|`
466    visible: bool,
467    /// Should spacing included, this is the difference between `|   ` and `|`
468    spacing: Option<Spacing>,
469}
470
471impl Indent {
472    const fn new(group: bool) -> Self {
473        Self {
474            group,
475            visible: true,
476            spacing: Some(Spacing::Full),
477        }
478    }
479
480    fn spacing(mut self, spacing: impl Into<Option<Spacing>>) -> Self {
481        self.spacing = spacing.into();
482        self
483    }
484
485    const fn visible(mut self, visible: bool) -> Self {
486        self.visible = visible;
487        self
488    }
489
490    const fn group() -> Self {
491        Self::new(true)
492    }
493
494    const fn no_group() -> Self {
495        Self::new(false)
496    }
497
498    const fn prepare(self) -> &'static [Symbol] {
499        match self {
500            Self {
501                group: true,
502                visible: true,
503                spacing: Some(_),
504            } => sym!(' ', '│', ' ', ' '),
505            Self {
506                group: true,
507                visible: true,
508                spacing: None,
509            } => sym!(' ', '│'),
510            Self {
511                group: false,
512                visible: true,
513                spacing: Some(Spacing::Full),
514            } => sym!('│', ' ', ' ', ' '),
515            Self {
516                group: false,
517                visible: true,
518                spacing: Some(Spacing::Minimal),
519            } => sym!('│', ' '),
520            Self {
521                group: false,
522                visible: true,
523                spacing: None,
524            } => sym!('│'),
525            Self {
526                visible: false,
527                spacing: Some(Spacing::Full),
528                ..
529            } => sym!(' ', ' ', ' ', ' '),
530            Self {
531                visible: false,
532                spacing: Some(Spacing::Minimal),
533                ..
534            } => sym!(' ', ' '),
535            Self {
536                visible: false,
537                spacing: None,
538                ..
539            } => &[],
540        }
541    }
542}
543
544impl From<Indent> for Instruction {
545    fn from(indent: Indent) -> Self {
546        Self::Indent(indent)
547    }
548}
549
550/// The display of content is using an instruction style architecture,
551/// where we first render every indentation and action as an [`Instruction`], these instructions
552/// are a lot easier to reason about and enable better manipulation of the stream of data.
553///
554/// Once generation of all data is done, it is interpreted as a String, with glyphs and color added
555/// (if supported and enabled).
556#[derive(Debug)]
557enum Instruction {
558    Value {
559        value: String,
560        style: Style,
561    },
562
563    Group {
564        position: Position,
565    },
566    /// This does not distinguish between first and middle, the true first is handled a bit
567    /// differently.
568    Context {
569        position: Position,
570    },
571    /// `Position::Final` means *that nothing follows*
572    Attachment {
573        position: Position,
574    },
575
576    Indent(Indent),
577}
578
579/// Minimized instruction, which looses information about what it represents and converts it to
580/// only symbols, this then is output instead.
581enum PreparedInstruction<'a> {
582    Symbols(&'a [Symbol]),
583    Content(&'a str, &'a Style),
584}
585
586impl Instruction {
587    // Reason: the match arms are the same intentionally, this makes it more clean which variant
588    //  emits which and also keeps it nicely formatted.
589    #[allow(clippy::match_same_arms)]
590    fn prepare(&self) -> PreparedInstruction {
591        match self {
592            Self::Value { value, style } => PreparedInstruction::Content(value, style),
593
594            Self::Group { position } => match position {
595                Position::First => PreparedInstruction::Symbols(sym!('╰', '┬', '▶', ' ')),
596                Position::Inner => PreparedInstruction::Symbols(sym!(' ', '├', '▶', ' ')),
597                Position::Final => PreparedInstruction::Symbols(sym!(' ', '╰', '▶', ' ')),
598            },
599
600            Self::Context { position } => match position {
601                Position::First => PreparedInstruction::Symbols(sym!('├', '─', '▶', ' ')),
602                Position::Inner => PreparedInstruction::Symbols(sym!('├', '─', '▶', ' ')),
603                Position::Final => PreparedInstruction::Symbols(sym!('╰', '─', '▶', ' ')),
604            },
605
606            Self::Attachment { position } => match position {
607                Position::First => PreparedInstruction::Symbols(sym!('├', '╴')),
608                Position::Inner => PreparedInstruction::Symbols(sym!('├', '╴')),
609                Position::Final => PreparedInstruction::Symbols(sym!('╰', '╴')),
610            },
611
612            // Indentation (like `|   ` or ` |  `)
613            Self::Indent(indent) => PreparedInstruction::Symbols(indent.prepare()),
614        }
615    }
616}
617
618struct InstructionDisplay<'a> {
619    color: ColorMode,
620    charset: Charset,
621
622    instruction: &'a Instruction,
623}
624
625impl Display for InstructionDisplay<'_> {
626    fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
627        match self.instruction.prepare() {
628            PreparedInstruction::Symbols(symbols) => {
629                let display = SymbolDisplay {
630                    inner: symbols,
631                    charset: self.charset,
632                };
633
634                let mut style = Style::new();
635
636                if self.color == ColorMode::Color {
637                    style.set_foreground(Color::Red, false);
638                }
639
640                Display::fmt(&style.apply(&display), fmt)?;
641            }
642            PreparedInstruction::Content(value, &style) => Display::fmt(&style.apply(&value), fmt)?,
643        }
644
645        Ok(())
646    }
647}
648
649struct Line(Vec<Instruction>);
650
651impl Line {
652    const fn new() -> Self {
653        Self(Vec::new())
654    }
655
656    fn push(mut self, instruction: Instruction) -> Self {
657        self.0.push(instruction);
658        self
659    }
660
661    fn into_lines(self) -> Lines {
662        let lines = Lines::new();
663        lines.after(self)
664    }
665}
666
667struct LineDisplay<'a> {
668    color: ColorMode,
669    charset: Charset,
670
671    line: &'a Line,
672}
673
674impl Display for LineDisplay<'_> {
675    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
676        for instruction in self.line.0.iter().rev() {
677            Display::fmt(
678                &InstructionDisplay {
679                    color: self.color,
680                    charset: self.charset,
681                    instruction,
682                },
683                f,
684            )?;
685        }
686
687        Ok(())
688    }
689}
690
691struct Lines(VecDeque<Line>);
692
693impl Lines {
694    fn new() -> Self {
695        Self(VecDeque::new())
696    }
697
698    fn into_iter(self) -> alloc::collections::vec_deque::IntoIter<Line> {
699        self.0.into_iter()
700    }
701
702    fn then(mut self, other: Self) -> Self {
703        self.0.extend(other.0);
704        self
705    }
706
707    fn before(mut self, line: Line) -> Self {
708        self.0.push_front(line);
709        self
710    }
711
712    fn after(mut self, line: Line) -> Self {
713        self.0.push_back(line);
714        self
715    }
716
717    fn into_vec(self) -> Vec<Line> {
718        self.0.into_iter().collect()
719    }
720}
721
722impl FromIterator<Line> for Lines {
723    fn from_iter<T: IntoIterator<Item = Line>>(iter: T) -> Self {
724        Self(iter.into_iter().collect())
725    }
726}
727
728/// Collect the current "stack", a stack are the current frames which only have a single
729/// source/parent.
730/// This searches until it finds a stack "split", where a frame has more than a single source.
731fn collect<'a>(root: &'a Frame, prefix: &'a [&Frame]) -> (Vec<&'a Frame>, &'a [Frame]) {
732    let mut stack = vec![];
733    stack.extend(prefix);
734    stack.push(root);
735
736    let mut ptr = Some(root);
737    let mut next: &'a [_] = &[];
738
739    while let Some(current) = ptr.take() {
740        let sources = current.sources();
741
742        match sources {
743            [parent] => {
744                stack.push(parent);
745                ptr = Some(parent);
746            }
747            sources => {
748                next = sources;
749            }
750        }
751    }
752
753    (stack, next)
754}
755
756/// Partition the tree, this looks for the first `Context`,
757/// then moves it up the chain and adds it to our results.
758/// Once we reach the end all remaining items on the stack are added to the prefix pile,
759/// which will be used in next iteration.
760fn partition<'a>(stack: &'a [&'a Frame]) -> (Vec<(&'a Frame, Vec<&'a Frame>)>, Vec<&'a Frame>) {
761    let mut result = vec![];
762    let mut queue = vec![];
763
764    for frame in stack {
765        if matches!(frame.kind(), FrameKind::Context(_)) {
766            let frames = mem::take(&mut queue);
767
768            result.push((*frame, frames));
769        } else {
770            queue.push(*frame);
771        }
772    }
773
774    (result, queue)
775}
776
777fn debug_context(context: &dyn Context, mode: ColorMode) -> Lines {
778    context
779        .to_string()
780        .lines()
781        .map(ToOwned::to_owned)
782        .enumerate()
783        .map(|(idx, value)| {
784            if idx == 0 {
785                let mut style = Style::new();
786
787                if mode == ColorMode::Color || mode == ColorMode::Emphasis {
788                    style.set_display(DisplayStyle::new().with_bold(true));
789                }
790
791                Line::new().push(Instruction::Value { value, style })
792            } else {
793                Line::new().push(Instruction::Value {
794                    value,
795                    style: Style::new(),
796                })
797            }
798        })
799        .collect()
800}
801
802struct Opaque(usize);
803
804impl Opaque {
805    const fn new() -> Self {
806        Self(0)
807    }
808
809    fn increase(&mut self) {
810        self.0 += 1;
811    }
812
813    fn render(self) -> Option<Line> {
814        match self.0 {
815            0 => None,
816            1 => Some(Line::new().push(Instruction::Value {
817                value: "1 additional opaque attachment".to_owned(),
818                style: Style::new(),
819            })),
820            n => Some(Line::new().push(Instruction::Value {
821                value: format!("{n} additional opaque attachments"),
822                style: Style::new(),
823            })),
824        }
825    }
826}
827
828#[allow(clippy::needless_pass_by_ref_mut)]
829fn debug_attachments_invoke<'a>(
830    frames: impl IntoIterator<Item = &'a Frame>,
831    config: &mut Config,
832) -> (Opaque, Vec<String>) {
833    let mut opaque = Opaque::new();
834
835    #[cfg(any(feature = "std", feature = "hooks"))]
836    let context = config.context();
837
838    let body = frames
839        .into_iter()
840        .map(|frame| match frame.kind() {
841            #[cfg(any(feature = "std", feature = "hooks"))]
842            FrameKind::Context(_) => Some(
843                Report::invoke_debug_format_hook(|hooks| hooks.call(frame, context))
844                    .then(|| context.take_body())
845                    .unwrap_or_default(),
846            ),
847            #[cfg(any(feature = "std", feature = "hooks"))]
848            FrameKind::Attachment(AttachmentKind::Printable(attachment)) => Some(
849                Report::invoke_debug_format_hook(|hooks| hooks.call(frame, context))
850                    .then(|| context.take_body())
851                    .unwrap_or_else(|| vec![attachment.to_string()]),
852            ),
853            #[cfg(any(feature = "std", feature = "hooks"))]
854            FrameKind::Attachment(AttachmentKind::Opaque(_)) => {
855                Report::invoke_debug_format_hook(|hooks| hooks.call(frame, context))
856                    .then(|| context.take_body())
857            }
858            #[cfg(not(any(feature = "std", feature = "hooks")))]
859            FrameKind::Context(_) => Some(vec![]),
860            #[cfg(not(any(feature = "std", feature = "hooks")))]
861            FrameKind::Attachment(AttachmentKind::Printable(attachment)) => {
862                Some(vec![attachment.to_string()])
863            }
864            #[cfg(not(any(feature = "std", feature = "hooks")))]
865            FrameKind::Attachment(AttachmentKind::Opaque(_)) => frame
866                .downcast_ref::<core::panic::Location<'static>>()
867                .map(|location| {
868                    vec![LocationAttachment::new(location, config.color_mode()).to_string()]
869                }),
870        })
871        .flat_map(|body| {
872            body.unwrap_or_else(|| {
873                // increase the opaque counter, if we're unable to determine the actual value of
874                // the frame
875                opaque.increase();
876                Vec::new()
877            })
878        })
879        .collect();
880
881    (opaque, body)
882}
883
884fn debug_attachments<'a>(
885    position: Position,
886    frames: impl IntoIterator<Item = &'a Frame>,
887    config: &mut Config,
888) -> Lines {
889    let last = matches!(position, Position::Final);
890
891    let (opaque, entries) = debug_attachments_invoke(frames, config);
892    let opaque = opaque.render();
893
894    // Calculate the expected end length, by adding all values that have would contribute to the
895    // line count later.
896    let len = entries.len() + opaque.as_ref().map_or(0, |_| 1);
897    let lines = entries.into_iter().map(|value| {
898        value
899            .lines()
900            .map(ToOwned::to_owned)
901            .map(|line| {
902                Line::new().push(Instruction::Value {
903                    value: line,
904                    style: Style::new(),
905                })
906            })
907            .collect::<Vec<_>>()
908    });
909
910    // indentation for every first line, use `Instruction::Attachment`, otherwise use minimal
911    // indent omit that indent when we're the last value
912    lines
913        .chain(opaque.into_iter().map(|line| line.into_lines().into_vec()))
914        .enumerate()
915        .flat_map(|(idx, lines)| {
916            let position = match idx {
917                pos if pos + 1 == len && last => Position::Final,
918                0 => Position::First,
919                _ => Position::Inner,
920            };
921
922            lines.into_iter().enumerate().map(move |(idx, line)| {
923                if idx == 0 {
924                    line.push(Instruction::Attachment { position })
925                } else {
926                    line.push(
927                        Indent::no_group()
928                            .visible(!matches!(position, Position::Final))
929                            .spacing(Spacing::Minimal)
930                            .into(),
931                    )
932                }
933            })
934        })
935        .collect()
936}
937
938fn debug_render(head: Lines, contexts: VecDeque<Lines>, sources: Vec<Lines>) -> Lines {
939    let len = sources.len();
940    let sources = sources
941        .into_iter()
942        .enumerate()
943        .map(|(idx, lines)| {
944            let position = match idx {
945                // this is first to make sure that 0 is caught as `Last` instead of `First`
946                pos if pos + 1 == len => Position::Final,
947                0 => Position::First,
948                _ => Position::Inner,
949            };
950
951            lines
952                .into_iter()
953                .enumerate()
954                .map(|(idx, line)| {
955                    if idx == 0 {
956                        line.push(Instruction::Group { position })
957                    } else {
958                        line.push(
959                            Indent::group()
960                                .visible(!matches!(position, Position::Final))
961                                .into(),
962                        )
963                    }
964                })
965                .collect::<Lines>()
966                .before(
967                    // add a buffer line for readability
968                    Line::new().push(Indent::new(idx != 0).spacing(None).into()),
969                )
970        })
971        .collect::<Vec<_>>();
972
973    let tail = !sources.is_empty();
974    let len = contexts.len();
975
976    // insert the arrows and buffer indentation
977    let contexts = contexts.into_iter().enumerate().flat_map(|(idx, lines)| {
978        let position = match idx {
979            pos if pos + 1 == len && !tail => Position::Final,
980            0 => Position::First,
981            _ => Position::Inner,
982        };
983
984        let mut lines = lines
985            .into_iter()
986            .enumerate()
987            .map(|(idx, line)| {
988                if idx == 0 {
989                    line.push(Instruction::Context { position })
990                } else {
991                    line.push(
992                        Indent::no_group()
993                            .visible(!matches!(position, Position::Final))
994                            .into(),
995                    )
996                }
997            })
998            .collect::<Vec<_>>();
999
1000        // this is not using `.collect<>().before()`, because somehow that kills rustfmt?!
1001        lines.insert(0, Line::new().push(Indent::no_group().spacing(None).into()));
1002
1003        lines
1004    });
1005
1006    head.into_iter()
1007        .chain(contexts)
1008        .chain(sources.into_iter().flat_map(Lines::into_vec))
1009        .collect()
1010}
1011
1012fn debug_frame(root: &Frame, prefix: &[&Frame], config: &mut Config) -> Vec<Lines> {
1013    let (stack, sources) = collect(root, prefix);
1014    let (stack, prefix) = partition(&stack);
1015
1016    let len = stack.len();
1017    // collect all the contexts that we have partitioned previously and render them
1018    let mut contexts: VecDeque<_> = stack
1019        .into_iter()
1020        .enumerate()
1021        .map(|(idx, (head, mut body))| {
1022            // each "paket" on the stack is made up of a head (guaranteed to be a `Context`) and
1023            // `n` attachments.
1024            // The attachments are rendered as direct descendants of the parent context
1025            let head_context = debug_context(
1026                match head.kind() {
1027                    FrameKind::Context(c) => c,
1028                    FrameKind::Attachment(_) => unreachable!(),
1029                },
1030                config.color_mode(),
1031            );
1032
1033            // reverse all attachments, to make it more logical relative to the attachment order
1034            body.reverse();
1035            let body = debug_attachments(
1036                // This makes sure that we use `╰─` instead of `├─`,
1037                // this is true whenever we only have a single context and no sources,
1038                // **or** if our idx is larger than `0`, this might sound false,
1039                // but this is because contexts other than the first context create a new
1040                // "indentation", in this indentation we are considered last.
1041                //
1042                // Context A
1043                // ├╴Attachment B
1044                // ├╴Attachment C <- not last, because we are not the only context
1045                // |
1046                // ╰─▶ Context D <- indentation here is handled by `debug_render`!
1047                //     ├╴Attachment E
1048                //     ╰╴Attachment F <- last because it's the last of the parent context!
1049                if (len == 1 && sources.is_empty()) || idx > 0 {
1050                    Position::Final
1051                } else {
1052                    Position::Inner
1053                },
1054                once(head).chain(body),
1055                config,
1056            );
1057            head_context.then(body)
1058        })
1059        .collect();
1060
1061    let sources = sources
1062        .iter()
1063        .flat_map(
1064            // if the group is "transparent" (has no context), it will return all it's parents
1065            // rendered this is why we must first flat_map.
1066            |source| debug_frame(source, &prefix, config),
1067        )
1068        .collect::<Vec<_>>();
1069
1070    // if there is no context, this is considered a "transparent" group,
1071    // and just directly returns all sources without modifying them
1072    if contexts.is_empty() {
1073        return sources;
1074    }
1075
1076    // take the first Context, this is our "root", all others are indented
1077    // this unwrap always succeeds due to the call before <3
1078    let head = contexts
1079        .pop_front()
1080        .expect("should always have single context");
1081
1082    // combine everything into a single group
1083    vec![debug_render(head, contexts, sources)]
1084}
1085
1086impl<C> Debug for Report<C> {
1087    fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
1088        let mut config = Config::load(fmt.alternate());
1089
1090        let color = config.color_mode();
1091        let charset = config.charset();
1092
1093        #[cfg_attr(not(any(feature = "std", feature = "hooks")), allow(unused_mut))]
1094        let mut lines = self
1095            .current_frames()
1096            .iter()
1097            .flat_map(|frame| debug_frame(frame, &[], &mut config))
1098            .enumerate()
1099            .flat_map(|(idx, lines)| {
1100                if idx == 0 {
1101                    lines.into_vec()
1102                } else {
1103                    lines
1104                        .before(
1105                            Line::new()
1106                                .push(Indent::no_group().visible(false).spacing(None).into()),
1107                        )
1108                        .into_vec()
1109                }
1110            })
1111            .map(|line| {
1112                LineDisplay {
1113                    color,
1114                    charset,
1115                    line: &line,
1116                }
1117                .to_string()
1118            })
1119            .collect::<Vec<_>>()
1120            .join("\n");
1121
1122        #[cfg(any(feature = "std", feature = "hooks"))]
1123        {
1124            let appendix = config
1125                .context::<Frame>()
1126                .appendix()
1127                .iter()
1128                .map(
1129                    // remove all trailing newlines for a more uniform look
1130                    |snippet| snippet.trim_end_matches('\n').to_owned(),
1131                )
1132                .collect::<Vec<_>>()
1133                .join("\n\n");
1134
1135            if !appendix.is_empty() {
1136                // 44 is the size for the separation.
1137                lines.reserve(44 + appendix.len());
1138
1139                lines.push_str("\n\n");
1140                if charset == Charset::Utf8 {
1141                    lines.push_str(&"\u{2501}".repeat(40)); // ━
1142                } else {
1143                    lines.push_str(&"=".repeat(40));
1144                }
1145
1146                lines.push_str("\n\n");
1147                lines.push_str(&appendix);
1148            }
1149        }
1150
1151        fmt.write_str(&lines)
1152    }
1153}
1154
1155impl<Context> Display for Report<Context> {
1156    fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
1157        for (index, frame) in self
1158            .frames()
1159            .filter_map(|frame| match frame.kind() {
1160                FrameKind::Context(context) => Some(context.to_string()),
1161                FrameKind::Attachment(_) => None,
1162            })
1163            .enumerate()
1164        {
1165            if index == 0 {
1166                fmt::Display::fmt(&frame, fmt)?;
1167                if !fmt.alternate() {
1168                    break;
1169                }
1170            } else {
1171                write!(fmt, ": {frame}")?;
1172            }
1173        }
1174
1175        Ok(())
1176    }
1177}