Skip to main content

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