Skip to main content

rich_ext/
diagnostic.rs

1//! Compiler-style diagnostics. They render supplied source only; no
2//! filesystem or clock access.
3//!
4//! A [`Diagnostic`] has a message and, optionally:
5//! - a [`Level`] and code (`error[E0308]: …`), the code linked to its docs
6//! - a [`Location`], linked through a [`Hyperlinker`]
7//! - [`SourceSnippet`]s with primary (`^^^`) and secondary (`---`) spans, each
8//!   with an optional label
9//! - causes, notes, help and [`Suggestion`]s that show the edited line
10//! - a [`StackTrace`]
11//!
12//! [`DiagnosticInfo`] lets an error type (a `thiserror` enum, for example)
13//! supply its level, code, help and location, so [`Diagnostic::from_info`]
14//! builds the whole diagnostic. With the `anyhow` feature,
15//! [`Diagnostic::from_anyhow`] maps an `anyhow::Error`'s context chain and
16//! captured backtrace.
17//!
18//! Without a level, labels or suggestions, a diagnostic renders as it did
19//! before these were added: the message in `diagnostic.message`, then plain
20//! rows.
21use crate::event::{EventView, Value, flatten, theme_style};
22use crate::hyperlink::Hyperlinker;
23use crate::layout::{OverflowPolicy, fit_segments};
24use crate::stacktrace::StackTrace;
25use rich::{Console, ConsoleOptions, Renderable, Segment, Style, Text};
26use std::ops::Range;
27
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub enum DiagnosticError {
30    InvalidSpan,
31}
32impl std::fmt::Display for DiagnosticError {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        write!(f, "source span is outside UTF-8 boundaries")
35    }
36}
37impl std::error::Error for DiagnosticError {}
38
39/// How serious a diagnostic is.
40#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub enum Level {
42    Error,
43    Warning,
44    Info,
45    Note,
46    Help,
47}
48
49impl Level {
50    /// The lowercase name shown in the header (`error`, `warning`, …).
51    pub fn name(self) -> &'static str {
52        match self {
53            Level::Error => "error",
54            Level::Warning => "warning",
55            Level::Info => "info",
56            Level::Note => "note",
57            Level::Help => "help",
58        }
59    }
60
61    /// The theme style for this level (`diagnostic.<name>`), with a default.
62    pub fn style(self, console: &Console) -> Style {
63        let fallback = match self {
64            Level::Error => "bold red",
65            Level::Warning => "bold yellow",
66            Level::Info => "bold blue",
67            Level::Note => "bold green",
68            Level::Help => "bold cyan",
69        };
70        theme_style(console, &format!("diagnostic.{}", self.name()), fallback)
71    }
72}
73
74/// A place in a file: `path:line:column`.
75#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
76pub struct Location {
77    pub path: String,
78    /// The 1-based line.
79    pub line: Option<usize>,
80    /// The 1-based column.
81    pub column: Option<usize>,
82}
83
84impl Location {
85    pub fn new(path: impl Into<String>, line: Option<usize>, column: Option<usize>) -> Self {
86        Location {
87            path: path.into(),
88            line,
89            column,
90        }
91    }
92}
93
94#[derive(Clone, Debug)]
95struct SpanLabel {
96    span: Range<usize>,
97    message: Option<String>,
98    primary: bool,
99}
100
101fn check_span(source: &str, span: &Range<usize>) -> Result<(), DiagnosticError> {
102    if span.start > span.end
103        || span.end > source.len()
104        || !source.is_char_boundary(span.start)
105        || !source.is_char_boundary(span.end)
106    {
107        return Err(DiagnosticError::InvalidSpan);
108    }
109    Ok(())
110}
111
112/// The display cells before `a` and from `a` to `b` in `line`, with tabs
113/// expanded; a byte inside a grapheme cluster widens to the whole cluster.
114fn marker_cells(line: &str, a: usize, b: usize) -> (usize, usize) {
115    let (mut a, mut b) = (a.min(line.len()), b.min(line.len()));
116    // A valid byte span may start inside a visible grapheme. Mark the whole
117    // cluster rather than the following display cell.
118    for (begin, end, _) in rich::cells::split_graphemes(line).0 {
119        if begin < a && a < end {
120            a = begin;
121        }
122        if begin < b && b < end {
123            b = end;
124        }
125    }
126    let mut before = Text::new(&line[..a]);
127    before.expand_tabs(8);
128    let mut through = Text::new(&line[..b]);
129    through.expand_tabs(8);
130    let left = before.cell_len();
131    (left, through.cell_len().saturating_sub(left).max(1))
132}
133
134#[derive(Clone, Debug)]
135pub struct SourceSnippet {
136    name: String,
137    source: String,
138    /// The first label is the snippet's own primary span.
139    labels: Vec<SpanLabel>,
140    context_lines: usize,
141}
142impl SourceSnippet {
143    pub fn new(
144        name: String,
145        source: String,
146        span: Range<usize>,
147        context_lines: usize,
148    ) -> Result<Self, DiagnosticError> {
149        check_span(&source, &span)?;
150        Ok(Self {
151            name,
152            source,
153            labels: vec![SpanLabel {
154                span,
155                message: None,
156                primary: true,
157            }],
158            context_lines,
159        })
160    }
161
162    /// Label the primary span (`^^^ message`).
163    #[must_use]
164    pub fn primary_label(mut self, message: impl Into<String>) -> Self {
165        self.labels[0].message = Some(message.into());
166        self
167    }
168
169    /// Add another primary span with a label.
170    pub fn primary(
171        mut self,
172        span: Range<usize>,
173        message: impl Into<String>,
174    ) -> Result<Self, DiagnosticError> {
175        check_span(&self.source, &span)?;
176        self.labels.push(SpanLabel {
177            span,
178            message: Some(message.into()).filter(|m| !m.is_empty()),
179            primary: true,
180        });
181        Ok(self)
182    }
183
184    /// Add a secondary span (`--- message`), for related code.
185    pub fn secondary(
186        mut self,
187        span: Range<usize>,
188        message: impl Into<String>,
189    ) -> Result<Self, DiagnosticError> {
190        check_span(&self.source, &span)?;
191        self.labels.push(SpanLabel {
192            span,
193            message: Some(message.into()).filter(|m| !m.is_empty()),
194            primary: false,
195        });
196        Ok(self)
197    }
198
199    /// The snippet's name (usually a path).
200    pub fn name(&self) -> &str {
201        &self.name
202    }
203
204    /// Where the primary span starts: line and column, both 1-based, the
205    /// column counted in characters.
206    pub fn location(&self) -> Location {
207        let start = self.labels[0].span.start;
208        let before = &self.source[..start];
209        let line = before.matches('\n').count() + 1;
210        let line_start = before.rfind('\n').map_or(0, |i| i + 1);
211        let column = before[line_start..].chars().count() + 1;
212        Location::new(self.name.clone(), Some(line), Some(column))
213    }
214
215    fn rows(
216        &self,
217        primary_style: Option<Style>,
218        secondary_style: Option<Style>,
219        linker: Option<&Hyperlinker>,
220    ) -> Vec<Vec<Segment>> {
221        struct Line<'a> {
222            number: usize,
223            offset: usize,
224            end: usize,
225            text: &'a str,
226        }
227        let mut lines = Vec::new();
228        let mut offset = 0;
229        for (i, line) in self.source.split('\n').enumerate() {
230            let end = offset + line.len();
231            // Offsets follow the raw source; a CRLF's `\r` must not reach the
232            // terminal, where it would move the cursor back over the row.
233            lines.push(Line {
234                number: i + 1,
235                offset,
236                end,
237                text: line.strip_suffix('\r').unwrap_or(line),
238            });
239            offset = end.saturating_add(1);
240        }
241        let touches = |label: &SpanLabel, line: &Line<'_>| {
242            if label.span.is_empty() {
243                label.span.start >= line.offset && label.span.start <= line.end
244            } else {
245                label.span.start <= line.end && label.span.end > line.offset
246            }
247        };
248        let active: Vec<bool> = lines
249            .iter()
250            .map(|line| self.labels.iter().any(|label| touches(label, line)))
251            .collect();
252        // A label's message goes on the last line it touches.
253        let last_line: Vec<Option<usize>> = self
254            .labels
255            .iter()
256            .map(|label| lines.iter().rposition(|line| touches(label, line)))
257            .collect();
258        let first = active.iter().position(|a| *a).unwrap_or(0);
259        let last = active.iter().rposition(|a| *a).unwrap_or(first);
260        let digits = (last + 1).to_string().len();
261        let gutter = format!("{} | ", " ".repeat(digits));
262
263        let mut header = vec![Segment::new("--> ", None)];
264        match linker.and_then(|linker| {
265            let location = self.location();
266            linker.file_url(&self.name, location.line, location.column)
267        }) {
268            Some(url) => header.push(Segment::new(
269                self.name.clone(),
270                Some(Style::new().with_link(url)),
271            )),
272            None => header.push(Segment::new(self.name.clone(), None)),
273        }
274        let mut out = vec![Segment::simplify(&header)];
275
276        let window_start = first.saturating_sub(self.context_lines);
277        let window_end = last
278            .saturating_add(self.context_lines)
279            .saturating_add(1)
280            .min(lines.len());
281        for (index, line) in lines.iter().enumerate().take(window_end).skip(window_start) {
282            let mut text = Text::new(line.text);
283            text.expand_tabs(8);
284            out.push(vec![Segment::new(
285                format!("{:>digits$} | {}", line.number, text.plain()),
286                None,
287            )]);
288            if !active[index] {
289                continue;
290            }
291            // Marker cells: secondary first, so a primary span wins overlaps.
292            let mut cells: Vec<Option<bool>> = Vec::new();
293            let mut messages: Vec<(usize, &str, bool)> = Vec::new();
294            let mut order: Vec<usize> = (0..self.labels.len()).collect();
295            order.sort_by_key(|&i| self.labels[i].primary);
296            for i in order {
297                let label = &self.labels[i];
298                if !touches(label, line) {
299                    continue;
300                }
301                let a = label.span.start.saturating_sub(line.offset);
302                let b = label.span.end.saturating_sub(line.offset);
303                let (left, count) = marker_cells(line.text, a, b);
304                if cells.len() < left + count {
305                    cells.resize(left + count, None);
306                }
307                for cell in &mut cells[left..left + count] {
308                    *cell = Some(label.primary);
309                }
310                if let (Some(message), Some(true)) =
311                    (&label.message, last_line[i].map(|l| l == index))
312                {
313                    messages.push((left, message, label.primary));
314                }
315            }
316            messages.sort_by_key(|(left, _, _)| *left);
317            let style_for = |primary: bool| {
318                if primary {
319                    primary_style.clone()
320                } else {
321                    secondary_style.clone()
322                }
323            };
324            let mut row = vec![Segment::new(gutter.clone(), None)];
325            let mut run = String::new();
326            let mut run_kind: Option<bool> = None;
327            let flush = |row: &mut Vec<Segment>, run: &mut String, kind: Option<bool>| {
328                if !run.is_empty() {
329                    let style = kind.and_then(style_for);
330                    row.push(Segment::new(std::mem::take(run), style));
331                }
332            };
333            for cell in &cells {
334                if *cell != run_kind {
335                    flush(&mut row, &mut run, run_kind);
336                    run_kind = *cell;
337                }
338                run.push(match cell {
339                    Some(true) => '^',
340                    Some(false) => '-',
341                    None => ' ',
342                });
343            }
344            flush(&mut row, &mut run, run_kind);
345            // The rightmost label shares the marker row; the others follow.
346            if let Some((_, message, primary)) = messages.pop() {
347                row.push(Segment::new(" ", None));
348                row.push(Segment::new(message.to_string(), style_for(primary)));
349            }
350            out.push(Segment::simplify(&row));
351            for (left, message, primary) in messages.into_iter().rev() {
352                out.push(Segment::simplify(&[
353                    Segment::new(format!("{gutter}{}", " ".repeat(left)), None),
354                    Segment::new(message.to_string(), style_for(primary)),
355                ]));
356            }
357        }
358        out
359    }
360}
361
362/// A suggested fix: a message, and optionally the line as it would read.
363#[derive(Clone, Debug)]
364pub struct Suggestion {
365    message: String,
366    edit: Option<SuggestedEdit>,
367}
368
369#[derive(Clone, Debug)]
370struct SuggestedEdit {
371    line_number: usize,
372    line: String,
373    left: usize,
374    count: usize,
375}
376
377impl Suggestion {
378    /// A suggestion with no edit.
379    pub fn new(message: impl Into<String>) -> Self {
380        Suggestion {
381            message: message.into(),
382            edit: None,
383        }
384    }
385
386    /// Suggest replacing `span` of `source` with `replacement`. The edited
387    /// line is shown with the replacement underlined; a span running past its
388    /// line is cut at the line's end.
389    pub fn replace(
390        message: impl Into<String>,
391        source: &str,
392        span: Range<usize>,
393        replacement: &str,
394    ) -> Result<Self, DiagnosticError> {
395        check_span(source, &span)?;
396        let line_start = source[..span.start].rfind('\n').map_or(0, |i| i + 1);
397        let line_end = source[span.start..]
398            .find('\n')
399            .map_or(source.len(), |i| span.start + i);
400        let line = &source[line_start..line_end];
401        let line = line.strip_suffix('\r').unwrap_or(line);
402        // A span starting at a CRLF line's `\n` is past the line once its
403        // `\r` is dropped: clamp it to the line's end.
404        let a = (span.start - line_start).min(line.len());
405        let b = (span.end.min(line_end) - line_start).clamp(a, line.len());
406        let edited = format!("{}{replacement}{}", &line[..a], &line[b..]);
407        let (left, count) = marker_cells(&edited, a, a + replacement.len());
408        let mut shown = Text::new(edited);
409        shown.expand_tabs(8);
410        Ok(Suggestion {
411            message: message.into(),
412            edit: Some(SuggestedEdit {
413                line_number: source[..span.start].matches('\n').count() + 1,
414                line: shown.plain().to_string(),
415                left,
416                count,
417            }),
418        })
419    }
420
421    /// The suggestion's message.
422    pub fn message(&self) -> &str {
423        &self.message
424    }
425}
426
427#[derive(Clone, Debug)]
428#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
429pub struct Diagnostic {
430    message: String,
431    level: Option<Level>,
432    code: Option<String>,
433    code_url: Option<String>,
434    location: Option<Location>,
435    causes: Vec<String>,
436    notes: Vec<String>,
437    help: Vec<String>,
438    suggestions: Vec<Suggestion>,
439    labels: Vec<String>,
440    snippets: Vec<SourceSnippet>,
441    metadata: Vec<(String, Value)>,
442    trace: Option<StackTrace>,
443    linker: Option<Hyperlinker>,
444    view: EventView,
445    overflow: OverflowPolicy,
446}
447impl Diagnostic {
448    pub fn new(message: impl Into<String>) -> Self {
449        Self {
450            message: message.into(),
451            level: None,
452            code: None,
453            code_url: None,
454            location: None,
455            causes: Vec::new(),
456            notes: Vec::new(),
457            help: Vec::new(),
458            suggestions: Vec::new(),
459            labels: Vec::new(),
460            snippets: Vec::new(),
461            metadata: Vec::new(),
462            trace: None,
463            linker: None,
464            view: EventView::Compact,
465            overflow: OverflowPolicy::Fold,
466        }
467    }
468    /// An error-level diagnostic: `Diagnostic::new(message).level(Level::Error)`.
469    pub fn error(message: impl Into<String>) -> Self {
470        Self::new(message).level(Level::Error)
471    }
472    /// A warning-level diagnostic.
473    pub fn warning(message: impl Into<String>) -> Self {
474        Self::new(message).level(Level::Warning)
475    }
476    pub fn from_error(error: &(dyn std::error::Error + 'static), max_depth: usize) -> Self {
477        let mut result = Self::new(error.to_string());
478        let mut seen = vec![error as *const dyn std::error::Error];
479        let mut next = error.source();
480        while let Some(error) = next {
481            if result.causes.len() >= max_depth {
482                result.causes.push("[truncated]".into());
483                break;
484            }
485            let ptr = error as *const dyn std::error::Error;
486            // Identity is address *and* type (the whole wide pointer): an error
487            // stored as its wrapper's first field shares the wrapper's address,
488            // so comparing addresses alone reported ordinary chains as cycles.
489            if seen.iter().any(|&p| std::ptr::eq(p, ptr)) {
490                result.causes.push("[cycle]".into());
491                break;
492            }
493            seen.push(ptr);
494            result.causes.push(error.to_string());
495            next = error.source();
496        }
497        result
498    }
499    /// A diagnostic from an error that describes itself through
500    /// [`DiagnosticInfo`]: its level, code, help, notes and location, and its
501    /// `source()` chain as causes.
502    pub fn from_info<E: DiagnosticInfo>(error: &E, max_depth: usize) -> Self {
503        let mut result = Self::from_error(error, max_depth).level(error.level());
504        result.code = error.code();
505        result.code_url = error.code_url();
506        result.location = error.location();
507        result.help.extend(error.help());
508        result.notes.extend(error.notes());
509        result
510    }
511    /// A diagnostic from an `anyhow::Error`: its context chain (outermost
512    /// first) as message and causes, and its backtrace, when one was
513    /// captured, as a stack trace.
514    #[cfg(feature = "anyhow")]
515    pub fn from_anyhow(error: &anyhow::Error, max_depth: usize) -> Self {
516        let inner: &(dyn std::error::Error + 'static) = error.as_ref();
517        let mut result = Self::from_error(inner, max_depth).level(Level::Error);
518        let backtrace = error.backtrace();
519        if backtrace.status() == std::backtrace::BacktraceStatus::Captured {
520            use crate::stacktrace::TraceParser;
521            result.trace = crate::stacktrace::RustParser.parse(&backtrace.to_string());
522        }
523        result
524    }
525    pub fn level(mut self, level: Level) -> Self {
526        self.level = Some(level);
527        self
528    }
529    /// An error code shown as `error[CODE]`.
530    pub fn code(mut self, code: impl Into<String>) -> Self {
531        self.code = Some(code.into());
532        self
533    }
534    /// Link the code to its documentation.
535    pub fn code_url(mut self, url: impl Into<String>) -> Self {
536        self.code_url = Some(url.into());
537        self
538    }
539    /// Where the problem is: `--> path:line:column` under the header.
540    pub fn location(mut self, location: Location) -> Self {
541        self.location = Some(location);
542        self
543    }
544    /// Link the location, snippet names and stack-trace frames with `linker`.
545    /// Without one, locations render as plain text.
546    pub fn hyperlinker(mut self, linker: Hyperlinker) -> Self {
547        self.linker = Some(linker);
548        self
549    }
550    pub fn cause(mut self, message: impl Into<String>) -> Self {
551        self.causes.push(message.into());
552        self
553    }
554    pub fn note(mut self, message: impl Into<String>) -> Self {
555        self.notes.push(message.into());
556        self
557    }
558    pub fn help(mut self, message: impl Into<String>) -> Self {
559        self.help.push(message.into());
560        self
561    }
562    pub fn suggestion(mut self, suggestion: Suggestion) -> Self {
563        self.suggestions.push(suggestion);
564        self
565    }
566    pub fn label(mut self, message: impl Into<String>) -> Self {
567        self.labels.push(message.into());
568        self
569    }
570    pub fn snippet(mut self, snippet: SourceSnippet) -> Self {
571        self.snippets.push(snippet);
572        self
573    }
574    pub fn metadata(mut self, key: impl Into<String>, value: Value) -> Self {
575        self.metadata.push((key.into(), value));
576        self
577    }
578    /// Show a stack trace in the expanded view.
579    pub fn trace(mut self, trace: StackTrace) -> Self {
580        self.trace = Some(trace);
581        self
582    }
583    pub fn view(mut self, view: EventView) -> Self {
584        self.view = view;
585        self
586    }
587    pub fn overflow(mut self, overflow: OverflowPolicy) -> Self {
588        self.overflow = overflow;
589        self
590    }
591
592    /// The headline message.
593    pub fn message(&self) -> &str {
594        &self.message
595    }
596    /// The level, if set.
597    pub fn get_level(&self) -> Option<Level> {
598        self.level
599    }
600    /// The code, if set.
601    pub fn get_code(&self) -> Option<&str> {
602        self.code.as_deref()
603    }
604    /// The location: the one set, else the first snippet's primary span.
605    pub fn get_location(&self) -> Option<Location> {
606        self.location
607            .clone()
608            .or_else(|| self.snippets.first().map(SourceSnippet::location))
609    }
610    /// The causes, outermost first.
611    pub fn causes(&self) -> &[String] {
612        &self.causes
613    }
614    /// The attached stack trace, if any.
615    pub fn get_trace(&self) -> Option<&StackTrace> {
616        self.trace.as_ref()
617    }
618    /// The documentation link for the code, if set.
619    pub fn get_code_url(&self) -> Option<&str> {
620        self.code_url.as_deref()
621    }
622    /// The notes, in order.
623    pub fn notes(&self) -> &[String] {
624        &self.notes
625    }
626    /// The help messages, in order.
627    pub fn help_messages(&self) -> &[String] {
628        &self.help
629    }
630    /// The suggested fixes, in order.
631    pub fn suggestions(&self) -> &[Suggestion] {
632        &self.suggestions
633    }
634    /// The labels, in order.
635    pub fn labels(&self) -> &[String] {
636        &self.labels
637    }
638
639    fn header(&self, c: &Console) -> Vec<Segment> {
640        let Some(level) = self.level else {
641            let mut segments = Vec::new();
642            if let Some(code) = &self.code {
643                segments.push(Segment::new(format!("[{code}] "), None));
644            }
645            segments.push(Segment::new(
646                &self.message,
647                Some(theme_style(c, "diagnostic.message", "bold red")),
648            ));
649            return segments;
650        };
651        let style = level.style(c);
652        let mut segments = vec![Segment::new(level.name(), Some(style.clone()))];
653        if let Some(code) = &self.code {
654            let code_style = match &self.code_url {
655                Some(url) => style.clone().with_link(url.clone()),
656                None => style.clone(),
657            };
658            segments.push(Segment::new(format!("[{code}]"), Some(code_style)));
659        }
660        segments.push(Segment::new(
661            ": ",
662            Some(theme_style(c, "diagnostic.headline", "bold")),
663        ));
664        segments.push(Segment::new(
665            &self.message,
666            Some(theme_style(c, "diagnostic.headline", "bold")),
667        ));
668        segments
669    }
670}
671
672/// An error that describes itself as a diagnostic. Implement it on an error
673/// type — a `thiserror` enum, for example — and [`Diagnostic::from_info`]
674/// maps it without hand-building the renderable. Every method has a default.
675pub trait DiagnosticInfo: std::error::Error + 'static {
676    /// The level (default error).
677    fn level(&self) -> Level {
678        Level::Error
679    }
680    /// An error code.
681    fn code(&self) -> Option<String> {
682        None
683    }
684    /// A documentation link for the code.
685    fn code_url(&self) -> Option<String> {
686        None
687    }
688    /// Help text.
689    fn help(&self) -> Option<String> {
690        None
691    }
692    /// Notes.
693    fn notes(&self) -> Vec<String> {
694        Vec::new()
695    }
696    /// Where the error is.
697    fn location(&self) -> Option<Location> {
698        None
699    }
700    /// This error as a diagnostic, following up to 16 causes.
701    fn to_diagnostic(&self) -> Diagnostic
702    where
703        Self: Sized,
704    {
705        Diagnostic::from_info(self, 16)
706    }
707}
708
709impl Renderable for Diagnostic {
710    fn rich_render(&self, c: &Console, o: &ConsoleOptions) -> Vec<Segment> {
711        if o.max_width == 0 || o.height == Some(0) {
712            return Vec::new();
713        }
714        let mut rows = fit_segments(&self.header(c), o.max_width, self.overflow);
715        if let Some(location) = &self.location {
716            let gutter = theme_style(c, "diagnostic.gutter", "bold blue");
717            let linker = self.linker.clone().unwrap_or_else(Hyperlinker::disabled);
718            let text = linker.location(&location.path, location.line, location.column, "");
719            let mut segments = vec![Segment::new("  --> ", Some(gutter))];
720            segments.extend(text.render(c.theme(), &Style::new()));
721            rows.extend(fit_segments(&segments, o.max_width, OverflowPolicy::Crop));
722        }
723        for cause in &self.causes {
724            rows.extend(fit_segments(
725                &[Segment::new(format!("caused by: {cause}"), None)],
726                o.max_width,
727                self.overflow,
728            ));
729        }
730        if self.view == EventView::Expanded {
731            for label in &self.labels {
732                rows.extend(fit_segments(
733                    &[Segment::new(label, None)],
734                    o.max_width,
735                    self.overflow,
736                ));
737            }
738            let (primary, secondary) = match self.level {
739                Some(level) => (
740                    Some(level.style(c)),
741                    Some(theme_style(c, "diagnostic.secondary", "bold blue")),
742                ),
743                None => (None, None),
744            };
745            for (index, snippet) in self.snippets.iter().enumerate() {
746                // The `  --> path:line:column` row already names the first
747                // snippet's file, so its own `--> path` header is dropped
748                // rather than repeating the location (as rustc shows it once).
749                let repeated = index == 0
750                    && self
751                        .location
752                        .as_ref()
753                        .is_some_and(|location| location.path == snippet.name);
754                // Crop paired source/underline rows together; wrapping them independently mislabels columns.
755                let lines = snippet.rows(primary.clone(), secondary.clone(), self.linker.as_ref());
756                for line in lines.into_iter().skip(usize::from(repeated)) {
757                    rows.extend(fit_segments(&line, o.max_width, OverflowPolicy::Crop));
758                }
759            }
760            for (key, value) in &self.metadata {
761                rows.extend(fit_segments(
762                    &[Segment::new(
763                        format!("{key}={}", value.format(true, 0)),
764                        None,
765                    )],
766                    o.max_width,
767                    self.overflow,
768                ));
769            }
770            for (prefix, values) in [("note", &self.notes), ("help", &self.help)] {
771                for value in values {
772                    rows.extend(fit_segments(
773                        &[Segment::new(format!("{prefix}: {value}"), None)],
774                        o.max_width,
775                        self.overflow,
776                    ));
777                }
778            }
779            let added = theme_style(c, "diagnostic.suggestion", "green");
780            for suggestion in &self.suggestions {
781                rows.extend(fit_segments(
782                    &[Segment::new(format!("help: {}", suggestion.message), None)],
783                    o.max_width,
784                    self.overflow,
785                ));
786                if let Some(edit) = &suggestion.edit {
787                    let digits = edit.line_number.to_string().len();
788                    let line = format!("{:>digits$} | {}", edit.line_number, edit.line);
789                    rows.extend(fit_segments(
790                        &[Segment::new(line, None)],
791                        o.max_width,
792                        OverflowPolicy::Crop,
793                    ));
794                    let marks = [
795                        Segment::new(
796                            format!("{} | {}", " ".repeat(digits), " ".repeat(edit.left)),
797                            None,
798                        ),
799                        Segment::new("+".repeat(edit.count), Some(added.clone())),
800                    ];
801                    rows.extend(fit_segments(&marks, o.max_width, OverflowPolicy::Crop));
802                }
803            }
804            if let Some(trace) = &self.trace {
805                let view = trace
806                    .render_options()
807                    .hyperlinker(self.linker.clone().unwrap_or_else(Hyperlinker::disabled));
808                rows.extend(c.render_lines(&view, &o.update_width(o.max_width), false));
809            }
810        }
811        if let Some(height) = o.height {
812            rows.truncate(height);
813        }
814        flatten(rows)
815    }
816}