ariadne/
write.rs

1use std::io;
2use std::ops::Range;
3
4use crate::{IndexType, LabelDisplay};
5
6use super::draw::{self, StreamAwareFmt, StreamType};
7use super::{Cache, CharSet, LabelAttach, Report, ReportKind, Show, Span, Write};
8
9// A WARNING, FOR ALL YE WHO VENTURE IN HERE
10//
11// - This code is complex and has a lot of implicit invariants
12// - Yes, it has some bugs
13// - Yes, it needs rewriting
14// - No, you are not expected to understand it. I will probably not understand it either in a month, but that will only
15//   give me a reason to rewrite it
16
17enum LabelKind {
18    Inline,
19    Multiline,
20}
21
22struct LabelInfo<'a> {
23    kind: LabelKind,
24    char_span: Range<usize>,
25    display_info: &'a LabelDisplay,
26}
27
28impl<'a> LabelInfo<'a> {
29    fn last_offset(&self) -> usize {
30        self.char_span
31            .end
32            .saturating_sub(1)
33            .max(self.char_span.start)
34    }
35}
36
37struct SourceGroup<'a, S: Span> {
38    src_id: &'a S::SourceId,
39    char_span: Range<usize>,
40    labels: Vec<LabelInfo<'a>>,
41}
42
43impl<S: Span> Report<'_, S> {
44    fn get_source_groups(&self, cache: &mut impl Cache<S::SourceId>) -> Vec<SourceGroup<S>> {
45        let mut groups = Vec::new();
46        for label in self.labels.iter() {
47            let label_source = label.span.source();
48
49            let src_display = cache.display(label_source);
50            let src = match cache.fetch(label_source) {
51                Ok(src) => src,
52                Err(e) => {
53                    eprintln!("Unable to fetch source '{}': {:?}", Show(src_display), e);
54                    continue;
55                }
56            };
57
58            let given_label_span = label.span.start()..label.span.end();
59
60            let (label_char_span, start_line, end_line) = match self.config.index_type {
61                IndexType::Char => {
62                    let Some(start_line) = src.get_offset_line(given_label_span.start) else {
63                        continue;
64                    };
65                    let end_line = if given_label_span.start >= given_label_span.end {
66                        start_line.1
67                    } else {
68                        let Some(end_line) = src.get_offset_line(given_label_span.end - 1) else {
69                            continue;
70                        };
71                        end_line.1
72                    };
73                    (given_label_span, start_line.1, end_line)
74                }
75                IndexType::Byte => {
76                    let Some((start_line_obj, start_line, start_byte_col)) =
77                        src.get_byte_line(given_label_span.start)
78                    else {
79                        continue;
80                    };
81                    let line_text = src.get_line_text(start_line_obj).unwrap();
82
83                    let num_chars_before_start = line_text[..start_byte_col.min(line_text.len())]
84                        .chars()
85                        .count();
86                    let start_char_offset = start_line_obj.offset() + num_chars_before_start;
87
88                    if given_label_span.start >= given_label_span.end {
89                        (start_char_offset..start_char_offset, start_line, start_line)
90                    } else {
91                        // We can subtract 1 from end, because get_byte_line doesn't actually index into the text.
92                        let end_pos = given_label_span.end - 1;
93                        let Some((end_line_obj, end_line, end_byte_col)) =
94                            src.get_byte_line(end_pos)
95                        else {
96                            continue;
97                        };
98                        let end_line_text = src.get_line_text(end_line_obj).unwrap();
99                        // Have to add 1 back now, so we don't cut a char in two.
100                        let num_chars_before_end =
101                            end_line_text[..end_byte_col + 1].chars().count();
102                        let end_char_offset = end_line_obj.offset() + num_chars_before_end;
103
104                        (start_char_offset..end_char_offset, start_line, end_line)
105                    }
106                }
107            };
108
109            let label_info = LabelInfo {
110                kind: if start_line == end_line {
111                    LabelKind::Inline
112                } else {
113                    LabelKind::Multiline
114                },
115                char_span: label_char_span,
116                display_info: &label.display_info,
117            };
118
119            if let Some(group) = groups
120                .iter_mut()
121                .find(|g: &&mut SourceGroup<S>| g.src_id == label_source)
122            {
123                group.char_span.start = group.char_span.start.min(label_info.char_span.start);
124                group.char_span.end = group.char_span.end.max(label_info.char_span.end);
125                group.labels.push(label_info);
126            } else {
127                groups.push(SourceGroup {
128                    src_id: label_source,
129                    char_span: label_info.char_span.clone(),
130                    labels: vec![label_info],
131                });
132            }
133        }
134        groups
135    }
136
137    /// Write this diagnostic to an implementor of [`Write`].
138    ///
139    /// If using the `concolor` feature, this method assumes that the output is ultimately going to be printed to
140    /// `stderr`.  If you are printing to `stdout`, use the [`write_for_stdout`](Self::write_for_stdout) method instead.
141    ///
142    /// If you wish to write to `stderr` or `stdout`, you can do so via [`Report::eprint`] or [`Report::print`] respectively.
143    pub fn write<C: Cache<S::SourceId>, W: Write>(&self, cache: C, w: W) -> io::Result<()> {
144        self.write_for_stream(cache, w, StreamType::Stderr)
145    }
146
147    /// Write this diagnostic to an implementor of [`Write`], assuming that the output is ultimately going to be printed
148    /// to `stdout`.
149    pub fn write_for_stdout<C: Cache<S::SourceId>, W: Write>(
150        &self,
151        cache: C,
152        w: W,
153    ) -> io::Result<()> {
154        self.write_for_stream(cache, w, StreamType::Stdout)
155    }
156
157    /// Write this diagnostic to an implementor of [`Write`], assuming that the output is ultimately going to be printed
158    /// to the given output stream (`stdout` or `stderr`).
159    fn write_for_stream<C: Cache<S::SourceId>, W: Write>(
160        &self,
161        mut cache: C,
162        mut w: W,
163        s: StreamType,
164    ) -> io::Result<()> {
165        let draw = match self.config.char_set {
166            CharSet::Unicode => draw::Characters::unicode(),
167            CharSet::Ascii => draw::Characters::ascii(),
168        };
169
170        // --- Header ---
171
172        let code = self.code.as_ref().map(|c| format!("[{}] ", c));
173        let id = format!("{}{}:", Show(code), self.kind);
174        let kind_color = match self.kind {
175            ReportKind::Error => self.config.error_color(),
176            ReportKind::Warning => self.config.warning_color(),
177            ReportKind::Advice => self.config.advice_color(),
178            ReportKind::Custom(_, color) => Some(color),
179        };
180        writeln!(w, "{} {}", id.fg(kind_color, s), Show(self.msg.as_ref()))?;
181
182        let groups = self.get_source_groups(&mut cache);
183
184        // Line number maximum width
185        let line_no_width = groups
186            .iter()
187            .filter_map(
188                |SourceGroup {
189                     char_span, src_id, ..
190                 }| {
191                    let src_name = cache
192                        .display(src_id)
193                        .map(|d| d.to_string())
194                        .unwrap_or_else(|| "<unknown>".to_string());
195
196                    let src = match cache.fetch(src_id) {
197                        Ok(src) => src,
198                        Err(e) => {
199                            eprintln!("Unable to fetch source {}: {:?}", src_name, e);
200                            return None;
201                        }
202                    };
203
204                    let line_range = src.get_line_range(char_span);
205                    Some(
206                        (1..)
207                            .map(|x| 10u32.pow(x))
208                            .take_while(|x| line_range.end as u32 / x != 0)
209                            .count()
210                            + 1,
211                    )
212                },
213            )
214            .max()
215            .unwrap_or(0);
216
217        // --- Source sections ---
218        let groups_len = groups.len();
219        for (
220            group_idx,
221            SourceGroup {
222                src_id,
223                char_span,
224                labels,
225            },
226        ) in groups.into_iter().enumerate()
227        {
228            let src_name = cache
229                .display(src_id)
230                .map(|d| d.to_string())
231                .unwrap_or_else(|| "<unknown>".to_string());
232
233            let src = match cache.fetch(src_id) {
234                Ok(src) => src,
235                Err(e) => {
236                    eprintln!("Unable to fetch source {}: {:?}", src_name, e);
237                    continue;
238                }
239            };
240
241            let line_range = src.get_line_range(&char_span);
242
243            // File name & reference
244            let location = if src_id == self.span.source() {
245                self.span.start()
246            } else {
247                labels[0].char_span.start
248            };
249            let line_and_col = match self.config.index_type {
250                IndexType::Char => src.get_offset_line(location),
251                IndexType::Byte => src.get_byte_line(location).map(|(line_obj, idx, col)| {
252                    let line_text = src.get_line_text(line_obj).unwrap();
253
254                    let col = line_text[..col.min(line_text.len())].chars().count();
255
256                    (line_obj, idx, col)
257                }),
258            };
259            let (line_no, col_no) = line_and_col
260                .map(|(_, idx, col)| {
261                    (
262                        format!("{}", idx + 1 + src.display_line_offset()),
263                        format!("{}", col + 1),
264                    )
265                })
266                .unwrap_or_else(|| ('?'.to_string(), '?'.to_string()));
267            let line_ref = format!("{}:{}:{}", src_name, line_no, col_no);
268            writeln!(
269                w,
270                "{}{}{}{} {} {}",
271                Show((' ', line_no_width + 2)),
272                if group_idx == 0 {
273                    draw.ltop
274                } else {
275                    draw.lcross
276                }
277                .fg(self.config.margin_color(), s),
278                draw.hbar.fg(self.config.margin_color(), s),
279                draw.lbox.fg(self.config.margin_color(), s),
280                line_ref,
281                draw.rbox.fg(self.config.margin_color(), s),
282            )?;
283
284            if !self.config.compact {
285                writeln!(
286                    w,
287                    "{}{}",
288                    Show((' ', line_no_width + 2)),
289                    draw.vbar.fg(self.config.margin_color(), s)
290                )?;
291            }
292
293            struct LineLabel<'a> {
294                col: usize,
295                label: &'a LabelInfo<'a>,
296                multi: bool,
297                draw_msg: bool,
298            }
299
300            // Generate a list of multi-line labels
301            let mut multi_labels = Vec::new();
302            let mut multi_labels_with_message = Vec::new();
303            for label_info in &labels {
304                if matches!(label_info.kind, LabelKind::Multiline) {
305                    multi_labels.push(label_info);
306                    if label_info.display_info.msg.is_some() {
307                        multi_labels_with_message.push(label_info);
308                    }
309                }
310            }
311
312            // Sort multiline labels by length
313            multi_labels.sort_by_key(|m| -(Span::len(&m.char_span) as isize));
314            multi_labels_with_message.sort_by_key(|m| -(Span::len(&m.char_span) as isize));
315
316            let write_margin = |w: &mut W,
317                                idx: usize,
318                                is_line: bool,
319                                is_ellipsis: bool,
320                                draw_labels: bool,
321                                report_row: Option<(usize, bool)>,
322                                line_labels: &[LineLabel],
323                                margin_label: &Option<LineLabel>|
324             -> std::io::Result<()> {
325                let line_no_margin = if is_line && !is_ellipsis {
326                    let line_no = format!("{}", idx + 1);
327                    format!(
328                        "{}{} {}",
329                        Show((' ', line_no_width - line_no.chars().count())),
330                        line_no,
331                        draw.vbar,
332                    )
333                    .fg(self.config.margin_color(), s)
334                } else {
335                    format!(
336                        "{}{}",
337                        Show((' ', line_no_width + 1)),
338                        if is_ellipsis {
339                            draw.vbar_gap
340                        } else {
341                            draw.vbar
342                        }
343                    )
344                    .fg(self.config.skipped_margin_color(), s)
345                };
346
347                write!(
348                    w,
349                    " {}{}",
350                    line_no_margin,
351                    Show(Some(' ').filter(|_| !self.config.compact)),
352                )?;
353
354                // Multi-line margins
355                if draw_labels {
356                    for col in 0..multi_labels_with_message.len()
357                        + (!multi_labels_with_message.is_empty()) as usize
358                    {
359                        let mut corner = None;
360                        let mut hbar: Option<&LabelInfo> = None;
361                        let mut vbar: Option<&LabelInfo> = None;
362                        let mut margin_ptr = None;
363
364                        let multi_label = multi_labels_with_message.get(col);
365                        let line_span = src.line(idx).unwrap().span();
366
367                        for (i, label) in multi_labels_with_message
368                            [0..(col + 1).min(multi_labels_with_message.len())]
369                            .iter()
370                            .enumerate()
371                        {
372                            let margin = margin_label
373                                .as_ref()
374                                .filter(|m| std::ptr::eq(*label, m.label));
375
376                            if label.char_span.start <= line_span.end
377                                && label.char_span.end > line_span.start
378                            {
379                                let is_parent = i != col;
380                                let is_start = line_span.contains(&label.char_span.start);
381                                let is_end = line_span.contains(&label.last_offset());
382
383                                if let Some(margin) = margin.filter(|_| is_line) {
384                                    margin_ptr = Some((margin, is_start));
385                                } else if !is_start && (!is_end || is_line) {
386                                    vbar = vbar.or(Some(*label).filter(|_| !is_parent));
387                                } else if let Some((report_row, is_arrow)) = report_row {
388                                    let label_row = line_labels
389                                        .iter()
390                                        .enumerate()
391                                        .find(|(_, l)| std::ptr::eq(*label, l.label))
392                                        .map_or(0, |(r, _)| r);
393                                    if report_row == label_row {
394                                        if let Some(margin) = margin {
395                                            vbar = Some(margin.label).filter(|_| col == i);
396                                            if is_start {
397                                                continue;
398                                            }
399                                        }
400
401                                        if is_arrow {
402                                            hbar = Some(*label);
403                                            if !is_parent {
404                                                corner = Some((label, is_start));
405                                            }
406                                        } else if !is_start {
407                                            vbar = vbar.or(Some(*label).filter(|_| !is_parent));
408                                        }
409                                    } else {
410                                        vbar = vbar.or(Some(*label).filter(|_| {
411                                            !is_parent && (is_start ^ (report_row < label_row))
412                                        }));
413                                    }
414                                }
415                            }
416                        }
417
418                        if let (Some((margin, _is_start)), true) = (margin_ptr, is_line) {
419                            let is_col =
420                                multi_label.map_or(false, |ml| std::ptr::eq(*ml, margin.label));
421                            let is_limit = col + 1 == multi_labels_with_message.len();
422                            if !is_col && !is_limit {
423                                hbar = hbar.or(Some(margin.label));
424                            }
425                        }
426
427                        hbar = hbar.filter(|l| {
428                            margin_label
429                                .as_ref()
430                                .map_or(true, |margin| !std::ptr::eq(margin.label, *l))
431                                || !is_line
432                        });
433
434                        let (a, b) = if let Some((label, is_start)) = corner {
435                            (
436                                if is_start { draw.ltop } else { draw.lbot }
437                                    .fg(label.display_info.color, s),
438                                draw.hbar.fg(label.display_info.color, s),
439                            )
440                        } else if let Some(label) =
441                            hbar.filter(|_| vbar.is_some() && !self.config.cross_gap)
442                        {
443                            (
444                                draw.xbar.fg(label.display_info.color, s),
445                                draw.hbar.fg(label.display_info.color, s),
446                            )
447                        } else if let Some(label) = hbar {
448                            (
449                                draw.hbar.fg(label.display_info.color, s),
450                                draw.hbar.fg(label.display_info.color, s),
451                            )
452                        } else if let Some(label) = vbar {
453                            (
454                                if is_ellipsis {
455                                    draw.vbar_gap
456                                } else {
457                                    draw.vbar
458                                }
459                                .fg(label.display_info.color, s),
460                                ' '.fg(None, s),
461                            )
462                        } else if let (Some((margin, is_start)), true) = (margin_ptr, is_line) {
463                            let is_col =
464                                multi_label.map_or(false, |ml| std::ptr::eq(*ml, margin.label));
465                            let is_limit = col == multi_labels_with_message.len();
466                            (
467                                if is_limit {
468                                    draw.rarrow
469                                } else if is_col {
470                                    if is_start {
471                                        draw.ltop
472                                    } else {
473                                        draw.lcross
474                                    }
475                                } else {
476                                    draw.hbar
477                                }
478                                .fg(margin.label.display_info.color, s),
479                                if !is_limit { draw.hbar } else { ' ' }
480                                    .fg(margin.label.display_info.color, s),
481                            )
482                        } else {
483                            (' '.fg(None, s), ' '.fg(None, s))
484                        };
485                        write!(w, "{}", a)?;
486                        if !self.config.compact {
487                            write!(w, "{}", b)?;
488                        }
489                    }
490                }
491
492                Ok(())
493            };
494
495            let mut is_ellipsis = false;
496            for idx in line_range {
497                let line = if let Some(line) = src.line(idx) {
498                    line
499                } else {
500                    continue;
501                };
502
503                let margin_label = multi_labels_with_message
504                    .iter()
505                    .enumerate()
506                    .filter_map(|(_i, label)| {
507                        let is_start = line.span().contains(&label.char_span.start);
508                        let is_end = line.span().contains(&label.last_offset());
509                        if is_start {
510                            // TODO: Check to see whether multi is the first on the start line or first on the end line
511                            Some(LineLabel {
512                                col: label.char_span.start - line.offset(),
513                                label,
514                                multi: true,
515                                draw_msg: false, // Multi-line spans don;t have their messages drawn at the start
516                            })
517                        } else if is_end {
518                            Some(LineLabel {
519                                col: label.last_offset() - line.offset(),
520                                label,
521                                multi: true,
522                                draw_msg: true, // Multi-line spans have their messages drawn at the end
523                            })
524                        } else {
525                            None
526                        }
527                    })
528                    .min_by_key(|ll| (ll.col, !ll.label.char_span.start));
529
530                // Generate a list of labels for this line, along with their label columns
531                let mut line_labels = multi_labels_with_message
532                    .iter()
533                    .enumerate()
534                    .filter_map(|(_i, label)| {
535                        let is_start = line.span().contains(&label.char_span.start);
536                        let is_end = line.span().contains(&label.last_offset());
537                        if is_start
538                            && margin_label
539                                .as_ref()
540                                .map_or(true, |m| !std::ptr::eq(*label, m.label))
541                        {
542                            // TODO: Check to see whether multi is the first on the start line or first on the end line
543                            Some(LineLabel {
544                                col: label.char_span.start - line.offset(),
545                                label,
546                                multi: true,
547                                draw_msg: false, // Multi-line spans don;t have their messages drawn at the start
548                            })
549                        } else if is_end {
550                            Some(LineLabel {
551                                col: label.last_offset() - line.offset(),
552                                label,
553                                multi: true,
554                                draw_msg: true, // Multi-line spans have their messages drawn at the end
555                            })
556                        } else {
557                            None
558                        }
559                    })
560                    .collect::<Vec<_>>();
561
562                for label_info in labels.iter().filter(|l| {
563                    l.char_span.start >= line.span().start && l.char_span.end <= line.span().end
564                }) {
565                    if matches!(label_info.kind, LabelKind::Inline) {
566                        line_labels.push(LineLabel {
567                            col: match &self.config.label_attach {
568                                LabelAttach::Start => label_info.char_span.start,
569                                LabelAttach::Middle => {
570                                    (label_info.char_span.start + label_info.char_span.end) / 2
571                                }
572                                LabelAttach::End => label_info.last_offset(),
573                            }
574                            .max(label_info.char_span.start)
575                                - line.offset(),
576                            label: label_info,
577                            multi: false,
578                            draw_msg: true,
579                        });
580                    }
581                }
582
583                // Skip this line if we don't have labels for it
584                if line_labels.is_empty() && margin_label.is_none() {
585                    let within_label = multi_labels
586                        .iter()
587                        .any(|label| label.char_span.contains(&line.span().start()));
588                    if !is_ellipsis && within_label {
589                        is_ellipsis = true;
590                    } else {
591                        if !self.config.compact && !is_ellipsis {
592                            write_margin(&mut w, idx, false, is_ellipsis, false, None, &[], &None)?;
593                            writeln!(w)?;
594                        }
595                        is_ellipsis = true;
596                        continue;
597                    }
598                } else {
599                    is_ellipsis = false;
600                }
601
602                // Sort the labels by their columns
603                line_labels.sort_by_key(|ll| {
604                    (
605                        ll.label.display_info.order,
606                        ll.col,
607                        !ll.label.char_span.start,
608                    )
609                });
610
611                // Determine label bounds so we know where to put error messages
612                let arrow_end_space = if self.config.compact { 1 } else { 2 };
613                let arrow_len = line_labels.iter().fold(0, |l, ll| {
614                    if ll.multi {
615                        line.len()
616                    } else {
617                        l.max(ll.label.char_span.end().saturating_sub(line.offset()))
618                    }
619                }) + arrow_end_space;
620
621                // Should we draw a vertical bar as part of a label arrow on this line?
622                let get_vbar = |col, row| {
623                    line_labels
624                        .iter()
625                        // Only labels with notes get an arrow
626                        .enumerate()
627                        .filter(|(_, ll)| {
628                            ll.label.display_info.msg.is_some()
629                                && margin_label
630                                    .as_ref()
631                                    .map_or(true, |m| !std::ptr::eq(ll.label, m.label))
632                        })
633                        .find(|(j, ll)| ll.col == col && row <= *j)
634                        .map(|(_, ll)| ll)
635                };
636
637                let get_highlight = |col| {
638                    margin_label
639                        .iter()
640                        .map(|ll| &ll.label)
641                        .chain(multi_labels.iter())
642                        .chain(line_labels.iter().map(|l| &l.label))
643                        .filter(|l| l.char_span.contains(&(line.offset() + col)))
644                        // Prioritise displaying smaller spans
645                        .min_by_key(|l| {
646                            (
647                                -l.display_info.priority,
648                                ExactSizeIterator::len(&l.char_span),
649                            )
650                        })
651                };
652
653                let get_underline = |col| {
654                    line_labels
655                        .iter()
656                        .filter(|ll| {
657                            self.config.underlines
658                        // Underlines only occur for inline spans (highlighting can occur for all spans)
659                        && !ll.multi
660                        && ll.label.char_span.contains(&(line.offset() + col))
661                        })
662                        // Prioritise displaying smaller spans
663                        .min_by_key(|ll| {
664                            (
665                                -ll.label.display_info.priority,
666                                ExactSizeIterator::len(&ll.label.char_span),
667                            )
668                        })
669                };
670
671                // Margin
672                write_margin(
673                    &mut w,
674                    idx,
675                    true,
676                    is_ellipsis,
677                    true,
678                    None,
679                    &line_labels,
680                    &margin_label,
681                )?;
682
683                // Line
684                if !is_ellipsis {
685                    for (col, c) in src
686                        .get_line_text(line)
687                        .unwrap()
688                        .trim_end()
689                        .chars()
690                        .enumerate()
691                    {
692                        let color = if let Some(highlight) = get_highlight(col) {
693                            highlight.display_info.color
694                        } else {
695                            self.config.unimportant_color()
696                        };
697                        let (c, width) = self.config.char_width(c, col);
698                        if c.is_whitespace() {
699                            for _ in 0..width {
700                                write!(w, "{}", c.fg(color, s))?;
701                            }
702                        } else {
703                            write!(w, "{}", c.fg(color, s))?;
704                        };
705                    }
706                }
707                writeln!(w)?;
708
709                // Arrows
710                for row in 0..line_labels.len() {
711                    let line_label = &line_labels[row];
712                    //No message to draw thus no arrow to draw
713                    if line_label.label.display_info.msg.is_none() {
714                        continue;
715                    }
716                    if !self.config.compact {
717                        // Margin alternate
718                        write_margin(
719                            &mut w,
720                            idx,
721                            false,
722                            is_ellipsis,
723                            true,
724                            Some((row, false)),
725                            &line_labels,
726                            &margin_label,
727                        )?;
728                        // Lines alternate
729                        let mut chars = src.get_line_text(line).unwrap().trim_end().chars();
730                        for col in 0..arrow_len {
731                            let width =
732                                chars.next().map_or(1, |c| self.config.char_width(c, col).1);
733
734                            let vbar = get_vbar(col, row);
735                            let underline = get_underline(col).filter(|_| row == 0);
736                            let [c, tail] = if let Some(vbar_ll) = vbar {
737                                let [c, tail] = if underline.is_some() {
738                                    // TODO: Is this good?
739                                    // The `true` is used here because it's temporarily disabling a
740                                    // feature that might be reenabled later.
741                                    #[allow(clippy::overly_complex_bool_expr)]
742                                    if ExactSizeIterator::len(&vbar_ll.label.char_span) <= 1 || true
743                                    {
744                                        [draw.underbar, draw.underline]
745                                    } else if line.offset() + col == vbar_ll.label.char_span.start {
746                                        [draw.ltop, draw.underbar]
747                                    } else if line.offset() + col == vbar_ll.label.last_offset() {
748                                        [draw.rtop, draw.underbar]
749                                    } else {
750                                        [draw.underbar, draw.underline]
751                                    }
752                                } else if vbar_ll.multi && row == 0 && self.config.multiline_arrows
753                                {
754                                    [draw.uarrow, ' ']
755                                } else {
756                                    [draw.vbar, ' ']
757                                };
758                                [
759                                    c.fg(vbar_ll.label.display_info.color, s),
760                                    tail.fg(vbar_ll.label.display_info.color, s),
761                                ]
762                            } else if let Some(underline_ll) = underline {
763                                [draw.underline.fg(underline_ll.label.display_info.color, s); 2]
764                            } else {
765                                [' '.fg(None, s); 2]
766                            };
767
768                            for i in 0..width {
769                                write!(w, "{}", if i == 0 { c } else { tail })?;
770                            }
771                        }
772                        writeln!(w)?;
773                    }
774
775                    // Margin
776                    write_margin(
777                        &mut w,
778                        idx,
779                        false,
780                        is_ellipsis,
781                        true,
782                        Some((row, true)),
783                        &line_labels,
784                        &margin_label,
785                    )?;
786                    // Lines
787                    let mut chars = src.get_line_text(line).unwrap().trim_end().chars();
788                    for col in 0..arrow_len {
789                        let width = chars.next().map_or(1, |c| self.config.char_width(c, col).1);
790
791                        let is_hbar = (((col > line_label.col) ^ line_label.multi)
792                            || (line_label.label.display_info.msg.is_some()
793                                && line_label.draw_msg
794                                && col > line_label.col))
795                            && line_label.label.display_info.msg.is_some();
796                        let [c, tail] = if col == line_label.col
797                            && line_label.label.display_info.msg.is_some()
798                            && margin_label
799                                .as_ref()
800                                .map_or(true, |m| !std::ptr::eq(line_label.label, m.label))
801                        {
802                            [
803                                if line_label.multi {
804                                    if line_label.draw_msg {
805                                        draw.mbot
806                                    } else {
807                                        draw.rbot
808                                    }
809                                } else {
810                                    draw.lbot
811                                }
812                                .fg(line_label.label.display_info.color, s),
813                                draw.hbar.fg(line_label.label.display_info.color, s),
814                            ]
815                        } else if let Some(vbar_ll) = get_vbar(col, row).filter(|_| {
816                            col != line_label.col || line_label.label.display_info.msg.is_some()
817                        }) {
818                            if !self.config.cross_gap && is_hbar {
819                                [
820                                    draw.xbar.fg(line_label.label.display_info.color, s),
821                                    ' '.fg(line_label.label.display_info.color, s),
822                                ]
823                            } else if is_hbar {
824                                [draw.hbar.fg(line_label.label.display_info.color, s); 2]
825                            } else {
826                                [
827                                    if vbar_ll.multi && row == 0 && self.config.compact {
828                                        draw.uarrow
829                                    } else {
830                                        draw.vbar
831                                    }
832                                    .fg(vbar_ll.label.display_info.color, s),
833                                    ' '.fg(line_label.label.display_info.color, s),
834                                ]
835                            }
836                        } else if is_hbar {
837                            [draw.hbar.fg(line_label.label.display_info.color, s); 2]
838                        } else {
839                            [' '.fg(None, s); 2]
840                        };
841
842                        if width > 0 {
843                            write!(w, "{}", c)?;
844                        }
845                        for _ in 1..width {
846                            write!(w, "{}", tail)?;
847                        }
848                    }
849                    if line_label.draw_msg {
850                        write!(w, " {}", Show(line_label.label.display_info.msg.as_ref()))?;
851                    }
852                    writeln!(w)?;
853                }
854            }
855
856            let is_final_group = group_idx + 1 == groups_len;
857
858            // Help
859            if is_final_group {
860                for (i, help) in self.help.iter().enumerate() {
861                    if !self.config.compact {
862                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
863                        writeln!(w)?;
864                    }
865                    let help_prefix = format!("{} {}", "Help", i + 1);
866                    let help_prefix_len = if self.help.len() > 1 {
867                        help_prefix.len()
868                    } else {
869                        4
870                    };
871                    let mut lines = help.lines();
872                    if let Some(line) = lines.next() {
873                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
874                        if self.help.len() > 1 {
875                            writeln!(
876                                w,
877                                "{}: {}",
878                                help_prefix.fg(self.config.note_color(), s),
879                                line
880                            )?;
881                        } else {
882                            writeln!(w, "{}: {}", "Help".fg(self.config.note_color(), s), line)?;
883                        }
884                    }
885                    for line in lines {
886                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
887                        writeln!(w, "{:>pad$}{}", "", line, pad = help_prefix_len + 2)?;
888                    }
889                }
890            }
891
892            // Note
893            if is_final_group {
894                for (i, note) in self.notes.iter().enumerate() {
895                    if !self.config.compact {
896                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
897                        writeln!(w)?;
898                    }
899                    let note_prefix = format!("{} {}", "Note", i + 1);
900                    let note_prefix_len = if self.notes.len() > 1 {
901                        note_prefix.len()
902                    } else {
903                        4
904                    };
905                    let mut lines = note.lines();
906                    if let Some(line) = lines.next() {
907                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
908                        if self.notes.len() > 1 {
909                            writeln!(
910                                w,
911                                "{}: {}",
912                                note_prefix.fg(self.config.note_color(), s),
913                                line
914                            )?;
915                        } else {
916                            writeln!(w, "{}: {}", "Note".fg(self.config.note_color(), s), line)?;
917                        }
918                    }
919                    for line in lines {
920                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
921                        writeln!(w, "{:>pad$}{}", "", line, pad = note_prefix_len + 2)?;
922                    }
923                }
924            }
925
926            // Tail of report
927            if !self.config.compact {
928                if is_final_group {
929                    let final_margin =
930                        format!("{}{}", Show((draw.hbar, line_no_width + 2)), draw.rbot);
931                    writeln!(w, "{}", final_margin.fg(self.config.margin_color(), s))?;
932                } else {
933                    writeln!(
934                        w,
935                        "{}{}",
936                        Show((' ', line_no_width + 2)),
937                        draw.vbar.fg(self.config.margin_color(), s)
938                    )?;
939                }
940            }
941        }
942        Ok(())
943    }
944}
945
946#[cfg(test)]
947mod tests {
948    //! These tests use [insta](https://insta.rs/). If you do `cargo install cargo-insta` you can
949    //! automatically update the snapshots with `cargo insta review` or `cargo insta accept`.
950    //!
951    //! When adding new tests you can leave the string in the `assert_snapshot!` macro call empty:
952    //!
953    //!     assert_snapshot!(msg, @"");
954    //!
955    //! and insta will fill it in.
956
957    use insta::assert_snapshot;
958
959    use crate::{Cache, CharSet, Config, IndexType, Label, Report, ReportKind, Source, Span};
960
961    impl<S: Span> Report<'_, S> {
962        fn write_to_string<C: Cache<S::SourceId>>(&self, cache: C) -> String {
963            let mut vec = Vec::new();
964            self.write(cache, &mut vec).unwrap();
965            String::from_utf8(vec).unwrap()
966        }
967    }
968
969    fn no_color_and_ascii() -> Config {
970        Config::default()
971            .with_color(false)
972            // Using Ascii so that the inline snapshots display correctly
973            // even with fonts where characters like '┬' take up more space.
974            .with_char_set(CharSet::Ascii)
975    }
976
977    fn remove_trailing(s: String) -> String {
978        s.lines().flat_map(|l| [l.trim_end(), "\n"]).collect()
979    }
980
981    #[test]
982    fn one_message() {
983        let msg = remove_trailing(
984            Report::build(ReportKind::Error, 0..0)
985                .with_config(no_color_and_ascii())
986                .with_message("can't compare apples with oranges")
987                .finish()
988                .write_to_string(Source::from("")),
989        );
990        assert_snapshot!(msg, @r###"
991        Error: can't compare apples with oranges
992        "###)
993    }
994
995    #[test]
996    fn two_labels_without_messages() {
997        let source = "apple == orange;";
998        let msg = remove_trailing(
999            Report::build(ReportKind::Error, 0..0)
1000                .with_config(no_color_and_ascii())
1001                .with_message("can't compare apples with oranges")
1002                .with_label(Label::new(0..5))
1003                .with_label(Label::new(9..15))
1004                .finish()
1005                .write_to_string(Source::from(source)),
1006        );
1007        // TODO: it would be nice if these spans still showed up (like codespan-reporting does)
1008        assert_snapshot!(msg, @r###"
1009        Error: can't compare apples with oranges
1010           ,-[ <unknown>:1:1 ]
1011           |
1012         1 | apple == orange;
1013        ---'
1014        "###);
1015    }
1016
1017    #[test]
1018    fn two_labels_with_messages() {
1019        let source = "apple == orange;";
1020        let msg = remove_trailing(
1021            Report::build(ReportKind::Error, 0..0)
1022                .with_config(no_color_and_ascii())
1023                .with_message("can't compare apples with oranges")
1024                .with_label(Label::new(0..5).with_message("This is an apple"))
1025                .with_label(Label::new(9..15).with_message("This is an orange"))
1026                .finish()
1027                .write_to_string(Source::from(source)),
1028        );
1029        // TODO: it would be nice if these lines didn't cross
1030        assert_snapshot!(msg, @r###"
1031        Error: can't compare apples with oranges
1032           ,-[ <unknown>:1:1 ]
1033           |
1034         1 | apple == orange;
1035           | ^^|^^    ^^^|^^
1036           |   `-------------- This is an apple
1037           |             |
1038           |             `---- This is an orange
1039        ---'
1040        "###);
1041    }
1042
1043    #[test]
1044    fn multi_byte_chars() {
1045        let source = "äpplë == örängë;";
1046        let msg = remove_trailing(
1047            Report::build(ReportKind::Error, 0..0)
1048                .with_config(no_color_and_ascii().with_index_type(IndexType::Char))
1049                .with_message("can't compare äpplës with örängës")
1050                .with_label(Label::new(0..5).with_message("This is an äpplë"))
1051                .with_label(Label::new(9..15).with_message("This is an örängë"))
1052                .finish()
1053                .write_to_string(Source::from(source)),
1054        );
1055        // TODO: it would be nice if these lines didn't cross
1056        assert_snapshot!(msg, @r###"
1057        Error: can't compare äpplës with örängës
1058           ,-[ <unknown>:1:1 ]
1059           |
1060         1 | äpplë == örängë;
1061           | ^^|^^    ^^^|^^
1062           |   `-------------- This is an äpplë
1063           |             |
1064           |             `---- This is an örängë
1065        ---'
1066        "###);
1067    }
1068
1069    #[test]
1070    fn byte_label() {
1071        let source = "äpplë == örängë;";
1072        let msg = remove_trailing(
1073            Report::build(ReportKind::Error, 0..0)
1074                .with_config(no_color_and_ascii().with_index_type(IndexType::Byte))
1075                .with_message("can't compare äpplës with örängës")
1076                .with_label(Label::new(0..7).with_message("This is an äpplë"))
1077                .with_label(Label::new(11..20).with_message("This is an örängë"))
1078                .finish()
1079                .write_to_string(Source::from(source)),
1080        );
1081        // TODO: it would be nice if these lines didn't cross
1082        assert_snapshot!(msg, @r###"
1083        Error: can't compare äpplës with örängës
1084           ,-[ <unknown>:1:1 ]
1085           |
1086         1 | äpplë == örängë;
1087           | ^^|^^    ^^^|^^
1088           |   `-------------- This is an äpplë
1089           |             |
1090           |             `---- This is an örängë
1091        ---'
1092        "###);
1093    }
1094
1095    #[test]
1096    fn byte_column() {
1097        let source = "äpplë == örängë;";
1098        let msg = remove_trailing(
1099            Report::build(ReportKind::Error, 11..11)
1100                .with_config(no_color_and_ascii().with_index_type(IndexType::Byte))
1101                .with_message("can't compare äpplës with örängës")
1102                .with_label(Label::new(0..7).with_message("This is an äpplë"))
1103                .with_label(Label::new(11..20).with_message("This is an örängë"))
1104                .finish()
1105                .write_to_string(Source::from(source)),
1106        );
1107        // TODO: it would be nice if these lines didn't cross
1108        assert_snapshot!(msg, @r###"
1109        Error: can't compare äpplës with örängës
1110           ,-[ <unknown>:1:10 ]
1111           |
1112         1 | äpplë == örängë;
1113           | ^^|^^    ^^^|^^
1114           |   `-------------- This is an äpplë
1115           |             |
1116           |             `---- This is an örängë
1117        ---'
1118        "###);
1119    }
1120
1121    #[test]
1122    fn label_at_end_of_long_line() {
1123        let source = format!("{}orange", "apple == ".repeat(100));
1124        let msg = remove_trailing(
1125            Report::build(ReportKind::Error, 0..0)
1126                .with_config(no_color_and_ascii())
1127                .with_message("can't compare apples with oranges")
1128                .with_label(
1129                    Label::new(source.len() - 5..source.len()).with_message("This is an orange"),
1130                )
1131                .finish()
1132                .write_to_string(Source::from(source)),
1133        );
1134        // TODO: it would be nice if the start of long lines would be omitted (like rustc does)
1135        assert_snapshot!(msg, @r###"
1136        Error: can't compare apples with oranges
1137           ,-[ <unknown>:1:1 ]
1138           |
1139         1 | apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == orange
1140           |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      ^^|^^
1141           |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        `---- This is an orange
1142        ---'
1143        "###);
1144    }
1145
1146    #[test]
1147    fn label_of_width_zero_at_end_of_line() {
1148        let source = "apple ==\n";
1149        let msg = remove_trailing(
1150            Report::build(ReportKind::Error, 0..0)
1151                .with_config(no_color_and_ascii().with_index_type(IndexType::Byte))
1152                .with_message("unexpected end of file")
1153                .with_label(Label::new(9..9).with_message("Unexpected end of file"))
1154                .finish()
1155                .write_to_string(Source::from(source)),
1156        );
1157
1158        assert_snapshot!(msg, @r###"
1159        Error: unexpected end of file
1160           ,-[ <unknown>:1:1 ]
1161           |
1162         1 | apple ==
1163           |          |
1164           |          `- Unexpected end of file
1165        ---'
1166        "###);
1167    }
1168
1169    #[test]
1170    fn empty_input() {
1171        let source = "";
1172        let msg = remove_trailing(
1173            Report::build(ReportKind::Error, 0..0)
1174                .with_config(no_color_and_ascii())
1175                .with_message("unexpected end of file")
1176                .with_label(Label::new(0..0).with_message("No more fruit!"))
1177                .finish()
1178                .write_to_string(Source::from(source)),
1179        );
1180
1181        assert_snapshot!(msg, @r###"
1182        Error: unexpected end of file
1183           ,-[ <unknown>:1:1 ]
1184           |
1185         1 |
1186           | |
1187           | `- No more fruit!
1188        ---'
1189        "###);
1190    }
1191
1192    #[test]
1193    fn empty_input_help() {
1194        let source = "";
1195        let msg = remove_trailing(
1196            Report::build(ReportKind::Error, 0..0)
1197                .with_config(no_color_and_ascii())
1198                .with_message("unexpected end of file")
1199                .with_label(Label::new(0..0).with_message("No more fruit!"))
1200                .with_help("have you tried going to the farmer's market?")
1201                .finish()
1202                .write_to_string(Source::from(source)),
1203        );
1204
1205        assert_snapshot!(msg, @r###"
1206        Error: unexpected end of file
1207           ,-[ <unknown>:1:1 ]
1208           |
1209         1 |
1210           | |
1211           | `- No more fruit!
1212           |
1213           | Help: have you tried going to the farmer's market?
1214        ---'
1215        "###);
1216    }
1217
1218    #[test]
1219    fn empty_input_note() {
1220        let source = "";
1221        let msg = remove_trailing(
1222            Report::build(ReportKind::Error, 0..0)
1223                .with_config(no_color_and_ascii())
1224                .with_message("unexpected end of file")
1225                .with_label(Label::new(0..0).with_message("No more fruit!"))
1226                .with_note("eat your greens!")
1227                .finish()
1228                .write_to_string(Source::from(source)),
1229        );
1230
1231        assert_snapshot!(msg, @r###"
1232        Error: unexpected end of file
1233           ,-[ <unknown>:1:1 ]
1234           |
1235         1 |
1236           | |
1237           | `- No more fruit!
1238           |
1239           | Note: eat your greens!
1240        ---'
1241        "###);
1242    }
1243
1244    #[test]
1245    fn empty_input_help_note() {
1246        let source = "";
1247        let msg = remove_trailing(
1248            Report::build(ReportKind::Error, 0..0)
1249                .with_config(no_color_and_ascii())
1250                .with_message("unexpected end of file")
1251                .with_label(Label::new(0..0).with_message("No more fruit!"))
1252                .with_note("eat your greens!")
1253                .with_help("have you tried going to the farmer's market?")
1254                .finish()
1255                .write_to_string(Source::from(source)),
1256        );
1257
1258        assert_snapshot!(msg, @r###"
1259        Error: unexpected end of file
1260           ,-[ <unknown>:1:1 ]
1261           |
1262         1 |
1263           | |
1264           | `- No more fruit!
1265           |
1266           | Help: have you tried going to the farmer's market?
1267           |
1268           | Note: eat your greens!
1269        ---'
1270        "###);
1271    }
1272
1273    #[test]
1274    fn byte_spans_never_crash() {
1275        let source = "apple\np\n\nempty\n";
1276
1277        for i in 0..=source.len() {
1278            for j in i..=source.len() {
1279                let _ = remove_trailing(
1280                    Report::build(ReportKind::Error, 0..0)
1281                        .with_config(no_color_and_ascii().with_index_type(IndexType::Byte))
1282                        .with_message("Label")
1283                        .with_label(Label::new(i..j).with_message("Label"))
1284                        .finish()
1285                        .write_to_string(Source::from(source)),
1286                );
1287            }
1288        }
1289    }
1290
1291    #[test]
1292    fn multiline_label() {
1293        let source = "apple\n==\norange";
1294        let msg = remove_trailing(
1295            Report::build(ReportKind::Error, 0..0)
1296                .with_config(no_color_and_ascii())
1297                .with_label(Label::new(0..source.len()).with_message("illegal comparison"))
1298                .finish()
1299                .write_to_string(Source::from(source)),
1300        );
1301        // TODO: it would be nice if the 2nd line wasn't omitted
1302        assert_snapshot!(msg, @r###"
1303        Error:
1304           ,-[ <unknown>:1:1 ]
1305           |
1306         1 | ,-> apple
1307           : :
1308         3 | |-> orange
1309           | |
1310           | `----------- illegal comparison
1311        ---'
1312        "###);
1313    }
1314
1315    #[test]
1316    fn partially_overlapping_labels() {
1317        let source = "https://example.com/";
1318        let msg = remove_trailing(
1319            Report::build(ReportKind::Error, 0..0)
1320                .with_config(no_color_and_ascii())
1321                .with_label(Label::new(0..source.len()).with_message("URL"))
1322                .with_label(Label::new(0..source.find(':').unwrap()).with_message("scheme"))
1323                .finish()
1324                .write_to_string(Source::from(source)),
1325        );
1326        // TODO: it would be nice if you could tell where the spans start and end.
1327        assert_snapshot!(msg, @r###"
1328        Error:
1329           ,-[ <unknown>:1:1 ]
1330           |
1331         1 | https://example.com/
1332           | ^^|^^^^^^^|^^^^^^^^^
1333           |   `------------------- scheme
1334           |           |
1335           |           `----------- URL
1336        ---'
1337        "###);
1338    }
1339
1340    #[test]
1341    fn multiple_labels_same_span() {
1342        let source = "apple == orange;";
1343        let msg = remove_trailing(
1344            Report::build(ReportKind::Error, 0..0)
1345                .with_config(no_color_and_ascii())
1346                .with_message("can't compare apples with oranges")
1347                .with_label(Label::new(0..5).with_message("This is an apple"))
1348                .with_label(
1349                    Label::new(0..5).with_message("Have I mentioned that this is an apple?"),
1350                )
1351                .with_label(Label::new(0..5).with_message("No really, have I mentioned that?"))
1352                .with_label(Label::new(9..15).with_message("This is an orange"))
1353                .with_label(
1354                    Label::new(9..15).with_message("Have I mentioned that this is an orange?"),
1355                )
1356                .with_label(Label::new(9..15).with_message("No really, have I mentioned that?"))
1357                .finish()
1358                .write_to_string(Source::from(source)),
1359        );
1360        assert_snapshot!(msg, @r###"
1361        Error: can't compare apples with oranges
1362           ,-[ <unknown>:1:1 ]
1363           |
1364         1 | apple == orange;
1365           | ^^|^^    ^^^|^^
1366           |   `-------------- This is an apple
1367           |   |         |
1368           |   `-------------- Have I mentioned that this is an apple?
1369           |   |         |
1370           |   `-------------- No really, have I mentioned that?
1371           |             |
1372           |             `---- This is an orange
1373           |             |
1374           |             `---- Have I mentioned that this is an orange?
1375           |             |
1376           |             `---- No really, have I mentioned that?
1377        ---'
1378        "###)
1379    }
1380
1381    #[test]
1382    fn note() {
1383        let source = "apple == orange;";
1384        let msg = remove_trailing(
1385            Report::build(ReportKind::Error, 0..0)
1386                .with_config(no_color_and_ascii())
1387                .with_message("can't compare apples with oranges")
1388                .with_label(Label::new(0..5).with_message("This is an apple"))
1389                .with_label(Label::new(9..15).with_message("This is an orange"))
1390                .with_note("stop trying ... this is a fruitless endeavor")
1391                .finish()
1392                .write_to_string(Source::from(source)),
1393        );
1394        assert_snapshot!(msg, @r###"
1395        Error: can't compare apples with oranges
1396           ,-[ <unknown>:1:1 ]
1397           |
1398         1 | apple == orange;
1399           | ^^|^^    ^^^|^^
1400           |   `-------------- This is an apple
1401           |             |
1402           |             `---- This is an orange
1403           |
1404           | Note: stop trying ... this is a fruitless endeavor
1405        ---'
1406        "###)
1407    }
1408
1409    #[test]
1410    fn help() {
1411        let source = "apple == orange;";
1412        let msg = remove_trailing(
1413            Report::build(ReportKind::Error, 0..0)
1414                .with_config(no_color_and_ascii())
1415                .with_message("can't compare apples with oranges")
1416                .with_label(Label::new(0..5).with_message("This is an apple"))
1417                .with_label(Label::new(9..15).with_message("This is an orange"))
1418                .with_help("have you tried peeling the orange?")
1419                .finish()
1420                .write_to_string(Source::from(source)),
1421        );
1422        assert_snapshot!(msg, @r###"
1423        Error: can't compare apples with oranges
1424           ,-[ <unknown>:1:1 ]
1425           |
1426         1 | apple == orange;
1427           | ^^|^^    ^^^|^^
1428           |   `-------------- This is an apple
1429           |             |
1430           |             `---- This is an orange
1431           |
1432           | Help: have you tried peeling the orange?
1433        ---'
1434        "###)
1435    }
1436
1437    #[test]
1438    fn help_and_note() {
1439        let source = "apple == orange;";
1440        let msg = remove_trailing(
1441            Report::build(ReportKind::Error, 0..0)
1442                .with_config(no_color_and_ascii())
1443                .with_message("can't compare apples with oranges")
1444                .with_label(Label::new(0..5).with_message("This is an apple"))
1445                .with_label(Label::new(9..15).with_message("This is an orange"))
1446                .with_help("have you tried peeling the orange?")
1447                .with_note("stop trying ... this is a fruitless endeavor")
1448                .finish()
1449                .write_to_string(Source::from(source)),
1450        );
1451        assert_snapshot!(msg, @r###"
1452        Error: can't compare apples with oranges
1453           ,-[ <unknown>:1:1 ]
1454           |
1455         1 | apple == orange;
1456           | ^^|^^    ^^^|^^
1457           |   `-------------- This is an apple
1458           |             |
1459           |             `---- This is an orange
1460           |
1461           | Help: have you tried peeling the orange?
1462           |
1463           | Note: stop trying ... this is a fruitless endeavor
1464        ---'
1465        "###)
1466    }
1467
1468    #[test]
1469    fn single_note_single_line() {
1470        let source = "apple == orange;";
1471        let msg = remove_trailing(
1472            Report::build(ReportKind::Error, 0..0)
1473                .with_config(no_color_and_ascii())
1474                .with_message("can't compare apples with oranges")
1475                .with_label(Label::new(0..15).with_message("This is a strange comparison"))
1476                .with_note("No need to try, they can't be compared.")
1477                .finish()
1478                .write_to_string(Source::from(source)),
1479        );
1480        assert_snapshot!(msg, @r###"
1481        Error: can't compare apples with oranges
1482           ,-[ <unknown>:1:1 ]
1483           |
1484         1 | apple == orange;
1485           | ^^^^^^^|^^^^^^^
1486           |        `--------- This is a strange comparison
1487           |
1488           | Note: No need to try, they can't be compared.
1489        ---'
1490        "###)
1491    }
1492
1493    #[test]
1494    fn multi_notes_single_lines() {
1495        let source = "apple == orange;";
1496        let msg = remove_trailing(
1497            Report::build(ReportKind::Error, 0..0)
1498                .with_config(no_color_and_ascii())
1499                .with_message("can't compare apples with oranges")
1500                .with_label(Label::new(0..15).with_message("This is a strange comparison"))
1501                .with_note("No need to try, they can't be compared.")
1502                .with_note("Yeah, really, please stop.")
1503                .finish()
1504                .write_to_string(Source::from(source)),
1505        );
1506        assert_snapshot!(msg, @r###"
1507        Error: can't compare apples with oranges
1508           ,-[ <unknown>:1:1 ]
1509           |
1510         1 | apple == orange;
1511           | ^^^^^^^|^^^^^^^
1512           |        `--------- This is a strange comparison
1513           |
1514           | Note 1: No need to try, they can't be compared.
1515           |
1516           | Note 2: Yeah, really, please stop.
1517        ---'
1518        "###)
1519    }
1520
1521    #[test]
1522    fn multi_notes_multi_lines() {
1523        let source = "apple == orange;";
1524        let msg = remove_trailing(
1525            Report::build(ReportKind::Error, 0..0)
1526                .with_config(no_color_and_ascii())
1527                .with_message("can't compare apples with oranges")
1528                .with_label(Label::new(0..15).with_message("This is a strange comparison"))
1529                .with_note("No need to try, they can't be compared.")
1530                .with_note("Yeah, really, please stop.\nIt has no resemblance.")
1531                .finish()
1532                .write_to_string(Source::from(source)),
1533        );
1534        assert_snapshot!(msg, @r###"
1535        Error: can't compare apples with oranges
1536           ,-[ <unknown>:1:1 ]
1537           |
1538         1 | apple == orange;
1539           | ^^^^^^^|^^^^^^^
1540           |        `--------- This is a strange comparison
1541           |
1542           | Note 1: No need to try, they can't be compared.
1543           |
1544           | Note 2: Yeah, really, please stop.
1545           |         It has no resemblance.
1546        ---'
1547        "###)
1548    }
1549
1550    #[test]
1551    fn multi_helps_multi_lines() {
1552        let source = "apple == orange;";
1553        let msg = remove_trailing(
1554            Report::build(ReportKind::Error, 0..0)
1555                .with_config(no_color_and_ascii())
1556                .with_message("can't compare apples with oranges")
1557                .with_label(Label::new(0..15).with_message("This is a strange comparison"))
1558                .with_help("No need to try, they can't be compared.")
1559                .with_help("Yeah, really, please stop.\nIt has no resemblance.")
1560                .finish()
1561                .write_to_string(Source::from(source)),
1562        );
1563        assert_snapshot!(msg, @r###"
1564        Error: can't compare apples with oranges
1565           ,-[ <unknown>:1:1 ]
1566           |
1567         1 | apple == orange;
1568           | ^^^^^^^|^^^^^^^
1569           |        `--------- This is a strange comparison
1570           |
1571           | Help 1: No need to try, they can't be compared.
1572           |
1573           | Help 2: Yeah, really, please stop.
1574           |         It has no resemblance.
1575        ---'
1576        "###)
1577    }
1578}