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