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