ariadne/
write.rs

1use std::borrow::Borrow;
2use std::io;
3use std::ops::Range;
4
5use super::draw::{self, StreamAwareFmt, StreamType};
6use super::{Cache, CharSet, Label, LabelAttach, Report, ReportKind, Show, Span, Write};
7
8// A WARNING, FOR ALL YE WHO VENTURE IN HERE
9//
10// - This code is complex and has a lot of implicit invariants
11// - Yes, it has some bugs
12// - Yes, it needs rewriting
13// - No, you are not expected to understand it. I will probably not understand it either in a month, but that will only
14//   give me a reason to rewrite it
15
16enum LabelKind {
17    Inline,
18    Multiline,
19}
20
21struct LabelInfo<'a, S> {
22    kind: LabelKind,
23    label: &'a Label<S>,
24}
25
26struct SourceGroup<'a, S: Span> {
27    src_id: &'a S::SourceId,
28    span: Range<usize>,
29    labels: Vec<LabelInfo<'a, S>>,
30}
31
32impl<S: Span> Report<'_, S> {
33    fn get_source_groups(&self, cache: &mut impl Cache<S::SourceId>) -> Vec<SourceGroup<S>> {
34        let mut groups = Vec::new();
35        for label in self.labels.iter() {
36            let src_display = cache.display(label.span.source());
37            let src = match cache.fetch(label.span.source()) {
38                Ok(src) => src,
39                Err(e) => {
40                    eprintln!("Unable to fetch source '{}': {:?}", Show(src_display), e);
41                    continue;
42                }
43            };
44
45            assert!(
46                label.span.start() <= label.span.end(),
47                "Label start is after its end"
48            );
49
50            let start_line = src.get_offset_line(label.span.start()).map(|(_, l, _)| l);
51            let end_line = src
52                .get_offset_line(label.span.end().saturating_sub(1).max(label.span.start()))
53                .map(|(_, l, _)| l);
54
55            let label_info = LabelInfo {
56                kind: if start_line == end_line {
57                    LabelKind::Inline
58                } else {
59                    LabelKind::Multiline
60                },
61                label,
62            };
63
64            if let Some(group) = groups
65                .iter_mut()
66                .find(|g: &&mut SourceGroup<S>| g.src_id == label.span.source())
67            {
68                group.span.start = group.span.start.min(label.span.start());
69                group.span.end = group.span.end.max(label.span.end());
70                group.labels.push(label_info);
71            } else {
72                groups.push(SourceGroup {
73                    src_id: label.span.source(),
74                    span: label.span.start()..label.span.end(),
75                    labels: vec![label_info],
76                });
77            }
78        }
79        groups
80    }
81
82    /// Write this diagnostic to an implementor of [`Write`].
83    ///
84    /// If using the `concolor` feature, this method assumes that the output is ultimately going to be printed to
85    /// `stderr`.  If you are printing to `stdout`, use the [`write_for_stdout`](Self::write_for_stdout) method instead.
86    ///
87    /// If you wish to write to `stderr` or `stdout`, you can do so via [`Report::eprint`] or [`Report::print`] respectively.
88    pub fn write<C: Cache<S::SourceId>, W: Write>(&self, cache: C, w: W) -> io::Result<()> {
89        self.write_for_stream(cache, w, StreamType::Stderr)
90    }
91
92    /// Write this diagnostic to an implementor of [`Write`], assuming that the output is ultimately going to be printed
93    /// to `stdout`.
94    pub fn write_for_stdout<C: Cache<S::SourceId>, W: Write>(
95        &self,
96        cache: C,
97        w: W,
98    ) -> io::Result<()> {
99        self.write_for_stream(cache, w, StreamType::Stdout)
100    }
101
102    /// Write this diagnostic to an implementor of [`Write`], assuming that the output is ultimately going to be printed
103    /// to the given output stream (`stdout` or `stderr`).
104    fn write_for_stream<C: Cache<S::SourceId>, W: Write>(
105        &self,
106        mut cache: C,
107        mut w: W,
108        s: StreamType,
109    ) -> io::Result<()> {
110        let draw = match self.config.char_set {
111            CharSet::Unicode => draw::Characters::unicode(),
112            CharSet::Ascii => draw::Characters::ascii(),
113        };
114
115        // --- Header ---
116
117        let code = self.code.as_ref().map(|c| format!("[{}] ", c));
118        let id = format!("{}{}:", Show(code), self.kind);
119        let kind_color = match self.kind {
120            ReportKind::Error => self.config.error_color(),
121            ReportKind::Warning => self.config.warning_color(),
122            ReportKind::Advice => self.config.advice_color(),
123            ReportKind::Custom(_, color) => Some(color),
124        };
125        writeln!(w, "{} {}", id.fg(kind_color, s), Show(self.msg.as_ref()))?;
126
127        let groups = self.get_source_groups(&mut cache);
128
129        // Line number maximum width
130        let line_no_width = groups
131            .iter()
132            .filter_map(|SourceGroup { span, src_id, .. }| {
133                let src_name = cache
134                    .display(src_id)
135                    .map(|d| d.to_string())
136                    .unwrap_or_else(|| "<unknown>".to_string());
137
138                let src = match cache.fetch(src_id) {
139                    Ok(src) => src,
140                    Err(e) => {
141                        eprintln!("Unable to fetch source {}: {:?}", src_name, e);
142                        return None;
143                    }
144                };
145
146                let line_range = src.get_line_range(span);
147                Some(
148                    (1..)
149                        .map(|x| 10u32.pow(x))
150                        .take_while(|x| line_range.end as u32 / x != 0)
151                        .count()
152                        + 1,
153                )
154            })
155            .max()
156            .unwrap_or(0);
157
158        // --- Source sections ---
159        let groups_len = groups.len();
160        for (
161            group_idx,
162            SourceGroup {
163                src_id,
164                span,
165                labels,
166            },
167        ) in groups.into_iter().enumerate()
168        {
169            let src_name = cache
170                .display(src_id)
171                .map(|d| d.to_string())
172                .unwrap_or_else(|| "<unknown>".to_string());
173
174            let src = match cache.fetch(src_id) {
175                Ok(src) => src,
176                Err(e) => {
177                    eprintln!("Unable to fetch source {}: {:?}", src_name, e);
178                    continue;
179                }
180            };
181
182            let line_range = src.get_line_range(&span);
183
184            // File name & reference
185            let location = if src_id == self.location.0.borrow() {
186                self.location.1
187            } else {
188                labels[0].label.span.start()
189            };
190            let (line_no, col_no) = src
191                .get_offset_line(location)
192                .map(|(_, idx, col)| (format!("{}", idx + 1), format!("{}", col + 1)))
193                .unwrap_or_else(|| ('?'.to_string(), '?'.to_string()));
194            let line_ref = format!(":{}:{}", line_no, col_no);
195            writeln!(
196                w,
197                "{}{}{}{}{}{}{}",
198                Show((' ', line_no_width + 2)),
199                if group_idx == 0 {
200                    draw.ltop
201                } else {
202                    draw.lcross
203                }
204                .fg(self.config.margin_color(), s),
205                draw.hbar.fg(self.config.margin_color(), s),
206                draw.lbox.fg(self.config.margin_color(), s),
207                src_name,
208                line_ref,
209                draw.rbox.fg(self.config.margin_color(), s),
210            )?;
211
212            if !self.config.compact {
213                writeln!(
214                    w,
215                    "{}{}",
216                    Show((' ', line_no_width + 2)),
217                    draw.vbar.fg(self.config.margin_color(), s)
218                )?;
219            }
220
221            struct LineLabel<'a, S> {
222                col: usize,
223                label: &'a Label<S>,
224                multi: bool,
225                draw_msg: bool,
226            }
227
228            // Generate a list of multi-line labels
229            let mut multi_labels = Vec::new();
230            for label_info in &labels {
231                if matches!(label_info.kind, LabelKind::Multiline) {
232                    multi_labels.push(&label_info.label);
233                }
234            }
235
236            // Sort multiline labels by length
237            multi_labels.sort_by_key(|m| -(m.span.len() as isize));
238
239            let write_margin = |w: &mut W,
240                                idx: usize,
241                                is_line: bool,
242                                is_ellipsis: bool,
243                                draw_labels: bool,
244                                report_row: Option<(usize, bool)>,
245                                line_labels: &[LineLabel<S>],
246                                margin_label: &Option<LineLabel<S>>|
247             -> std::io::Result<()> {
248                let line_no_margin = if is_line && !is_ellipsis {
249                    let line_no = format!("{}", idx + 1);
250                    format!(
251                        "{}{} {}",
252                        Show((' ', line_no_width - line_no.chars().count())),
253                        line_no,
254                        draw.vbar,
255                    )
256                    .fg(self.config.margin_color(), s)
257                } else {
258                    format!(
259                        "{}{}",
260                        Show((' ', line_no_width + 1)),
261                        if is_ellipsis {
262                            draw.vbar_gap
263                        } else {
264                            draw.vbar
265                        }
266                    )
267                    .fg(self.config.skipped_margin_color(), s)
268                };
269
270                write!(
271                    w,
272                    " {}{}",
273                    line_no_margin,
274                    Show(Some(' ').filter(|_| !self.config.compact)),
275                )?;
276
277                // Multi-line margins
278                if draw_labels {
279                    for col in 0..multi_labels.len() + (multi_labels.len() > 0) as usize {
280                        let mut corner = None;
281                        let mut hbar = None;
282                        let mut vbar: Option<&&Label<S>> = None;
283                        let mut margin_ptr = None;
284
285                        let multi_label = multi_labels.get(col);
286                        let line_span = src.line(idx).unwrap().span();
287
288                        for (i, label) in multi_labels[0..(col + 1).min(multi_labels.len())]
289                            .iter()
290                            .enumerate()
291                        {
292                            let margin = margin_label
293                                .as_ref()
294                                .filter(|m| **label as *const _ == m.label as *const _);
295
296                            if label.span.start() <= line_span.end
297                                && label.span.end() > line_span.start
298                            {
299                                let is_parent = i != col;
300                                let is_start = line_span.contains(&label.span.start());
301                                let is_end = line_span.contains(&label.last_offset());
302
303                                if let Some(margin) = margin.filter(|_| is_line) {
304                                    margin_ptr = Some((margin, is_start));
305                                } else if !is_start && (!is_end || is_line) {
306                                    vbar = vbar.or(Some(*label).filter(|_| !is_parent));
307                                } else if let Some((report_row, is_arrow)) = report_row {
308                                    let label_row = line_labels
309                                        .iter()
310                                        .enumerate()
311                                        .find(|(_, l)| **label as *const _ == l.label as *const _)
312                                        .map_or(0, |(r, _)| r);
313                                    if report_row == label_row {
314                                        if let Some(margin) = margin {
315                                            vbar = Some(&margin.label).filter(|_| col == i);
316                                            if is_start {
317                                                continue;
318                                            }
319                                        }
320
321                                        if is_arrow {
322                                            hbar = Some(**label);
323                                            if !is_parent {
324                                                corner = Some((label, is_start));
325                                            }
326                                        } else if !is_start {
327                                            vbar = vbar.or(Some(*label).filter(|_| !is_parent));
328                                        }
329                                    } else {
330                                        vbar = vbar.or(Some(*label).filter(|_| {
331                                            !is_parent && (is_start ^ (report_row < label_row))
332                                        }));
333                                    }
334                                }
335                            }
336                        }
337
338                        if let (Some((margin, _is_start)), true) = (margin_ptr, is_line) {
339                            let is_col = multi_label
340                                .map_or(false, |ml| **ml as *const _ == margin.label as *const _);
341                            let is_limit = col + 1 == multi_labels.len();
342                            if !is_col && !is_limit {
343                                hbar = hbar.or(Some(margin.label));
344                            }
345                        }
346
347                        hbar = hbar.filter(|l| {
348                            margin_label
349                                .as_ref()
350                                .map_or(true, |margin| margin.label as *const _ != *l as *const _)
351                                || !is_line
352                        });
353
354                        let (a, b) = if let Some((label, is_start)) = corner {
355                            (
356                                if is_start { draw.ltop } else { draw.lbot }.fg(label.color, s),
357                                draw.hbar.fg(label.color, s),
358                            )
359                        } else if let Some(label) =
360                            hbar.filter(|_| vbar.is_some() && !self.config.cross_gap)
361                        {
362                            (draw.xbar.fg(label.color, s), draw.hbar.fg(label.color, s))
363                        } else if let Some(label) = hbar {
364                            (draw.hbar.fg(label.color, s), draw.hbar.fg(label.color, s))
365                        } else if let Some(label) = vbar {
366                            (
367                                if is_ellipsis {
368                                    draw.vbar_gap
369                                } else {
370                                    draw.vbar
371                                }
372                                .fg(label.color, s),
373                                ' '.fg(None, s),
374                            )
375                        } else if let (Some((margin, is_start)), true) = (margin_ptr, is_line) {
376                            let is_col = multi_label
377                                .map_or(false, |ml| **ml as *const _ == margin.label as *const _);
378                            let is_limit = col == multi_labels.len();
379                            (
380                                if is_limit {
381                                    draw.rarrow
382                                } else if is_col {
383                                    if is_start {
384                                        draw.ltop
385                                    } else {
386                                        draw.lcross
387                                    }
388                                } else {
389                                    draw.hbar
390                                }
391                                .fg(margin.label.color, s),
392                                if !is_limit { draw.hbar } else { ' ' }.fg(margin.label.color, s),
393                            )
394                        } else {
395                            (' '.fg(None, s), ' '.fg(None, s))
396                        };
397                        write!(w, "{}", a)?;
398                        if !self.config.compact {
399                            write!(w, "{}", b)?;
400                        }
401                    }
402                }
403
404                Ok(())
405            };
406
407            let mut is_ellipsis = false;
408            for idx in line_range {
409                let line = if let Some(line) = src.line(idx) {
410                    line
411                } else {
412                    continue;
413                };
414
415                let margin_label = multi_labels
416                    .iter()
417                    .enumerate()
418                    .filter_map(|(_i, label)| {
419                        let is_start = line.span().contains(&label.span.start());
420                        let is_end = line.span().contains(&label.last_offset());
421                        if is_start {
422                            // TODO: Check to see whether multi is the first on the start line or first on the end line
423                            Some(LineLabel {
424                                col: label.span.start() - line.offset(),
425                                label: **label,
426                                multi: true,
427                                draw_msg: false, // Multi-line spans don;t have their messages drawn at the start
428                            })
429                        } else if is_end {
430                            Some(LineLabel {
431                                col: label.last_offset() - line.offset(),
432                                label: **label,
433                                multi: true,
434                                draw_msg: true, // Multi-line spans have their messages drawn at the end
435                            })
436                        } else {
437                            None
438                        }
439                    })
440                    .min_by_key(|ll| (ll.col, !ll.label.span.start()));
441
442                // Generate a list of labels for this line, along with their label columns
443                let mut line_labels = multi_labels
444                    .iter()
445                    .enumerate()
446                    .filter_map(|(_i, label)| {
447                        let is_start = line.span().contains(&label.span.start());
448                        let is_end = line.span().contains(&label.last_offset());
449                        if is_start
450                            && margin_label
451                                .as_ref()
452                                .map_or(true, |m| **label as *const _ != m.label as *const _)
453                        {
454                            // TODO: Check to see whether multi is the first on the start line or first on the end line
455                            Some(LineLabel {
456                                col: label.span.start() - line.offset(),
457                                label: **label,
458                                multi: true,
459                                draw_msg: false, // Multi-line spans don;t have their messages drawn at the start
460                            })
461                        } else if is_end {
462                            Some(LineLabel {
463                                col: label.last_offset() - line.offset(),
464                                label: **label,
465                                multi: true,
466                                draw_msg: true, // Multi-line spans have their messages drawn at the end
467                            })
468                        } else {
469                            None
470                        }
471                    })
472                    .collect::<Vec<_>>();
473
474                for label_info in labels.iter().filter(|l| {
475                    l.label.span.start() >= line.span().start
476                        && l.label.span.end() <= line.span().end
477                }) {
478                    if matches!(label_info.kind, LabelKind::Inline) {
479                        line_labels.push(LineLabel {
480                            col: match &self.config.label_attach {
481                                LabelAttach::Start => label_info.label.span.start(),
482                                LabelAttach::Middle => {
483                                    (label_info.label.span.start() + label_info.label.span.end())
484                                        / 2
485                                }
486                                LabelAttach::End => label_info.label.last_offset(),
487                            }
488                            .max(label_info.label.span.start())
489                                - line.offset(),
490                            label: label_info.label,
491                            multi: false,
492                            draw_msg: true,
493                        });
494                    }
495                }
496
497                // Skip this line if we don't have labels for it
498                if line_labels.len() == 0 && margin_label.is_none() {
499                    let within_label = multi_labels
500                        .iter()
501                        .any(|label| label.span.contains(line.span().start()));
502                    if !is_ellipsis && within_label {
503                        is_ellipsis = true;
504                    } else {
505                        if !self.config.compact && !is_ellipsis {
506                            write_margin(&mut w, idx, false, is_ellipsis, false, None, &[], &None)?;
507                            write!(w, "\n")?;
508                        }
509                        is_ellipsis = true;
510                        continue;
511                    }
512                } else {
513                    is_ellipsis = false;
514                }
515
516                // Sort the labels by their columns
517                line_labels.sort_by_key(|ll| (ll.label.order, ll.col, !ll.label.span.start()));
518
519                // Determine label bounds so we know where to put error messages
520                let arrow_end_space = if self.config.compact { 1 } else { 2 };
521                let arrow_len = line_labels.iter().fold(0, |l, ll| {
522                    if ll.multi {
523                        line.len()
524                    } else {
525                        l.max(ll.label.span.end().saturating_sub(line.offset()))
526                    }
527                }) + arrow_end_space;
528
529                // Should we draw a vertical bar as part of a label arrow on this line?
530                let get_vbar = |col, row| {
531                    line_labels
532                        .iter()
533                        // Only labels with notes get an arrow
534                        .enumerate()
535                        .filter(|(_, ll)| {
536                            ll.label.msg.is_some()
537                                && margin_label
538                                    .as_ref()
539                                    .map_or(true, |m| ll.label as *const _ != m.label as *const _)
540                        })
541                        .find(|(j, ll)| {
542                            ll.col == col && ((row <= *j && !ll.multi) || (row <= *j && ll.multi))
543                        })
544                        .map(|(_, ll)| ll)
545                };
546
547                let get_highlight = |col| {
548                    margin_label
549                        .iter()
550                        .map(|ll| ll.label)
551                        .chain(multi_labels.iter().map(|l| **l))
552                        .chain(line_labels.iter().map(|l| l.label))
553                        .filter(|l| l.span.contains(line.offset() + col))
554                        // Prioritise displaying smaller spans
555                        .min_by_key(|l| (-l.priority, l.span.len()))
556                };
557
558                let get_underline = |col| {
559                    line_labels
560                        .iter()
561                        .filter(|ll| {
562                            self.config.underlines
563                        // Underlines only occur for inline spans (highlighting can occur for all spans)
564                        && !ll.multi
565                        && ll.label.span.contains(line.offset() + col)
566                        })
567                        // Prioritise displaying smaller spans
568                        .min_by_key(|ll| (-ll.label.priority, ll.label.span.len()))
569                };
570
571                // Margin
572                write_margin(
573                    &mut w,
574                    idx,
575                    true,
576                    is_ellipsis,
577                    true,
578                    None,
579                    &line_labels,
580                    &margin_label,
581                )?;
582
583                // Line
584                if !is_ellipsis {
585                    for (col, c) in line.chars().enumerate() {
586                        let color = if let Some(highlight) = get_highlight(col) {
587                            highlight.color
588                        } else {
589                            self.config.unimportant_color()
590                        };
591                        let (c, width) = self.config.char_width(c, col);
592                        if c.is_whitespace() {
593                            for _ in 0..width {
594                                write!(w, "{}", c.fg(color, s))?;
595                            }
596                        } else {
597                            write!(w, "{}", c.fg(color, s))?;
598                        };
599                    }
600                }
601                write!(w, "\n")?;
602
603                // Arrows
604                for row in 0..line_labels.len() {
605                    let line_label = &line_labels[row];
606
607                    if !self.config.compact {
608                        // Margin alternate
609                        write_margin(
610                            &mut w,
611                            idx,
612                            false,
613                            is_ellipsis,
614                            true,
615                            Some((row, false)),
616                            &line_labels,
617                            &margin_label,
618                        )?;
619                        // Lines alternate
620                        let mut chars = line.chars();
621                        for col in 0..arrow_len {
622                            let width =
623                                chars.next().map_or(1, |c| self.config.char_width(c, col).1);
624
625                            let vbar = get_vbar(col, row);
626                            let underline = get_underline(col).filter(|_| row == 0);
627                            let [c, tail] = if let Some(vbar_ll) = vbar {
628                                let [c, tail] = if underline.is_some() {
629                                    // TODO: Is this good?
630                                    if vbar_ll.label.span.len() <= 1 || true {
631                                        [draw.underbar, draw.underline]
632                                    } else if line.offset() + col == vbar_ll.label.span.start() {
633                                        [draw.ltop, draw.underbar]
634                                    } else if line.offset() + col == vbar_ll.label.last_offset() {
635                                        [draw.rtop, draw.underbar]
636                                    } else {
637                                        [draw.underbar, draw.underline]
638                                    }
639                                } else if vbar_ll.multi && row == 0 && self.config.multiline_arrows
640                                {
641                                    [draw.uarrow, ' ']
642                                } else {
643                                    [draw.vbar, ' ']
644                                };
645                                [
646                                    c.fg(vbar_ll.label.color, s),
647                                    tail.fg(vbar_ll.label.color, s),
648                                ]
649                            } else if let Some(underline_ll) = underline {
650                                [draw.underline.fg(underline_ll.label.color, s); 2]
651                            } else {
652                                [' '.fg(None, s); 2]
653                            };
654
655                            for i in 0..width {
656                                write!(w, "{}", if i == 0 { c } else { tail })?;
657                            }
658                        }
659                        write!(w, "\n")?;
660                    }
661
662                    // Margin
663                    write_margin(
664                        &mut w,
665                        idx,
666                        false,
667                        is_ellipsis,
668                        true,
669                        Some((row, true)),
670                        &line_labels,
671                        &margin_label,
672                    )?;
673                    // Lines
674                    let mut chars = line.chars();
675                    for col in 0..arrow_len {
676                        let width = chars.next().map_or(1, |c| self.config.char_width(c, col).1);
677
678                        let is_hbar = (((col > line_label.col) ^ line_label.multi)
679                            || (line_label.label.msg.is_some()
680                                && line_label.draw_msg
681                                && col > line_label.col))
682                            && line_label.label.msg.is_some();
683                        let [c, tail] = if col == line_label.col
684                            && line_label.label.msg.is_some()
685                            && margin_label.as_ref().map_or(true, |m| {
686                                line_label.label as *const _ != m.label as *const _
687                            }) {
688                            [
689                                if line_label.multi {
690                                    if line_label.draw_msg {
691                                        draw.mbot
692                                    } else {
693                                        draw.rbot
694                                    }
695                                } else {
696                                    draw.lbot
697                                }
698                                .fg(line_label.label.color, s),
699                                draw.hbar.fg(line_label.label.color, s),
700                            ]
701                        } else if let Some(vbar_ll) = get_vbar(col, row)
702                            .filter(|_| (col != line_label.col || line_label.label.msg.is_some()))
703                        {
704                            if !self.config.cross_gap && is_hbar {
705                                [
706                                    draw.xbar.fg(line_label.label.color, s),
707                                    ' '.fg(line_label.label.color, s),
708                                ]
709                            } else if is_hbar {
710                                [draw.hbar.fg(line_label.label.color, s); 2]
711                            } else {
712                                [
713                                    if vbar_ll.multi && row == 0 && self.config.compact {
714                                        draw.uarrow
715                                    } else {
716                                        draw.vbar
717                                    }
718                                    .fg(vbar_ll.label.color, s),
719                                    ' '.fg(line_label.label.color, s),
720                                ]
721                            }
722                        } else if is_hbar {
723                            [draw.hbar.fg(line_label.label.color, s); 2]
724                        } else {
725                            [' '.fg(None, s); 2]
726                        };
727
728                        if width > 0 {
729                            write!(w, "{}", c)?;
730                        }
731                        for _ in 1..width {
732                            write!(w, "{}", tail)?;
733                        }
734                    }
735                    if line_label.draw_msg {
736                        write!(w, " {}", Show(line_label.label.msg.as_ref()))?;
737                    }
738                    write!(w, "\n")?;
739                }
740            }
741
742            let is_final_group = group_idx + 1 == groups_len;
743
744            // Help
745            if let (Some(note), true) = (&self.help, is_final_group) {
746                if !self.config.compact {
747                    write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
748                    write!(w, "\n")?;
749                }
750                write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
751                write!(w, "{}: {}\n", "Help".fg(self.config.note_color(), s), note)?;
752            }
753
754            // Note
755            if let (Some(note), true) = (&self.note, is_final_group) {
756                if !self.config.compact {
757                    write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
758                    write!(w, "\n")?;
759                }
760                write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
761                write!(w, "{}: {}\n", "Note".fg(self.config.note_color(), s), note)?;
762            }
763
764            // Tail of report
765            if !self.config.compact {
766                if is_final_group {
767                    let final_margin =
768                        format!("{}{}", Show((draw.hbar, line_no_width + 2)), draw.rbot);
769                    writeln!(w, "{}", final_margin.fg(self.config.margin_color(), s))?;
770                } else {
771                    writeln!(
772                        w,
773                        "{}{}",
774                        Show((' ', line_no_width + 2)),
775                        draw.vbar.fg(self.config.margin_color(), s)
776                    )?;
777                }
778            }
779        }
780        Ok(())
781    }
782}
783
784impl<S: Span> Label<S> {
785    fn last_offset(&self) -> usize {
786        self.span.end().saturating_sub(1).max(self.span.start())
787    }
788}