Skip to main content

rich_ext/
stacktrace.rs

1//! Stack traces from Rust, Python, Java and JavaScript, in one shape.
2//!
3//! [`parse`] recognises a trace's language and normalises it into a
4//! [`StackTrace`]: the error kind and message, its [`Frame`]s ordered with the
5//! most recent call **last** (Python's order), and any chained cause. Each
6//! language is a [`TraceParser`]; [`Parsers`] holds the built-in four and
7//! takes more, so other formats plug in without changing this module.
8//!
9//! A [`StackTrace`] renders with frame locations linked through a
10//! [`Hyperlinker`], library frames dimmed and runs of them collapsed, and each
11//! cause after the trace it caused. [`capture`] and [`panic_hook`] build one
12//! for the current thread from `std::backtrace`.
13
14use fancy_regex::Regex;
15use rich::{Console, ConsoleOptions, Renderable, Segment, Text};
16use std::sync::OnceLock;
17
18use crate::event::theme_style;
19use crate::hyperlink::Hyperlinker;
20
21/// The most causes the built-in parsers keep, and a view renders, below the
22/// outermost error. Traces are untrusted input; a longer chain keeps the
23/// causes nearest the error and records how many were left out.
24pub const MAX_CAUSES: usize = 64;
25
26/// The language a trace came from.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum Language {
29    Rust,
30    Python,
31    Java,
32    JavaScript,
33    /// A language from a caller-supplied parser.
34    Other(String),
35}
36
37/// One stack frame.
38#[derive(Clone, Debug, Default, PartialEq, Eq)]
39pub struct Frame {
40    /// The function, method or symbol, when the trace names one.
41    pub function: Option<String>,
42    /// The source file.
43    pub path: Option<String>,
44    /// The 1-based line.
45    pub line: Option<usize>,
46    /// The 1-based column.
47    pub column: Option<usize>,
48    /// The source line, when the trace quotes it (Python does).
49    pub source: Option<String>,
50    /// Language-specific details kept from the trace (`module`, `native`, …).
51    pub metadata: Vec<(String, String)>,
52    /// Whether the frame is in a standard library, runtime or dependency
53    /// rather than the application.
54    pub library: bool,
55}
56
57/// How a trace relates to its [`StackTrace::cause`].
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum CauseKind {
60    /// The cause led to this error (Python's `raise … from`, Java's
61    /// `Caused by:`, JavaScript's `[cause]`).
62    CausedBy,
63    /// This error was raised while handling the cause (Python's implicit
64    /// chaining).
65    DuringHandling,
66}
67
68/// A normalised stack trace.
69#[derive(Clone, Debug, PartialEq, Eq)]
70pub struct StackTrace {
71    /// The source language.
72    pub language: Language,
73    /// The error type (`ValueError`, `java.io.IOException`, `panic`).
74    pub kind: Option<String>,
75    /// The error message.
76    pub message: Option<String>,
77    /// Frames, most recent call last.
78    pub frames: Vec<Frame>,
79    /// The error this one came from.
80    pub cause: Option<Box<StackTrace>>,
81    /// How `cause` relates to this error.
82    pub cause_kind: CauseKind,
83    /// Causes below this one that were left out because the chain was longer
84    /// than [`MAX_CAUSES`]. Rendered as `… N more causes`.
85    pub omitted_causes: usize,
86    /// Where the error was raised, when the trace names it apart from the
87    /// frames (a Rust panic's `panicked at` location).
88    pub location: Option<Frame>,
89}
90
91impl StackTrace {
92    /// An empty trace for `language`.
93    pub fn new(language: Language) -> Self {
94        StackTrace {
95            language,
96            kind: None,
97            message: None,
98            frames: Vec::new(),
99            cause: None,
100            cause_kind: CauseKind::CausedBy,
101            omitted_causes: 0,
102            location: None,
103        }
104    }
105
106    /// The traces from this one down its causes.
107    pub fn chain(&self) -> impl Iterator<Item = &StackTrace> {
108        std::iter::successors(Some(self), |trace| trace.cause.as_deref())
109    }
110
111    /// The most recent application (non-library) frame, if any: where to look.
112    pub fn origin(&self) -> Option<&Frame> {
113        self.frames.iter().rev().find(|frame| !frame.library)
114    }
115
116    /// A renderable view with the default options.
117    pub fn render_options(&self) -> StackTraceView<'_> {
118        StackTraceView {
119            trace: self,
120            linker: Hyperlinker::new(),
121            show_library: false,
122        }
123    }
124}
125
126/// Unlinks the cause chain one trace at a time: the derived drop recurses
127/// once per cause and overflows the stack on a long hand-built chain.
128impl Drop for StackTrace {
129    fn drop(&mut self) {
130        let mut next = self.cause.take();
131        while let Some(mut trace) = next {
132            next = trace.cause.take();
133        }
134    }
135}
136
137/// Link `traces`, outermost first, each the cause of the one before it,
138/// keeping at most [`MAX_CAUSES`] causes. The deepest kept trace counts the
139/// ones left out.
140fn link_causes(mut traces: Vec<StackTrace>) -> Option<StackTrace> {
141    let omitted = traces.len().saturating_sub(MAX_CAUSES + 1);
142    traces.truncate(MAX_CAUSES + 1);
143    if let Some(last) = traces.last_mut() {
144        last.omitted_causes += omitted;
145    }
146    let mut result: Option<StackTrace> = None;
147    for mut trace in traces.into_iter().rev() {
148        trace.cause = result.take().map(Box::new);
149        result = Some(trace);
150    }
151    result
152}
153
154/// Recognises and parses one trace format.
155pub trait TraceParser: Send + Sync {
156    /// Whether `text` looks like this format.
157    fn detect(&self, text: &str) -> bool;
158    /// Parse `text`, or `None` when it has no frames or header to keep.
159    fn parse(&self, text: &str) -> Option<StackTrace>;
160}
161
162/// An ordered set of parsers; the first that detects a trace parses it.
163pub struct Parsers {
164    parsers: Vec<Box<dyn TraceParser>>,
165}
166
167impl Default for Parsers {
168    fn default() -> Self {
169        Parsers {
170            parsers: vec![
171                Box::new(PythonParser),
172                Box::new(JavaParser),
173                Box::new(RustParser),
174                Box::new(JavaScriptParser),
175            ],
176        }
177    }
178}
179
180impl Parsers {
181    /// The built-in Python, Java, Rust and JavaScript parsers.
182    pub fn new() -> Self {
183        Parsers::default()
184    }
185
186    /// Try `parser` before the built-in ones.
187    #[must_use]
188    pub fn with_parser(mut self, parser: impl TraceParser + 'static) -> Self {
189        self.parsers.insert(0, Box::new(parser));
190        self
191    }
192
193    /// Parse `text` with the first parser that detects it.
194    pub fn parse(&self, text: &str) -> Option<StackTrace> {
195        self.parsers
196            .iter()
197            .find(|parser| parser.detect(text))
198            .and_then(|parser| parser.parse(text))
199    }
200}
201
202/// Parse `text` with the built-in parsers.
203pub fn parse(text: &str) -> Option<StackTrace> {
204    Parsers::default().parse(text)
205}
206
207fn regex(cell: &'static OnceLock<Regex>, pattern: &str) -> &'static Regex {
208    cell.get_or_init(|| Regex::new(pattern).expect("valid trace pattern"))
209}
210
211fn number(value: Option<fancy_regex::Match<'_>>) -> Option<usize> {
212    value.and_then(|value| value.as_str().parse().ok())
213}
214
215/// `kind: message`, splitting on the first `": "` when the kind looks like a
216/// type name.
217fn kind_and_message(line: &str) -> (Option<String>, Option<String>) {
218    let line = line.trim();
219    match line.split_once(": ") {
220        Some((kind, message)) if !kind.contains(' ') => {
221            (Some(kind.to_string()), Some(message.to_string()))
222        }
223        _ if !line.contains(' ') && !line.is_empty() => (Some(line.to_string()), None),
224        _ => (None, Some(line.to_string()).filter(|line| !line.is_empty())),
225    }
226}
227
228/// Parses `std::backtrace` output and panic messages.
229pub struct RustParser;
230
231impl RustParser {
232    fn is_library(function: &str, path: Option<&str>) -> bool {
233        const CRATES: [&str; 6] = [
234            "std::",
235            "core::",
236            "alloc::",
237            "backtrace::",
238            "rust_begin_unwind",
239            "__rust",
240        ];
241        CRATES.iter().any(|prefix| {
242            function.starts_with(prefix) || function.starts_with(&format!("<{prefix}"))
243        }) || path.is_some_and(|path| {
244            path.starts_with("/rustc/")
245                || path.contains("/.cargo/registry/")
246                || path.contains("\\.cargo\\registry\\")
247        })
248    }
249}
250
251impl TraceParser for RustParser {
252    fn detect(&self, text: &str) -> bool {
253        static FRAME: OnceLock<Regex> = OnceLock::new();
254        text.contains("panicked at")
255            || regex(&FRAME, r"(?m)^\s*\d+: \S")
256                .is_match(text)
257                .unwrap_or(false)
258    }
259
260    fn parse(&self, text: &str) -> Option<StackTrace> {
261        static FRAME: OnceLock<Regex> = OnceLock::new();
262        static AT: OnceLock<Regex> = OnceLock::new();
263        static PANIC: OnceLock<Regex> = OnceLock::new();
264        static OLD_PANIC: OnceLock<Regex> = OnceLock::new();
265        let frame_re = regex(&FRAME, r"^\s*\d+:\s+(.+?)\s*$");
266        let at_re = regex(&AT, r"^\s+at\s+(.+?):(\d+)(?::(\d+))?\s*$");
267        let mut trace = StackTrace::new(Language::Rust);
268        let lines: Vec<&str> = text.lines().collect();
269
270        // `thread 'main' panicked at src/main.rs:4:5:` then the message
271        // (Rust 1.73+), or `panicked at 'message', src/main.rs:4:5` before.
272        let panic_re = regex(&PANIC, r"panicked at (.+?):(\d+):(\d+):\s*$");
273        let old_re = regex(&OLD_PANIC, r"panicked at '(.*)', (.+?):(\d+):(\d+)");
274        let mut index = 0;
275        while index < lines.len() {
276            let line = lines[index];
277            if let Ok(Some(caps)) = old_re.captures(line) {
278                trace.kind = Some("panic".into());
279                trace.message = Some(caps[1].to_string());
280                trace.metadata_location(&caps[2], number(caps.get(3)), number(caps.get(4)));
281            } else if let Ok(Some(caps)) = panic_re.captures(line) {
282                trace.kind = Some("panic".into());
283                trace.metadata_location(&caps[1], number(caps.get(2)), number(caps.get(3)));
284                let mut message = Vec::new();
285                while index + 1 < lines.len()
286                    && !lines[index + 1].starts_with("stack backtrace:")
287                    && !lines[index + 1].starts_with("note:")
288                    && !frame_re.is_match(lines[index + 1]).unwrap_or(false)
289                {
290                    index += 1;
291                    message.push(lines[index]);
292                }
293                trace.message = Some(message.join("\n")).filter(|message| !message.is_empty());
294            } else if let Ok(Some(caps)) = frame_re.captures(line) {
295                let mut function = caps[1].to_string();
296                // `path::to::fn::h0123456789abcdef`: drop the symbol hash.
297                if let Some(stripped) = function.rsplit_once("::h").filter(|(_, hash)| {
298                    hash.len() == 16 && hash.chars().all(|c| c.is_ascii_hexdigit())
299                }) {
300                    function = stripped.0.to_string();
301                }
302                let mut frame = Frame {
303                    function: Some(function),
304                    ..Frame::default()
305                };
306                let next = lines.get(index + 1).copied().unwrap_or("");
307                if let Ok(Some(at)) = at_re.captures(next) {
308                    frame.path = at.get(1).map(|path| path.as_str().to_string());
309                    frame.line = number(at.get(2));
310                    frame.column = number(at.get(3));
311                    index += 1;
312                }
313                frame.library = Self::is_library(
314                    frame.function.as_deref().unwrap_or(""),
315                    frame.path.as_deref(),
316                );
317                trace.frames.push(frame);
318            }
319            index += 1;
320        }
321        // Backtraces list the most recent call first.
322        trace.frames.reverse();
323        (trace.kind.is_some() || !trace.frames.is_empty()).then_some(trace)
324    }
325}
326
327impl StackTrace {
328    /// Where a panic happened, kept as a frame-less location on the trace.
329    fn metadata_location(&mut self, path: &str, line: Option<usize>, column: Option<usize>) {
330        self.location = Some(Frame {
331            path: Some(path.to_string()),
332            line,
333            column,
334            ..Frame::default()
335        });
336    }
337}
338
339/// Parses CPython tracebacks, including chained exceptions.
340pub struct PythonParser;
341
342const PY_CAUSE: &str = "The above exception was the direct cause of the following exception:";
343const PY_CONTEXT: &str = "During handling of the above exception, another exception occurred:";
344
345impl PythonParser {
346    fn block(text: &str) -> Option<StackTrace> {
347        static FILE: OnceLock<Regex> = OnceLock::new();
348        let file_re = regex(&FILE, r#"^\s*File "(.+?)", line (\d+)(?:, in (.+))?$"#);
349        let mut trace = StackTrace::new(Language::Python);
350        let mut last_line = None;
351        let lines: Vec<&str> = text.lines().collect();
352        let mut index = 0;
353        while index < lines.len() {
354            let line = lines[index];
355            if let Ok(Some(caps)) = file_re.captures(line) {
356                let path = caps[1].to_string();
357                let library = path.contains("site-packages")
358                    || path.contains("/lib/python")
359                    || path.starts_with('<');
360                let mut frame = Frame {
361                    function: caps.get(3).map(|m| m.as_str().to_string()),
362                    path: Some(path),
363                    line: number(caps.get(2)),
364                    library,
365                    ..Frame::default()
366                };
367                // The quoted source line, and any `^^^^` marker row under it.
368                while let Some(next) = lines.get(index + 1) {
369                    let trimmed = next.trim();
370                    if next.starts_with("    ")
371                        && !trimmed.is_empty()
372                        && !file_re.is_match(next).unwrap_or(false)
373                    {
374                        if trimmed.chars().all(|c| matches!(c, '^' | '~' | ' ')) {
375                            index += 1;
376                            continue;
377                        }
378                        if frame.source.is_none() {
379                            frame.source = Some(trimmed.to_string());
380                        }
381                        index += 1;
382                    } else {
383                        break;
384                    }
385                }
386                trace.frames.push(frame);
387            } else if !line.trim().is_empty()
388                && !line.starts_with("Traceback (most recent call last):")
389                && !line.starts_with(' ')
390            {
391                last_line = Some(index);
392            }
393            index += 1;
394        }
395        if let Some(last) = last_line {
396            // The exception line, plus any message lines after it.
397            let rest = lines[last..].join("\n");
398            let (kind, message) = kind_and_message(&rest);
399            trace.kind = kind;
400            trace.message = message;
401        }
402        (trace.kind.is_some() || !trace.frames.is_empty()).then_some(trace)
403    }
404}
405
406impl TraceParser for PythonParser {
407    fn detect(&self, text: &str) -> bool {
408        text.contains("Traceback (most recent call last):")
409    }
410
411    fn parse(&self, text: &str) -> Option<StackTrace> {
412        // Earlier blocks are causes of later ones; split on the chaining lines.
413        let mut blocks: Vec<(String, Option<CauseKind>)> = Vec::new();
414        let mut current = String::new();
415        let mut kind = None;
416        for line in text.lines() {
417            let relation = match line.trim() {
418                PY_CAUSE => Some(CauseKind::CausedBy),
419                PY_CONTEXT => Some(CauseKind::DuringHandling),
420                _ => None,
421            };
422            if let Some(relation) = relation {
423                blocks.push((std::mem::take(&mut current), kind));
424                kind = Some(relation);
425            } else {
426                current.push_str(line);
427                current.push('\n');
428            }
429        }
430        blocks.push((current, kind));
431        // Root cause first; a block with no relation starts a new chain.
432        let mut chain: Vec<StackTrace> = Vec::new();
433        for (block, relation) in blocks {
434            let Some(mut trace) = Self::block(&block) else {
435                continue;
436            };
437            match relation {
438                Some(relation) if !chain.is_empty() => trace.cause_kind = relation,
439                _ => chain.clear(),
440            }
441            chain.push(trace);
442        }
443        chain.reverse();
444        link_causes(chain)
445    }
446}
447
448/// Parses JVM stack traces with `Caused by:` chains.
449pub struct JavaParser;
450
451impl TraceParser for JavaParser {
452    fn detect(&self, text: &str) -> bool {
453        static AT: OnceLock<Regex> = OnceLock::new();
454        regex(&AT, r"(?m)^\s+at [\w$.<>]+\(.*\)\s*$")
455            .is_match(text)
456            .unwrap_or(false)
457    }
458
459    fn parse(&self, text: &str) -> Option<StackTrace> {
460        static AT: OnceLock<Regex> = OnceLock::new();
461        let at_re = regex(&AT, r"^\s+at (?:[\w.]+/)?([\w$.<>]+)\((.*)\)\s*$");
462        let mut traces: Vec<StackTrace> = Vec::new();
463        let mut header: Option<&str> = None;
464        for line in text.lines() {
465            if let Ok(Some(caps)) = at_re.captures(line) {
466                // The header is the line right above the first frame.
467                if traces.is_empty() {
468                    let line = header.unwrap_or("").trim();
469                    let line = line
470                        .strip_prefix("Exception in thread ")
471                        .and_then(|rest| rest.split_once("\" ").map(|(_, rest)| rest))
472                        .unwrap_or(line);
473                    let mut trace = StackTrace::new(Language::Java);
474                    (trace.kind, trace.message) = kind_and_message(line);
475                    traces.push(trace);
476                }
477                let Some(trace) = traces.last_mut() else {
478                    continue;
479                };
480                let function = caps[1].to_string();
481                let location = &caps[2];
482                let mut frame = Frame {
483                    library: ["java.", "javax.", "jdk.", "sun.", "com.sun.", "kotlin."]
484                        .iter()
485                        .any(|prefix| function.starts_with(prefix)),
486                    function: Some(function),
487                    ..Frame::default()
488                };
489                match location.rsplit_once(':') {
490                    Some((file, line)) if line.parse::<usize>().is_ok() => {
491                        frame.path = Some(file.to_string());
492                        frame.line = line.parse().ok();
493                    }
494                    _ if location == "Native Method" => {
495                        frame.metadata.push(("native".into(), "true".into()));
496                    }
497                    _ if location != "Unknown Source" && !location.is_empty() => {
498                        frame.path = Some(location.to_string());
499                    }
500                    _ => {}
501                }
502                trace.frames.push(frame);
503            } else if let Some(rest) = line.trim_start().strip_prefix("Caused by: ") {
504                let mut trace = StackTrace::new(Language::Java);
505                (trace.kind, trace.message) = kind_and_message(rest);
506                traces.push(trace);
507            } else if line.trim_start().starts_with("... ") && line.trim_end().ends_with(" more") {
508                if let Some(trace) = traces.last_mut() {
509                    trace.frames.push(Frame {
510                        metadata: vec![("elided".into(), line.trim().to_string())],
511                        library: true,
512                        ..Frame::default()
513                    });
514                }
515            } else if traces.is_empty() && !line.trim().is_empty() {
516                header = Some(line);
517            }
518        }
519        // JVM frames list the most recent call first; each `Caused by` is
520        // the cause of the trace before it.
521        for trace in &mut traces {
522            trace.frames.reverse();
523        }
524        link_causes(traces)
525    }
526}
527
528/// Parses V8 (`node`, Chrome) stack traces.
529pub struct JavaScriptParser;
530
531impl TraceParser for JavaScriptParser {
532    fn detect(&self, text: &str) -> bool {
533        static AT: OnceLock<Regex> = OnceLock::new();
534        regex(&AT, r"(?m)^\s+at .*:\d+:\d+\)?(?: \{)?\s*$")
535            .is_match(text)
536            .unwrap_or(false)
537    }
538
539    fn parse(&self, text: &str) -> Option<StackTrace> {
540        static AT: OnceLock<Regex> = OnceLock::new();
541        let at_re = regex(
542            &AT,
543            r"^\s+at (?:(?:async )?(.+?) \()?(.+?):(\d+):(\d+)\)?(?: \{)?\s*$",
544        );
545        let mut traces: Vec<StackTrace> = Vec::new();
546        let mut header: Option<&str> = None;
547        for line in text.lines() {
548            if let Ok(Some(caps)) = at_re.captures(line) {
549                // The header is the line right above the first frame; Node
550                // prints a source excerpt before it.
551                if traces.is_empty() {
552                    let mut trace = StackTrace::new(Language::JavaScript);
553                    (trace.kind, trace.message) = kind_and_message(header.unwrap_or(""));
554                    traces.push(trace);
555                }
556                let Some(trace) = traces.last_mut() else {
557                    continue;
558                };
559                let path = caps[2].to_string();
560                trace.frames.push(Frame {
561                    library: path.starts_with("node:") || path.contains("node_modules"),
562                    function: caps.get(1).map(|m| m.as_str().to_string()),
563                    path: Some(path),
564                    line: number(caps.get(3)),
565                    column: number(caps.get(4)),
566                    ..Frame::default()
567                });
568            } else if let Some(rest) = line.trim_start().strip_prefix("[cause]: ") {
569                let mut trace = StackTrace::new(Language::JavaScript);
570                (trace.kind, trace.message) = kind_and_message(rest.trim_end_matches(['{', ' ']));
571                traces.push(trace);
572            } else if line.trim_start().starts_with("... ") && line.trim_end().ends_with(" ...") {
573                if let Some(trace) = traces.last_mut() {
574                    trace.frames.push(Frame {
575                        metadata: vec![("elided".into(), line.trim().to_string())],
576                        library: true,
577                        ..Frame::default()
578                    });
579                }
580            } else if traces.is_empty() && !line.trim().is_empty() {
581                header = Some(line);
582            }
583        }
584        for trace in &mut traces {
585            trace.frames.reverse();
586        }
587        link_causes(traces)
588    }
589}
590
591/// The current thread's backtrace as a Rust [`StackTrace`] with `message`.
592/// Frames come from `std::backtrace::Backtrace::force_capture`, so they need
593/// debug info to carry file locations.
594pub fn capture(message: impl Into<String>) -> StackTrace {
595    let backtrace = std::backtrace::Backtrace::force_capture().to_string();
596    let mut trace = RustParser
597        .parse(&backtrace)
598        .unwrap_or_else(|| StackTrace::new(Language::Rust));
599    trace.kind = Some("panic".into());
600    trace.message = Some(message.into());
601    trace
602}
603
604/// A panic hook printing the panic as a [`StackTrace`] to `console`.
605/// Install it with `std::panic::set_hook(Box::new(panic_hook(console)))`;
606/// this module never installs anything itself.
607pub fn panic_hook(
608    console: Console,
609) -> impl Fn(&std::panic::PanicHookInfo<'_>) + Send + Sync + 'static {
610    let console = std::sync::Mutex::new(console);
611    move |info| {
612        let payload = info.payload();
613        let message = payload
614            .downcast_ref::<&str>()
615            .map(|message| message.to_string())
616            .or_else(|| payload.downcast_ref::<String>().cloned())
617            .unwrap_or_else(|| "Box<dyn Any>".to_string());
618        let mut trace = capture(message);
619        if let Some(location) = info.location() {
620            trace.location = Some(Frame {
621                path: Some(location.file().to_string()),
622                line: Some(location.line() as usize),
623                column: Some(location.column() as usize),
624                ..Frame::default()
625            });
626        }
627        let console = console.lock().unwrap_or_else(|e| e.into_inner());
628        console.print(&trace.render_options());
629    }
630}
631
632/// Trace text is untrusted input: show its control codes, never run them.
633fn clean(text: &str) -> String {
634    crate::sanitize_terminal_controls(text)
635}
636
637/// Rendering options for a [`StackTrace`].
638#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
639pub struct StackTraceView<'a> {
640    trace: &'a StackTrace,
641    linker: Hyperlinker,
642    show_library: bool,
643}
644
645impl StackTraceView<'_> {
646    /// Link frame locations with `linker` (default: `file://` links).
647    pub fn hyperlinker(mut self, linker: Hyperlinker) -> Self {
648        self.linker = linker;
649        self
650    }
651
652    /// Show every library frame instead of collapsing runs of them.
653    pub fn show_library(mut self, show: bool) -> Self {
654        self.show_library = show;
655        self
656    }
657
658    /// Every trace in the chain, root cause first, each after its cause and
659    /// the bridge naming how they relate. Iterative, and capped at
660    /// [`MAX_CAUSES`] causes like the parsers, so any chain renders.
661    fn append_chain(&self, console: &Console, text: &mut Text) {
662        let mut chain: Vec<&StackTrace> = Vec::new();
663        let mut omitted = 0usize;
664        for trace in self.trace.chain() {
665            if chain.len() <= MAX_CAUSES {
666                chain.push(trace);
667            } else {
668                omitted += 1;
669            }
670        }
671        if let Some(last) = chain.last() {
672            omitted += last.omitted_causes;
673        }
674        if omitted > 0 {
675            let label = if omitted == 1 { "cause" } else { "causes" };
676            text.append(
677                &format!("… {omitted} more {label}\n\n"),
678                Some(theme_style(console, "stacktrace.library", "dim").into()),
679            );
680        }
681        for (index, trace) in chain.iter().enumerate().rev() {
682            self.append_trace(console, text, trace);
683            let Some(caused) = index.checked_sub(1).map(|at| chain[at]) else {
684                continue;
685            };
686            let bridge = match caused.cause_kind {
687                CauseKind::CausedBy => "The error above caused the following error:",
688                CauseKind::DuringHandling => {
689                    "The error below happened while handling the error above:"
690                }
691            };
692            text.append("\n", None);
693            text.append(
694                bridge,
695                Some(theme_style(console, "stacktrace.bridge", "italic dim").into()),
696            );
697            text.append("\n\n", None);
698        }
699    }
700
701    /// One trace's frames and error line, without its causes.
702    fn append_trace(&self, console: &Console, text: &mut Text, trace: &StackTrace) {
703        let dim = theme_style(console, "stacktrace.library", "dim");
704        let function_style = theme_style(console, "stacktrace.function", "green");
705        let location_style = theme_style(console, "stacktrace.location", "magenta");
706        let mut hidden = 0usize;
707        let flush_hidden = |text: &mut Text, hidden: &mut usize| {
708            if *hidden > 0 {
709                let label = if *hidden == 1 { "frame" } else { "frames" };
710                text.append(
711                    &format!("  … {hidden} library {label}\n"),
712                    Some(dim.clone().into()),
713                );
714                *hidden = 0;
715            }
716        };
717        for frame in &trace.frames {
718            if frame.library && !self.show_library {
719                hidden += 1;
720                continue;
721            }
722            flush_hidden(text, &mut hidden);
723            let base = if frame.library {
724                Some(dim.clone())
725            } else {
726                None
727            };
728            let name = frame.function.as_deref().unwrap_or("<unknown>");
729            let elided = frame.metadata.iter().find(|(key, _)| key == "elided");
730            text.append("  ", None);
731            match elided {
732                Some((_, label)) => text.append(&clean(label), Some(dim.clone().into())),
733                None => text.append(
734                    &clean(name),
735                    Some(base.clone().unwrap_or(function_style.clone()).into()),
736                ),
737            }
738            text.append("\n", None);
739            if let Some(path) = &frame.path {
740                text.append("    at ", base.clone().map(Into::into));
741                let location = self.linker.location(
742                    path,
743                    frame.line,
744                    frame.column,
745                    base.clone().unwrap_or(location_style.clone()),
746                );
747                *text = std::mem::take(text).append_text(&location);
748                text.append("\n", None);
749            }
750            if let Some(source) = &frame.source {
751                text.append(
752                    &format!("      {}\n", clean(source)),
753                    base.clone().map(Into::into),
754                );
755            }
756        }
757        flush_hidden(text, &mut hidden);
758        let error_style = theme_style(console, "stacktrace.error", "bold red");
759        let kind = trace.kind.as_deref().unwrap_or("error");
760        text.append(&clean(kind), Some(error_style.into()));
761        if let Some(message) = &trace.message {
762            text.append(": ", None);
763            text.append(&clean(message), None);
764        }
765        if let Some(location) = &trace.location
766            && let Some(path) = &location.path
767        {
768            text.append("\n  at ", None);
769            let linked = self
770                .linker
771                .location(path, location.line, location.column, location_style);
772            *text = std::mem::take(text).append_text(&linked);
773        }
774    }
775}
776
777impl Renderable for StackTraceView<'_> {
778    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
779        let mut text = Text::new("");
780        self.append_chain(console, &mut text);
781        text.rich_render(console, options)
782    }
783}
784
785impl Renderable for StackTrace {
786    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
787        self.render_options().rich_render(console, options)
788    }
789}