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