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    pub fn with_parser(mut self, parser: impl TraceParser + 'static) -> Self {
188        self.parsers.insert(0, Box::new(parser));
189        self
190    }
191
192    /// Parse `text` with the first parser that detects it.
193    pub fn parse(&self, text: &str) -> Option<StackTrace> {
194        self.parsers
195            .iter()
196            .find(|parser| parser.detect(text))
197            .and_then(|parser| parser.parse(text))
198    }
199}
200
201/// Parse `text` with the built-in parsers.
202pub fn parse(text: &str) -> Option<StackTrace> {
203    Parsers::default().parse(text)
204}
205
206fn regex(cell: &'static OnceLock<Regex>, pattern: &str) -> &'static Regex {
207    cell.get_or_init(|| Regex::new(pattern).expect("valid trace pattern"))
208}
209
210fn number(value: Option<fancy_regex::Match<'_>>) -> Option<usize> {
211    value.and_then(|value| value.as_str().parse().ok())
212}
213
214/// `kind: message`, splitting on the first `": "` when the kind looks like a
215/// type name.
216fn kind_and_message(line: &str) -> (Option<String>, Option<String>) {
217    let line = line.trim();
218    match line.split_once(": ") {
219        Some((kind, message)) if !kind.contains(' ') => {
220            (Some(kind.to_string()), Some(message.to_string()))
221        }
222        _ if !line.contains(' ') && !line.is_empty() => (Some(line.to_string()), None),
223        _ => (None, Some(line.to_string()).filter(|line| !line.is_empty())),
224    }
225}
226
227/// Parses `std::backtrace` output and panic messages.
228pub struct RustParser;
229
230impl RustParser {
231    fn is_library(function: &str, path: Option<&str>) -> bool {
232        const CRATES: [&str; 6] = [
233            "std::",
234            "core::",
235            "alloc::",
236            "backtrace::",
237            "rust_begin_unwind",
238            "__rust",
239        ];
240        CRATES.iter().any(|prefix| {
241            function.starts_with(prefix) || function.starts_with(&format!("<{prefix}"))
242        }) || path.is_some_and(|path| {
243            path.starts_with("/rustc/")
244                || path.contains("/.cargo/registry/")
245                || path.contains("\\.cargo\\registry\\")
246        })
247    }
248}
249
250impl TraceParser for RustParser {
251    fn detect(&self, text: &str) -> bool {
252        static FRAME: OnceLock<Regex> = OnceLock::new();
253        text.contains("panicked at")
254            || regex(&FRAME, r"(?m)^\s*\d+: \S")
255                .is_match(text)
256                .unwrap_or(false)
257    }
258
259    fn parse(&self, text: &str) -> Option<StackTrace> {
260        static FRAME: OnceLock<Regex> = OnceLock::new();
261        static AT: OnceLock<Regex> = OnceLock::new();
262        static PANIC: OnceLock<Regex> = OnceLock::new();
263        static OLD_PANIC: OnceLock<Regex> = OnceLock::new();
264        let frame_re = regex(&FRAME, r"^\s*\d+:\s+(.+?)\s*$");
265        let at_re = regex(&AT, r"^\s+at\s+(.+?):(\d+)(?::(\d+))?\s*$");
266        let mut trace = StackTrace::new(Language::Rust);
267        let lines: Vec<&str> = text.lines().collect();
268
269        // `thread 'main' panicked at src/main.rs:4:5:` then the message
270        // (Rust 1.73+), or `panicked at 'message', src/main.rs:4:5` before.
271        let panic_re = regex(&PANIC, r"panicked at (.+?):(\d+):(\d+):\s*$");
272        let old_re = regex(&OLD_PANIC, r"panicked at '(.*)', (.+?):(\d+):(\d+)");
273        let mut index = 0;
274        while index < lines.len() {
275            let line = lines[index];
276            if let Ok(Some(caps)) = old_re.captures(line) {
277                trace.kind = Some("panic".into());
278                trace.message = Some(caps[1].to_string());
279                trace.metadata_location(&caps[2], number(caps.get(3)), number(caps.get(4)));
280            } else if let Ok(Some(caps)) = panic_re.captures(line) {
281                trace.kind = Some("panic".into());
282                trace.metadata_location(&caps[1], number(caps.get(2)), number(caps.get(3)));
283                let mut message = Vec::new();
284                while index + 1 < lines.len()
285                    && !lines[index + 1].starts_with("stack backtrace:")
286                    && !lines[index + 1].starts_with("note:")
287                    && !frame_re.is_match(lines[index + 1]).unwrap_or(false)
288                {
289                    index += 1;
290                    message.push(lines[index]);
291                }
292                trace.message = Some(message.join("\n")).filter(|message| !message.is_empty());
293            } else if let Ok(Some(caps)) = frame_re.captures(line) {
294                let mut function = caps[1].to_string();
295                // `path::to::fn::h0123456789abcdef`: drop the symbol hash.
296                if let Some(stripped) = function.rsplit_once("::h").filter(|(_, hash)| {
297                    hash.len() == 16 && hash.chars().all(|c| c.is_ascii_hexdigit())
298                }) {
299                    function = stripped.0.to_string();
300                }
301                let mut frame = Frame {
302                    function: Some(function),
303                    ..Frame::default()
304                };
305                let next = lines.get(index + 1).copied().unwrap_or("");
306                if let Ok(Some(at)) = at_re.captures(next) {
307                    frame.path = at.get(1).map(|path| path.as_str().to_string());
308                    frame.line = number(at.get(2));
309                    frame.column = number(at.get(3));
310                    index += 1;
311                }
312                frame.library = Self::is_library(
313                    frame.function.as_deref().unwrap_or(""),
314                    frame.path.as_deref(),
315                );
316                trace.frames.push(frame);
317            }
318            index += 1;
319        }
320        // Backtraces list the most recent call first.
321        trace.frames.reverse();
322        (trace.kind.is_some() || !trace.frames.is_empty()).then_some(trace)
323    }
324}
325
326impl StackTrace {
327    /// Where a panic happened, kept as a frame-less location on the trace.
328    fn metadata_location(&mut self, path: &str, line: Option<usize>, column: Option<usize>) {
329        self.location = Some(Frame {
330            path: Some(path.to_string()),
331            line,
332            column,
333            ..Frame::default()
334        });
335    }
336}
337
338/// Parses CPython tracebacks, including chained exceptions.
339pub struct PythonParser;
340
341const PY_CAUSE: &str = "The above exception was the direct cause of the following exception:";
342const PY_CONTEXT: &str = "During handling of the above exception, another exception occurred:";
343
344impl PythonParser {
345    fn block(text: &str) -> Option<StackTrace> {
346        static FILE: OnceLock<Regex> = OnceLock::new();
347        let file_re = regex(&FILE, r#"^\s*File "(.+?)", line (\d+)(?:, in (.+))?$"#);
348        let mut trace = StackTrace::new(Language::Python);
349        let mut last_line = None;
350        let lines: Vec<&str> = text.lines().collect();
351        let mut index = 0;
352        while index < lines.len() {
353            let line = lines[index];
354            if let Ok(Some(caps)) = file_re.captures(line) {
355                let path = caps[1].to_string();
356                let library = path.contains("site-packages")
357                    || path.contains("/lib/python")
358                    || path.starts_with('<');
359                let mut frame = Frame {
360                    function: caps.get(3).map(|m| m.as_str().to_string()),
361                    path: Some(path),
362                    line: number(caps.get(2)),
363                    library,
364                    ..Frame::default()
365                };
366                // The quoted source line, and any `^^^^` marker row under it.
367                while let Some(next) = lines.get(index + 1) {
368                    let trimmed = next.trim();
369                    if next.starts_with("    ")
370                        && !trimmed.is_empty()
371                        && !file_re.is_match(next).unwrap_or(false)
372                    {
373                        if trimmed.chars().all(|c| matches!(c, '^' | '~' | ' ')) {
374                            index += 1;
375                            continue;
376                        }
377                        if frame.source.is_none() {
378                            frame.source = Some(trimmed.to_string());
379                        }
380                        index += 1;
381                    } else {
382                        break;
383                    }
384                }
385                trace.frames.push(frame);
386            } else if !line.trim().is_empty()
387                && !line.starts_with("Traceback (most recent call last):")
388                && !line.starts_with(' ')
389            {
390                last_line = Some(index);
391            }
392            index += 1;
393        }
394        if let Some(last) = last_line {
395            // The exception line, plus any message lines after it.
396            let rest = lines[last..].join("\n");
397            let (kind, message) = kind_and_message(&rest);
398            trace.kind = kind;
399            trace.message = message;
400        }
401        (trace.kind.is_some() || !trace.frames.is_empty()).then_some(trace)
402    }
403}
404
405impl TraceParser for PythonParser {
406    fn detect(&self, text: &str) -> bool {
407        text.contains("Traceback (most recent call last):")
408    }
409
410    fn parse(&self, text: &str) -> Option<StackTrace> {
411        // Earlier blocks are causes of later ones; split on the chaining lines.
412        let mut blocks: Vec<(String, Option<CauseKind>)> = Vec::new();
413        let mut current = String::new();
414        let mut kind = None;
415        for line in text.lines() {
416            let relation = match line.trim() {
417                PY_CAUSE => Some(CauseKind::CausedBy),
418                PY_CONTEXT => Some(CauseKind::DuringHandling),
419                _ => None,
420            };
421            if let Some(relation) = relation {
422                blocks.push((std::mem::take(&mut current), kind));
423                kind = Some(relation);
424            } else {
425                current.push_str(line);
426                current.push('\n');
427            }
428        }
429        blocks.push((current, kind));
430        // Root cause first; a block with no relation starts a new chain.
431        let mut chain: Vec<StackTrace> = Vec::new();
432        for (block, relation) in blocks {
433            let Some(mut trace) = Self::block(&block) else {
434                continue;
435            };
436            match relation {
437                Some(relation) if !chain.is_empty() => trace.cause_kind = relation,
438                _ => chain.clear(),
439            }
440            chain.push(trace);
441        }
442        chain.reverse();
443        link_causes(chain)
444    }
445}
446
447/// Parses JVM stack traces with `Caused by:` chains.
448pub struct JavaParser;
449
450impl TraceParser for JavaParser {
451    fn detect(&self, text: &str) -> bool {
452        static AT: OnceLock<Regex> = OnceLock::new();
453        regex(&AT, r"(?m)^\s+at [\w$.<>]+\(.*\)\s*$")
454            .is_match(text)
455            .unwrap_or(false)
456    }
457
458    fn parse(&self, text: &str) -> Option<StackTrace> {
459        static AT: OnceLock<Regex> = OnceLock::new();
460        let at_re = regex(&AT, r"^\s+at (?:[\w.]+/)?([\w$.<>]+)\((.*)\)\s*$");
461        let mut traces: Vec<StackTrace> = Vec::new();
462        let mut header: Option<&str> = None;
463        for line in text.lines() {
464            if let Ok(Some(caps)) = at_re.captures(line) {
465                // The header is the line right above the first frame.
466                if traces.is_empty() {
467                    let line = header.unwrap_or("").trim();
468                    let line = line
469                        .strip_prefix("Exception in thread ")
470                        .and_then(|rest| rest.split_once("\" ").map(|(_, rest)| rest))
471                        .unwrap_or(line);
472                    let mut trace = StackTrace::new(Language::Java);
473                    (trace.kind, trace.message) = kind_and_message(line);
474                    traces.push(trace);
475                }
476                let Some(trace) = traces.last_mut() else {
477                    continue;
478                };
479                let function = caps[1].to_string();
480                let location = &caps[2];
481                let mut frame = Frame {
482                    library: ["java.", "javax.", "jdk.", "sun.", "com.sun.", "kotlin."]
483                        .iter()
484                        .any(|prefix| function.starts_with(prefix)),
485                    function: Some(function),
486                    ..Frame::default()
487                };
488                match location.rsplit_once(':') {
489                    Some((file, line)) if line.parse::<usize>().is_ok() => {
490                        frame.path = Some(file.to_string());
491                        frame.line = line.parse().ok();
492                    }
493                    _ if location == "Native Method" => {
494                        frame.metadata.push(("native".into(), "true".into()));
495                    }
496                    _ if location != "Unknown Source" && !location.is_empty() => {
497                        frame.path = Some(location.to_string());
498                    }
499                    _ => {}
500                }
501                trace.frames.push(frame);
502            } else if let Some(rest) = line.trim_start().strip_prefix("Caused by: ") {
503                let mut trace = StackTrace::new(Language::Java);
504                (trace.kind, trace.message) = kind_and_message(rest);
505                traces.push(trace);
506            } else if line.trim_start().starts_with("... ") && line.trim_end().ends_with(" more") {
507                if let Some(trace) = traces.last_mut() {
508                    trace.frames.push(Frame {
509                        metadata: vec![("elided".into(), line.trim().to_string())],
510                        library: true,
511                        ..Frame::default()
512                    });
513                }
514            } else if traces.is_empty() && !line.trim().is_empty() {
515                header = Some(line);
516            }
517        }
518        // JVM frames list the most recent call first; each `Caused by` is
519        // the cause of the trace before it.
520        for trace in &mut traces {
521            trace.frames.reverse();
522        }
523        link_causes(traces)
524    }
525}
526
527/// Parses V8 (`node`, Chrome) stack traces.
528pub struct JavaScriptParser;
529
530impl TraceParser for JavaScriptParser {
531    fn detect(&self, text: &str) -> bool {
532        static AT: OnceLock<Regex> = OnceLock::new();
533        regex(&AT, r"(?m)^\s+at .*:\d+:\d+\)?(?: \{)?\s*$")
534            .is_match(text)
535            .unwrap_or(false)
536    }
537
538    fn parse(&self, text: &str) -> Option<StackTrace> {
539        static AT: OnceLock<Regex> = OnceLock::new();
540        let at_re = regex(
541            &AT,
542            r"^\s+at (?:(?:async )?(.+?) \()?(.+?):(\d+):(\d+)\)?(?: \{)?\s*$",
543        );
544        let mut traces: Vec<StackTrace> = Vec::new();
545        let mut header: Option<&str> = None;
546        for line in text.lines() {
547            if let Ok(Some(caps)) = at_re.captures(line) {
548                // The header is the line right above the first frame; Node
549                // prints a source excerpt before it.
550                if traces.is_empty() {
551                    let mut trace = StackTrace::new(Language::JavaScript);
552                    (trace.kind, trace.message) = kind_and_message(header.unwrap_or(""));
553                    traces.push(trace);
554                }
555                let Some(trace) = traces.last_mut() else {
556                    continue;
557                };
558                let path = caps[2].to_string();
559                trace.frames.push(Frame {
560                    library: path.starts_with("node:") || path.contains("node_modules"),
561                    function: caps.get(1).map(|m| m.as_str().to_string()),
562                    path: Some(path),
563                    line: number(caps.get(3)),
564                    column: number(caps.get(4)),
565                    ..Frame::default()
566                });
567            } else if let Some(rest) = line.trim_start().strip_prefix("[cause]: ") {
568                let mut trace = StackTrace::new(Language::JavaScript);
569                (trace.kind, trace.message) = kind_and_message(rest.trim_end_matches(['{', ' ']));
570                traces.push(trace);
571            } else if line.trim_start().starts_with("... ") && line.trim_end().ends_with(" ...") {
572                if let Some(trace) = traces.last_mut() {
573                    trace.frames.push(Frame {
574                        metadata: vec![("elided".into(), line.trim().to_string())],
575                        library: true,
576                        ..Frame::default()
577                    });
578                }
579            } else if traces.is_empty() && !line.trim().is_empty() {
580                header = Some(line);
581            }
582        }
583        for trace in &mut traces {
584            trace.frames.reverse();
585        }
586        link_causes(traces)
587    }
588}
589
590/// The current thread's backtrace as a Rust [`StackTrace`] with `message`.
591/// Frames come from `std::backtrace::Backtrace::force_capture`, so they need
592/// debug info to carry file locations.
593pub fn capture(message: impl Into<String>) -> StackTrace {
594    let backtrace = std::backtrace::Backtrace::force_capture().to_string();
595    let mut trace = RustParser
596        .parse(&backtrace)
597        .unwrap_or_else(|| StackTrace::new(Language::Rust));
598    trace.kind = Some("panic".into());
599    trace.message = Some(message.into());
600    trace
601}
602
603/// A panic hook printing the panic as a [`StackTrace`] to `console`.
604/// Install it with `std::panic::set_hook(Box::new(panic_hook(console)))`;
605/// this module never installs anything itself.
606pub fn panic_hook(
607    console: Console,
608) -> impl Fn(&std::panic::PanicHookInfo<'_>) + Send + Sync + 'static {
609    let console = std::sync::Mutex::new(console);
610    move |info| {
611        let payload = info.payload();
612        let message = payload
613            .downcast_ref::<&str>()
614            .map(|message| message.to_string())
615            .or_else(|| payload.downcast_ref::<String>().cloned())
616            .unwrap_or_else(|| "Box<dyn Any>".to_string());
617        let mut trace = capture(message);
618        if let Some(location) = info.location() {
619            trace.location = Some(Frame {
620                path: Some(location.file().to_string()),
621                line: Some(location.line() as usize),
622                column: Some(location.column() as usize),
623                ..Frame::default()
624            });
625        }
626        let console = console.lock().unwrap_or_else(|e| e.into_inner());
627        console.print(&trace.render_options());
628    }
629}
630
631/// Trace text is untrusted input: show its control codes, never run them.
632fn clean(text: &str) -> String {
633    crate::sanitize_terminal_controls(text)
634}
635
636/// Rendering options for a [`StackTrace`].
637pub struct StackTraceView<'a> {
638    trace: &'a StackTrace,
639    linker: Hyperlinker,
640    show_library: bool,
641}
642
643impl StackTraceView<'_> {
644    /// Link frame locations with `linker` (default: `file://` links).
645    pub fn hyperlinker(mut self, linker: Hyperlinker) -> Self {
646        self.linker = linker;
647        self
648    }
649
650    /// Show every library frame instead of collapsing runs of them.
651    pub fn show_library(mut self, show: bool) -> Self {
652        self.show_library = show;
653        self
654    }
655
656    /// Every trace in the chain, root cause first, each after its cause and
657    /// the bridge naming how they relate. Iterative, and capped at
658    /// [`MAX_CAUSES`] causes like the parsers, so any chain renders.
659    fn append_chain(&self, console: &Console, text: &mut Text) {
660        let mut chain: Vec<&StackTrace> = Vec::new();
661        let mut omitted = 0usize;
662        for trace in self.trace.chain() {
663            if chain.len() <= MAX_CAUSES {
664                chain.push(trace);
665            } else {
666                omitted += 1;
667            }
668        }
669        if let Some(last) = chain.last() {
670            omitted += last.omitted_causes;
671        }
672        if omitted > 0 {
673            let label = if omitted == 1 { "cause" } else { "causes" };
674            text.append(
675                &format!("… {omitted} more {label}\n\n"),
676                Some(theme_style(console, "stacktrace.library", "dim").into()),
677            );
678        }
679        for (index, trace) in chain.iter().enumerate().rev() {
680            self.append_trace(console, text, trace);
681            let Some(caused) = index.checked_sub(1).map(|at| chain[at]) else {
682                continue;
683            };
684            let bridge = match caused.cause_kind {
685                CauseKind::CausedBy => "The error above caused the following error:",
686                CauseKind::DuringHandling => {
687                    "The error below happened while handling the error above:"
688                }
689            };
690            text.append("\n", None);
691            text.append(
692                bridge,
693                Some(theme_style(console, "stacktrace.bridge", "italic dim").into()),
694            );
695            text.append("\n\n", None);
696        }
697    }
698
699    /// One trace's frames and error line, without its causes.
700    fn append_trace(&self, console: &Console, text: &mut Text, trace: &StackTrace) {
701        let dim = theme_style(console, "stacktrace.library", "dim");
702        let function_style = theme_style(console, "stacktrace.function", "green");
703        let location_style = theme_style(console, "stacktrace.location", "magenta");
704        let mut hidden = 0usize;
705        let flush_hidden = |text: &mut Text, hidden: &mut usize| {
706            if *hidden > 0 {
707                let label = if *hidden == 1 { "frame" } else { "frames" };
708                text.append(
709                    &format!("  … {hidden} library {label}\n"),
710                    Some(dim.clone().into()),
711                );
712                *hidden = 0;
713            }
714        };
715        for frame in &trace.frames {
716            if frame.library && !self.show_library {
717                hidden += 1;
718                continue;
719            }
720            flush_hidden(text, &mut hidden);
721            let base = if frame.library {
722                Some(dim.clone())
723            } else {
724                None
725            };
726            let name = frame.function.as_deref().unwrap_or("<unknown>");
727            let elided = frame.metadata.iter().find(|(key, _)| key == "elided");
728            text.append("  ", None);
729            match elided {
730                Some((_, label)) => text.append(&clean(label), Some(dim.clone().into())),
731                None => text.append(
732                    &clean(name),
733                    Some(base.clone().unwrap_or(function_style.clone()).into()),
734                ),
735            }
736            text.append("\n", None);
737            if let Some(path) = &frame.path {
738                text.append("    at ", base.clone().map(Into::into));
739                let location = self.linker.location(
740                    path,
741                    frame.line,
742                    frame.column,
743                    base.clone().unwrap_or(location_style.clone()),
744                );
745                *text = std::mem::take(text).append_text(&location);
746                text.append("\n", None);
747            }
748            if let Some(source) = &frame.source {
749                text.append(
750                    &format!("      {}\n", clean(source)),
751                    base.clone().map(Into::into),
752                );
753            }
754        }
755        flush_hidden(text, &mut hidden);
756        let error_style = theme_style(console, "stacktrace.error", "bold red");
757        let kind = trace.kind.as_deref().unwrap_or("error");
758        text.append(&clean(kind), Some(error_style.into()));
759        if let Some(message) = &trace.message {
760            text.append(": ", None);
761            text.append(&clean(message), None);
762        }
763        if let Some(location) = &trace.location {
764            if let Some(path) = &location.path {
765                text.append("\n  at ", None);
766                let linked =
767                    self.linker
768                        .location(path, location.line, location.column, location_style);
769                *text = std::mem::take(text).append_text(&linked);
770            }
771        }
772    }
773}
774
775impl Renderable for StackTraceView<'_> {
776    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
777        let mut text = Text::new("");
778        self.append_chain(console, &mut text);
779        text.rich_render(console, options)
780    }
781}
782
783impl Renderable for StackTrace {
784    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
785        self.render_options().rich_render(console, options)
786    }
787}