Skip to main content

annotate_snippets/renderer/
graphics.rs

1// Most of this file is adapted from https://github.com/rust-lang/rust/blob/160905b6253f42967ed4aef4b98002944c7df24c/compiler/rustc_errors/src/emitter.rs
2
3use alloc::borrow::Cow;
4use alloc::borrow::ToOwned;
5use alloc::collections::BTreeMap;
6use alloc::string::{String, ToString};
7use alloc::{format, vec, vec::Vec};
8use core::cmp::{Ordering, Reverse, max, min};
9use core::fmt;
10
11use anstyle::Style;
12
13use super::DecorStyle;
14use super::Renderer;
15use super::margin::Margin;
16use super::preprocess::{DisplaySuggestion, Preprocessed, PreprocessedElement, PreprocessedGroup};
17use super::stylesheet::Stylesheet;
18use crate::level::{Level, LevelInner};
19use crate::renderer::source_map::{
20    AnnotatedLineInfo, LineInfo, Loc, SourceMap, SplicedLines, SubstitutionHighlight,
21};
22use crate::renderer::styled_buffer::StyledBuffer;
23use crate::snippet::Id;
24use crate::{
25    Annotation, AnnotationKind, Element, Group, Message, Origin, Patch, Report, Snippet, Title,
26};
27
28pub(crate) const ANONYMIZED_LINE_NUM: &str = "LL";
29
30pub(crate) fn render(renderer: &Renderer, groups: Report<'_>) -> String {
31    if renderer.short_message {
32        render_short_message(renderer, groups).unwrap()
33    } else {
34        render_full_message(renderer, groups)
35    }
36}
37
38pub(crate) fn render_full_message(renderer: &Renderer, groups: Report<'_>) -> String {
39    let Preprocessed {
40        max_line_num,
41        report_primary_path,
42        groups,
43    } = Preprocessed::preprocess(groups);
44    let max_line_num_len = if renderer.anonymized_snippet_line_numbers {
45        ANONYMIZED_LINE_NUM.len()
46    } else {
47        num_decimal_digits(max_line_num)
48    };
49    let mut out_string = String::new();
50    let group_len = groups.len();
51    for (
52        g,
53        PreprocessedGroup {
54            group,
55            elements,
56            primary_path,
57            max_depth,
58        },
59    ) in groups.into_iter().enumerate()
60    {
61        let mut buffer = StyledBuffer::new();
62        let level = group.primary_level.clone();
63        let mut message_iter = elements.into_iter().enumerate().peekable();
64        if let Some(title) = &group.title {
65            let peek = message_iter.peek().map(|(_, s)| s);
66            let title_style = if title.allows_styling {
67                TitleStyle::Secondary
68            } else {
69                TitleStyle::Primary
70            };
71            let buffer_msg_line_offset = buffer.num_lines();
72            render_title(
73                renderer,
74                &mut buffer,
75                title,
76                max_line_num_len,
77                title_style,
78                matches!(peek, Some(PreprocessedElement::Message(_))),
79                buffer_msg_line_offset,
80            );
81            let buffer_msg_line_offset = buffer.num_lines();
82
83            if matches!(peek, Some(PreprocessedElement::Message(_))) {
84                draw_col_separator_no_space(
85                    renderer,
86                    &mut buffer,
87                    buffer_msg_line_offset,
88                    max_line_num_len + 1,
89                );
90            }
91            if peek.is_none() && title_style == TitleStyle::Primary && g == 0 && group_len > 1 {
92                draw_col_separator_end(
93                    renderer,
94                    &mut buffer,
95                    buffer_msg_line_offset,
96                    max_line_num_len + 1,
97                );
98            }
99        }
100        let mut seen_primary = false;
101        let mut last_suggestion_path = None;
102        while let Some((i, section)) = message_iter.next() {
103            let peek = message_iter.peek().map(|(_, s)| s);
104            let is_first = i == 0;
105            match section {
106                PreprocessedElement::Message(title) => {
107                    let title_style = TitleStyle::Message;
108                    let buffer_msg_line_offset = buffer.num_lines();
109                    render_title(
110                        renderer,
111                        &mut buffer,
112                        title,
113                        max_line_num_len,
114                        title_style,
115                        peek.is_some(),
116                        buffer_msg_line_offset,
117                    );
118                }
119                PreprocessedElement::Cause((cause, source_map, annotated_lines)) => {
120                    let is_primary = primary_path == cause.path.as_ref() && !seen_primary;
121                    seen_primary |= is_primary;
122                    render_snippet_annotations(
123                        renderer,
124                        &mut buffer,
125                        max_line_num_len,
126                        cause,
127                        is_primary,
128                        &source_map,
129                        &annotated_lines,
130                        max_depth,
131                        peek.is_some() || (g == 0 && group_len > 1),
132                        is_first,
133                    );
134
135                    if g == 0 {
136                        let current_line = buffer.num_lines();
137                        match peek {
138                            Some(PreprocessedElement::Message(_)) => {
139                                draw_col_separator_no_space(
140                                    renderer,
141                                    &mut buffer,
142                                    current_line,
143                                    max_line_num_len + 1,
144                                );
145                            }
146                            None if group_len > 1 => draw_col_separator_end(
147                                renderer,
148                                &mut buffer,
149                                current_line,
150                                max_line_num_len + 1,
151                            ),
152                            _ => {}
153                        }
154                    }
155                }
156                PreprocessedElement::Suggestion((
157                    suggestion,
158                    source_map,
159                    spliced_lines,
160                    display_suggestion,
161                )) => {
162                    let matches_previous_suggestion =
163                        last_suggestion_path == Some(suggestion.path.as_ref());
164                    emit_suggestion_default(
165                        renderer,
166                        &mut buffer,
167                        suggestion,
168                        spliced_lines,
169                        display_suggestion,
170                        max_line_num_len,
171                        &source_map,
172                        primary_path.or(report_primary_path),
173                        matches_previous_suggestion,
174                        is_first,
175                        //matches!(peek, Some(Element::Message(_) | Element::Padding(_))),
176                        peek.is_some(),
177                    );
178
179                    if matches!(peek, Some(PreprocessedElement::Suggestion(_))) {
180                        last_suggestion_path = Some(suggestion.path.as_ref());
181                    } else {
182                        last_suggestion_path = None;
183                    }
184                }
185
186                PreprocessedElement::Origin(origin) => {
187                    let buffer_msg_line_offset = buffer.num_lines();
188                    let is_primary = primary_path == Some(&origin.path) && !seen_primary;
189                    seen_primary |= is_primary;
190                    render_origin(
191                        renderer,
192                        &mut buffer,
193                        max_line_num_len,
194                        origin,
195                        is_primary,
196                        is_first,
197                        peek.is_none(),
198                        buffer_msg_line_offset,
199                    );
200                    let current_line = buffer.num_lines();
201                    if g == 0 && peek.is_none() && group_len > 1 {
202                        draw_col_separator_end(
203                            renderer,
204                            &mut buffer,
205                            current_line,
206                            max_line_num_len + 1,
207                        );
208                    }
209                }
210                PreprocessedElement::Padding(_) => {
211                    let current_line = buffer.num_lines();
212                    if peek.is_none() {
213                        draw_col_separator_end(
214                            renderer,
215                            &mut buffer,
216                            current_line,
217                            max_line_num_len + 1,
218                        );
219                    } else {
220                        draw_col_separator_no_space(
221                            renderer,
222                            &mut buffer,
223                            current_line,
224                            max_line_num_len + 1,
225                        );
226                    }
227                }
228            }
229        }
230        buffer
231            .render(&level, &renderer.stylesheet, &mut out_string)
232            .unwrap();
233        if g != group_len - 1 {
234            out_string.push('\n');
235        }
236    }
237    out_string
238}
239
240fn render_short_message(renderer: &Renderer, groups: &[Group<'_>]) -> Result<String, fmt::Error> {
241    let mut buffer = StyledBuffer::new();
242    let mut labels = None;
243    let group = groups.first().expect("Expected at least one group");
244
245    let Some(title) = &group.title else {
246        panic!("Expected a Title");
247    };
248
249    if let Some(Element::Cause(cause)) = group
250        .elements
251        .iter()
252        .find(|e| matches!(e, Element::Cause(_)))
253    {
254        let labels_inner = cause
255            .markers
256            .iter()
257            .filter_map(|ann| match &ann.label {
258                Some(msg) if ann.kind.is_primary() => {
259                    if !msg.trim().is_empty() {
260                        Some(msg.to_string())
261                    } else {
262                        None
263                    }
264                }
265                _ => None,
266            })
267            .collect::<Vec<_>>()
268            .join(", ");
269        if !labels_inner.is_empty() {
270            labels = Some(labels_inner);
271        }
272
273        if let Some(path) = &cause.path {
274            let mut origin = Origin::path(path.as_ref());
275
276            let source_map = SourceMap::new(&cause.source, cause.line_start);
277            let (_depth, annotated_lines) =
278                source_map.annotated_lines(cause.markers.clone(), cause.fold);
279
280            if let Some(primary_line) = annotated_lines
281                .iter()
282                .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
283                .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
284            {
285                origin.line = Some(primary_line.line_index);
286                if let Some(first_annotation) = primary_line
287                    .annotations
288                    .iter()
289                    .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
290                {
291                    origin.char_column = Some(first_annotation.start.char + 1);
292                }
293            }
294
295            let str = format_origin(&origin, renderer.anonymized_origin_line_numbers);
296            buffer.append(0, &str, ElementStyle::LineAndColumn);
297            buffer.append(0, ": ", ElementStyle::LineAndColumn);
298        }
299    }
300
301    render_title(
302        renderer,
303        &mut buffer,
304        title,
305        0, // No line numbers in short messages
306        TitleStyle::Primary,
307        false,
308        0,
309    );
310
311    if let Some(labels) = labels {
312        buffer.append(0, &format!(": {labels}"), ElementStyle::NoStyle);
313    }
314
315    let mut out_string = String::new();
316    buffer.render(&title.level, &renderer.stylesheet, &mut out_string)?;
317
318    Ok(out_string)
319}
320
321#[allow(clippy::too_many_arguments, reason = "All arguments are necessary")]
322fn render_title(
323    renderer: &Renderer,
324    buffer: &mut StyledBuffer,
325    title: &dyn MessageOrTitle,
326    max_line_num_len: usize,
327    title_style: TitleStyle,
328    is_cont: bool,
329    buffer_msg_line_offset: usize,
330) {
331    let (label_style, title_element_style) = match title_style {
332        TitleStyle::Primary => (
333            ElementStyle::Level(title.level().level),
334            if renderer.short_message {
335                ElementStyle::NoStyle
336            } else {
337                ElementStyle::MainHeaderMsg
338            },
339        ),
340        TitleStyle::Secondary => (
341            ElementStyle::Level(title.level().level),
342            ElementStyle::HeaderMsg,
343        ),
344        TitleStyle::Message => {
345            for _ in 0..max_line_num_len {
346                buffer.append(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
347            }
348
349            draw_note_separator(
350                renderer,
351                buffer,
352                buffer_msg_line_offset,
353                max_line_num_len + 1,
354                is_cont,
355            );
356            (ElementStyle::MainHeaderMsg, ElementStyle::NoStyle)
357        }
358    };
359
360    let mut label_width = 0;
361    let level_is_visible = title.level().name != Some(None);
362    if level_is_visible || title.id().is_some() {
363        if level_is_visible {
364            buffer.append(buffer_msg_line_offset, title.level().as_str(), label_style);
365            label_width += str_width(title.level().as_str());
366        }
367
368        if let Some(Id { id: Some(id), url }) = &title.id() {
369            let url = url
370                .as_deref()
371                .filter(|_| renderer.hyperlink)
372                .map(Hyperlink::with_url)
373                .unwrap_or_default();
374
375            if level_is_visible {
376                buffer.append(buffer_msg_line_offset, "[", label_style);
377                label_width += 1;
378            }
379
380            buffer.append(buffer_msg_line_offset, &format!("{url}"), label_style);
381            buffer.append(buffer_msg_line_offset, id, label_style);
382            buffer.append(buffer_msg_line_offset, &format!("{url:#}"), label_style);
383            label_width += str_width(id);
384
385            if level_is_visible {
386                buffer.append(buffer_msg_line_offset, "]", label_style);
387                label_width += 1;
388            }
389        }
390
391        buffer.append(buffer_msg_line_offset, ": ", title_element_style);
392        label_width += 2;
393    }
394
395    let padding = " ".repeat(if title_style == TitleStyle::Message {
396        // The extra 3 ` ` is padding that's always needed to align to the
397        // label i.e. `note: `:
398        //
399        //   error: message
400        //     --> file.rs:13:20
401        //      |
402        //   13 |     <CODE>
403        //      |      ^^^^
404        //      |
405        //      = note: multiline
406        //              message
407        //   ++^^^------
408        //    |  |     |
409        //    |  |     |
410        //    |  |     width of label
411        //    |  magic `3`
412        //    `max_line_num_len`
413        max_line_num_len + 3 + label_width
414    } else {
415        label_width
416    });
417
418    let (title_str, style) = if title.allows_styling() {
419        (Cow::Borrowed(title.text()), ElementStyle::NoStyle)
420    } else {
421        (normalize_whitespace(title.text()), title_element_style)
422    };
423    for (i, text) in title_str.split('\n').enumerate() {
424        if i != 0 {
425            buffer.append(buffer_msg_line_offset + i, &padding, ElementStyle::NoStyle);
426            if title_style == TitleStyle::Message
427                && is_cont
428                && matches!(renderer.decor_style, DecorStyle::Unicode)
429            {
430                // There's another note after this one, associated to the subwindow above.
431                // We write additional vertical lines to join them:
432                //   ╭▸ test.rs:3:3
433                //   │
434                // 3 │   code
435                //   │   ━━━━
436                //   │
437                //   ├ note: foo
438                //   │       bar
439                //   ╰ note: foo
440                //           bar
441                draw_col_separator_no_space(
442                    renderer,
443                    buffer,
444                    buffer_msg_line_offset + i,
445                    max_line_num_len + 1,
446                );
447            }
448        }
449        buffer.append(buffer_msg_line_offset + i, text, style);
450    }
451}
452
453#[allow(clippy::too_many_arguments, reason = "All arguments are necessary")]
454fn render_origin(
455    renderer: &Renderer,
456    buffer: &mut StyledBuffer,
457    max_line_num_len: usize,
458    origin: &Origin<'_>,
459    is_primary: bool,
460    is_first: bool,
461    alone: bool,
462    buffer_msg_line_offset: usize,
463) {
464    if !renderer.short_message {
465        for _ in 0..max_line_num_len {
466            buffer.append(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
467        }
468
469        if is_primary {
470            buffer.append(
471                buffer_msg_line_offset,
472                renderer.decor_style.file_start(is_first, alone),
473                ElementStyle::LineNumber,
474            );
475        } else {
476            // if !origin.standalone {
477            //     // Add spacing line, as shown:
478            //     //   --> $DIR/file:54:15
479            //     //    |
480            //     // LL |         code
481            //     //    |         ^^^^
482            //     //    | (<- It prints *this* line)
483            //     //   ::: $DIR/other_file.rs:15:5
484            //     //    |
485            //     // LL |     code
486            //     //    |     ----
487            //     draw_col_separator_no_space(renderer,
488            //         buffer,
489            //         buffer_msg_line_offset,
490            //         max_line_num_len + 1,
491            //     );
492            //
493            //     buffer_msg_line_offset += 1;
494            // }
495            // Then, the secondary file indicator
496            buffer.append(
497                buffer_msg_line_offset,
498                renderer.decor_style.secondary_file_start(),
499                ElementStyle::LineNumber,
500            );
501        }
502    }
503
504    let str = format_origin(origin, renderer.anonymized_origin_line_numbers);
505    buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn);
506}
507
508fn format_origin(origin: &Origin<'_>, anonymized_origin_line_numbers: bool) -> String {
509    use core::fmt::Write as _;
510
511    let mut buffer = origin.path.as_ref().to_owned();
512    if let Some(line) = origin.line {
513        if anonymized_origin_line_numbers {
514            let line = ANONYMIZED_LINE_NUM;
515            write!(&mut buffer, ":{line}").unwrap();
516        } else {
517            write!(&mut buffer, ":{line}").unwrap();
518        }
519        if let Some(col) = origin.char_column {
520            write!(&mut buffer, ":{col}").unwrap();
521        }
522    }
523    buffer
524}
525
526#[allow(clippy::too_many_arguments, reason = "All arguments are necessary")]
527fn render_snippet_annotations(
528    renderer: &Renderer,
529    buffer: &mut StyledBuffer,
530    max_line_num_len: usize,
531    snippet: &Snippet<'_, Annotation<'_>>,
532    is_primary: bool,
533    sm: &SourceMap<'_>,
534    annotated_lines: &[AnnotatedLineInfo<'_>],
535    multiline_depth: usize,
536    is_cont: bool,
537    is_first: bool,
538) {
539    if let Some(path) = &snippet.path {
540        let mut origin = Origin::path(path.as_ref());
541        // print out the span location and spacer before we print the annotated source
542        // to do this, we need to know if this span will be primary
543        //let is_primary = primary_path == Some(&origin.path);
544
545        if is_primary {
546            if let Some(primary_line) = annotated_lines
547                .iter()
548                .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
549                .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
550            {
551                origin.line = Some(primary_line.line_index);
552                if let Some(first_annotation) = primary_line
553                    .annotations
554                    .iter()
555                    .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
556                {
557                    origin.char_column = Some(first_annotation.start.char + 1);
558                }
559            }
560        } else {
561            let buffer_msg_line_offset = buffer.num_lines();
562            // Add spacing line, as shown:
563            //   --> $DIR/file:54:15
564            //    |
565            // LL |         code
566            //    |         ^^^^
567            //    | (<- It prints *this* line)
568            //   ::: $DIR/other_file.rs:15:5
569            //    |
570            // LL |     code
571            //    |     ----
572            draw_col_separator_no_space(
573                renderer,
574                buffer,
575                buffer_msg_line_offset,
576                max_line_num_len + 1,
577            );
578            if let Some(first_line) = annotated_lines
579                .iter()
580                .find(|l| !l.annotations.is_empty())
581                .or(annotated_lines.first())
582            {
583                origin.line = Some(first_line.line_index);
584                if let Some(first_annotation) = first_line.annotations.first() {
585                    origin.char_column = Some(first_annotation.start.char + 1);
586                }
587            }
588        }
589        let buffer_msg_line_offset = buffer.num_lines();
590        render_origin(
591            renderer,
592            buffer,
593            max_line_num_len,
594            &origin,
595            is_primary,
596            is_first,
597            false,
598            buffer_msg_line_offset,
599        );
600        // Put in the spacer between the location and annotated source
601        draw_col_separator_no_space(
602            renderer,
603            buffer,
604            buffer_msg_line_offset + 1,
605            max_line_num_len + 1,
606        );
607    } else {
608        let buffer_msg_line_offset = buffer.num_lines();
609        if is_primary {
610            if renderer.decor_style == DecorStyle::Unicode {
611                buffer.puts(
612                    buffer_msg_line_offset,
613                    max_line_num_len,
614                    renderer.decor_style.file_start(is_first, false),
615                    ElementStyle::LineNumber,
616                );
617            } else {
618                draw_col_separator_no_space(
619                    renderer,
620                    buffer,
621                    buffer_msg_line_offset,
622                    max_line_num_len + 1,
623                );
624            }
625        } else {
626            // Add spacing line, as shown:
627            //   --> $DIR/file:54:15
628            //    |
629            // LL |         code
630            //    |         ^^^^
631            //    | (<- It prints *this* line)
632            //   ::: $DIR/other_file.rs:15:5
633            //    |
634            // LL |     code
635            //    |     ----
636            draw_col_separator_no_space(
637                renderer,
638                buffer,
639                buffer_msg_line_offset,
640                max_line_num_len + 1,
641            );
642
643            buffer.puts(
644                buffer_msg_line_offset + 1,
645                max_line_num_len,
646                renderer.decor_style.secondary_file_start(),
647                ElementStyle::LineNumber,
648            );
649        }
650    }
651
652    // Contains the vertical lines' positions for active multiline annotations
653    let mut multilines = Vec::new();
654
655    // Get the left-side margin to remove it
656    let mut whitespace_margin = usize::MAX;
657    for line_info in annotated_lines {
658        let leading_whitespace = line_info
659            .line
660            .chars()
661            .take_while(|c| c.is_whitespace())
662            .map(|c| {
663                match c {
664                    // Tabs are displayed as 4 spaces
665                    '\t' => 4,
666                    _ => 1,
667                }
668            })
669            .sum();
670        if line_info.line.chars().any(|c| !c.is_whitespace()) {
671            whitespace_margin = min(whitespace_margin, leading_whitespace);
672        }
673    }
674    if whitespace_margin == usize::MAX {
675        whitespace_margin = 0;
676    }
677
678    // Left-most column any visible span points at.
679    let mut span_left_margin = usize::MAX;
680    for line_info in annotated_lines {
681        for ann in &line_info.annotations {
682            // The spans from `LineAnnotationType::MultilineLine` are always
683            // zero, which leads to incorrect calculation of the left margin,
684            // so we skip them here.
685            if matches!(ann.annotation_type, LineAnnotationType::MultilineLine(_)) {
686                continue;
687            }
688            span_left_margin = min(span_left_margin, ann.start.display);
689            span_left_margin = min(span_left_margin, ann.end.display);
690        }
691    }
692    if span_left_margin == usize::MAX {
693        span_left_margin = 0;
694    }
695
696    // Right-most column any visible span points at.
697    let mut span_right_margin = 0;
698    let mut label_right_margin = 0;
699    let mut max_line_len = 0;
700    for line_info in annotated_lines {
701        max_line_len = max(max_line_len, str_width(line_info.line));
702        for ann in &line_info.annotations {
703            span_right_margin = max(span_right_margin, ann.start.display);
704            span_right_margin = max(span_right_margin, ann.end.display);
705            // FIXME: account for labels not in the same line
706            let label_right = ann.label.as_ref().map_or(0, |l| str_width(l) + 1);
707            label_right_margin = max(label_right_margin, ann.end.display + label_right);
708        }
709    }
710    let width_offset = 3 + max_line_num_len;
711    let code_offset = if multiline_depth == 0 {
712        width_offset
713    } else {
714        width_offset + multiline_depth + 1
715    };
716
717    let column_width = renderer.term_width.saturating_sub(code_offset);
718
719    let margin = Margin::new(
720        whitespace_margin,
721        span_left_margin,
722        span_right_margin,
723        label_right_margin,
724        column_width,
725        max_line_len,
726    );
727
728    // Next, output the annotate source for this file
729    for annotated_line_idx in 0..annotated_lines.len() {
730        let previous_buffer_line = buffer.num_lines();
731
732        let depths = render_source_line(
733            renderer,
734            &annotated_lines[annotated_line_idx],
735            buffer,
736            width_offset,
737            code_offset,
738            max_line_num_len,
739            margin,
740            !is_cont && annotated_line_idx + 1 == annotated_lines.len(),
741            snippet.line_numbering,
742        );
743
744        let mut to_add = BTreeMap::new();
745
746        for (depth, style) in depths {
747            if let Some(index) = multilines.iter().position(|(d, _)| d == &depth) {
748                multilines.swap_remove(index);
749            } else {
750                to_add.insert(depth, style);
751            }
752        }
753
754        // Set the multiline annotation vertical lines to the left of
755        // the code in this line.
756        for (depth, style) in &multilines {
757            for line in previous_buffer_line..buffer.num_lines() {
758                draw_multiline_line(renderer, buffer, line, width_offset, *depth, *style, false);
759            }
760        }
761        // check to see if we need to print out or elide lines that come between
762        // this annotated line and the next one.
763        if annotated_line_idx < (annotated_lines.len() - 1) {
764            let line_idx_delta = annotated_lines[annotated_line_idx + 1].line_index
765                - annotated_lines[annotated_line_idx].line_index;
766            match line_idx_delta.cmp(&2) {
767                Ordering::Greater => {
768                    let last_buffer_line_num = buffer.num_lines();
769
770                    draw_line_separator(renderer, buffer, last_buffer_line_num, width_offset);
771
772                    // Set the multiline annotation vertical lines on `...` bridging line.
773                    for (depth, style) in &multilines {
774                        draw_multiline_line(
775                            renderer,
776                            buffer,
777                            last_buffer_line_num,
778                            width_offset,
779                            *depth,
780                            *style,
781                            true,
782                        );
783                    }
784                    if let Some(line) = annotated_lines.get(annotated_line_idx) {
785                        for ann in &line.annotations {
786                            if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type {
787                                // In the case where we have elided the entire start of the
788                                // multispan because those lines were empty, we still need
789                                // to draw the `|`s across the `...`.
790                                draw_multiline_line(
791                                    renderer,
792                                    buffer,
793                                    last_buffer_line_num,
794                                    width_offset,
795                                    pos,
796                                    if ann.is_primary() {
797                                        ElementStyle::UnderlinePrimary
798                                    } else {
799                                        ElementStyle::UnderlineSecondary
800                                    },
801                                    true,
802                                );
803                            }
804                        }
805                    }
806                }
807
808                Ordering::Equal => {
809                    let unannotated_line = sm
810                        .get_line(annotated_lines[annotated_line_idx].line_index + 1)
811                        .unwrap_or("");
812
813                    let last_buffer_line_num = buffer.num_lines();
814
815                    draw_line(
816                        renderer,
817                        buffer,
818                        &normalize_whitespace(unannotated_line),
819                        annotated_lines[annotated_line_idx + 1].line_index - 1,
820                        last_buffer_line_num,
821                        width_offset,
822                        code_offset,
823                        max_line_num_len,
824                        margin,
825                        snippet.line_numbering,
826                    );
827
828                    for (depth, style) in &multilines {
829                        draw_multiline_line(
830                            renderer,
831                            buffer,
832                            last_buffer_line_num,
833                            width_offset,
834                            *depth,
835                            *style,
836                            false,
837                        );
838                    }
839                    if let Some(line) = annotated_lines.get(annotated_line_idx) {
840                        for ann in &line.annotations {
841                            if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type {
842                                draw_multiline_line(
843                                    renderer,
844                                    buffer,
845                                    last_buffer_line_num,
846                                    width_offset,
847                                    pos,
848                                    if ann.is_primary() {
849                                        ElementStyle::UnderlinePrimary
850                                    } else {
851                                        ElementStyle::UnderlineSecondary
852                                    },
853                                    false,
854                                );
855                            }
856                        }
857                    }
858                }
859                Ordering::Less => {}
860            }
861        }
862
863        multilines.extend(to_add);
864    }
865}
866
867#[allow(clippy::too_many_arguments, reason = "All arguments are necessary")]
868fn render_source_line(
869    renderer: &Renderer,
870    line_info: &AnnotatedLineInfo<'_>,
871    buffer: &mut StyledBuffer,
872    width_offset: usize,
873    code_offset: usize,
874    max_line_num_len: usize,
875    margin: Margin,
876    close_window: bool,
877    line_numbering: bool,
878) -> Vec<(usize, ElementStyle)> {
879    // Draw:
880    //
881    //   LL | ... code ...
882    //      |     ^^-^ span label
883    //      |       |
884    //      |       secondary span label
885    //
886    //   ^^ ^ ^^^ ^^^^ ^^^ we don't care about code too far to the right of a span, we trim it
887    //   |  | |   |
888    //   |  | |   actual code found in your source code and the spans we use to mark it
889    //   |  | when there's too much wasted space to the left, trim it
890    //   |  vertical divider between the column number and the code
891    //   column number
892
893    let source_string = normalize_whitespace(line_info.line);
894
895    let line_offset = buffer.num_lines();
896
897    let left = draw_line(
898        renderer,
899        buffer,
900        &source_string,
901        line_info.line_index,
902        line_offset,
903        width_offset,
904        code_offset,
905        max_line_num_len,
906        margin,
907        line_numbering,
908    );
909
910    // If there are no annotations, we are done
911    if line_info.annotations.is_empty() {
912        // `close_window` normally gets handled later, but we are early
913        // returning, so it needs to be handled here
914        if close_window {
915            draw_col_separator_end(renderer, buffer, line_offset + 1, width_offset - 2);
916        }
917        return vec![];
918    }
919
920    // Special case when there's only one annotation involved, it is the start of a multiline
921    // span and there's no text at the beginning of the code line. Instead of doing the whole
922    // graph:
923    //
924    // 2 |   fn foo() {
925    //   |  _^
926    // 3 | |
927    // 4 | | }
928    //   | |_^ test
929    //
930    // we simplify the output to:
931    //
932    // 2 | / fn foo() {
933    // 3 | |
934    // 4 | | }
935    //   | |_^ test
936    let mut buffer_ops = vec![];
937    let mut annotations = vec![];
938    let mut short_start = true;
939    for ann in &line_info.annotations {
940        if let LineAnnotationType::MultilineStart(depth) = ann.annotation_type {
941            if source_string
942                .chars()
943                .take(ann.start.display)
944                .all(char::is_whitespace)
945            {
946                let uline = renderer.decor_style.underline(ann.is_primary());
947                let chr = uline.multiline_whole_line;
948                annotations.push((depth, uline.style));
949                buffer_ops.push((line_offset, width_offset + depth - 1, chr, uline.style));
950            } else {
951                short_start = false;
952                break;
953            }
954        } else if let LineAnnotationType::MultilineLine(_) = ann.annotation_type {
955        } else {
956            short_start = false;
957            break;
958        }
959    }
960    if short_start {
961        for (y, x, c, s) in buffer_ops {
962            buffer.putc(y, x, c, s);
963        }
964        return annotations;
965    }
966
967    // We want to display like this:
968    //
969    //      vec.push(vec.pop().unwrap());
970    //      ---      ^^^               - previous borrow ends here
971    //      |        |
972    //      |        error occurs here
973    //      previous borrow of `vec` occurs here
974    //
975    // But there are some weird edge cases to be aware of:
976    //
977    //      vec.push(vec.pop().unwrap());
978    //      --------                    - previous borrow ends here
979    //      ||
980    //      |this makes no sense
981    //      previous borrow of `vec` occurs here
982    //
983    // For this reason, we group the lines into "highlight lines"
984    // and "annotations lines", where the highlight lines have the `^`.
985
986    // Sort the annotations by (start, end col)
987    // The labels are reversed, sort and then reversed again.
988    // Consider a list of annotations (A1, A2, C1, C2, B1, B2) where
989    // the letter signifies the span. Here we are only sorting by the
990    // span and hence, the order of the elements with the same span will
991    // not change. On reversing the ordering (|a, b| but b.cmp(a)), you get
992    // (C1, C2, B1, B2, A1, A2). All the elements with the same span are
993    // still ordered first to last, but all the elements with different
994    // spans are ordered by their spans in last to first order. Last to
995    // first order is important, because the jiggly lines and | are on
996    // the left, so the rightmost span needs to be rendered first,
997    // otherwise the lines would end up needing to go over a message.
998
999    let mut annotations = line_info.annotations.clone();
1000    annotations.sort_by_key(|a| Reverse((a.start.display, a.start.char)));
1001
1002    // First, figure out where each label will be positioned.
1003    //
1004    // In the case where you have the following annotations:
1005    //
1006    //      vec.push(vec.pop().unwrap());
1007    //      --------                    - previous borrow ends here [C]
1008    //      ||
1009    //      |this makes no sense [B]
1010    //      previous borrow of `vec` occurs here [A]
1011    //
1012    // `annotations_position` will hold [(2, A), (1, B), (0, C)].
1013    //
1014    // We try, when possible, to stick the rightmost annotation at the end
1015    // of the highlight line:
1016    //
1017    //      vec.push(vec.pop().unwrap());
1018    //      ---      ---               - previous borrow ends here
1019    //
1020    // But sometimes that's not possible because one of the other
1021    // annotations overlaps it. For example, from the test
1022    // `span_overlap_label`, we have the following annotations
1023    // (written on distinct lines for clarity):
1024    //
1025    //      fn foo(x: u32) {
1026    //      --------------
1027    //             -
1028    //
1029    // In this case, we can't stick the rightmost-most label on
1030    // the highlight line, or we would get:
1031    //
1032    //      fn foo(x: u32) {
1033    //      -------- x_span
1034    //      |
1035    //      fn_span
1036    //
1037    // which is totally weird. Instead we want:
1038    //
1039    //      fn foo(x: u32) {
1040    //      --------------
1041    //      |      |
1042    //      |      x_span
1043    //      fn_span
1044    //
1045    // which is...less weird, at least. In fact, in general, if
1046    // the rightmost span overlaps with any other span, we should
1047    // use the "hang below" version, so we can at least make it
1048    // clear where the span *starts*. There's an exception for this
1049    // logic, when the labels do not have a message:
1050    //
1051    //      fn foo(x: u32) {
1052    //      --------------
1053    //             |
1054    //             x_span
1055    //
1056    // instead of:
1057    //
1058    //      fn foo(x: u32) {
1059    //      --------------
1060    //      |      |
1061    //      |      x_span
1062    //      <EMPTY LINE>
1063    //
1064    let mut overlap = vec![false; annotations.len()];
1065    let mut annotations_position = vec![];
1066    let mut line_len: usize = 0;
1067    let mut p = 0;
1068    for (i, annotation) in annotations.iter().enumerate() {
1069        for (j, next) in annotations.iter().enumerate() {
1070            if overlaps(next, annotation, 0) && j > 1 {
1071                overlap[i] = true;
1072                overlap[j] = true;
1073            }
1074            if overlaps(next, annotation, 0)  // This label overlaps with another one and both
1075                    && annotation.has_label()     // take space (they have text and are not
1076                    && j > i                      // multiline lines).
1077                    && p == 0
1078            // We're currently on the first line, move the label one line down
1079            {
1080                // If we're overlapping with an un-labelled annotation with the same span
1081                // we can just merge them in the output
1082                if next.start.display == annotation.start.display
1083                    && next.start.char == annotation.start.char
1084                    && next.end.display == annotation.end.display
1085                    && next.end.char == annotation.end.char
1086                    && !next.has_label()
1087                {
1088                    continue;
1089                }
1090
1091                // This annotation needs a new line in the output.
1092                p += 1;
1093                break;
1094            }
1095        }
1096        annotations_position.push((p, annotation));
1097        for (j, next) in annotations.iter().enumerate() {
1098            if j > i {
1099                let l = next.label.as_ref().map_or(0, |label| label.len() + 2);
1100                if (overlaps(next, annotation, l) // Do not allow two labels to be in the same
1101                        // line if they overlap including padding, to
1102                        // avoid situations like:
1103                        //
1104                        //      fn foo(x: u32) {
1105                        //      -------^------
1106                        //      |      |
1107                        //      fn_spanx_span
1108                        //
1109                        && annotation.has_label()    // Both labels must have some text, otherwise
1110                        && next.has_label())         // they are not overlapping.
1111                        // Do not add a new line if this annotation
1112                        // or the next are vertical line placeholders.
1113                        || (annotation.takes_space() // If either this or the next annotation is
1114                        && next.has_label())     // multiline start/end, move it to a new line
1115                        || (annotation.has_label()   // so as not to overlap the horizontal lines.
1116                        && next.takes_space())
1117                        || (annotation.takes_space() && next.takes_space())
1118                        || (overlaps(next, annotation, l)
1119                        && (next.end.display, next.end.char) <= (annotation.end.display, annotation.end.char)
1120                        && next.has_label()
1121                        && p == 0)
1122                // Avoid #42595.
1123                {
1124                    // This annotation needs a new line in the output.
1125                    p += 1;
1126                    break;
1127                }
1128            }
1129        }
1130        line_len = max(line_len, p);
1131    }
1132
1133    if line_len != 0 {
1134        line_len += 1;
1135    }
1136
1137    // If there are no annotations or the only annotations on this line are
1138    // MultilineLine, then there's only code being shown, stop processing.
1139    if line_info.annotations.iter().all(LineAnnotation::is_line) {
1140        return vec![];
1141    }
1142
1143    if annotations_position
1144        .iter()
1145        .all(|(_, ann)| matches!(ann.annotation_type, LineAnnotationType::MultilineStart(_)))
1146        && let Some(max_pos) = annotations_position.iter().map(|(pos, _)| *pos).max()
1147    {
1148        // Special case the following, so that we minimize overlapping multiline spans.
1149        //
1150        // 3 │       X0 Y0 Z0
1151        //   │ ┏━━━━━┛  │  │     < We are writing these lines
1152        //   │ ┃┌───────┘  │     < by reverting the "depth" of
1153        //   │ ┃│┌─────────┘     < their multiline spans.
1154        // 4 │ ┃││   X1 Y1 Z1
1155        // 5 │ ┃││   X2 Y2 Z2
1156        //   │ ┃│└────╿──│──┘ `Z` label
1157        //   │ ┃└─────│──┤
1158        //   │ ┗━━━━━━┥  `Y` is a good letter too
1159        //   ╰╴       `X` is a good letter
1160        for (pos, _) in &mut annotations_position {
1161            *pos = max_pos - *pos;
1162        }
1163        // We know then that we don't need an additional line for the span label, saving us
1164        // one line of vertical space.
1165        line_len = line_len.saturating_sub(1);
1166    }
1167
1168    // Write the column separator.
1169    //
1170    // After this we will have:
1171    //
1172    // 2 |   fn foo() {
1173    //   |
1174    //   |
1175    //   |
1176    // 3 |
1177    // 4 |   }
1178    //   |
1179    for pos in 0..=line_len {
1180        draw_col_separator_no_space(renderer, buffer, line_offset + pos + 1, width_offset - 2);
1181    }
1182    if close_window {
1183        draw_col_separator_end(
1184            renderer,
1185            buffer,
1186            line_offset + line_len + 1,
1187            width_offset - 2,
1188        );
1189    }
1190    // Write the horizontal lines for multiline annotations
1191    // (only the first and last lines need this).
1192    //
1193    // After this we will have:
1194    //
1195    // 2 |   fn foo() {
1196    //   |  __________
1197    //   |
1198    //   |
1199    // 3 |
1200    // 4 |   }
1201    //   |  _
1202    for &(pos, annotation) in &annotations_position {
1203        let underline = renderer.decor_style.underline(annotation.is_primary());
1204        let pos = pos + 1;
1205        match annotation.annotation_type {
1206            LineAnnotationType::MultilineStart(depth) | LineAnnotationType::MultilineEnd(depth) => {
1207                draw_range(
1208                    buffer,
1209                    underline.multiline_horizontal,
1210                    line_offset + pos,
1211                    width_offset + depth,
1212                    (code_offset + annotation.start.display).saturating_sub(left),
1213                    underline.style,
1214                );
1215            }
1216            _ if annotation.highlight_source => {
1217                buffer.set_style_range(
1218                    line_offset,
1219                    (code_offset + annotation.start.char).saturating_sub(left),
1220                    (code_offset + annotation.end.char).saturating_sub(left),
1221                    underline.style,
1222                    annotation.is_primary(),
1223                );
1224            }
1225            _ => {}
1226        }
1227    }
1228
1229    // Write the vertical lines for labels that are on a different line as the underline.
1230    //
1231    // After this we will have:
1232    //
1233    // 2 |   fn foo() {
1234    //   |  __________
1235    //   | |    |
1236    //   | |
1237    // 3 | |
1238    // 4 | | }
1239    //   | |_
1240    for &(pos, annotation) in &annotations_position {
1241        let underline = renderer.decor_style.underline(annotation.is_primary());
1242        let pos = pos + 1;
1243
1244        if pos > 1 && (annotation.has_label() || annotation.takes_space()) {
1245            for p in line_offset + 1..=line_offset + pos {
1246                buffer.putc(
1247                    p,
1248                    (code_offset + annotation.start.display).saturating_sub(left),
1249                    match annotation.annotation_type {
1250                        LineAnnotationType::MultilineLine(_) => underline.multiline_vertical,
1251                        _ => underline.vertical_text_line,
1252                    },
1253                    underline.style,
1254                );
1255            }
1256            if let LineAnnotationType::MultilineStart(_) = annotation.annotation_type {
1257                buffer.putc(
1258                    line_offset + pos,
1259                    (code_offset + annotation.start.display).saturating_sub(left),
1260                    underline.bottom_right,
1261                    underline.style,
1262                );
1263            }
1264            if matches!(
1265                annotation.annotation_type,
1266                LineAnnotationType::MultilineEnd(_)
1267            ) && annotation.has_label()
1268            {
1269                buffer.putc(
1270                    line_offset + pos,
1271                    (code_offset + annotation.start.display).saturating_sub(left),
1272                    underline.multiline_bottom_right_with_text,
1273                    underline.style,
1274                );
1275            }
1276        }
1277        match annotation.annotation_type {
1278            LineAnnotationType::MultilineStart(depth) => {
1279                buffer.putc(
1280                    line_offset + pos,
1281                    width_offset + depth - 1,
1282                    underline.top_left,
1283                    underline.style,
1284                );
1285                for p in line_offset + pos + 1..line_offset + line_len + 2 {
1286                    buffer.putc(
1287                        p,
1288                        width_offset + depth - 1,
1289                        underline.multiline_vertical,
1290                        underline.style,
1291                    );
1292                }
1293            }
1294            LineAnnotationType::MultilineEnd(depth) => {
1295                for p in line_offset..line_offset + pos {
1296                    buffer.putc(
1297                        p,
1298                        width_offset + depth - 1,
1299                        underline.multiline_vertical,
1300                        underline.style,
1301                    );
1302                }
1303                buffer.putc(
1304                    line_offset + pos,
1305                    width_offset + depth - 1,
1306                    underline.bottom_left,
1307                    underline.style,
1308                );
1309            }
1310            _ => (),
1311        }
1312    }
1313
1314    // Write the labels on the annotations that actually have a label.
1315    //
1316    // After this we will have:
1317    //
1318    // 2 |   fn foo() {
1319    //   |  __________
1320    //   |      |
1321    //   |      something about `foo`
1322    // 3 |
1323    // 4 |   }
1324    //   |  _  test
1325    for &(pos, annotation) in &annotations_position {
1326        let style = if annotation.is_primary() {
1327            ElementStyle::LabelPrimary
1328        } else {
1329            ElementStyle::LabelSecondary
1330        };
1331        let (pos, col) = if pos == 0 {
1332            if annotation.end.display == 0 {
1333                (pos + 1, (annotation.end.display + 2).saturating_sub(left))
1334            } else {
1335                (pos + 1, (annotation.end.display + 1).saturating_sub(left))
1336            }
1337        } else {
1338            (pos + 2, annotation.start.display.saturating_sub(left))
1339        };
1340        if let Some(label) = &annotation.label {
1341            buffer.puts(line_offset + pos, code_offset + col, label, style);
1342        }
1343    }
1344
1345    // Sort from biggest span to smallest span so that smaller spans are
1346    // represented in the output:
1347    //
1348    // x | fn foo()
1349    //   | ^^^---^^
1350    //   | |  |
1351    //   | |  something about `foo`
1352    //   | something about `fn foo()`
1353    annotations_position.sort_by_key(|(_, ann)| {
1354        // Decreasing order. When annotations share the same length, prefer `Primary`.
1355        (Reverse(ann.len()), ann.is_primary())
1356    });
1357
1358    // Write the underlines.
1359    //
1360    // After this we will have:
1361    //
1362    // 2 |   fn foo() {
1363    //   |  ____-_____^
1364    //   |      |
1365    //   |      something about `foo`
1366    // 3 |
1367    // 4 |   }
1368    //   |  _^  test
1369    for &(pos, annotation) in &annotations_position {
1370        let uline = renderer.decor_style.underline(annotation.is_primary());
1371        for p in annotation.start.display..annotation.end.display {
1372            // The default span label underline.
1373            buffer.putc(
1374                line_offset + 1,
1375                (code_offset + p).saturating_sub(left),
1376                uline.underline,
1377                uline.style,
1378            );
1379        }
1380
1381        if pos == 0
1382            && matches!(
1383                annotation.annotation_type,
1384                LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1385            )
1386        {
1387            // The beginning of a multiline span with its leftward moving line on the same line.
1388            buffer.putc(
1389                line_offset + 1,
1390                (code_offset + annotation.start.display).saturating_sub(left),
1391                match annotation.annotation_type {
1392                    LineAnnotationType::MultilineStart(_) => uline.top_right_flat,
1393                    LineAnnotationType::MultilineEnd(_) => uline.multiline_end_same_line,
1394                    _ => panic!("unexpected annotation type: {annotation:?}"),
1395                },
1396                uline.style,
1397            );
1398        } else if pos != 0
1399            && matches!(
1400                annotation.annotation_type,
1401                LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1402            )
1403        {
1404            // The beginning of a multiline span with its leftward moving line on another line,
1405            // so we start going down first.
1406            buffer.putc(
1407                line_offset + 1,
1408                (code_offset + annotation.start.display).saturating_sub(left),
1409                match annotation.annotation_type {
1410                    LineAnnotationType::MultilineStart(_) => uline.multiline_start_down,
1411                    LineAnnotationType::MultilineEnd(_) => uline.multiline_end_up,
1412                    _ => panic!("unexpected annotation type: {annotation:?}"),
1413                },
1414                uline.style,
1415            );
1416        } else if pos != 0 && annotation.has_label() {
1417            // The beginning of a span label with an actual label, we'll point down.
1418            buffer.putc(
1419                line_offset + 1,
1420                (code_offset + annotation.start.display).saturating_sub(left),
1421                uline.label_start,
1422                uline.style,
1423            );
1424        }
1425    }
1426
1427    // We look for individual *long* spans, and we trim the *middle*, so that we render
1428    // LL | ...= [0, 0, 0, ..., 0, 0];
1429    //    |      ^^^^^^^^^^...^^^^^^^ expected `&[u8]`, found `[{integer}; 1680]`
1430    for (i, (_pos, annotation)) in annotations_position.iter().enumerate() {
1431        // Skip cases where multiple spans overlap eachother.
1432        if overlap[i] {
1433            continue;
1434        };
1435        let LineAnnotationType::Singleline = annotation.annotation_type else {
1436            continue;
1437        };
1438        let width = annotation.end.display - annotation.start.display;
1439
1440        static MIN_PAD: usize = 5;
1441        let margin_width = str_width(renderer.decor_style.margin());
1442        if width > margin.term_width * 2 && width > (MIN_PAD * 2 + margin_width) {
1443            // If the terminal is *too* small, we keep at least a tiny bit of the span for
1444            // display.
1445            let pad = max(margin.term_width / 3, MIN_PAD);
1446            // Code line
1447            buffer.replace(
1448                line_offset,
1449                code_offset + (annotation.start.display + pad).saturating_sub(left),
1450                code_offset + (annotation.end.display - pad).saturating_sub(left),
1451                renderer.decor_style.margin(),
1452            );
1453            // Underline line
1454            buffer.replace(
1455                line_offset + 1,
1456                code_offset + (annotation.start.display + pad).saturating_sub(left),
1457                code_offset + (annotation.end.display - pad).saturating_sub(left),
1458                renderer.decor_style.margin(),
1459            );
1460        }
1461    }
1462    annotations_position
1463        .iter()
1464        .filter_map(|&(_, annotation)| match annotation.annotation_type {
1465            LineAnnotationType::MultilineStart(p) | LineAnnotationType::MultilineEnd(p) => {
1466                let style = if annotation.is_primary() {
1467                    ElementStyle::LabelPrimary
1468                } else {
1469                    ElementStyle::LabelSecondary
1470                };
1471                Some((p, style))
1472            }
1473            _ => None,
1474        })
1475        .collect::<Vec<_>>()
1476}
1477
1478#[allow(clippy::too_many_arguments, reason = "All arguments are necessary")]
1479fn emit_suggestion_default(
1480    renderer: &Renderer,
1481    buffer: &mut StyledBuffer,
1482    suggestion: &Snippet<'_, Patch<'_>>,
1483    spliced_lines: SplicedLines<'_>,
1484    show_code_change: DisplaySuggestion,
1485    max_line_num_len: usize,
1486    sm: &SourceMap<'_>,
1487    primary_path: Option<&Cow<'_, str>>,
1488    matches_previous_suggestion: bool,
1489    is_first: bool,
1490    is_cont: bool,
1491) {
1492    let buffer_offset = buffer.num_lines();
1493    let mut row_num = buffer_offset + usize::from(!matches_previous_suggestion);
1494    let is_multiline = spliced_lines.complete.lines().count() > 1;
1495
1496    if suggestion.path.as_ref() != primary_path
1497        && let Some(path) = suggestion.path.as_ref()
1498        && !matches_previous_suggestion
1499    {
1500        let (loc, _) = sm.span_to_locations(spliced_lines.patches[0].span.clone());
1501        // --> file.rs:line:col
1502        //  |
1503        for _ in 0..max_line_num_len {
1504            buffer.append(row_num - 1, " ", ElementStyle::NoStyle);
1505        }
1506        let arrow = renderer.decor_style.file_start(is_first, false);
1507        buffer.append(row_num - 1, arrow, ElementStyle::LineNumber);
1508        let mut origin = Origin::path(path.as_ref());
1509        origin.line = Some(loc.line);
1510        origin.char_column = Some(loc.char + 1);
1511        let message = format_origin(&origin, renderer.anonymized_origin_line_numbers);
1512        buffer.append(row_num - 1, &message, ElementStyle::LineAndColumn);
1513
1514        draw_col_separator_no_space(renderer, buffer, row_num, max_line_num_len + 1);
1515        row_num += 1;
1516    } else if matches_previous_suggestion {
1517        buffer.puts(
1518            row_num - 1,
1519            max_line_num_len + 1,
1520            renderer.decor_style.multi_suggestion_separator(),
1521            ElementStyle::LineNumber,
1522        );
1523    } else {
1524        draw_col_separator_start(renderer, buffer, row_num - 1, max_line_num_len + 1);
1525    }
1526
1527    if let DisplaySuggestion::Diff = show_code_change {
1528        row_num += 1;
1529    }
1530
1531    let lo = spliced_lines
1532        .patches
1533        .iter()
1534        .map(|p| p.span.start)
1535        .min()
1536        .unwrap();
1537    let hi = spliced_lines
1538        .patches
1539        .iter()
1540        .map(|p| p.span.end)
1541        .max()
1542        .unwrap();
1543
1544    let file_lines = sm.span_to_lines(lo..hi);
1545    let (line_start, line_end) = if suggestion.fold {
1546        // We use the original span to get original line_start
1547        sm.span_to_locations(spliced_lines.patches[0].original_span.clone())
1548    } else {
1549        sm.span_to_locations(0..sm.source.len())
1550    };
1551    let mut lines = spliced_lines.complete.lines();
1552    if lines.clone().next().is_none() {
1553        // Account for a suggestion to completely remove a line(s) with whitespace (#94192).
1554        for line in line_start.line..=line_end.line {
1555            buffer.puts(
1556                row_num - 1 + line - line_start.line,
1557                0,
1558                &maybe_anonymized(renderer, line, max_line_num_len, suggestion.line_numbering),
1559                ElementStyle::LineNumber,
1560            );
1561            buffer.puts(
1562                row_num - 1 + line - line_start.line,
1563                max_line_num_len + 1,
1564                "- ",
1565                ElementStyle::Removal,
1566            );
1567            buffer.puts(
1568                row_num - 1 + line - line_start.line,
1569                max_line_num_len + 3,
1570                &normalize_whitespace(sm.get_line(line).unwrap()),
1571                ElementStyle::Removal,
1572            );
1573        }
1574        row_num += line_end.line - line_start.line;
1575    }
1576    let mut unhighlighted_lines = Vec::new();
1577    for (line_pos, (line, highlight_parts)) in
1578        lines.by_ref().zip(spliced_lines.highlights).enumerate()
1579    {
1580        // Remember lines that are not highlighted to hide them if needed
1581        if highlight_parts.is_empty() && suggestion.fold {
1582            unhighlighted_lines.push((line_pos, line));
1583            continue;
1584        }
1585
1586        match unhighlighted_lines.len() {
1587            0 => (),
1588            // Since we show first line, "..." line and last line,
1589            // There is no reason to hide if there are 3 or less lines
1590            // (because then we just replace a line with ... which is
1591            // not helpful)
1592            n if n <= 3 => unhighlighted_lines.drain(..).for_each(|(p, l)| {
1593                draw_code_line(
1594                    renderer,
1595                    buffer,
1596                    &mut row_num,
1597                    &[],
1598                    &[],
1599                    p + line_start.line,
1600                    l,
1601                    show_code_change,
1602                    max_line_num_len,
1603                    &file_lines,
1604                    is_multiline,
1605                    suggestion.line_numbering,
1606                );
1607            }),
1608            // Print first unhighlighted line, "..." and last unhighlighted line, like so:
1609            //
1610            // LL | this line was highlighted
1611            // LL | this line is just for context
1612            // ...
1613            // LL | this line is just for context
1614            // LL | this line was highlighted
1615            _ => {
1616                let last_line = unhighlighted_lines.pop();
1617                let first_line = unhighlighted_lines.drain(..).next();
1618
1619                if let Some((p, l)) = first_line {
1620                    draw_code_line(
1621                        renderer,
1622                        buffer,
1623                        &mut row_num,
1624                        &[],
1625                        &[],
1626                        p + line_start.line,
1627                        l,
1628                        show_code_change,
1629                        max_line_num_len,
1630                        &file_lines,
1631                        is_multiline,
1632                        suggestion.line_numbering,
1633                    );
1634                }
1635
1636                let placeholder = renderer.decor_style.margin();
1637                let padding = str_width(placeholder);
1638                buffer.puts(
1639                    row_num,
1640                    max_line_num_len.saturating_sub(padding),
1641                    placeholder,
1642                    ElementStyle::LineNumber,
1643                );
1644                row_num += 1;
1645
1646                if let Some((p, l)) = last_line {
1647                    draw_code_line(
1648                        renderer,
1649                        buffer,
1650                        &mut row_num,
1651                        &[],
1652                        &[],
1653                        p + line_start.line,
1654                        l,
1655                        show_code_change,
1656                        max_line_num_len,
1657                        &file_lines,
1658                        is_multiline,
1659                        suggestion.line_numbering,
1660                    );
1661                }
1662            }
1663        }
1664        draw_code_line(
1665            renderer,
1666            buffer,
1667            &mut row_num,
1668            &highlight_parts,
1669            &spliced_lines.replaced_highlights,
1670            line_pos + line_start.line,
1671            line,
1672            show_code_change,
1673            max_line_num_len,
1674            &file_lines,
1675            is_multiline,
1676            suggestion.line_numbering,
1677        );
1678    }
1679
1680    // This offset and the ones below need to be signed to account for replacement code
1681    // that is shorter than the original code.
1682    let mut offsets: Vec<(usize, isize)> = Vec::new();
1683    // Only show an underline in the suggestions if the suggestion is not the
1684    // entirety of the code being shown and the displayed code is not multiline.
1685    if let DisplaySuggestion::Diff | DisplaySuggestion::Underline | DisplaySuggestion::Add =
1686        show_code_change
1687    {
1688        for part in spliced_lines.patches {
1689            let (span_start, span_end) = sm.span_to_locations(part.span.clone());
1690            let span_start_pos = span_start.display;
1691            let span_end_pos = span_end.display;
1692
1693            // If this addition is _only_ whitespace, then don't trim it,
1694            // or else we're just not rendering anything.
1695            let is_whitespace_addition = part.replacement.trim().is_empty();
1696
1697            // Do not underline the leading...
1698            let start = if is_whitespace_addition {
1699                0
1700            } else {
1701                part.replacement
1702                    .len()
1703                    .saturating_sub(part.replacement.trim_start().len())
1704            };
1705            // ...or trailing spaces. Account for substitutions containing unicode
1706            // characters.
1707            let sub_len: usize = str_width(if is_whitespace_addition {
1708                &part.replacement
1709            } else {
1710                part.replacement.trim()
1711            });
1712
1713            let offset: isize = offsets
1714                .iter()
1715                .filter_map(|(start, v)| {
1716                    if span_start_pos < *start {
1717                        None
1718                    } else {
1719                        Some(v)
1720                    }
1721                })
1722                .sum();
1723            let underline_start = (span_start_pos + start).cast_signed() + offset;
1724            let underline_end = (span_start_pos + start + sub_len).cast_signed() + offset;
1725            assert!(underline_start >= 0 && underline_end >= 0);
1726            let padding: usize = max_line_num_len + 3;
1727            for p in underline_start..underline_end {
1728                if matches!(show_code_change, DisplaySuggestion::Underline) {
1729                    // If this is a replacement, underline with `~`, if this is an addition
1730                    // underline with `+`.
1731                    buffer.putc(
1732                        row_num,
1733                        (padding.cast_signed() + p).cast_unsigned(),
1734                        if part.is_addition(sm) {
1735                            '+'
1736                        } else {
1737                            renderer.decor_style.diff()
1738                        },
1739                        ElementStyle::Addition,
1740                    );
1741                }
1742            }
1743
1744            // length of the code after substitution
1745            let full_sub_len = str_width(&part.replacement).cast_signed();
1746
1747            // length of the code to be substituted
1748            let snippet_len = span_end_pos.cast_signed() - span_start_pos.cast_signed();
1749            // For multiple substitutions, use the position *after* the previous
1750            // substitutions have happened, only when further substitutions are
1751            // located strictly after.
1752            offsets.push((span_end_pos, full_sub_len - snippet_len));
1753        }
1754        row_num += 1;
1755    }
1756
1757    // if we elided some lines, add an ellipsis
1758    if lines.next().is_some() {
1759        let placeholder = renderer.decor_style.margin();
1760        let padding = str_width(placeholder);
1761        buffer.puts(
1762            row_num,
1763            max_line_num_len.saturating_sub(padding),
1764            placeholder,
1765            ElementStyle::LineNumber,
1766        );
1767    } else {
1768        let row = match show_code_change {
1769            DisplaySuggestion::Diff | DisplaySuggestion::Add | DisplaySuggestion::Underline => {
1770                row_num - 1
1771            }
1772            DisplaySuggestion::None => row_num,
1773        };
1774        if is_cont {
1775            draw_col_separator_no_space(renderer, buffer, row, max_line_num_len + 1);
1776        } else {
1777            draw_col_separator_end(renderer, buffer, row, max_line_num_len + 1);
1778        }
1779    }
1780}
1781
1782#[allow(clippy::too_many_arguments, reason = "All arguments are necessary")]
1783fn draw_code_line(
1784    renderer: &Renderer,
1785    buffer: &mut StyledBuffer,
1786    row_num: &mut usize,
1787    highlight_parts: &[SubstitutionHighlight],
1788    replaced_parts: &[Vec<SubstitutionHighlight>],
1789    line_num: usize,
1790    line_to_add: &str,
1791    show_code_change: DisplaySuggestion,
1792    max_line_num_len: usize,
1793    file_lines: &[&LineInfo<'_>],
1794    is_multiline: bool,
1795    line_numbering: bool,
1796) {
1797    if let DisplaySuggestion::Diff = show_code_change {
1798        // We need to print more than one line if the span we need to remove is multiline.
1799        // For more info: https://github.com/rust-lang/rust/issues/92741
1800        let lines_to_remove = file_lines.iter().take(file_lines.len() - 1);
1801        for (index, (line_to_remove, parts)) in lines_to_remove.zip(replaced_parts).enumerate() {
1802            buffer.puts(
1803                *row_num - 1,
1804                0,
1805                &maybe_anonymized(renderer, line_num + index, max_line_num_len, line_numbering),
1806                ElementStyle::LineNumber,
1807            );
1808            buffer.puts(
1809                *row_num - 1,
1810                max_line_num_len + 1,
1811                "- ",
1812                ElementStyle::Removal,
1813            );
1814            let line = normalize_whitespace(line_to_remove.line);
1815            buffer.puts(
1816                *row_num - 1,
1817                max_line_num_len + 3,
1818                &line,
1819                ElementStyle::NoStyle,
1820            );
1821            style_substitution_highlights(
1822                parts,
1823                ElementStyle::Removal,
1824                *row_num - 1,
1825                line_to_remove.line,
1826                max_line_num_len,
1827                buffer,
1828            );
1829            *row_num += 1;
1830        }
1831        // If the last line is exactly equal to the line we need to add, we can skip both of
1832        // them. This allows us to avoid output like the following:
1833        // 2 - &
1834        // 2 + if true { true } else { false }
1835        // 3 - if true { true } else { false }
1836        // If those lines aren't equal, we print their diff
1837        let last_line = &file_lines.last().unwrap();
1838        if last_line.line == line_to_add {
1839            *row_num -= 2;
1840            // The last original line collapses into the previous drawn row, so
1841            // fold its replaced-code highlights onto that row too.
1842            style_substitution_highlights(
1843                replaced_parts.last().unwrap(),
1844                ElementStyle::Removal,
1845                *row_num,
1846                last_line.line,
1847                max_line_num_len,
1848                buffer,
1849            );
1850        } else {
1851            buffer.puts(
1852                *row_num - 1,
1853                0,
1854                &maybe_anonymized(
1855                    renderer,
1856                    line_num + file_lines.len() - 1,
1857                    max_line_num_len,
1858                    line_numbering,
1859                ),
1860                ElementStyle::LineNumber,
1861            );
1862            buffer.puts(
1863                *row_num - 1,
1864                max_line_num_len + 1,
1865                "- ",
1866                ElementStyle::Removal,
1867            );
1868            buffer.puts(
1869                *row_num - 1,
1870                max_line_num_len + 3,
1871                &normalize_whitespace(last_line.line),
1872                ElementStyle::NoStyle,
1873            );
1874            style_substitution_highlights(
1875                replaced_parts.last().unwrap(),
1876                ElementStyle::Removal,
1877                *row_num - 1,
1878                last_line.line,
1879                max_line_num_len,
1880                buffer,
1881            );
1882
1883            if line_to_add.trim().is_empty() {
1884                *row_num -= 1;
1885            } else {
1886                // Check if after the removal, the line is left with only whitespace. If so, we
1887                // will not show an "addition" line, as removing the whole line is what the user
1888                // would really want.
1889                // For example, for the following:
1890                //   |
1891                // 2 -     .await
1892                // 2 +     (note the left over whitespace)
1893                //   |
1894                // We really want
1895                //   |
1896                // 2 -     .await
1897                //   |
1898                // *row_num -= 1;
1899                buffer.puts(
1900                    *row_num,
1901                    0,
1902                    &maybe_anonymized(renderer, line_num, max_line_num_len, line_numbering),
1903                    ElementStyle::LineNumber,
1904                );
1905                buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
1906                buffer.append(
1907                    *row_num,
1908                    &normalize_whitespace(line_to_add),
1909                    ElementStyle::NoStyle,
1910                );
1911            }
1912        }
1913    } else if is_multiline {
1914        buffer.puts(
1915            *row_num,
1916            0,
1917            &maybe_anonymized(renderer, line_num, max_line_num_len, line_numbering),
1918            ElementStyle::LineNumber,
1919        );
1920        match &highlight_parts {
1921            [SubstitutionHighlight { start: 0, end }] if *end == line_to_add.len() => {
1922                buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
1923            }
1924            [] | [SubstitutionHighlight { start: 0, end: 0 }] => {
1925                // FIXME: needed? Doesn't get exercised in any test.
1926                draw_col_separator_no_space(renderer, buffer, *row_num, max_line_num_len + 1);
1927            }
1928            _ => {
1929                let diff = renderer.decor_style.diff();
1930                buffer.puts(
1931                    *row_num,
1932                    max_line_num_len + 1,
1933                    &format!("{diff} "),
1934                    ElementStyle::Addition,
1935                );
1936            }
1937        }
1938        //   LL | line_to_add
1939        //   ++^^^
1940        //    |  |
1941        //    |  magic `3`
1942        //    `max_line_num_len`
1943        buffer.puts(
1944            *row_num,
1945            max_line_num_len + 3,
1946            &normalize_whitespace(line_to_add),
1947            ElementStyle::NoStyle,
1948        );
1949    } else if let DisplaySuggestion::Add = show_code_change {
1950        buffer.puts(
1951            *row_num,
1952            0,
1953            &maybe_anonymized(renderer, line_num, max_line_num_len, line_numbering),
1954            ElementStyle::LineNumber,
1955        );
1956        buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
1957        buffer.append(
1958            *row_num,
1959            &normalize_whitespace(line_to_add),
1960            ElementStyle::NoStyle,
1961        );
1962    } else {
1963        buffer.puts(
1964            *row_num,
1965            0,
1966            &maybe_anonymized(renderer, line_num, max_line_num_len, line_numbering),
1967            ElementStyle::LineNumber,
1968        );
1969        draw_col_separator(renderer, buffer, *row_num, max_line_num_len + 1);
1970        buffer.append(
1971            *row_num,
1972            &normalize_whitespace(line_to_add),
1973            ElementStyle::NoStyle,
1974        );
1975    }
1976
1977    style_substitution_highlights(
1978        highlight_parts,
1979        ElementStyle::Addition,
1980        *row_num,
1981        line_to_add,
1982        max_line_num_len,
1983        buffer,
1984    );
1985
1986    *row_num += 1;
1987}
1988
1989fn style_substitution_highlights(
1990    highlight_parts: &[SubstitutionHighlight],
1991    style: ElementStyle,
1992    row_num: usize,
1993    unnormalized_line: &str,
1994    max_line_num_len: usize,
1995    buffer: &mut StyledBuffer,
1996) {
1997    for &SubstitutionHighlight { start, end } in highlight_parts {
1998        // This is a no-op for empty ranges
1999        if start != end {
2000            // We calculate the extra width from tabs for both the start and end
2001            // of the span, as tabs could be present in the middle of the span
2002            let extra_width_start: usize = extra_width_from_tabs(unnormalized_line, start);
2003            let extra_width_end: usize = extra_width_from_tabs(unnormalized_line, end);
2004            buffer.set_style_range(
2005                row_num,
2006                max_line_num_len + 3 + start + extra_width_start,
2007                max_line_num_len + 3 + end + extra_width_end,
2008                style,
2009                true,
2010            );
2011        }
2012    }
2013}
2014
2015#[allow(clippy::too_many_arguments, reason = "All arguments are necessary")]
2016fn draw_line(
2017    renderer: &Renderer,
2018    buffer: &mut StyledBuffer,
2019    source_string: &str,
2020    line_index: usize,
2021    line_offset: usize,
2022    width_offset: usize,
2023    code_offset: usize,
2024    max_line_num_len: usize,
2025    margin: Margin,
2026    line_numbering: bool,
2027) -> usize {
2028    // Tabs are assumed to have been replaced by spaces in calling code.
2029    debug_assert!(!source_string.contains('\t'));
2030    let line_len = str_width(source_string);
2031    // Create the source line we will highlight.
2032    let mut left = margin.left(line_len);
2033    let right = margin.right(line_len);
2034
2035    let mut taken = 0;
2036    let mut skipped = 0;
2037    let code: String = source_string
2038        .chars()
2039        .skip_while(|ch| {
2040            let w = char_width(*ch);
2041            // If `skipped` is less than `left`, always skip the next `ch`,
2042            // even if `ch` is a multi-width char that would make `skipped`
2043            // exceed `left`. This ensures that we do not exceed term width on
2044            // source lines.
2045            if skipped < left {
2046                skipped += w;
2047                true
2048            } else {
2049                false
2050            }
2051        })
2052        .take_while(|ch| {
2053            // Make sure that the trimming on the right will fall within the terminal width.
2054            taken += char_width(*ch);
2055            taken <= (right - left)
2056        })
2057        .collect();
2058    // If we skipped more than `left`, adjust `left` to account for it.
2059    if skipped > left {
2060        left += skipped - left;
2061    }
2062    let placeholder = renderer.decor_style.margin();
2063    let padding = str_width(placeholder);
2064    let (width_taken, bytes_taken) = if margin.was_cut_left() {
2065        // We have stripped some code/whitespace from the beginning, make it clear.
2066        let mut bytes_taken = 0;
2067        let mut width_taken = 0;
2068        for ch in code.chars() {
2069            width_taken += char_width(ch);
2070            bytes_taken += ch.len_utf8();
2071
2072            if width_taken >= padding {
2073                break;
2074            }
2075        }
2076
2077        buffer.puts(
2078            line_offset,
2079            code_offset,
2080            placeholder,
2081            ElementStyle::LineNumber,
2082        );
2083        (width_taken, bytes_taken)
2084    } else {
2085        (0, 0)
2086    };
2087
2088    buffer.puts(
2089        line_offset,
2090        code_offset + width_taken,
2091        &code[bytes_taken..],
2092        ElementStyle::Quotation,
2093    );
2094
2095    if line_len > right {
2096        // We have stripped some code/whitespace from the beginning, make it clear.
2097        let mut char_taken = 0;
2098        let mut width_taken_inner = 0;
2099        for ch in code.chars().rev() {
2100            width_taken_inner += char_width(ch);
2101            char_taken += 1;
2102
2103            if width_taken_inner >= padding {
2104                break;
2105            }
2106        }
2107
2108        buffer.puts(
2109            line_offset,
2110            code_offset + width_taken + code[bytes_taken..].chars().count() - char_taken,
2111            placeholder,
2112            ElementStyle::LineNumber,
2113        );
2114    }
2115
2116    buffer.puts(
2117        line_offset,
2118        0,
2119        &maybe_anonymized(renderer, line_index, max_line_num_len, line_numbering),
2120        ElementStyle::LineNumber,
2121    );
2122
2123    draw_col_separator_no_space(renderer, buffer, line_offset, width_offset - 2);
2124
2125    left
2126}
2127
2128fn draw_range(
2129    buffer: &mut StyledBuffer,
2130    symbol: char,
2131    line: usize,
2132    col_from: usize,
2133    col_to: usize,
2134    style: ElementStyle,
2135) {
2136    for col in col_from..col_to {
2137        buffer.putc(line, col, symbol, style);
2138    }
2139}
2140
2141fn draw_multiline_line(
2142    renderer: &Renderer,
2143    buffer: &mut StyledBuffer,
2144    line: usize,
2145    offset: usize,
2146    depth: usize,
2147    style: ElementStyle,
2148    elided: bool,
2149) {
2150    let chr = match (style, renderer.decor_style) {
2151        (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Ascii) => '|',
2152        (_, DecorStyle::Ascii) => '|',
2153        (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Unicode) => {
2154            if elided {
2155                '┇'
2156            } else {
2157                '┃'
2158            }
2159        }
2160        (_, DecorStyle::Unicode) => {
2161            if elided {
2162                '┆'
2163            } else {
2164                '│'
2165            }
2166        }
2167    };
2168    buffer.putc(line, offset + depth - 1, chr, style);
2169}
2170
2171fn draw_col_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
2172    let chr = renderer.decor_style.col_separator();
2173    buffer.puts(line, col, &format!("{chr} "), ElementStyle::LineNumber);
2174}
2175
2176fn draw_col_separator_no_space(
2177    renderer: &Renderer,
2178    buffer: &mut StyledBuffer,
2179    line: usize,
2180    col: usize,
2181) {
2182    let chr = renderer.decor_style.col_separator();
2183    draw_col_separator_no_space_with_style(buffer, chr, line, col, ElementStyle::LineNumber);
2184}
2185
2186fn draw_col_separator_start(
2187    renderer: &Renderer,
2188    buffer: &mut StyledBuffer,
2189    line: usize,
2190    col: usize,
2191) {
2192    match renderer.decor_style {
2193        DecorStyle::Ascii => {
2194            draw_col_separator_no_space_with_style(
2195                buffer,
2196                '|',
2197                line,
2198                col,
2199                ElementStyle::LineNumber,
2200            );
2201        }
2202        DecorStyle::Unicode => {
2203            draw_col_separator_no_space_with_style(
2204                buffer,
2205                '╭',
2206                line,
2207                col,
2208                ElementStyle::LineNumber,
2209            );
2210            draw_col_separator_no_space_with_style(
2211                buffer,
2212                '╴',
2213                line,
2214                col + 1,
2215                ElementStyle::LineNumber,
2216            );
2217        }
2218    }
2219}
2220
2221fn draw_col_separator_end(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
2222    match renderer.decor_style {
2223        DecorStyle::Ascii => {
2224            draw_col_separator_no_space_with_style(
2225                buffer,
2226                '|',
2227                line,
2228                col,
2229                ElementStyle::LineNumber,
2230            );
2231        }
2232        DecorStyle::Unicode => {
2233            draw_col_separator_no_space_with_style(
2234                buffer,
2235                '╰',
2236                line,
2237                col,
2238                ElementStyle::LineNumber,
2239            );
2240            draw_col_separator_no_space_with_style(
2241                buffer,
2242                '╴',
2243                line,
2244                col + 1,
2245                ElementStyle::LineNumber,
2246            );
2247        }
2248    }
2249}
2250
2251fn draw_col_separator_no_space_with_style(
2252    buffer: &mut StyledBuffer,
2253    chr: char,
2254    line: usize,
2255    col: usize,
2256    style: ElementStyle,
2257) {
2258    buffer.putc(line, col, chr, style);
2259}
2260
2261fn maybe_anonymized(
2262    renderer: &Renderer,
2263    line_num: usize,
2264    max_line_num_len: usize,
2265    line_numbering: bool,
2266) -> String {
2267    format!(
2268        "{:>max_line_num_len$}",
2269        if !line_numbering {
2270            Cow::Borrowed("")
2271        } else if renderer.anonymized_snippet_line_numbers {
2272            Cow::Borrowed(ANONYMIZED_LINE_NUM)
2273        } else {
2274            Cow::Owned(line_num.to_string())
2275        }
2276    )
2277}
2278
2279fn draw_note_separator(
2280    renderer: &Renderer,
2281    buffer: &mut StyledBuffer,
2282    line: usize,
2283    col: usize,
2284    is_cont: bool,
2285) {
2286    let chr = renderer.decor_style.note_separator(is_cont);
2287    buffer.puts(line, col, chr, ElementStyle::LineNumber);
2288}
2289
2290fn draw_line_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
2291    let (column, dots) = match renderer.decor_style {
2292        DecorStyle::Ascii => (0, "..."),
2293        DecorStyle::Unicode => (col - 2, "┆"),
2294    };
2295    buffer.puts(line, column, dots, ElementStyle::LineNumber);
2296}
2297
2298pub(crate) trait MessageOrTitle {
2299    fn level(&self) -> &Level<'_>;
2300    fn id(&self) -> Option<&Id<'_>>;
2301    fn text(&self) -> &str;
2302    fn allows_styling(&self) -> bool;
2303}
2304
2305impl MessageOrTitle for Title<'_> {
2306    fn level(&self) -> &Level<'_> {
2307        &self.level
2308    }
2309    fn id(&self) -> Option<&Id<'_>> {
2310        self.id.as_ref()
2311    }
2312    fn text(&self) -> &str {
2313        self.text.as_ref()
2314    }
2315    fn allows_styling(&self) -> bool {
2316        self.allows_styling
2317    }
2318}
2319
2320impl MessageOrTitle for Message<'_> {
2321    fn level(&self) -> &Level<'_> {
2322        &self.level
2323    }
2324    fn id(&self) -> Option<&Id<'_>> {
2325        None
2326    }
2327    fn text(&self) -> &str {
2328        self.text.as_ref()
2329    }
2330    fn allows_styling(&self) -> bool {
2331        true
2332    }
2333}
2334
2335/// Count extra display columns from tabs in the first `n` chars of `s`.
2336/// Each tab is displayed as 4 spaces, so the extra width per tab is 3.
2337fn extra_width_from_tabs(s: &str, n: usize) -> usize {
2338    s.chars().take(n).filter(|&ch| ch == '\t').count() * 3
2339}
2340
2341// instead of taking the String length or dividing by 10 while > 0, we multiply a limit by 10 until
2342// we're higher. If the loop isn't exited by the `return`, the last multiplication will wrap, which
2343// is OK, because while we cannot fit a higher power of 10 in a usize, the loop will end anyway.
2344// This is also why we need the max number of decimal digits within a `usize`.
2345fn num_decimal_digits(num: Option<usize>) -> usize {
2346    #[cfg(target_pointer_width = "64")]
2347    const MAX_DIGITS: usize = 20;
2348
2349    #[cfg(target_pointer_width = "32")]
2350    const MAX_DIGITS: usize = 10;
2351
2352    #[cfg(target_pointer_width = "16")]
2353    const MAX_DIGITS: usize = 5;
2354
2355    let Some(num) = num else {
2356        return 0;
2357    };
2358
2359    let mut lim = 10;
2360    for num_digits in 1..MAX_DIGITS {
2361        if num < lim {
2362            return num_digits;
2363        }
2364        lim = lim.wrapping_mul(10);
2365    }
2366    MAX_DIGITS
2367}
2368
2369pub(crate) fn str_width(s: &str) -> usize {
2370    s.chars().map(char_width).sum()
2371}
2372
2373pub(crate) fn char_width(ch: char) -> usize {
2374    // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` is. For now,
2375    // just accept that sometimes the code line will be longer than desired.
2376    match ch {
2377        '\t' => 4,
2378        // Keep the following list in sync with `rustc_errors::emitter::OUTPUT_REPLACEMENTS`. These
2379        // are control points that we replace before printing with a visible codepoint for the sake
2380        // of being able to point at them with underlines.
2381        '\u{0000}' | '\u{0001}' | '\u{0002}' | '\u{0003}' | '\u{0004}' | '\u{0005}'
2382        | '\u{0006}' | '\u{0007}' | '\u{0008}' | '\u{000B}' | '\u{000C}' | '\u{000D}'
2383        | '\u{000E}' | '\u{000F}' | '\u{0010}' | '\u{0011}' | '\u{0012}' | '\u{0013}'
2384        | '\u{0014}' | '\u{0015}' | '\u{0016}' | '\u{0017}' | '\u{0018}' | '\u{0019}'
2385        | '\u{001A}' | '\u{001B}' | '\u{001C}' | '\u{001D}' | '\u{001E}' | '\u{001F}'
2386        | '\u{007F}' | '\u{202A}' | '\u{202B}' | '\u{202D}' | '\u{202E}' | '\u{2066}'
2387        | '\u{2067}' | '\u{2068}' | '\u{202C}' | '\u{2069}' => 1,
2388        _ => unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1),
2389    }
2390}
2391
2392pub(crate) fn num_overlap(
2393    a_start: usize,
2394    a_end: usize,
2395    b_start: usize,
2396    b_end: usize,
2397    inclusive: bool,
2398) -> bool {
2399    let extra = usize::from(inclusive);
2400    (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start)
2401}
2402
2403fn overlaps(a1: &LineAnnotation<'_>, a2: &LineAnnotation<'_>, padding: usize) -> bool {
2404    num_overlap(
2405        a1.start.display,
2406        a1.end.display + padding,
2407        a2.start.display,
2408        a2.end.display,
2409        false,
2410    )
2411}
2412
2413#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2414pub(crate) enum LineAnnotationType {
2415    /// Annotation under a single line of code
2416    Singleline,
2417
2418    // The Multiline type above is replaced with the following three in order
2419    // to reuse the current label drawing code.
2420    //
2421    // Each of these corresponds to one part of the following diagram:
2422    //
2423    //     x |   foo(1 + bar(x,
2424    //       |  _________^              < MultilineStart
2425    //     x | |             y),        < MultilineLine
2426    //       | |______________^ label   < MultilineEnd
2427    //     x |       z);
2428    /// Annotation marking the first character of a fully shown multiline span
2429    MultilineStart(usize),
2430    /// Annotation marking the last character of a fully shown multiline span
2431    MultilineEnd(usize),
2432    /// Line at the left enclosing the lines of a fully shown multiline span
2433    // Just a placeholder for the drawing algorithm, to know that it shouldn't skip the first 4
2434    // and last 2 lines of code. The actual line is drawn in `emit_message_default` and not in
2435    // `draw_multiline_line`.
2436    MultilineLine(usize),
2437}
2438
2439#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2440pub(crate) struct LineAnnotation<'a> {
2441    /// Start column.
2442    /// Note that it is important that this field goes
2443    /// first, so that when we sort, we sort orderings by start
2444    /// column.
2445    pub start: Loc,
2446
2447    /// End column within the line (exclusive)
2448    pub end: Loc,
2449
2450    /// level
2451    pub kind: AnnotationKind,
2452
2453    /// Optional label to display adjacent to the annotation.
2454    pub label: Option<Cow<'a, str>>,
2455
2456    /// Is this a single line, multiline or multiline span minimized down to a
2457    /// smaller span.
2458    pub annotation_type: LineAnnotationType,
2459
2460    /// Whether the source code should be highlighted
2461    pub highlight_source: bool,
2462}
2463
2464impl LineAnnotation<'_> {
2465    pub(crate) fn is_primary(&self) -> bool {
2466        self.kind == AnnotationKind::Primary
2467    }
2468
2469    /// Whether this annotation is a vertical line placeholder.
2470    pub(crate) fn is_line(&self) -> bool {
2471        matches!(self.annotation_type, LineAnnotationType::MultilineLine(_))
2472    }
2473
2474    /// Length of this annotation as displayed in the stderr output
2475    pub(crate) fn len(&self) -> usize {
2476        // Account for usize underflows
2477        self.end.display.abs_diff(self.start.display)
2478    }
2479
2480    pub(crate) fn has_label(&self) -> bool {
2481        if let Some(label) = &self.label {
2482            // Consider labels with no text as effectively not being there
2483            // to avoid weird output with unnecessary vertical lines, like:
2484            //
2485            //     X | fn foo(x: u32) {
2486            //       | -------^------
2487            //       | |      |
2488            //       | |
2489            //       |
2490            //
2491            // Note that this would be the complete output users would see.
2492            !label.is_empty()
2493        } else {
2494            false
2495        }
2496    }
2497
2498    pub(crate) fn takes_space(&self) -> bool {
2499        // Multiline annotations always have to keep vertical space.
2500        matches!(
2501            self.annotation_type,
2502            LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
2503        )
2504    }
2505}
2506
2507// We replace some characters so the CLI output is always consistent and underlines aligned.
2508// Keep the following list in sync with `rustc_span::char_width`.
2509const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[
2510    // In terminals without Unicode support the following will be garbled, but in *all* terminals
2511    // the underlying codepoint will be as well. We could gate this replacement behind a "unicode
2512    // support" gate.
2513    ('\0', "␀"),
2514    ('\u{0001}', "␁"),
2515    ('\u{0002}', "␂"),
2516    ('\u{0003}', "␃"),
2517    ('\u{0004}', "␄"),
2518    ('\u{0005}', "␅"),
2519    ('\u{0006}', "␆"),
2520    ('\u{0007}', "␇"),
2521    ('\u{0008}', "␈"),
2522    ('\t', "    "), // We do our own tab replacement
2523    ('\u{000b}', "␋"),
2524    ('\u{000c}', "␌"),
2525    ('\u{000d}', "␍"),
2526    ('\u{000e}', "␎"),
2527    ('\u{000f}', "␏"),
2528    ('\u{0010}', "␐"),
2529    ('\u{0011}', "␑"),
2530    ('\u{0012}', "␒"),
2531    ('\u{0013}', "␓"),
2532    ('\u{0014}', "␔"),
2533    ('\u{0015}', "␕"),
2534    ('\u{0016}', "␖"),
2535    ('\u{0017}', "␗"),
2536    ('\u{0018}', "␘"),
2537    ('\u{0019}', "␙"),
2538    ('\u{001a}', "␚"),
2539    ('\u{001b}', "␛"),
2540    ('\u{001c}', "␜"),
2541    ('\u{001d}', "␝"),
2542    ('\u{001e}', "␞"),
2543    ('\u{001f}', "␟"),
2544    ('\u{007f}', "␡"),
2545    ('\u{200d}', ""), // Replace ZWJ for consistent terminal output of grapheme clusters.
2546    ('\u{202a}', "�"), // The following unicode text flow control characters are inconsistently
2547    ('\u{202b}', "�"), // supported across CLIs and can cause confusion due to the bytes on disk
2548    ('\u{202c}', "�"), // not corresponding to the visible source code, so we replace them always.
2549    ('\u{202d}', "�"),
2550    ('\u{202e}', "�"),
2551    ('\u{2066}', "�"),
2552    ('\u{2067}', "�"),
2553    ('\u{2068}', "�"),
2554    ('\u{2069}', "�"),
2555];
2556
2557pub(crate) fn normalize_whitespace(s: &str) -> Cow<'_, str> {
2558    if !s
2559        .chars()
2560        .any(|user| OUTPUT_REPLACEMENTS.iter().any(|(bad, _)| user == *bad))
2561    {
2562        return Cow::Borrowed(s);
2563    }
2564
2565    // Scan the input string for a character in the ordered table above.
2566    // If it's present, replace it with its alternative string (it can be more than 1 char!).
2567    // Otherwise, retain the input char.
2568    let normalized = s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
2569        match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) {
2570            Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1),
2571            _ => s.push(c),
2572        }
2573        s
2574    });
2575    Cow::Owned(normalized)
2576}
2577
2578#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)]
2579pub(crate) enum ElementStyle {
2580    MainHeaderMsg,
2581    HeaderMsg,
2582    LineAndColumn,
2583    LineNumber,
2584    Quotation,
2585    UnderlinePrimary,
2586    UnderlineSecondary,
2587    LabelPrimary,
2588    LabelSecondary,
2589    NoStyle,
2590    Level(LevelInner),
2591    Addition,
2592    Removal,
2593}
2594
2595impl ElementStyle {
2596    pub(crate) fn color_spec(&self, level: &Level<'_>, stylesheet: &Stylesheet) -> Style {
2597        match self {
2598            Self::Addition => stylesheet.addition,
2599            Self::Removal => stylesheet.removal,
2600            Self::LineAndColumn => stylesheet.none,
2601            Self::LineNumber => stylesheet.line_num,
2602            Self::Quotation => stylesheet.none,
2603            Self::MainHeaderMsg => stylesheet.emphasis,
2604            Self::UnderlinePrimary | Self::LabelPrimary => level.style(stylesheet),
2605            Self::UnderlineSecondary | Self::LabelSecondary => stylesheet.context,
2606            Self::HeaderMsg | Self::NoStyle => stylesheet.none,
2607            Self::Level(lvl) => lvl.style(stylesheet),
2608        }
2609    }
2610}
2611
2612#[derive(Debug, Clone, Copy)]
2613pub(crate) struct UnderlineParts {
2614    pub(crate) style: ElementStyle,
2615    pub(crate) underline: char,
2616    pub(crate) label_start: char,
2617    pub(crate) vertical_text_line: char,
2618    pub(crate) multiline_vertical: char,
2619    pub(crate) multiline_horizontal: char,
2620    pub(crate) multiline_whole_line: char,
2621    pub(crate) multiline_start_down: char,
2622    pub(crate) bottom_right: char,
2623    pub(crate) top_left: char,
2624    pub(crate) top_right_flat: char,
2625    pub(crate) bottom_left: char,
2626    pub(crate) multiline_end_up: char,
2627    pub(crate) multiline_end_same_line: char,
2628    pub(crate) multiline_bottom_right_with_text: char,
2629}
2630
2631#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2632pub(crate) enum TitleStyle {
2633    Primary,
2634    Secondary,
2635    Message,
2636}
2637
2638pub(crate) struct Hyperlink<D: fmt::Display> {
2639    url: Option<D>,
2640}
2641
2642impl<D: fmt::Display> Hyperlink<D> {
2643    pub(crate) fn with_url(url: D) -> Self {
2644        Self { url: Some(url) }
2645    }
2646}
2647
2648impl<D: fmt::Display> Default for Hyperlink<D> {
2649    fn default() -> Self {
2650        Self { url: None }
2651    }
2652}
2653
2654impl<D: fmt::Display> fmt::Display for Hyperlink<D> {
2655    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2656        let Some(url) = self.url.as_ref() else {
2657            return Ok(());
2658        };
2659        if f.alternate() {
2660            write!(f, "\x1B]8;;\x1B\\")
2661        } else {
2662            write!(f, "\x1B]8;;{url}\x1B\\")
2663        }
2664    }
2665}
2666
2667#[cfg(test)]
2668mod test {
2669    use super::OUTPUT_REPLACEMENTS;
2670    use snapbox::IntoData;
2671
2672    fn format_replacements(replacements: Vec<(char, &str)>) -> String {
2673        replacements
2674            .into_iter()
2675            .map(|r| format!("    {r:?}"))
2676            .collect::<Vec<_>>()
2677            .join("\n")
2678    }
2679
2680    #[test]
2681    /// The [`OUTPUT_REPLACEMENTS`] array must be sorted (for binary search to
2682    /// work) and must contain no duplicate entries
2683    fn ensure_output_replacements_is_sorted() {
2684        let mut expected = OUTPUT_REPLACEMENTS.to_owned();
2685        expected.sort_by_key(|r| r.0);
2686        expected.dedup_by_key(|r| r.0);
2687        let expected = format_replacements(expected);
2688        let actual = format_replacements(OUTPUT_REPLACEMENTS.to_owned());
2689        snapbox::assert_data_eq!(actual, expected.into_data().raw());
2690    }
2691}