annotate_snippets/renderer/
mod.rs

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