• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rust-lang / annotate-snippets-rs / 15987985633

01 Jul 2025 02:05AM UTC coverage: 87.105% (-0.2%) from 87.259%
15987985633

push

github

web-flow
Merge pull request #235 from Muscraft/group-primary-level

feat: Allow setting the primary level for a group

5 of 9 new or added lines in 2 files covered. (55.56%)

1405 of 1613 relevant lines covered (87.1%)

5.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.62
/src/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 for [`Group`]s
4
//!
5
//! # Example
6
//! ```
7
//! use annotate_snippets::*;
8
//! use annotate_snippets::Level;
9
//!
10
//! let source = r#"
11
//! use baz::zed::bar;
12
//!
13
//! mod baz {}
14
//! mod zed {
15
//!     pub fn bar() { println!("bar3"); }
16
//! }
17
//! fn main() {
18
//!     bar();
19
//! }
20
//! "#;
21
//!
22
//!
23
//!  Group::new()
24
//!     .element(
25
//!         Level::ERROR
26
//!             .title("unresolved import `baz::zed`")
27
//!             .id("E0432")
28
//!     )
29
//!     .element(
30
//!         Snippet::source(source)
31
//!             .path("temp.rs")
32
//!             .line_start(1)
33
//!             .fold(true)
34
//!             .annotation(
35
//!                 AnnotationKind::Primary
36
//!                     .span(10..13)
37
//!                     .label("could not find `zed` in `baz`"),
38
//!             )
39
//!     );
40
//! ```
41

42
mod margin;
43
pub(crate) mod source_map;
44
mod styled_buffer;
45
pub(crate) mod stylesheet;
46

47
use crate::level::{Level, LevelInner};
48
use crate::renderer::source_map::{
49
    AnnotatedLineInfo, LineInfo, Loc, SourceMap, SubstitutionHighlight,
50
};
51
use crate::renderer::styled_buffer::StyledBuffer;
52
use crate::snippet::Id;
53
use crate::{Annotation, AnnotationKind, Element, Group, Origin, Patch, Snippet, Title};
54
pub use anstyle::*;
55
use margin::Margin;
56
use std::borrow::Cow;
57
use std::cmp::{max, min, Ordering, Reverse};
58
use std::collections::{HashMap, VecDeque};
59
use std::fmt;
60
use std::ops::Range;
61
use stylesheet::Stylesheet;
62

63
const ANONYMIZED_LINE_NUM: &str = "LL";
64
pub const DEFAULT_TERM_WIDTH: usize = 140;
65

66
/// A renderer for [`Group`]s
67
#[derive(Clone, Debug)]
68
pub struct Renderer {
69
    anonymized_line_numbers: bool,
70
    term_width: usize,
71
    theme: OutputTheme,
72
    stylesheet: Stylesheet,
73
    short_message: bool,
74
}
75

76
impl Renderer {
77
    /// No terminal styling
78
    pub const fn plain() -> Self {
9✔
79
        Self {
80
            anonymized_line_numbers: false,
81
            term_width: DEFAULT_TERM_WIDTH,
82
            theme: OutputTheme::Ascii,
83
            stylesheet: Stylesheet::plain(),
6✔
84
            short_message: false,
85
        }
86
    }
87

88
    /// Default terminal styling
89
    ///
90
    /// # Note
91
    /// When testing styled terminal output, see the [`testing-colors` feature](crate#features)
92
    pub const fn styled() -> Self {
4✔
93
        const USE_WINDOWS_COLORS: bool = cfg!(windows) && !cfg!(feature = "testing-colors");
94
        const BRIGHT_BLUE: Style = if USE_WINDOWS_COLORS {
95
            AnsiColor::BrightCyan.on_default()
96
        } else {
97
            AnsiColor::BrightBlue.on_default()
98
        };
99
        Self {
100
            stylesheet: Stylesheet {
4✔
101
                error: AnsiColor::BrightRed.on_default().effects(Effects::BOLD),
102
                warning: if USE_WINDOWS_COLORS {
103
                    AnsiColor::BrightYellow.on_default()
104
                } else {
105
                    AnsiColor::Yellow.on_default()
106
                }
107
                .effects(Effects::BOLD),
108
                info: BRIGHT_BLUE.effects(Effects::BOLD),
109
                note: AnsiColor::BrightGreen.on_default().effects(Effects::BOLD),
110
                help: AnsiColor::BrightCyan.on_default().effects(Effects::BOLD),
111
                line_num: BRIGHT_BLUE.effects(Effects::BOLD),
112
                emphasis: if USE_WINDOWS_COLORS {
113
                    AnsiColor::BrightWhite.on_default()
114
                } else {
115
                    Style::new()
116
                }
117
                .effects(Effects::BOLD),
118
                none: Style::new(),
119
                context: BRIGHT_BLUE.effects(Effects::BOLD),
120
                addition: AnsiColor::BrightGreen.on_default(),
121
                removal: AnsiColor::BrightRed.on_default(),
122
            },
123
            ..Self::plain()
124
        }
125
    }
126

127
    /// Anonymize line numbers
128
    ///
129
    /// This enables (or disables) line number anonymization. When enabled, line numbers are replaced
130
    /// with `LL`.
131
    ///
132
    /// # Example
133
    ///
134
    /// ```text
135
    ///   --> $DIR/whitespace-trimming.rs:4:193
136
    ///    |
137
    /// LL | ...                   let _: () = 42;
138
    ///    |                                   ^^ expected (), found integer
139
    ///    |
140
    /// ```
141
    pub const fn anonymized_line_numbers(mut self, anonymized_line_numbers: bool) -> Self {
3✔
142
        self.anonymized_line_numbers = anonymized_line_numbers;
5✔
143
        self
5✔
144
    }
145

146
    pub const fn short_message(mut self, short_message: bool) -> Self {
1✔
147
        self.short_message = short_message;
2✔
148
        self
2✔
149
    }
150

151
    // Set the terminal width
152
    pub const fn term_width(mut self, term_width: usize) -> Self {
2✔
153
        self.term_width = term_width;
2✔
154
        self
2✔
155
    }
156

157
    pub const fn theme(mut self, output_theme: OutputTheme) -> Self {
2✔
158
        self.theme = output_theme;
2✔
159
        self
2✔
160
    }
161

162
    /// Set the output style for `error`
163
    pub const fn error(mut self, style: Style) -> Self {
×
164
        self.stylesheet.error = style;
×
165
        self
×
166
    }
167

168
    /// Set the output style for `warning`
169
    pub const fn warning(mut self, style: Style) -> Self {
×
170
        self.stylesheet.warning = style;
×
171
        self
×
172
    }
173

174
    /// Set the output style for `info`
175
    pub const fn info(mut self, style: Style) -> Self {
×
176
        self.stylesheet.info = style;
×
177
        self
×
178
    }
179

180
    /// Set the output style for `note`
181
    pub const fn note(mut self, style: Style) -> Self {
×
182
        self.stylesheet.note = style;
×
183
        self
×
184
    }
185

186
    /// Set the output style for `help`
187
    pub const fn help(mut self, style: Style) -> Self {
×
188
        self.stylesheet.help = style;
×
189
        self
×
190
    }
191

192
    /// Set the output style for line numbers
193
    pub const fn line_num(mut self, style: Style) -> Self {
×
194
        self.stylesheet.line_num = style;
×
195
        self
×
196
    }
197

198
    /// Set the output style for emphasis
199
    pub const fn emphasis(mut self, style: Style) -> Self {
×
200
        self.stylesheet.emphasis = style;
×
201
        self
×
202
    }
203

204
    /// Set the output style for none
205
    pub const fn none(mut self, style: Style) -> Self {
×
206
        self.stylesheet.none = style;
×
207
        self
×
208
    }
209

210
    /// Set the output style for [`AnnotationKind::Context`]
211
    pub const fn context(mut self, style: Style) -> Self {
×
212
        self.stylesheet.context = style;
×
213
        self
×
214
    }
215

216
    /// Set the output style for additions
217
    pub const fn addition(mut self, style: Style) -> Self {
×
218
        self.stylesheet.addition = style;
×
219
        self
×
220
    }
221

222
    /// Set the output style for removals
223
    pub const fn removal(mut self, style: Style) -> Self {
×
224
        self.stylesheet.removal = style;
×
225
        self
×
226
    }
227
}
228

229
impl Renderer {
230
    pub fn render(&self, groups: &[Group<'_>]) -> String {
7✔
231
        if self.short_message {
6✔
232
            self.render_short_message(groups).unwrap()
2✔
233
        } else {
234
            let max_line_num_len = if self.anonymized_line_numbers {
7✔
235
                ANONYMIZED_LINE_NUM.len()
4✔
236
            } else {
237
                num_decimal_digits(max_line_number(groups))
7✔
238
            };
239
            let mut out_string = String::new();
5✔
240
            let group_len = groups.len();
7✔
241
            let mut og_primary_path = None;
7✔
242
            for (g, group) in groups.iter().enumerate() {
14✔
243
                let mut buffer = StyledBuffer::new();
7✔
244
                let primary_path = group
20✔
245
                    .elements
246
                    .iter()
247
                    .find_map(|s| match &s {
14✔
248
                        Element::Cause(cause) => {
6✔
249
                            if cause.markers.iter().any(|m| m.kind.is_primary()) {
27✔
250
                                Some(cause.path.as_ref())
6✔
251
                            } else {
252
                                None
3✔
253
                            }
254
                        }
255
                        Element::Origin(origin) => {
2✔
256
                            if origin.primary {
4✔
257
                                Some(Some(&origin.path))
1✔
258
                            } else {
259
                                None
1✔
260
                            }
261
                        }
262
                        _ => None,
7✔
263
                    })
264
                    .unwrap_or(
265
                        group
7✔
266
                            .elements
267
                            .iter()
268
                            .find_map(|s| match &s {
14✔
269
                                Element::Cause(cause) => Some(cause.path.as_ref()),
7✔
270
                                Element::Origin(origin) => Some(Some(&origin.path)),
2✔
271
                                _ => None,
7✔
272
                            })
273
                            .unwrap_or_default(),
274
                    );
275
                if og_primary_path.is_none() && primary_path.is_some() {
22✔
276
                    og_primary_path = primary_path;
7✔
277
                }
278
                let level = group.primary_level.clone().unwrap_or_else(|| {
22✔
279
                    group
8✔
280
                        .elements
281
                        .first()
282
                        .and_then(|s| match &s {
15✔
283
                            Element::Title(title) => Some(title.level.clone()),
7✔
NEW
284
                            _ => None,
×
285
                        })
286
                        .unwrap_or(Level::ERROR)
287
                });
288
                let mut source_map_annotated_lines = VecDeque::new();
8✔
289
                let mut max_depth = 0;
7✔
290
                for e in &group.elements {
15✔
291
                    if let Element::Cause(cause) = e {
22✔
292
                        let source_map = SourceMap::new(&cause.source, cause.line_start);
8✔
293
                        let (depth, annotated_lines) =
5✔
294
                            source_map.annotated_lines(cause.markers.clone(), cause.fold);
295
                        max_depth = max(max_depth, depth);
11✔
296
                        source_map_annotated_lines.push_back((source_map, annotated_lines));
6✔
297
                    }
298
                }
299
                let mut message_iter = group.elements.iter().enumerate().peekable();
6✔
300
                let mut last_was_suggestion = false;
6✔
301
                while let Some((i, section)) = message_iter.next() {
5✔
302
                    let peek = message_iter.peek().map(|(_, s)| s).copied();
22✔
303
                    match &section {
5✔
304
                        Element::Title(title) => {
5✔
305
                            let title_style = match (i == 0, g == 0) {
11✔
306
                                (true, true) => TitleStyle::MainHeader,
6✔
307
                                (true, false) => TitleStyle::Header,
3✔
308
                                (false, _) => TitleStyle::Secondary,
3✔
309
                            };
310
                            let buffer_msg_line_offset = buffer.num_lines();
11✔
311
                            self.render_title(
6✔
312
                                &mut buffer,
313
                                title,
314
                                max_line_num_len,
5✔
315
                                title_style,
6✔
316
                                matches!(peek, Some(Element::Title(_))),
6✔
317
                                buffer_msg_line_offset,
318
                            );
319
                            last_was_suggestion = false;
9✔
320
                        }
321
                        Element::Cause(cause) => {
9✔
322
                            if let Some((source_map, annotated_lines)) =
9✔
323
                                source_map_annotated_lines.pop_front()
324
                            {
325
                                self.render_snippet_annotations(
9✔
326
                                    &mut buffer,
327
                                    max_line_num_len,
9✔
328
                                    cause,
329
                                    primary_path,
330
                                    &source_map,
331
                                    &annotated_lines,
7✔
332
                                    max_depth,
9✔
333
                                    peek.is_some() || (g == 0 && group_len > 1),
7✔
334
                                );
335

336
                                if g == 0 {
4✔
337
                                    let current_line = buffer.num_lines();
8✔
338
                                    match peek {
4✔
339
                                        Some(Element::Title(level))
3✔
340
                                            if level.level.name != Some(None) =>
3✔
341
                                        {
342
                                            self.draw_col_separator_no_space(
6✔
343
                                                &mut buffer,
344
                                                current_line,
345
                                                max_line_num_len + 1,
3✔
346
                                            );
347
                                        }
348

349
                                        None if group_len > 1 => self.draw_col_separator_end(
8✔
350
                                            &mut buffer,
351
                                            current_line,
352
                                            max_line_num_len + 1,
2✔
353
                                        ),
354
                                        _ => {}
355
                                    }
356
                                }
357
                            }
358

359
                            last_was_suggestion = false;
4✔
360
                        }
361
                        Element::Suggestion(suggestion) => {
3✔
362
                            let source_map =
6✔
363
                                SourceMap::new(&suggestion.source, suggestion.line_start);
364
                            self.emit_suggestion_default(
3✔
365
                                &mut buffer,
366
                                suggestion,
367
                                max_line_num_len,
3✔
368
                                &source_map,
369
                                primary_path.or(og_primary_path),
3✔
370
                                last_was_suggestion,
3✔
371
                            );
372
                            last_was_suggestion = true;
4✔
373
                        }
374

375
                        Element::Origin(origin) => {
2✔
376
                            let buffer_msg_line_offset = buffer.num_lines();
4✔
377
                            self.render_origin(
2✔
378
                                &mut buffer,
379
                                max_line_num_len,
2✔
380
                                origin,
381
                                buffer_msg_line_offset,
382
                            );
383
                            last_was_suggestion = false;
2✔
384
                        }
385
                        Element::Padding(_) => {
386
                            let current_line = buffer.num_lines();
2✔
387
                            self.draw_col_separator_no_space(
2✔
388
                                &mut buffer,
389
                                current_line,
390
                                max_line_num_len + 1,
1✔
391
                            );
392
                        }
393
                    }
394
                    if g == 0
10✔
395
                        && (matches!(section, Element::Origin(_))
9✔
396
                            || (matches!(section, Element::Title(_)) && i == 0)
7✔
397
                            || matches!(section, Element::Title(level) if level.level.name == Some(None)))
4✔
398
                    {
399
                        let current_line = buffer.num_lines();
16✔
400
                        if peek.is_none() && group_len > 1 {
8✔
401
                            self.draw_col_separator_end(
2✔
402
                                &mut buffer,
403
                                current_line,
404
                                max_line_num_len + 1,
1✔
405
                            );
406
                        } else if matches!(peek, Some(Element::Title(level)) if level.level.name != Some(None))
16✔
407
                        {
408
                            self.draw_col_separator_no_space(
2✔
409
                                &mut buffer,
410
                                current_line,
411
                                max_line_num_len + 1,
1✔
412
                            );
413
                        }
414
                    }
415
                }
416
                buffer
6✔
417
                    .render(&level, &self.stylesheet, &mut out_string)
3✔
418
                    .unwrap();
419
                if g != group_len - 1 {
6✔
420
                    use std::fmt::Write;
421

422
                    writeln!(out_string).unwrap();
3✔
423
                }
424
            }
425
            out_string
3✔
426
        }
427
    }
428

429
    fn render_short_message(&self, groups: &[Group<'_>]) -> Result<String, fmt::Error> {
2✔
430
        let mut buffer = StyledBuffer::new();
2✔
431
        let mut labels = None;
2✔
432
        let group = groups.first().expect("Expected at least one group");
4✔
433

434
        let Some(Element::Title(title)) = group.elements.first() else {
4✔
435
            panic!(
×
436
                "Expected first element to be a Title, got: {:?}",
437
                group.elements.first()
438
            );
439
        };
440

441
        if let Some(Element::Cause(cause)) = group
6✔
442
            .elements
443
            .iter()
444
            .find(|e| matches!(e, Element::Cause(_)))
4✔
445
        {
446
            let labels_inner = cause
3✔
447
                .markers
448
                .iter()
449
                .filter_map(|ann| match &ann.label {
4✔
450
                    Some(msg) if ann.kind.is_primary() => {
2✔
451
                        if !msg.trim().is_empty() {
4✔
452
                            Some(msg.to_string())
2✔
453
                        } else {
454
                            None
×
455
                        }
456
                    }
457
                    _ => None,
1✔
458
                })
459
                .collect::<Vec<_>>()
460
                .join(", ");
461
            if !labels_inner.is_empty() {
4✔
462
                labels = Some(labels_inner);
2✔
463
            }
464

465
            if let Some(path) = &cause.path {
4✔
466
                let mut origin = Origin::new(path.as_ref());
4✔
467
                origin.primary = true;
2✔
468

469
                let source_map = SourceMap::new(&cause.source, cause.line_start);
4✔
470
                let (_depth, annotated_lines) =
2✔
471
                    source_map.annotated_lines(cause.markers.clone(), cause.fold);
472

473
                if let Some(primary_line) = annotated_lines
6✔
474
                    .iter()
475
                    .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
4✔
476
                    .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
6✔
477
                {
478
                    origin.line = Some(primary_line.line_index);
2✔
479
                    if let Some(first_annotation) = primary_line
6✔
480
                        .annotations
481
                        .iter()
482
                        .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
4✔
483
                    {
484
                        origin.char_column = Some(first_annotation.start.char + 1);
2✔
485
                    }
486
                }
487

488
                self.render_origin(&mut buffer, 0, &origin, 0);
2✔
489
                buffer.append(0, ": ", ElementStyle::LineAndColumn);
2✔
490
            }
491
        }
492

493
        self.render_title(
2✔
494
            &mut buffer,
495
            title,
496
            0, // No line numbers in short messages
497
            TitleStyle::MainHeader,
2✔
498
            false,
499
            0,
500
        );
501

502
        if let Some(labels) = labels {
2✔
503
            buffer.append(0, &format!(": {labels}"), ElementStyle::NoStyle);
4✔
504
        }
505

506
        let mut out_string = String::new();
2✔
507
        buffer.render(&title.level, &self.stylesheet, &mut out_string)?;
4✔
508

509
        Ok(out_string)
2✔
510
    }
511

512
    #[allow(clippy::too_many_arguments)]
513
    fn render_title(
6✔
514
        &self,
515
        buffer: &mut StyledBuffer,
516
        title: &Title<'_>,
517
        max_line_num_len: usize,
518
        title_style: TitleStyle,
519
        is_cont: bool,
520
        buffer_msg_line_offset: usize,
521
    ) {
522
        let (label_style, title_element_style) = match title_style {
11✔
523
            TitleStyle::MainHeader => (
5✔
524
                ElementStyle::Level(title.level.level),
5✔
525
                if self.short_message {
10✔
526
                    ElementStyle::NoStyle
2✔
527
                } else {
528
                    ElementStyle::MainHeaderMsg
5✔
529
                },
530
            ),
531
            TitleStyle::Header => (
3✔
532
                ElementStyle::Level(title.level.level),
3✔
533
                ElementStyle::HeaderMsg,
3✔
534
            ),
535
            TitleStyle::Secondary => {
536
                for _ in 0..max_line_num_len {
6✔
537
                    buffer.prepend(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
3✔
538
                }
539

540
                self.draw_note_separator(
3✔
541
                    buffer,
542
                    buffer_msg_line_offset,
543
                    max_line_num_len + 1,
3✔
544
                    is_cont,
545
                );
546
                (ElementStyle::MainHeaderMsg, ElementStyle::NoStyle)
3✔
547
            }
548
        };
549
        let mut label_width = 0;
6✔
550

551
        if title.level.name != Some(None) {
10✔
552
            buffer.append(buffer_msg_line_offset, title.level.as_str(), label_style);
5✔
553
            label_width += title.level.as_str().len();
4✔
554
            if let Some(Id { id: Some(id), url }) = &title.id {
15✔
555
                buffer.append(buffer_msg_line_offset, "[", label_style);
3✔
556
                if let Some(url) = url.as_ref() {
3✔
557
                    buffer.append(
×
558
                        buffer_msg_line_offset,
559
                        &format!("\x1B]8;;{url}\x1B\\"),
×
560
                        label_style,
561
                    );
562
                }
563
                buffer.append(buffer_msg_line_offset, id, label_style);
3✔
564
                if url.is_some() {
3✔
565
                    buffer.append(buffer_msg_line_offset, "\x1B]8;;\x1B\\", label_style);
×
566
                }
567
                buffer.append(buffer_msg_line_offset, "]", label_style);
3✔
568
                label_width += 2 + id.len();
7✔
569
            }
570
            buffer.append(buffer_msg_line_offset, ": ", title_element_style);
4✔
571
            label_width += 2;
8✔
572
        }
573

574
        let padding = " ".repeat(if title_style == TitleStyle::Secondary {
15✔
575
            // The extra 3 ` ` is padding that's always needed to align to the
576
            // label i.e. `note: `:
577
            //
578
            //   error: message
579
            //     --> file.rs:13:20
580
            //      |
581
            //   13 |     <CODE>
582
            //      |      ^^^^
583
            //      |
584
            //      = note: multiline
585
            //              message
586
            //   ++^^^------
587
            //    |  |     |
588
            //    |  |     |
589
            //    |  |     width of label
590
            //    |  magic `3`
591
            //    `max_line_num_len`
592
            max_line_num_len + 3 + label_width
6✔
593
        } else {
594
            label_width
8✔
595
        });
596

597
        let (title_str, style) = if title.is_pre_styled {
16✔
598
            (title.title.to_string(), ElementStyle::NoStyle)
×
599
        } else {
600
            (normalize_whitespace(&title.title), title_element_style)
12✔
601
        };
602
        for (i, text) in title_str.lines().enumerate() {
19✔
603
            if i != 0 {
9✔
604
                buffer.append(buffer_msg_line_offset + i, &padding, ElementStyle::NoStyle);
2✔
605
                if title_style == TitleStyle::Secondary
2✔
606
                    && is_cont
2✔
607
                    && matches!(self.theme, OutputTheme::Unicode)
1✔
608
                {
609
                    // There's another note after this one, associated to the subwindow above.
610
                    // We write additional vertical lines to join them:
611
                    //   ╭▸ test.rs:3:3
612
                    //   │
613
                    // 3 │   code
614
                    //   │   ━━━━
615
                    //   │
616
                    //   ├ note: foo
617
                    //   │       bar
618
                    //   ╰ note: foo
619
                    //           bar
620
                    self.draw_col_separator_no_space(
2✔
621
                        buffer,
622
                        buffer_msg_line_offset + i,
1✔
623
                        max_line_num_len + 1,
1✔
624
                    );
625
                }
626
            }
627
            buffer.append(buffer_msg_line_offset + i, text, style);
23✔
628
        }
629
    }
630

631
    fn render_origin(
4✔
632
        &self,
633
        buffer: &mut StyledBuffer,
634
        max_line_num_len: usize,
635
        origin: &Origin<'_>,
636
        buffer_msg_line_offset: usize,
637
    ) {
638
        if origin.primary && !self.short_message {
12✔
639
            buffer.prepend(
7✔
640
                buffer_msg_line_offset,
641
                self.file_start(),
5✔
642
                ElementStyle::LineNumber,
6✔
643
            );
644
        } else if !self.short_message {
3✔
645
            // if !origin.standalone {
646
            //     // Add spacing line, as shown:
647
            //     //   --> $DIR/file:54:15
648
            //     //    |
649
            //     // LL |         code
650
            //     //    |         ^^^^
651
            //     //    | (<- It prints *this* line)
652
            //     //   ::: $DIR/other_file.rs:15:5
653
            //     //    |
654
            //     // LL |     code
655
            //     //    |     ----
656
            //     self.draw_col_separator_no_space(
657
            //         buffer,
658
            //         buffer_msg_line_offset,
659
            //         max_line_num_len + 1,
660
            //     );
661
            //
662
            //     buffer_msg_line_offset += 1;
663
            // }
664
            // Then, the secondary file indicator
665
            buffer.prepend(
3✔
666
                buffer_msg_line_offset,
667
                self.secondary_file_start(),
3✔
668
                ElementStyle::LineNumber,
3✔
669
            );
670
        }
671

672
        let str = match (&origin.line, &origin.char_column) {
12✔
673
            (Some(line), Some(col)) => {
5✔
674
                format!("{}:{}:{}", origin.path, line, col)
7✔
675
            }
676
            (Some(line), None) => format!("{}:{}", origin.path, line),
1✔
677
            _ => origin.path.to_string(),
1✔
678
        };
679

680
        buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn);
12✔
681
        if !self.short_message {
6✔
682
            for _ in 0..max_line_num_len {
14✔
683
                buffer.prepend(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
7✔
684
            }
685
        }
686
    }
687

688
    #[allow(clippy::too_many_arguments)]
689
    fn render_snippet_annotations(
7✔
690
        &self,
691
        buffer: &mut StyledBuffer,
692
        max_line_num_len: usize,
693
        snippet: &Snippet<'_, Annotation<'_>>,
694
        primary_path: Option<&Cow<'_, str>>,
695
        sm: &SourceMap<'_>,
696
        annotated_lines: &[AnnotatedLineInfo<'_>],
697
        multiline_depth: usize,
698
        is_cont: bool,
699
    ) {
700
        if let Some(path) = &snippet.path {
9✔
701
            let mut origin = Origin::new(path.as_ref());
6✔
702
            // print out the span location and spacer before we print the annotated source
703
            // to do this, we need to know if this span will be primary
704
            let is_primary = primary_path == Some(&origin.path);
16✔
705

706
            if is_primary {
10✔
707
                origin.primary = true;
6✔
708
                if let Some(primary_line) = annotated_lines
20✔
709
                    .iter()
710
                    .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
12✔
711
                    .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
19✔
712
                {
713
                    origin.line = Some(primary_line.line_index);
6✔
714
                    if let Some(first_annotation) = primary_line
8✔
715
                        .annotations
716
                        .iter()
717
                        .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
11✔
718
                    {
719
                        origin.char_column = Some(first_annotation.start.char + 1);
8✔
720
                    }
721
                }
722
            } else {
723
                let buffer_msg_line_offset = buffer.num_lines();
6✔
724
                // Add spacing line, as shown:
725
                //   --> $DIR/file:54:15
726
                //    |
727
                // LL |         code
728
                //    |         ^^^^
729
                //    | (<- It prints *this* line)
730
                //   ::: $DIR/other_file.rs:15:5
731
                //    |
732
                // LL |     code
733
                //    |     ----
734
                self.draw_col_separator_no_space(
3✔
735
                    buffer,
736
                    buffer_msg_line_offset,
737
                    max_line_num_len + 1,
3✔
738
                );
739
                if let Some(first_line) = annotated_lines.first() {
3✔
740
                    origin.line = Some(first_line.line_index);
3✔
741
                    if let Some(first_annotation) = first_line.annotations.first() {
8✔
742
                        origin.char_column = Some(first_annotation.start.char + 1);
2✔
743
                    }
744
                }
745
            }
746
            let buffer_msg_line_offset = buffer.num_lines();
13✔
747
            self.render_origin(buffer, max_line_num_len, &origin, buffer_msg_line_offset);
7✔
748
        }
749

750
        // Put in the spacer between the location and annotated source
751
        let buffer_msg_line_offset = buffer.num_lines();
7✔
752
        self.draw_col_separator_no_space(buffer, buffer_msg_line_offset, max_line_num_len + 1);
12✔
753

754
        // Contains the vertical lines' positions for active multiline annotations
755
        let mut multilines = Vec::new();
8✔
756

757
        // Get the left-side margin to remove it
758
        let mut whitespace_margin = usize::MAX;
6✔
759
        for line_info in annotated_lines {
15✔
760
            // Whitespace can only be removed (aka considered leading)
761
            // if the lexer considers it whitespace.
762
            // non-rustc_lexer::is_whitespace() chars are reported as an
763
            // error (ex. no-break-spaces \u{a0}), and thus can't be considered
764
            // for removal during error reporting.
765
            let leading_whitespace = line_info
12✔
766
                .line
767
                .chars()
768
                .take_while(|c| c.is_whitespace())
12✔
769
                .map(|c| {
5✔
770
                    match c {
6✔
771
                        // Tabs are displayed as 4 spaces
772
                        '\t' => 4,
1✔
773
                        _ => 1,
6✔
774
                    }
775
                })
776
                .sum();
777
            if line_info.line.chars().any(|c| !c.is_whitespace()) {
22✔
778
                whitespace_margin = min(whitespace_margin, leading_whitespace);
5✔
779
            }
780
        }
781
        if whitespace_margin == usize::MAX {
7✔
782
            whitespace_margin = 0;
2✔
783
        }
784

785
        // Left-most column any visible span points at.
786
        let mut span_left_margin = usize::MAX;
9✔
787
        for line_info in annotated_lines {
12✔
788
            for ann in &line_info.annotations {
16✔
789
                span_left_margin = min(span_left_margin, ann.start.display);
5✔
790
                span_left_margin = min(span_left_margin, ann.end.display);
9✔
791
            }
792
        }
793
        if span_left_margin == usize::MAX {
8✔
794
            span_left_margin = 0;
1✔
795
        }
796

797
        // Right-most column any visible span points at.
798
        let mut span_right_margin = 0;
5✔
799
        let mut label_right_margin = 0;
6✔
800
        let mut max_line_len = 0;
5✔
801
        for line_info in annotated_lines {
12✔
802
            max_line_len = max(max_line_len, line_info.line.len());
12✔
803
            for ann in &line_info.annotations {
14✔
804
                span_right_margin = max(span_right_margin, ann.start.display);
6✔
805
                span_right_margin = max(span_right_margin, ann.end.display);
6✔
806
                // FIXME: account for labels not in the same line
807
                let label_right = ann.label.as_ref().map_or(0, |l| l.len() + 1);
17✔
808
                label_right_margin = max(label_right_margin, ann.end.display + label_right);
4✔
809
            }
810
        }
811
        let width_offset = 3 + max_line_num_len;
5✔
812
        let code_offset = if multiline_depth == 0 {
17✔
813
            width_offset
3✔
814
        } else {
815
            width_offset + multiline_depth + 1
12✔
816
        };
817

818
        let column_width = self.term_width.saturating_sub(code_offset);
6✔
819

820
        let margin = Margin::new(
821
            whitespace_margin,
4✔
822
            span_left_margin,
4✔
823
            span_right_margin,
4✔
824
            label_right_margin,
6✔
825
            column_width,
826
            max_line_len,
5✔
827
        );
828

829
        // Next, output the annotate source for this file
830
        for annotated_line_idx in 0..annotated_lines.len() {
10✔
831
            let previous_buffer_line = buffer.num_lines();
12✔
832

833
            let depths = self.render_source_line(
5✔
834
                &annotated_lines[annotated_line_idx],
7✔
835
                buffer,
836
                width_offset,
837
                code_offset,
7✔
838
                max_line_num_len,
839
                margin,
840
                !is_cont && annotated_line_idx + 1 == annotated_lines.len(),
11✔
841
            );
842

843
            let mut to_add = HashMap::new();
4✔
844

845
            for (depth, style) in depths {
19✔
846
                if let Some(index) = multilines.iter().position(|(d, _)| d == &depth) {
18✔
847
                    multilines.swap_remove(index);
10✔
848
                } else {
849
                    to_add.insert(depth, style);
8✔
850
                }
851
            }
852

853
            // Set the multiline annotation vertical lines to the left of
854
            // the code in this line.
855
            for (depth, style) in &multilines {
5✔
856
                for line in previous_buffer_line..buffer.num_lines() {
6✔
857
                    self.draw_multiline_line(buffer, line, width_offset, *depth, *style);
3✔
858
                }
859
            }
860
            // check to see if we need to print out or elide lines that come between
861
            // this annotated line and the next one.
862
            if annotated_line_idx < (annotated_lines.len() - 1) {
4✔
863
                let line_idx_delta = annotated_lines[annotated_line_idx + 1].line_index
9✔
864
                    - annotated_lines[annotated_line_idx].line_index;
6✔
865
                match line_idx_delta.cmp(&2) {
8✔
866
                    Ordering::Greater => {
867
                        let last_buffer_line_num = buffer.num_lines();
6✔
868

869
                        self.draw_line_separator(buffer, last_buffer_line_num, width_offset);
3✔
870

871
                        // Set the multiline annotation vertical lines on `...` bridging line.
872
                        for (depth, style) in &multilines {
3✔
873
                            self.draw_multiline_line(
6✔
874
                                buffer,
875
                                last_buffer_line_num,
876
                                width_offset,
877
                                *depth,
3✔
878
                                *style,
879
                            );
880
                        }
881
                        if let Some(line) = annotated_lines.get(annotated_line_idx) {
3✔
882
                            for ann in &line.annotations {
3✔
883
                                if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type
3✔
884
                                {
885
                                    // In the case where we have elided the entire start of the
886
                                    // multispan because those lines were empty, we still need
887
                                    // to draw the `|`s across the `...`.
888
                                    self.draw_multiline_line(
1✔
889
                                        buffer,
890
                                        last_buffer_line_num,
891
                                        width_offset,
892
                                        pos,
893
                                        if ann.is_primary() {
1✔
894
                                            ElementStyle::UnderlinePrimary
1✔
895
                                        } else {
896
                                            ElementStyle::UnderlineSecondary
×
897
                                        },
898
                                    );
899
                                }
900
                            }
901
                        }
902
                    }
903

904
                    Ordering::Equal => {
905
                        let unannotated_line = sm
4✔
906
                            .get_line(annotated_lines[annotated_line_idx].line_index + 1)
4✔
907
                            .unwrap_or("");
908

909
                        let last_buffer_line_num = buffer.num_lines();
2✔
910

911
                        self.draw_line(
2✔
912
                            buffer,
913
                            &normalize_whitespace(unannotated_line),
2✔
914
                            annotated_lines[annotated_line_idx + 1].line_index - 1,
2✔
915
                            last_buffer_line_num,
916
                            width_offset,
917
                            code_offset,
2✔
918
                            max_line_num_len,
919
                            margin,
920
                        );
921

922
                        for (depth, style) in &multilines {
2✔
923
                            self.draw_multiline_line(
2✔
924
                                buffer,
925
                                last_buffer_line_num,
926
                                width_offset,
927
                                *depth,
1✔
928
                                *style,
929
                            );
930
                        }
931
                        if let Some(line) = annotated_lines.get(annotated_line_idx) {
2✔
932
                            for ann in &line.annotations {
2✔
933
                                if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type
2✔
934
                                {
935
                                    self.draw_multiline_line(
1✔
936
                                        buffer,
937
                                        last_buffer_line_num,
938
                                        width_offset,
939
                                        pos,
940
                                        if ann.is_primary() {
1✔
941
                                            ElementStyle::UnderlinePrimary
1✔
942
                                        } else {
943
                                            ElementStyle::UnderlineSecondary
×
944
                                        },
945
                                    );
946
                                }
947
                            }
948
                        }
949
                    }
950
                    Ordering::Less => {}
951
                }
952
            }
953

954
            multilines.extend(to_add);
4✔
955
        }
956
    }
957

958
    #[allow(clippy::too_many_arguments)]
959
    fn render_source_line(
5✔
960
        &self,
961
        line_info: &AnnotatedLineInfo<'_>,
962
        buffer: &mut StyledBuffer,
963
        width_offset: usize,
964
        code_offset: usize,
965
        max_line_num_len: usize,
966
        margin: Margin,
967
        close_window: bool,
968
    ) -> Vec<(usize, ElementStyle)> {
969
        // Draw:
970
        //
971
        //   LL | ... code ...
972
        //      |     ^^-^ span label
973
        //      |       |
974
        //      |       secondary span label
975
        //
976
        //   ^^ ^ ^^^ ^^^^ ^^^ we don't care about code too far to the right of a span, we trim it
977
        //   |  | |   |
978
        //   |  | |   actual code found in your source code and the spans we use to mark it
979
        //   |  | when there's too much wasted space to the left, trim it
980
        //   |  vertical divider between the column number and the code
981
        //   column number
982

983
        if line_info.line_index == 0 {
3✔
984
            return Vec::new();
×
985
        }
986

987
        let source_string = normalize_whitespace(line_info.line);
8✔
988

989
        let line_offset = buffer.num_lines();
15✔
990

991
        let left = self.draw_line(
7✔
992
            buffer,
993
            &source_string,
6✔
994
            line_info.line_index,
9✔
995
            line_offset,
996
            width_offset,
997
            code_offset,
998
            max_line_num_len,
999
            margin,
1000
        );
1001

1002
        // Special case when there's only one annotation involved, it is the start of a multiline
1003
        // span and there's no text at the beginning of the code line. Instead of doing the whole
1004
        // graph:
1005
        //
1006
        // 2 |   fn foo() {
1007
        //   |  _^
1008
        // 3 | |
1009
        // 4 | | }
1010
        //   | |_^ test
1011
        //
1012
        // we simplify the output to:
1013
        //
1014
        // 2 | / fn foo() {
1015
        // 3 | |
1016
        // 4 | | }
1017
        //   | |_^ test
1018
        let mut buffer_ops = vec![];
6✔
1019
        let mut annotations = vec![];
9✔
1020
        let mut short_start = true;
6✔
1021
        for ann in &line_info.annotations {
12✔
1022
            if let LineAnnotationType::MultilineStart(depth) = ann.annotation_type {
10✔
1023
                if source_string
12✔
1024
                    .chars()
1025
                    .take(ann.start.display)
4✔
1026
                    .all(char::is_whitespace)
1027
                {
1028
                    let uline = self.underline(ann.is_primary());
3✔
1029
                    let chr = uline.multiline_whole_line;
3✔
1030
                    annotations.push((depth, uline.style));
3✔
1031
                    buffer_ops.push((line_offset, width_offset + depth - 1, chr, uline.style));
3✔
1032
                } else {
1033
                    short_start = false;
3✔
1034
                    break;
1035
                }
1036
            } else if let LineAnnotationType::MultilineLine(_) = ann.annotation_type {
6✔
1037
            } else {
1038
                short_start = false;
6✔
1039
                break;
6✔
1040
            }
1041
        }
1042
        if short_start {
6✔
1043
            for (y, x, c, s) in buffer_ops {
12✔
1044
                buffer.putc(y, x, c, s);
9✔
1045
            }
1046
            return annotations;
3✔
1047
        }
1048

1049
        // We want to display like this:
1050
        //
1051
        //      vec.push(vec.pop().unwrap());
1052
        //      ---      ^^^               - previous borrow ends here
1053
        //      |        |
1054
        //      |        error occurs here
1055
        //      previous borrow of `vec` occurs here
1056
        //
1057
        // But there are some weird edge cases to be aware of:
1058
        //
1059
        //      vec.push(vec.pop().unwrap());
1060
        //      --------                    - previous borrow ends here
1061
        //      ||
1062
        //      |this makes no sense
1063
        //      previous borrow of `vec` occurs here
1064
        //
1065
        // For this reason, we group the lines into "highlight lines"
1066
        // and "annotations lines", where the highlight lines have the `^`.
1067

1068
        // Sort the annotations by (start, end col)
1069
        // The labels are reversed, sort and then reversed again.
1070
        // Consider a list of annotations (A1, A2, C1, C2, B1, B2) where
1071
        // the letter signifies the span. Here we are only sorting by the
1072
        // span and hence, the order of the elements with the same span will
1073
        // not change. On reversing the ordering (|a, b| but b.cmp(a)), you get
1074
        // (C1, C2, B1, B2, A1, A2). All the elements with the same span are
1075
        // still ordered first to last, but all the elements with different
1076
        // spans are ordered by their spans in last to first order. Last to
1077
        // first order is important, because the jiggly lines and | are on
1078
        // the left, so the rightmost span needs to be rendered first,
1079
        // otherwise the lines would end up needing to go over a message.
1080

1081
        let mut annotations = line_info.annotations.clone();
6✔
1082
        annotations.sort_by_key(|a| Reverse(a.start.display));
17✔
1083

1084
        // First, figure out where each label will be positioned.
1085
        //
1086
        // In the case where you have the following annotations:
1087
        //
1088
        //      vec.push(vec.pop().unwrap());
1089
        //      --------                    - previous borrow ends here [C]
1090
        //      ||
1091
        //      |this makes no sense [B]
1092
        //      previous borrow of `vec` occurs here [A]
1093
        //
1094
        // `annotations_position` will hold [(2, A), (1, B), (0, C)].
1095
        //
1096
        // We try, when possible, to stick the rightmost annotation at the end
1097
        // of the highlight line:
1098
        //
1099
        //      vec.push(vec.pop().unwrap());
1100
        //      ---      ---               - previous borrow ends here
1101
        //
1102
        // But sometimes that's not possible because one of the other
1103
        // annotations overlaps it. For example, from the test
1104
        // `span_overlap_label`, we have the following annotations
1105
        // (written on distinct lines for clarity):
1106
        //
1107
        //      fn foo(x: u32) {
1108
        //      --------------
1109
        //             -
1110
        //
1111
        // In this case, we can't stick the rightmost-most label on
1112
        // the highlight line, or we would get:
1113
        //
1114
        //      fn foo(x: u32) {
1115
        //      -------- x_span
1116
        //      |
1117
        //      fn_span
1118
        //
1119
        // which is totally weird. Instead we want:
1120
        //
1121
        //      fn foo(x: u32) {
1122
        //      --------------
1123
        //      |      |
1124
        //      |      x_span
1125
        //      fn_span
1126
        //
1127
        // which is...less weird, at least. In fact, in general, if
1128
        // the rightmost span overlaps with any other span, we should
1129
        // use the "hang below" version, so we can at least make it
1130
        // clear where the span *starts*. There's an exception for this
1131
        // logic, when the labels do not have a message:
1132
        //
1133
        //      fn foo(x: u32) {
1134
        //      --------------
1135
        //             |
1136
        //             x_span
1137
        //
1138
        // instead of:
1139
        //
1140
        //      fn foo(x: u32) {
1141
        //      --------------
1142
        //      |      |
1143
        //      |      x_span
1144
        //      <EMPTY LINE>
1145
        //
1146
        let mut overlap = vec![false; annotations.len()];
6✔
1147
        let mut annotations_position = vec![];
5✔
1148
        let mut line_len: usize = 0;
7✔
1149
        let mut p = 0;
5✔
1150
        for (i, annotation) in annotations.iter().enumerate() {
18✔
1151
            for (j, next) in annotations.iter().enumerate() {
12✔
1152
                if overlaps(next, annotation, 0) && j > 1 {
25✔
1153
                    overlap[i] = true;
2✔
1154
                    overlap[j] = true;
2✔
1155
                }
1156
                if overlaps(next, annotation, 0)  // This label overlaps with another one and both
16✔
1157
                    && annotation.has_label()     // take space (they have text and are not
8✔
1158
                    && j > i                      // multiline lines).
3✔
1159
                    && p == 0
2✔
1160
                // We're currently on the first line, move the label one line down
1161
                {
1162
                    // If we're overlapping with an un-labelled annotation with the same span
1163
                    // we can just merge them in the output
1164
                    if next.start.display == annotation.start.display
2✔
1165
                        && next.end.display == annotation.end.display
2✔
1166
                        && !next.has_label()
2✔
1167
                    {
1168
                        continue;
1169
                    }
1170

1171
                    // This annotation needs a new line in the output.
1172
                    p += 1;
4✔
1173
                    break;
1174
                }
1175
            }
1176
            annotations_position.push((p, annotation));
5✔
1177
            for (j, next) in annotations.iter().enumerate() {
7✔
1178
                if j > i {
6✔
1179
                    let l = next.label.as_ref().map_or(0, |label| label.len() + 2);
9✔
1180
                    if (overlaps(next, annotation, l) // Do not allow two labels to be in the same
3✔
1181
                        // line if they overlap including padding, to
1182
                        // avoid situations like:
1183
                        //
1184
                        //      fn foo(x: u32) {
1185
                        //      -------^------
1186
                        //      |      |
1187
                        //      fn_spanx_span
1188
                        //
1189
                        && annotation.has_label()    // Both labels must have some text, otherwise
3✔
1190
                        && next.has_label())         // they are not overlapping.
3✔
1191
                        // Do not add a new line if this annotation
1192
                        // or the next are vertical line placeholders.
1193
                        || (annotation.takes_space() // If either this or the next annotation is
4✔
1194
                        && next.has_label())     // multiline start/end, move it to a new line
2✔
1195
                        || (annotation.has_label()   // so as not to overlap the horizontal lines.
4✔
1196
                        && next.takes_space())
2✔
1197
                        || (annotation.takes_space() && next.takes_space())
6✔
1198
                        || (overlaps(next, annotation, l)
4✔
1199
                        && next.end.display <= annotation.end.display
1✔
1200
                        && next.has_label()
1✔
1201
                        && p == 0)
1✔
1202
                    // Avoid #42595.
1203
                    {
1204
                        // This annotation needs a new line in the output.
1205
                        p += 1;
6✔
1206
                        break;
1207
                    }
1208
                }
1209
            }
1210
            line_len = max(line_len, p);
17✔
1211
        }
1212

1213
        if line_len != 0 {
12✔
1214
            line_len += 1;
3✔
1215
        }
1216

1217
        // If there are no annotations or the only annotations on this line are
1218
        // MultilineLine, then there's only code being shown, stop processing.
1219
        if line_info.annotations.iter().all(LineAnnotation::is_line) {
16✔
1220
            return vec![];
×
1221
        }
1222

1223
        if annotations_position
16✔
1224
            .iter()
1225
            .all(|(_, ann)| matches!(ann.annotation_type, LineAnnotationType::MultilineStart(_)))
16✔
1226
        {
1227
            if let Some(max_pos) = annotations_position.iter().map(|(pos, _)| *pos).max() {
18✔
1228
                // Special case the following, so that we minimize overlapping multiline spans.
1229
                //
1230
                // 3 │       X0 Y0 Z0
1231
                //   │ ┏━━━━━┛  │  │     < We are writing these lines
1232
                //   │ ┃┌───────┘  │     < by reverting the "depth" of
1233
                //   │ ┃│┌─────────┘     < their multiline spans.
1234
                // 4 │ ┃││   X1 Y1 Z1
1235
                // 5 │ ┃││   X2 Y2 Z2
1236
                //   │ ┃│└────╿──│──┘ `Z` label
1237
                //   │ ┃└─────│──┤
1238
                //   │ ┗━━━━━━┥  `Y` is a good letter too
1239
                //   ╰╴       `X` is a good letter
1240
                for (pos, _) in &mut annotations_position {
8✔
1241
                    *pos = max_pos - *pos;
8✔
1242
                }
1243
                // We know then that we don't need an additional line for the span label, saving us
1244
                // one line of vertical space.
1245
                line_len = line_len.saturating_sub(1);
4✔
1246
            }
1247
        }
1248

1249
        // Write the column separator.
1250
        //
1251
        // After this we will have:
1252
        //
1253
        // 2 |   fn foo() {
1254
        //   |
1255
        //   |
1256
        //   |
1257
        // 3 |
1258
        // 4 |   }
1259
        //   |
1260
        for pos in 0..=line_len {
10✔
1261
            self.draw_col_separator_no_space(buffer, line_offset + pos + 1, width_offset - 2);
11✔
1262
        }
1263
        if close_window {
6✔
1264
            self.draw_col_separator_end(buffer, line_offset + line_len + 1, width_offset - 2);
5✔
1265
        }
1266
        // Write the horizontal lines for multiline annotations
1267
        // (only the first and last lines need this).
1268
        //
1269
        // After this we will have:
1270
        //
1271
        // 2 |   fn foo() {
1272
        //   |  __________
1273
        //   |
1274
        //   |
1275
        // 3 |
1276
        // 4 |   }
1277
        //   |  _
1278
        for &(pos, annotation) in &annotations_position {
6✔
1279
            let underline = self.underline(annotation.is_primary());
6✔
1280
            let pos = pos + 1;
7✔
1281
            match annotation.annotation_type {
11✔
1282
                LineAnnotationType::MultilineStart(depth)
5✔
1283
                | LineAnnotationType::MultilineEnd(depth) => {
1284
                    self.draw_range(
5✔
1285
                        buffer,
1286
                        underline.multiline_horizontal,
4✔
1287
                        line_offset + pos,
4✔
1288
                        width_offset + depth,
5✔
1289
                        (code_offset + annotation.start.display).saturating_sub(left),
10✔
1290
                        underline.style,
1291
                    );
1292
                }
1293
                _ if annotation.highlight_source => {
5✔
1294
                    buffer.set_style_range(
×
1295
                        line_offset,
1296
                        (code_offset + annotation.start.display).saturating_sub(left),
×
1297
                        (code_offset + annotation.end.display).saturating_sub(left),
×
1298
                        underline.style,
×
1299
                        annotation.is_primary(),
×
1300
                    );
1301
                }
1302
                _ => {}
1303
            }
1304
        }
1305

1306
        // Write the vertical lines for labels that are on a different line as the underline.
1307
        //
1308
        // After this we will have:
1309
        //
1310
        // 2 |   fn foo() {
1311
        //   |  __________
1312
        //   | |    |
1313
        //   | |
1314
        // 3 | |
1315
        // 4 | | }
1316
        //   | |_
1317
        for &(pos, annotation) in &annotations_position {
5✔
1318
            let underline = self.underline(annotation.is_primary());
10✔
1319
            let pos = pos + 1;
6✔
1320

1321
            if pos > 1 && (annotation.has_label() || annotation.takes_space()) {
9✔
1322
                for p in line_offset + 1..=line_offset + pos {
6✔
1323
                    buffer.putc(
3✔
1324
                        p,
1325
                        (code_offset + annotation.start.display).saturating_sub(left),
6✔
1326
                        match annotation.annotation_type {
3✔
1327
                            LineAnnotationType::MultilineLine(_) => underline.multiline_vertical,
×
1328
                            _ => underline.vertical_text_line,
3✔
1329
                        },
1330
                        underline.style,
1331
                    );
1332
                }
1333
                if let LineAnnotationType::MultilineStart(_) = annotation.annotation_type {
3✔
1334
                    buffer.putc(
2✔
1335
                        line_offset + pos,
2✔
1336
                        (code_offset + annotation.start.display).saturating_sub(left),
4✔
1337
                        underline.bottom_right,
2✔
1338
                        underline.style,
1339
                    );
1340
                }
1341
                if matches!(
3✔
1342
                    annotation.annotation_type,
1343
                    LineAnnotationType::MultilineEnd(_)
1344
                ) && annotation.has_label()
2✔
1345
                {
1346
                    buffer.putc(
3✔
1347
                        line_offset + pos,
3✔
1348
                        (code_offset + annotation.start.display).saturating_sub(left),
6✔
1349
                        underline.multiline_bottom_right_with_text,
3✔
1350
                        underline.style,
1351
                    );
1352
                }
1353
            }
1354
            match annotation.annotation_type {
6✔
1355
                LineAnnotationType::MultilineStart(depth) => {
4✔
1356
                    buffer.putc(
4✔
1357
                        line_offset + pos,
4✔
1358
                        width_offset + depth - 1,
8✔
1359
                        underline.top_left,
4✔
1360
                        underline.style,
1361
                    );
1362
                    for p in line_offset + pos + 1..line_offset + line_len + 2 {
4✔
1363
                        buffer.putc(
4✔
1364
                            p,
1365
                            width_offset + depth - 1,
2✔
1366
                            underline.multiline_vertical,
2✔
1367
                            underline.style,
1368
                        );
1369
                    }
1370
                }
1371
                LineAnnotationType::MultilineEnd(depth) => {
4✔
1372
                    for p in line_offset..line_offset + pos {
8✔
1373
                        buffer.putc(
8✔
1374
                            p,
1375
                            width_offset + depth - 1,
8✔
1376
                            underline.multiline_vertical,
4✔
1377
                            underline.style,
1378
                        );
1379
                    }
1380
                    buffer.putc(
8✔
1381
                        line_offset + pos,
4✔
1382
                        width_offset + depth - 1,
8✔
1383
                        underline.bottom_left,
4✔
1384
                        underline.style,
1385
                    );
1386
                }
1387
                _ => (),
1388
            }
1389
        }
1390

1391
        // Write the labels on the annotations that actually have a label.
1392
        //
1393
        // After this we will have:
1394
        //
1395
        // 2 |   fn foo() {
1396
        //   |  __________
1397
        //   |      |
1398
        //   |      something about `foo`
1399
        // 3 |
1400
        // 4 |   }
1401
        //   |  _  test
1402
        for &(pos, annotation) in &annotations_position {
5✔
1403
            let style = if annotation.is_primary() {
15✔
1404
                ElementStyle::LabelPrimary
7✔
1405
            } else {
1406
                ElementStyle::LabelSecondary
4✔
1407
            };
1408
            let (pos, col) = if pos == 0 {
14✔
1409
                if annotation.end.display == 0 {
10✔
1410
                    (pos + 1, (annotation.end.display + 2).saturating_sub(left))
9✔
1411
                } else {
1412
                    (pos + 1, (annotation.end.display + 1).saturating_sub(left))
10✔
1413
                }
1414
            } else {
1415
                (pos + 2, annotation.start.display.saturating_sub(left))
6✔
1416
            };
1417
            if let Some(label) = &annotation.label {
9✔
1418
                buffer.puts(line_offset + pos, code_offset + col, label, style);
4✔
1419
            }
1420
        }
1421

1422
        // Sort from biggest span to smallest span so that smaller spans are
1423
        // represented in the output:
1424
        //
1425
        // x | fn foo()
1426
        //   | ^^^---^^
1427
        //   | |  |
1428
        //   | |  something about `foo`
1429
        //   | something about `fn foo()`
1430
        annotations_position.sort_by_key(|(_, ann)| {
10✔
1431
            // Decreasing order. When annotations share the same length, prefer `Primary`.
1432
            (Reverse(ann.len()), ann.is_primary())
3✔
1433
        });
1434

1435
        // Write the underlines.
1436
        //
1437
        // After this we will have:
1438
        //
1439
        // 2 |   fn foo() {
1440
        //   |  ____-_____^
1441
        //   |      |
1442
        //   |      something about `foo`
1443
        // 3 |
1444
        // 4 |   }
1445
        //   |  _^  test
1446
        for &(pos, annotation) in &annotations_position {
5✔
1447
            let uline = self.underline(annotation.is_primary());
14✔
1448
            for p in annotation.start.display..annotation.end.display {
8✔
1449
                // The default span label underline.
1450
                buffer.putc(
7✔
1451
                    line_offset + 1,
8✔
1452
                    (code_offset + p).saturating_sub(left),
15✔
1453
                    uline.underline,
5✔
1454
                    uline.style,
1455
                );
1456
            }
1457

1458
            if pos == 0
8✔
1459
                && matches!(
14✔
1460
                    annotation.annotation_type,
8✔
1461
                    LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1462
                )
1463
            {
1464
                // The beginning of a multiline span with its leftward moving line on the same line.
1465
                buffer.putc(
4✔
1466
                    line_offset + 1,
5✔
1467
                    (code_offset + annotation.start.display).saturating_sub(left),
10✔
1468
                    match annotation.annotation_type {
4✔
1469
                        LineAnnotationType::MultilineStart(_) => uline.top_right_flat,
4✔
1470
                        LineAnnotationType::MultilineEnd(_) => uline.multiline_end_same_line,
5✔
1471
                        _ => panic!("unexpected annotation type: {annotation:?}"),
×
1472
                    },
1473
                    uline.style,
1474
                );
1475
            } else if pos != 0
6✔
1476
                && matches!(
3✔
1477
                    annotation.annotation_type,
3✔
1478
                    LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1479
                )
1480
            {
1481
                // The beginning of a multiline span with its leftward moving line on another line,
1482
                // so we start going down first.
1483
                buffer.putc(
2✔
1484
                    line_offset + 1,
2✔
1485
                    (code_offset + annotation.start.display).saturating_sub(left),
4✔
1486
                    match annotation.annotation_type {
2✔
1487
                        LineAnnotationType::MultilineStart(_) => uline.multiline_start_down,
2✔
1488
                        LineAnnotationType::MultilineEnd(_) => uline.multiline_end_up,
3✔
1489
                        _ => panic!("unexpected annotation type: {annotation:?}"),
×
1490
                    },
1491
                    uline.style,
1492
                );
1493
            } else if pos != 0 && annotation.has_label() {
9✔
1494
                // The beginning of a span label with an actual label, we'll point down.
1495
                buffer.putc(
3✔
1496
                    line_offset + 1,
3✔
1497
                    (code_offset + annotation.start.display).saturating_sub(left),
6✔
1498
                    uline.label_start,
3✔
1499
                    uline.style,
1500
                );
1501
            }
1502
        }
1503

1504
        // We look for individual *long* spans, and we trim the *middle*, so that we render
1505
        // LL | ...= [0, 0, 0, ..., 0, 0];
1506
        //    |      ^^^^^^^^^^...^^^^^^^ expected `&[u8]`, found `[{integer}; 1680]`
1507
        for (i, (_pos, annotation)) in annotations_position.iter().enumerate() {
4✔
1508
            // Skip cases where multiple spans overlap eachother.
1509
            if overlap[i] {
11✔
1510
                continue;
1511
            };
1512
            let LineAnnotationType::Singleline = annotation.annotation_type else {
9✔
1513
                continue;
1514
            };
1515
            let width = annotation.end.display - annotation.start.display;
4✔
1516
            if width > margin.term_width * 2 && width > 10 {
8✔
1517
                // If the terminal is *too* small, we keep at least a tiny bit of the span for
1518
                // display.
1519
                let pad = max(margin.term_width / 3, 5);
1✔
1520
                // Code line
1521
                buffer.replace(
1✔
1522
                    line_offset,
1523
                    annotation.start.display + pad,
1✔
1524
                    annotation.end.display - pad,
1✔
1525
                    self.margin(),
1✔
1526
                );
1527
                // Underline line
1528
                buffer.replace(
1✔
1529
                    line_offset + 1,
1✔
1530
                    annotation.start.display + pad,
1✔
1531
                    annotation.end.display - pad,
1✔
1532
                    self.margin(),
1✔
1533
                );
1534
            }
1535
        }
1536
        annotations_position
4✔
1537
            .iter()
1538
            .filter_map(|&(_, annotation)| match annotation.annotation_type {
14✔
1539
                LineAnnotationType::MultilineStart(p) | LineAnnotationType::MultilineEnd(p) => {
9✔
1540
                    let style = if annotation.is_primary() {
7✔
1541
                        ElementStyle::LabelPrimary
4✔
1542
                    } else {
1543
                        ElementStyle::LabelSecondary
3✔
1544
                    };
1545
                    Some((p, style))
3✔
1546
                }
1547
                _ => None,
5✔
1548
            })
1549
            .collect::<Vec<_>>()
1550
    }
1551

1552
    fn emit_suggestion_default(
3✔
1553
        &self,
1554
        buffer: &mut StyledBuffer,
1555
        suggestion: &Snippet<'_, Patch<'_>>,
1556
        max_line_num_len: usize,
1557
        sm: &SourceMap<'_>,
1558
        primary_path: Option<&Cow<'_, str>>,
1559
        is_cont: bool,
1560
    ) {
1561
        let suggestions = sm.splice_lines(suggestion.markers.clone());
3✔
1562

1563
        let buffer_offset = buffer.num_lines();
8✔
1564
        let mut row_num = buffer_offset + usize::from(!is_cont);
4✔
1565
        for (i, (complete, parts, highlights)) in suggestions.iter().enumerate() {
8✔
1566
            let has_deletion = parts
8✔
1567
                .iter()
1568
                .any(|p| p.is_deletion(sm) || p.is_destructive_replacement(sm));
8✔
1569
            let is_multiline = complete.lines().count() > 1;
4✔
1570

1571
            if i == 0 {
3✔
1572
                self.draw_col_separator_start(buffer, row_num - 1, max_line_num_len + 1);
8✔
1573
            } else {
1574
                buffer.puts(
×
1575
                    row_num - 1,
×
1576
                    max_line_num_len + 1,
×
1577
                    self.multi_suggestion_separator(),
×
1578
                    ElementStyle::LineNumber,
×
1579
                );
1580
            }
1581
            if suggestion.path.as_ref() != primary_path {
8✔
1582
                if let Some(path) = suggestion.path.as_ref() {
1✔
1583
                    let (loc, _) = sm.span_to_locations(parts[0].span.clone());
×
1584
                    // --> file.rs:line:col
1585
                    //  |
1586
                    let arrow = self.file_start();
×
1587
                    buffer.puts(row_num - 1, 0, arrow, ElementStyle::LineNumber);
×
1588
                    let message = format!("{}:{}:{}", path, loc.line, loc.char + 1);
×
1589
                    if is_cont {
×
1590
                        buffer.append(row_num - 1, &message, ElementStyle::LineAndColumn);
×
1591
                    } else {
1592
                        let col = usize::max(max_line_num_len + 1, arrow.len());
×
1593
                        buffer.puts(row_num - 1, col, &message, ElementStyle::LineAndColumn);
×
1594
                    }
1595
                    for _ in 0..max_line_num_len {
×
1596
                        buffer.prepend(row_num - 1, " ", ElementStyle::NoStyle);
×
1597
                    }
1598
                    self.draw_col_separator_no_space(buffer, row_num, max_line_num_len + 1);
×
1599
                    row_num += 1;
×
1600
                }
1601
            }
1602
            let show_code_change = if has_deletion && !is_multiline {
7✔
1603
                DisplaySuggestion::Diff
3✔
1604
            } else if parts.len() == 1
5✔
1605
                && parts.first().map_or(false, |p| {
6✔
1606
                    p.replacement.ends_with('\n') && p.replacement.trim() == complete.trim()
2✔
1607
                })
1608
            {
1609
                // We are adding a line(s) of code before code that was already there.
1610
                DisplaySuggestion::Add
1✔
1611
            } else if (parts.len() != 1 || parts[0].replacement.trim() != complete.trim())
9✔
1612
                && !is_multiline
2✔
1613
            {
1614
                DisplaySuggestion::Underline
2✔
1615
            } else {
1616
                DisplaySuggestion::None
1✔
1617
            };
1618

1619
            if let DisplaySuggestion::Diff = show_code_change {
6✔
1620
                row_num += 1;
6✔
1621
            }
1622

1623
            let file_lines = sm.span_to_lines(parts[0].span.clone());
6✔
1624
            let (line_start, line_end) = sm.span_to_locations(parts[0].span.clone());
6✔
1625
            let mut lines = complete.lines();
3✔
1626
            if lines.clone().next().is_none() {
4✔
1627
                // Account for a suggestion to completely remove a line(s) with whitespace (#94192).
1628
                for line in line_start.line..=line_end.line {
1✔
1629
                    buffer.puts(
1✔
1630
                        row_num - 1 + line - line_start.line,
2✔
1631
                        0,
1632
                        &self.maybe_anonymized(line),
2✔
1633
                        ElementStyle::LineNumber,
1✔
1634
                    );
1635
                    buffer.puts(
1✔
1636
                        row_num - 1 + line - line_start.line,
1✔
1637
                        max_line_num_len + 1,
1✔
1638
                        "- ",
1639
                        ElementStyle::Removal,
1✔
1640
                    );
1641
                    buffer.puts(
1✔
1642
                        row_num - 1 + line - line_start.line,
1✔
1643
                        max_line_num_len + 3,
1✔
1644
                        &normalize_whitespace(sm.get_line(line).unwrap()),
2✔
1645
                        ElementStyle::Removal,
1✔
1646
                    );
1647
                }
1648
                row_num += line_end.line - line_start.line;
1✔
1649
            }
1650
            let mut last_pos = 0;
4✔
1651
            let mut is_item_attribute = false;
3✔
1652
            let mut unhighlighted_lines = Vec::new();
4✔
1653
            for (line_pos, (line, highlight_parts)) in lines.by_ref().zip(highlights).enumerate() {
8✔
1654
                last_pos = line_pos;
4✔
1655

1656
                // Remember lines that are not highlighted to hide them if needed
1657
                if highlight_parts.is_empty() {
9✔
1658
                    unhighlighted_lines.push((line_pos, line));
2✔
1659
                    continue;
1660
                }
1661
                if highlight_parts.len() == 1
8✔
1662
                    && line.trim().starts_with("#[")
8✔
1663
                    && line.trim().ends_with(']')
×
1664
                {
1665
                    is_item_attribute = true;
×
1666
                }
1667

1668
                match unhighlighted_lines.len() {
8✔
1669
                    0 => (),
1670
                    // Since we show first line, "..." line and last line,
1671
                    // There is no reason to hide if there are 3 or less lines
1672
                    // (because then we just replace a line with ... which is
1673
                    // not helpful)
1674
                    n if n <= 3 => unhighlighted_lines.drain(..).for_each(|(p, l)| {
5✔
1675
                        self.draw_code_line(
2✔
1676
                            buffer,
1✔
1677
                            &mut row_num,
1✔
1678
                            &[],
1679
                            p + line_start.line,
1✔
1680
                            l,
1681
                            show_code_change,
1✔
1682
                            max_line_num_len,
1✔
1683
                            &file_lines,
1✔
1684
                            is_multiline,
1✔
1685
                        );
1686
                    }),
1687
                    // Print first unhighlighted line, "..." and last unhighlighted line, like so:
1688
                    //
1689
                    // LL | this line was highlighted
1690
                    // LL | this line is just for context
1691
                    // ...
1692
                    // LL | this line is just for context
1693
                    // LL | this line was highlighted
1694
                    _ => {
1695
                        let last_line = unhighlighted_lines.pop();
×
1696
                        let first_line = unhighlighted_lines.drain(..).next();
×
1697

1698
                        if let Some((p, l)) = first_line {
×
1699
                            self.draw_code_line(
×
1700
                                buffer,
1701
                                &mut row_num,
1702
                                &[],
1703
                                p + line_start.line,
×
1704
                                l,
1705
                                show_code_change,
×
1706
                                max_line_num_len,
1707
                                &file_lines,
×
1708
                                is_multiline,
1709
                            );
1710
                        }
1711

1712
                        let placeholder = self.margin();
×
1713
                        let padding = str_width(placeholder);
×
1714
                        buffer.puts(
×
1715
                            row_num,
×
1716
                            max_line_num_len.saturating_sub(padding),
×
1717
                            placeholder,
1718
                            ElementStyle::LineNumber,
×
1719
                        );
1720
                        row_num += 1;
×
1721

1722
                        if let Some((p, l)) = last_line {
×
1723
                            self.draw_code_line(
×
1724
                                buffer,
1725
                                &mut row_num,
1726
                                &[],
1727
                                p + line_start.line,
×
1728
                                l,
1729
                                show_code_change,
×
1730
                                max_line_num_len,
1731
                                &file_lines,
×
1732
                                is_multiline,
1733
                            );
1734
                        }
1735
                    }
1736
                }
1737
                self.draw_code_line(
4✔
1738
                    buffer,
1739
                    &mut row_num,
1740
                    highlight_parts,
4✔
1741
                    line_pos + line_start.line,
4✔
1742
                    line,
1743
                    show_code_change,
4✔
1744
                    max_line_num_len,
1745
                    &file_lines,
4✔
1746
                    is_multiline,
1747
                );
1748
            }
1749

1750
            if matches!(show_code_change, DisplaySuggestion::Add) && is_item_attribute {
3✔
1751
                // The suggestion adds an entire line of code, ending on a newline, so we'll also
1752
                // print the *following* line, to provide context of what we're advising people to
1753
                // do. Otherwise you would only see contextless code that can be confused for
1754
                // already existing code, despite the colors and UI elements.
1755
                // We special case `#[derive(_)]\n` and other attribute suggestions, because those
1756
                // are the ones where context is most useful.
1757
                let file_lines = sm.span_to_lines(parts[0].span.end..parts[0].span.end);
×
1758
                let (lo, _) = sm.span_to_locations(parts[0].span.clone());
×
1759
                let line_num = lo.line;
×
1760
                if let Some(line) = sm.get_line(line_num) {
×
1761
                    let line = normalize_whitespace(line);
×
1762
                    self.draw_code_line(
×
1763
                        buffer,
1764
                        &mut row_num,
1765
                        &[],
1766
                        line_num + last_pos + 1,
×
1767
                        &line,
×
1768
                        DisplaySuggestion::None,
×
1769
                        max_line_num_len,
1770
                        &file_lines,
×
1771
                        is_multiline,
1772
                    );
1773
                }
1774
            }
1775
            // This offset and the ones below need to be signed to account for replacement code
1776
            // that is shorter than the original code.
1777
            let mut offsets: Vec<(usize, isize)> = Vec::new();
3✔
1778
            // Only show an underline in the suggestions if the suggestion is not the
1779
            // entirety of the code being shown and the displayed code is not multiline.
1780
            if let DisplaySuggestion::Diff | DisplaySuggestion::Underline | DisplaySuggestion::Add =
7✔
1781
                show_code_change
1782
            {
1783
                for part in parts {
7✔
1784
                    let snippet = sm.span_to_snippet(part.span.clone()).unwrap_or_default();
7✔
1785
                    let (span_start, span_end) = sm.span_to_locations(part.span.clone());
3✔
1786
                    let span_start_pos = span_start.display;
4✔
1787
                    let span_end_pos = span_end.display;
3✔
1788

1789
                    // If this addition is _only_ whitespace, then don't trim it,
1790
                    // or else we're just not rendering anything.
1791
                    let is_whitespace_addition = part.replacement.trim().is_empty();
5✔
1792

1793
                    // Do not underline the leading...
1794
                    let start = if is_whitespace_addition {
5✔
1795
                        0
2✔
1796
                    } else {
1797
                        part.replacement
7✔
1798
                            .len()
1799
                            .saturating_sub(part.replacement.trim_start().len())
3✔
1800
                    };
1801
                    // ...or trailing spaces. Account for substitutions containing unicode
1802
                    // characters.
1803
                    let sub_len: usize = str_width(if is_whitespace_addition {
10✔
1804
                        &part.replacement
4✔
1805
                    } else {
1806
                        part.replacement.trim()
5✔
1807
                    });
1808

1809
                    let offset: isize = offsets
4✔
1810
                        .iter()
1811
                        .filter_map(|(start, v)| {
4✔
1812
                            if span_start_pos < *start {
5✔
1813
                                None
×
1814
                            } else {
1815
                                Some(v)
3✔
1816
                            }
1817
                        })
1818
                        .sum();
1819
                    let underline_start = (span_start_pos + start) as isize + offset;
3✔
1820
                    let underline_end = (span_start_pos + start + sub_len) as isize + offset;
7✔
1821
                    assert!(underline_start >= 0 && underline_end >= 0);
5✔
1822
                    let padding: usize = max_line_num_len + 3;
3✔
1823
                    for p in underline_start..underline_end {
8✔
1824
                        if matches!(show_code_change, DisplaySuggestion::Underline)
3✔
1825
                            && is_different(sm, &part.replacement, part.span.clone())
2✔
1826
                        {
1827
                            // If this is a replacement, underline with `~`, if this is an addition
1828
                            // underline with `+`.
1829
                            buffer.putc(
2✔
1830
                                row_num,
2✔
1831
                                (padding as isize + p) as usize,
2✔
1832
                                if part.is_addition(sm) {
6✔
1833
                                    '+'
2✔
1834
                                } else {
1835
                                    self.diff()
×
1836
                                },
1837
                                ElementStyle::Addition,
2✔
1838
                            );
1839
                        }
1840
                    }
1841
                    if let DisplaySuggestion::Diff = show_code_change {
3✔
1842
                        // Colorize removal with red in diff format.
1843

1844
                        // Below, there's some tricky buffer indexing going on. `row_num` at this
1845
                        // point corresponds to:
1846
                        //
1847
                        //    |
1848
                        // LL | CODE
1849
                        //    | ++++  <- `row_num`
1850
                        //
1851
                        // in the buffer. When we have a diff format output, we end up with
1852
                        //
1853
                        //    |
1854
                        // LL - OLDER   <- row_num - 2
1855
                        // LL + NEWER
1856
                        //    |         <- row_num
1857
                        //
1858
                        // The `row_num - 2` is to select the buffer line that has the "old version
1859
                        // of the diff" at that point. When the removal is a single line, `i` is
1860
                        // `0`, `newlines` is `1` so `(newlines - i - 1)` ends up being `0`, so row
1861
                        // points at `LL - OLDER`. When the removal corresponds to multiple lines,
1862
                        // we end up with `newlines > 1` and `i` being `0..newlines - 1`.
1863
                        //
1864
                        //    |
1865
                        // LL - OLDER   <- row_num - 2 - (newlines - last_i - 1)
1866
                        // LL - CODE
1867
                        // LL - BEING
1868
                        // LL - REMOVED <- row_num - 2 - (newlines - first_i - 1)
1869
                        // LL + NEWER
1870
                        //    |         <- row_num
1871

1872
                        let newlines = snippet.lines().count();
6✔
1873
                        if newlines > 0 && row_num > newlines {
6✔
1874
                            // Account for removals where the part being removed spans multiple
1875
                            // lines.
1876
                            // FIXME: We check the number of rows because in some cases, like in
1877
                            // `tests/ui/lint/invalid-nan-comparison-suggestion.rs`, the rendered
1878
                            // suggestion will only show the first line of code being replaced. The
1879
                            // proper way of doing this would be to change the suggestion rendering
1880
                            // logic to show the whole prior snippet, but the current output is not
1881
                            // too bad to begin with, so we side-step that issue here.
1882
                            for (i, line) in snippet.lines().enumerate() {
6✔
1883
                                let line = normalize_whitespace(line);
3✔
1884
                                let row = row_num - 2 - (newlines - i - 1);
6✔
1885
                                // On the first line, we highlight between the start of the part
1886
                                // span, and the end of that line.
1887
                                // On the last line, we highlight between the start of the line, and
1888
                                // the column of the part span end.
1889
                                // On all others, we highlight the whole line.
1890
                                let start = if i == 0 {
9✔
1891
                                    (padding as isize + span_start_pos as isize) as usize
6✔
1892
                                } else {
1893
                                    padding
3✔
1894
                                };
1895
                                let end = if i == 0 {
7✔
1896
                                    (padding as isize
6✔
1897
                                        + span_start_pos as isize
1898
                                        + line.len() as isize)
3✔
1899
                                        as usize
1900
                                } else if i == newlines - 1 {
12✔
1901
                                    (padding as isize + span_end_pos as isize) as usize
6✔
1902
                                } else {
1903
                                    (padding as isize + line.len() as isize) as usize
6✔
1904
                                };
1905
                                buffer.set_style_range(
4✔
1906
                                    row,
1907
                                    start,
4✔
1908
                                    end,
4✔
1909
                                    ElementStyle::Removal,
4✔
1910
                                    true,
1911
                                );
1912
                            }
1913
                        } else {
1914
                            // The removed code fits all in one line.
1915
                            buffer.set_style_range(
4✔
1916
                                row_num - 2,
2✔
1917
                                (padding as isize + span_start_pos as isize) as usize,
2✔
1918
                                (padding as isize + span_end_pos as isize) as usize,
2✔
1919
                                ElementStyle::Removal,
2✔
1920
                                true,
1921
                            );
1922
                        }
1923
                    }
1924

1925
                    // length of the code after substitution
1926
                    let full_sub_len = str_width(&part.replacement) as isize;
7✔
1927

1928
                    // length of the code to be substituted
1929
                    let snippet_len = span_end_pos as isize - span_start_pos as isize;
4✔
1930
                    // For multiple substitutions, use the position *after* the previous
1931
                    // substitutions have happened, only when further substitutions are
1932
                    // located strictly after.
1933
                    offsets.push((span_end_pos, full_sub_len - snippet_len));
7✔
1934
                }
1935
                row_num += 1;
3✔
1936
            }
1937

1938
            // if we elided some lines, add an ellipsis
1939
            if lines.next().is_some() {
10✔
1940
                let placeholder = self.margin();
×
1941
                let padding = str_width(placeholder);
×
1942
                buffer.puts(
×
1943
                    row_num,
×
1944
                    max_line_num_len.saturating_sub(padding),
×
1945
                    placeholder,
1946
                    ElementStyle::LineNumber,
×
1947
                );
1948
            } else {
1949
                let row = match show_code_change {
3✔
1950
                    DisplaySuggestion::Diff
7✔
1951
                    | DisplaySuggestion::Add
1952
                    | DisplaySuggestion::Underline => row_num - 1,
1953
                    DisplaySuggestion::None => row_num,
1✔
1954
                };
1955
                self.draw_col_separator_end(buffer, row, max_line_num_len + 1);
7✔
1956
                row_num = row + 1;
4✔
1957
            }
1958
        }
1959
    }
1960

1961
    #[allow(clippy::too_many_arguments)]
1962
    fn draw_code_line(
4✔
1963
        &self,
1964
        buffer: &mut StyledBuffer,
1965
        row_num: &mut usize,
1966
        highlight_parts: &[SubstitutionHighlight],
1967
        line_num: usize,
1968
        line_to_add: &str,
1969
        show_code_change: DisplaySuggestion,
1970
        max_line_num_len: usize,
1971
        file_lines: &[&LineInfo<'_>],
1972
        is_multiline: bool,
1973
    ) {
1974
        if let DisplaySuggestion::Diff = show_code_change {
4✔
1975
            // We need to print more than one line if the span we need to remove is multiline.
1976
            // For more info: https://github.com/rust-lang/rust/issues/92741
1977
            let lines_to_remove = file_lines.iter().take(file_lines.len() - 1);
8✔
1978
            for (index, line_to_remove) in lines_to_remove.enumerate() {
8✔
1979
                buffer.puts(
2✔
1980
                    *row_num - 1,
2✔
1981
                    0,
1982
                    &self.maybe_anonymized(line_num + index),
4✔
1983
                    ElementStyle::LineNumber,
2✔
1984
                );
1985
                buffer.puts(
2✔
1986
                    *row_num - 1,
2✔
1987
                    max_line_num_len + 1,
2✔
1988
                    "- ",
1989
                    ElementStyle::Removal,
2✔
1990
                );
1991
                let line = normalize_whitespace(line_to_remove.line);
2✔
1992
                buffer.puts(
2✔
1993
                    *row_num - 1,
2✔
1994
                    max_line_num_len + 3,
2✔
1995
                    &line,
2✔
1996
                    ElementStyle::NoStyle,
2✔
1997
                );
1998
                *row_num += 1;
2✔
1999
            }
2000
            // If the last line is exactly equal to the line we need to add, we can skip both of
2001
            // them. This allows us to avoid output like the following:
2002
            // 2 - &
2003
            // 2 + if true { true } else { false }
2004
            // 3 - if true { true } else { false }
2005
            // If those lines aren't equal, we print their diff
2006
            let last_line = &file_lines.last().unwrap();
4✔
2007
            if last_line.line == line_to_add {
4✔
2008
                *row_num -= 2;
×
2009
            } else {
2010
                buffer.puts(
4✔
2011
                    *row_num - 1,
4✔
2012
                    0,
2013
                    &self.maybe_anonymized(line_num + file_lines.len() - 1),
8✔
2014
                    ElementStyle::LineNumber,
4✔
2015
                );
2016
                buffer.puts(
4✔
2017
                    *row_num - 1,
4✔
2018
                    max_line_num_len + 1,
4✔
2019
                    "- ",
2020
                    ElementStyle::Removal,
3✔
2021
                );
2022
                buffer.puts(
3✔
2023
                    *row_num - 1,
3✔
2024
                    max_line_num_len + 3,
4✔
2025
                    &normalize_whitespace(last_line.line),
3✔
2026
                    ElementStyle::NoStyle,
3✔
2027
                );
2028
                if line_to_add.trim().is_empty() {
3✔
2029
                    *row_num -= 1;
×
2030
                } else {
2031
                    // Check if after the removal, the line is left with only whitespace. If so, we
2032
                    // will not show an "addition" line, as removing the whole line is what the user
2033
                    // would really want.
2034
                    // For example, for the following:
2035
                    //   |
2036
                    // 2 -     .await
2037
                    // 2 +     (note the left over whitespace)
2038
                    //   |
2039
                    // We really want
2040
                    //   |
2041
                    // 2 -     .await
2042
                    //   |
2043
                    // *row_num -= 1;
2044
                    buffer.puts(
3✔
2045
                        *row_num,
3✔
2046
                        0,
2047
                        &self.maybe_anonymized(line_num),
3✔
2048
                        ElementStyle::LineNumber,
3✔
2049
                    );
2050
                    buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
3✔
2051
                    buffer.append(
3✔
2052
                        *row_num,
3✔
2053
                        &normalize_whitespace(line_to_add),
3✔
2054
                        ElementStyle::NoStyle,
3✔
2055
                    );
2056
                }
2057
            }
2058
        } else if is_multiline {
2✔
2059
            buffer.puts(
1✔
2060
                *row_num,
1✔
2061
                0,
2062
                &self.maybe_anonymized(line_num),
1✔
2063
                ElementStyle::LineNumber,
1✔
2064
            );
2065
            match &highlight_parts {
2✔
2066
                [SubstitutionHighlight { start: 0, end }] if *end == line_to_add.len() => {
2✔
2067
                    buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
×
2068
                }
2069
                [] => {
1✔
2070
                    // FIXME: needed? Doesn't get exercised in any test.
2071
                    self.draw_col_separator_no_space(buffer, *row_num, max_line_num_len + 1);
1✔
2072
                }
2073
                _ => {
2074
                    let diff = self.diff();
2✔
2075
                    buffer.puts(
2✔
2076
                        *row_num,
2✔
2077
                        max_line_num_len + 1,
2✔
2078
                        &format!("{diff} "),
2✔
2079
                        ElementStyle::Addition,
2✔
2080
                    );
2081
                }
2082
            }
2083
            //   LL | line_to_add
2084
            //   ++^^^
2085
            //    |  |
2086
            //    |  magic `3`
2087
            //    `max_line_num_len`
2088
            buffer.puts(
2✔
2089
                *row_num,
2✔
2090
                max_line_num_len + 3,
2✔
2091
                &normalize_whitespace(line_to_add),
2✔
2092
                ElementStyle::NoStyle,
2✔
2093
            );
2094
        } else if let DisplaySuggestion::Add = show_code_change {
2✔
2095
            buffer.puts(
1✔
2096
                *row_num,
1✔
2097
                0,
2098
                &self.maybe_anonymized(line_num),
1✔
2099
                ElementStyle::LineNumber,
1✔
2100
            );
2101
            buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
1✔
2102
            buffer.append(
1✔
2103
                *row_num,
1✔
2104
                &normalize_whitespace(line_to_add),
1✔
2105
                ElementStyle::NoStyle,
1✔
2106
            );
2107
        } else {
2108
            buffer.puts(
2✔
2109
                *row_num,
2✔
2110
                0,
2111
                &self.maybe_anonymized(line_num),
2✔
2112
                ElementStyle::LineNumber,
2✔
2113
            );
2114
            self.draw_col_separator(buffer, *row_num, max_line_num_len + 1);
2✔
2115
            buffer.append(
2✔
2116
                *row_num,
2✔
2117
                &normalize_whitespace(line_to_add),
2✔
2118
                ElementStyle::NoStyle,
2✔
2119
            );
2120
        }
2121

2122
        // Colorize addition/replacements with green.
2123
        for &SubstitutionHighlight { start, end } in highlight_parts {
6✔
2124
            // This is a no-op for empty ranges
2125
            if start != end {
3✔
2126
                // Account for tabs when highlighting (#87972).
2127
                let tabs: usize = line_to_add
2✔
2128
                    .chars()
2129
                    .take(start)
2130
                    .map(|ch| match ch {
4✔
2131
                        '\t' => 3,
×
2132
                        _ => 0,
2✔
2133
                    })
2134
                    .sum();
2135
                buffer.set_style_range(
2✔
2136
                    *row_num,
3✔
2137
                    max_line_num_len + 3 + start + tabs,
2✔
2138
                    max_line_num_len + 3 + end + tabs,
5✔
2139
                    ElementStyle::Addition,
3✔
2140
                    true,
2141
                );
2142
            }
2143
        }
2144
        *row_num += 1;
4✔
2145
    }
2146

2147
    #[allow(clippy::too_many_arguments)]
2148
    fn draw_line(
8✔
2149
        &self,
2150
        buffer: &mut StyledBuffer,
2151
        source_string: &str,
2152
        line_index: usize,
2153
        line_offset: usize,
2154
        width_offset: usize,
2155
        code_offset: usize,
2156
        max_line_num_len: usize,
2157
        margin: Margin,
2158
    ) -> usize {
2159
        // Tabs are assumed to have been replaced by spaces in calling code.
2160
        debug_assert!(!source_string.contains('\t'));
7✔
2161
        let line_len = str_width(source_string);
9✔
2162
        // Create the source line we will highlight.
2163
        let mut left = margin.left(line_len);
7✔
2164
        let right = margin.right(line_len);
9✔
2165
        // FIXME: The following code looks fishy. See #132860.
2166
        // On long lines, we strip the source line, accounting for unicode.
2167
        let mut taken = 0;
7✔
2168
        let mut skipped = 0;
8✔
2169
        let code: String = source_string
16✔
2170
            .chars()
2171
            .skip_while(|ch| {
8✔
2172
                skipped += char_width(*ch);
8✔
2173
                skipped <= left
8✔
2174
            })
2175
            .take_while(|ch| {
15✔
2176
                // Make sure that the trimming on the right will fall within the terminal width.
2177
                taken += char_width(*ch);
9✔
2178
                taken <= (right - left)
20✔
2179
            })
2180
            .collect();
2181

2182
        let placeholder = self.margin();
20✔
2183
        let padding = str_width(placeholder);
10✔
2184
        let (width_taken, bytes_taken) = if margin.was_cut_left() {
32✔
2185
            // We have stripped some code/whitespace from the beginning, make it clear.
2186
            let mut bytes_taken = 0;
3✔
2187
            let mut width_taken = 0;
4✔
2188
            for ch in code.chars() {
8✔
2189
                width_taken += char_width(ch);
8✔
2190
                bytes_taken += ch.len_utf8();
8✔
2191

2192
                if width_taken >= padding {
4✔
2193
                    break;
2194
                }
2195
            }
2196

2197
            if width_taken > padding {
5✔
2198
                left -= width_taken - padding;
1✔
2199
            }
2200

2201
            buffer.puts(
4✔
2202
                line_offset,
2203
                code_offset,
2204
                placeholder,
2205
                ElementStyle::LineNumber,
4✔
2206
            );
2207
            (width_taken, bytes_taken)
4✔
2208
        } else {
2209
            (0, 0)
8✔
2210
        };
2211

2212
        buffer.puts(
8✔
2213
            line_offset,
2214
            code_offset + width_taken,
10✔
2215
            &code[bytes_taken..],
8✔
2216
            ElementStyle::Quotation,
10✔
2217
        );
2218

2219
        if line_len > right {
10✔
2220
            // We have stripped some code/whitespace from the beginning, make it clear.
2221
            let mut char_taken = 0;
2✔
2222
            let mut width_taken_inner = 0;
2✔
2223
            for ch in code.chars().rev() {
2✔
2224
                width_taken_inner += char_width(ch);
4✔
2225
                char_taken += 1;
4✔
2226

2227
                if width_taken_inner >= padding {
2✔
2228
                    break;
2229
                }
2230
            }
2231

2232
            buffer.puts(
4✔
2233
                line_offset,
2234
                code_offset + width_taken + code[bytes_taken..].chars().count() - char_taken,
4✔
2235
                placeholder,
2236
                ElementStyle::LineNumber,
2✔
2237
            );
2238
        }
2239

2240
        buffer.puts(
10✔
2241
            line_offset,
2242
            0,
2243
            &format!("{:>max_line_num_len$}", self.maybe_anonymized(line_index)),
16✔
2244
            ElementStyle::LineNumber,
8✔
2245
        );
2246

2247
        self.draw_col_separator_no_space(buffer, line_offset, width_offset - 2);
6✔
2248

2249
        left
10✔
2250
    }
2251

2252
    fn draw_range(
5✔
2253
        &self,
2254
        buffer: &mut StyledBuffer,
2255
        symbol: char,
2256
        line: usize,
2257
        col_from: usize,
2258
        col_to: usize,
2259
        style: ElementStyle,
2260
    ) {
2261
        for col in col_from..col_to {
10✔
2262
            buffer.putc(line, col, symbol, style);
4✔
2263
        }
2264
    }
2265

2266
    fn draw_multiline_line(
3✔
2267
        &self,
2268
        buffer: &mut StyledBuffer,
2269
        line: usize,
2270
        offset: usize,
2271
        depth: usize,
2272
        style: ElementStyle,
2273
    ) {
2274
        let chr = match (style, self.theme) {
3✔
2275
            (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, OutputTheme::Ascii) => {
2276
                '|'
3✔
2277
            }
2278
            (_, OutputTheme::Ascii) => '|',
1✔
2279
            (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, OutputTheme::Unicode) => {
2280
                '┃'
1✔
2281
            }
2282
            (_, OutputTheme::Unicode) => '│',
1✔
2283
        };
2284
        buffer.putc(line, offset + depth - 1, chr, style);
6✔
2285
    }
2286

2287
    fn col_separator(&self) -> char {
4✔
2288
        match self.theme {
8✔
2289
            OutputTheme::Ascii => '|',
3✔
2290
            OutputTheme::Unicode => '│',
2✔
2291
        }
2292
    }
2293

2294
    fn multi_suggestion_separator(&self) -> &'static str {
×
2295
        match self.theme {
×
2296
            OutputTheme::Ascii => "|",
×
2297
            OutputTheme::Unicode => "├╴",
×
2298
        }
2299
    }
2300

2301
    fn draw_col_separator(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
2✔
2302
        let chr = self.col_separator();
2✔
2303
        buffer.puts(line, col, &format!("{chr} "), ElementStyle::LineNumber);
2✔
2304
    }
2305

2306
    fn draw_col_separator_no_space(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
4✔
2307
        let chr = self.col_separator();
8✔
2308
        self.draw_col_separator_no_space_with_style(
8✔
2309
            buffer,
2310
            chr,
2311
            line,
2312
            col,
2313
            ElementStyle::LineNumber,
7✔
2314
        );
2315
    }
2316

2317
    fn draw_col_separator_start(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
4✔
2318
        match self.theme {
4✔
2319
            OutputTheme::Ascii => {
2320
                self.draw_col_separator_no_space_with_style(
4✔
2321
                    buffer,
2322
                    '|',
2323
                    line,
2324
                    col,
2325
                    ElementStyle::LineNumber,
4✔
2326
                );
2327
            }
2328
            OutputTheme::Unicode => {
2329
                self.draw_col_separator_no_space_with_style(
1✔
2330
                    buffer,
2331
                    '╭',
2332
                    line,
2333
                    col,
2334
                    ElementStyle::LineNumber,
1✔
2335
                );
2336
                self.draw_col_separator_no_space_with_style(
1✔
2337
                    buffer,
2338
                    '╴',
2339
                    line,
2340
                    col + 1,
1✔
2341
                    ElementStyle::LineNumber,
1✔
2342
                );
2343
            }
2344
        }
2345
    }
2346

2347
    fn draw_col_separator_end(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
5✔
2348
        match self.theme {
5✔
2349
            OutputTheme::Ascii => {
2350
                self.draw_col_separator_no_space_with_style(
5✔
2351
                    buffer,
2352
                    '|',
2353
                    line,
2354
                    col,
2355
                    ElementStyle::LineNumber,
5✔
2356
                );
2357
            }
2358
            OutputTheme::Unicode => {
2359
                self.draw_col_separator_no_space_with_style(
2✔
2360
                    buffer,
2361
                    '╰',
2362
                    line,
2363
                    col,
2364
                    ElementStyle::LineNumber,
2✔
2365
                );
2366
                self.draw_col_separator_no_space_with_style(
2✔
2367
                    buffer,
2368
                    '╴',
2369
                    line,
2370
                    col + 1,
2✔
2371
                    ElementStyle::LineNumber,
2✔
2372
                );
2373
            }
2374
        }
2375
    }
2376

2377
    fn draw_col_separator_no_space_with_style(
8✔
2378
        &self,
2379
        buffer: &mut StyledBuffer,
2380
        chr: char,
2381
        line: usize,
2382
        col: usize,
2383
        style: ElementStyle,
2384
    ) {
2385
        buffer.putc(line, col, chr, style);
8✔
2386
    }
2387

2388
    fn maybe_anonymized(&self, line_num: usize) -> Cow<'static, str> {
10✔
2389
        if self.anonymized_line_numbers {
15✔
2390
            Cow::Borrowed(ANONYMIZED_LINE_NUM)
4✔
2391
        } else {
2392
            Cow::Owned(line_num.to_string())
9✔
2393
        }
2394
    }
2395

2396
    fn file_start(&self) -> &'static str {
6✔
2397
        match self.theme {
5✔
2398
            OutputTheme::Ascii => "--> ",
7✔
2399
            OutputTheme::Unicode => " ╭▸ ",
2✔
2400
        }
2401
    }
2402

2403
    fn secondary_file_start(&self) -> &'static str {
3✔
2404
        match self.theme {
3✔
2405
            OutputTheme::Ascii => "::: ",
3✔
2406
            OutputTheme::Unicode => " ⸬  ",
×
2407
        }
2408
    }
2409

2410
    fn draw_note_separator(
3✔
2411
        &self,
2412
        buffer: &mut StyledBuffer,
2413
        line: usize,
2414
        col: usize,
2415
        is_cont: bool,
2416
    ) {
2417
        let chr = match self.theme {
3✔
2418
            OutputTheme::Ascii => "= ",
3✔
2419
            OutputTheme::Unicode if is_cont => "├ ",
2✔
2420
            OutputTheme::Unicode => "╰ ",
1✔
2421
        };
2422
        buffer.puts(line, col, chr, ElementStyle::LineNumber);
3✔
2423
    }
2424

2425
    fn diff(&self) -> char {
2✔
2426
        match self.theme {
2✔
2427
            OutputTheme::Ascii => '~',
2✔
2428
            OutputTheme::Unicode => '±',
×
2429
        }
2430
    }
2431

2432
    fn draw_line_separator(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
3✔
2433
        let (column, dots) = match self.theme {
6✔
2434
            OutputTheme::Ascii => (0, "..."),
3✔
2435
            OutputTheme::Unicode => (col - 2, "‡"),
2✔
2436
        };
2437
        buffer.puts(line, column, dots, ElementStyle::LineNumber);
3✔
2438
    }
2439

2440
    fn margin(&self) -> &'static str {
10✔
2441
        match self.theme {
10✔
2442
            OutputTheme::Ascii => "...",
10✔
2443
            OutputTheme::Unicode => "…",
2✔
2444
        }
2445
    }
2446

2447
    fn underline(&self, is_primary: bool) -> UnderlineParts {
5✔
2448
        //               X0 Y0
2449
        // label_start > ┯━━━━ < underline
2450
        //               │ < vertical_text_line
2451
        //               text
2452

2453
        //    multiline_start_down ⤷ X0 Y0
2454
        //            top_left > ┌───╿──┘ < top_right_flat
2455
        //           top_left > ┏│━━━┙ < top_right
2456
        // multiline_vertical > ┃│
2457
        //                      ┃│   X1 Y1
2458
        //                      ┃│   X2 Y2
2459
        //                      ┃└────╿──┘ < multiline_end_same_line
2460
        //        bottom_left > ┗━━━━━┥ < bottom_right_with_text
2461
        //   multiline_horizontal ^   `X` is a good letter
2462

2463
        // multiline_whole_line > ┏ X0 Y0
2464
        //                        ┃   X1 Y1
2465
        //                        ┗━━━━┛ < multiline_end_same_line
2466

2467
        // multiline_whole_line > ┏ X0 Y0
2468
        //                        ┃ X1 Y1
2469
        //                        ┃  ╿ < multiline_end_up
2470
        //                        ┗━━┛ < bottom_right
2471

2472
        match (self.theme, is_primary) {
7✔
2473
            (OutputTheme::Ascii, true) => UnderlineParts {
2474
                style: ElementStyle::UnderlinePrimary,
2475
                underline: '^',
2476
                label_start: '^',
2477
                vertical_text_line: '|',
2478
                multiline_vertical: '|',
2479
                multiline_horizontal: '_',
2480
                multiline_whole_line: '/',
2481
                multiline_start_down: '^',
2482
                bottom_right: '|',
2483
                top_left: ' ',
2484
                top_right_flat: '^',
2485
                bottom_left: '|',
2486
                multiline_end_up: '^',
2487
                multiline_end_same_line: '^',
2488
                multiline_bottom_right_with_text: '|',
2489
            },
2490
            (OutputTheme::Ascii, false) => UnderlineParts {
2491
                style: ElementStyle::UnderlineSecondary,
2492
                underline: '-',
2493
                label_start: '-',
2494
                vertical_text_line: '|',
2495
                multiline_vertical: '|',
2496
                multiline_horizontal: '_',
2497
                multiline_whole_line: '/',
2498
                multiline_start_down: '-',
2499
                bottom_right: '|',
2500
                top_left: ' ',
2501
                top_right_flat: '-',
2502
                bottom_left: '|',
2503
                multiline_end_up: '-',
2504
                multiline_end_same_line: '-',
2505
                multiline_bottom_right_with_text: '|',
2506
            },
2507
            (OutputTheme::Unicode, true) => UnderlineParts {
2508
                style: ElementStyle::UnderlinePrimary,
2509
                underline: '━',
2510
                label_start: '┯',
2511
                vertical_text_line: '│',
2512
                multiline_vertical: '┃',
2513
                multiline_horizontal: '━',
2514
                multiline_whole_line: '┏',
2515
                multiline_start_down: '╿',
2516
                bottom_right: '┙',
2517
                top_left: '┏',
2518
                top_right_flat: '┛',
2519
                bottom_left: '┗',
2520
                multiline_end_up: '╿',
2521
                multiline_end_same_line: '┛',
2522
                multiline_bottom_right_with_text: '┥',
2523
            },
2524
            (OutputTheme::Unicode, false) => UnderlineParts {
2525
                style: ElementStyle::UnderlineSecondary,
2526
                underline: '─',
2527
                label_start: '┬',
2528
                vertical_text_line: '│',
2529
                multiline_vertical: '│',
2530
                multiline_horizontal: '─',
2531
                multiline_whole_line: '┌',
2532
                multiline_start_down: '│',
2533
                bottom_right: '┘',
2534
                top_left: '┌',
2535
                top_right_flat: '┘',
2536
                bottom_left: '└',
2537
                multiline_end_up: '│',
2538
                multiline_end_same_line: '┘',
2539
                multiline_bottom_right_with_text: '┤',
2540
            },
2541
        }
2542
    }
2543
}
2544

2545
// instead of taking the String length or dividing by 10 while > 0, we multiply a limit by 10 until
2546
// we're higher. If the loop isn't exited by the `return`, the last multiplication will wrap, which
2547
// is OK, because while we cannot fit a higher power of 10 in a usize, the loop will end anyway.
2548
// This is also why we need the max number of decimal digits within a `usize`.
2549
fn num_decimal_digits(num: usize) -> usize {
7✔
2550
    #[cfg(target_pointer_width = "64")]
2551
    const MAX_DIGITS: usize = 20;
2552

2553
    #[cfg(target_pointer_width = "32")]
2554
    const MAX_DIGITS: usize = 10;
2555

2556
    #[cfg(target_pointer_width = "16")]
2557
    const MAX_DIGITS: usize = 5;
2558

2559
    let mut lim = 10;
7✔
2560
    for num_digits in 1..MAX_DIGITS {
11✔
2561
        if num < lim {
7✔
2562
            return num_digits;
5✔
2563
        }
2564
        lim = lim.wrapping_mul(10);
4✔
2565
    }
2566
    MAX_DIGITS
×
2567
}
2568

2569
pub fn str_width(s: &str) -> usize {
7✔
2570
    s.chars().map(char_width).sum()
9✔
2571
}
2572

2573
pub fn char_width(ch: char) -> usize {
7✔
2574
    // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` is. For now,
2575
    // just accept that sometimes the code line will be longer than desired.
2576
    match ch {
9✔
2577
        '\t' => 4,
1✔
2578
        // Keep the following list in sync with `rustc_errors::emitter::OUTPUT_REPLACEMENTS`. These
2579
        // are control points that we replace before printing with a visible codepoint for the sake
2580
        // of being able to point at them with underlines.
2581
        '\u{0000}' | '\u{0001}' | '\u{0002}' | '\u{0003}' | '\u{0004}' | '\u{0005}'
×
2582
        | '\u{0006}' | '\u{0007}' | '\u{0008}' | '\u{000B}' | '\u{000C}' | '\u{000D}'
2583
        | '\u{000E}' | '\u{000F}' | '\u{0010}' | '\u{0011}' | '\u{0012}' | '\u{0013}'
2584
        | '\u{0014}' | '\u{0015}' | '\u{0016}' | '\u{0017}' | '\u{0018}' | '\u{0019}'
2585
        | '\u{001A}' | '\u{001B}' | '\u{001C}' | '\u{001D}' | '\u{001E}' | '\u{001F}'
2586
        | '\u{007F}' | '\u{202A}' | '\u{202B}' | '\u{202D}' | '\u{202E}' | '\u{2066}'
2587
        | '\u{2067}' | '\u{2068}' | '\u{202C}' | '\u{2069}' => 1,
2588
        _ => unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1),
7✔
2589
    }
2590
}
2591

2592
fn num_overlap(
5✔
2593
    a_start: usize,
2594
    a_end: usize,
2595
    b_start: usize,
2596
    b_end: usize,
2597
    inclusive: bool,
2598
) -> bool {
2599
    let extra = usize::from(inclusive);
5✔
2600
    (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start)
4✔
2601
}
2602

2603
fn overlaps(a1: &LineAnnotation<'_>, a2: &LineAnnotation<'_>, padding: usize) -> bool {
8✔
2604
    num_overlap(
2605
        a1.start.display,
8✔
2606
        a1.end.display + padding,
8✔
2607
        a2.start.display,
8✔
2608
        a2.end.display,
8✔
2609
        false,
2610
    )
2611
}
2612

2613
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2614
pub(crate) enum LineAnnotationType {
2615
    /// Annotation under a single line of code
2616
    Singleline,
2617

2618
    // The Multiline type above is replaced with the following three in order
2619
    // to reuse the current label drawing code.
2620
    //
2621
    // Each of these corresponds to one part of the following diagram:
2622
    //
2623
    //     x |   foo(1 + bar(x,
2624
    //       |  _________^              < MultilineStart
2625
    //     x | |             y),        < MultilineLine
2626
    //       | |______________^ label   < MultilineEnd
2627
    //     x |       z);
2628
    /// Annotation marking the first character of a fully shown multiline span
2629
    MultilineStart(usize),
2630
    /// Annotation marking the last character of a fully shown multiline span
2631
    MultilineEnd(usize),
2632
    /// Line at the left enclosing the lines of a fully shown multiline span
2633
    // Just a placeholder for the drawing algorithm, to know that it shouldn't skip the first 4
2634
    // and last 2 lines of code. The actual line is drawn in `emit_message_default` and not in
2635
    // `draw_multiline_line`.
2636
    MultilineLine(usize),
2637
}
2638

2639
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2640
pub(crate) struct LineAnnotation<'a> {
2641
    /// Start column.
2642
    /// Note that it is important that this field goes
2643
    /// first, so that when we sort, we sort orderings by start
2644
    /// column.
2645
    pub start: Loc,
2646

2647
    /// End column within the line (exclusive)
2648
    pub end: Loc,
2649

2650
    /// level
2651
    pub kind: AnnotationKind,
2652

2653
    /// Optional label to display adjacent to the annotation.
2654
    pub label: Option<Cow<'a, str>>,
2655

2656
    /// Is this a single line, multiline or multiline span minimized down to a
2657
    /// smaller span.
2658
    pub annotation_type: LineAnnotationType,
2659

2660
    /// Whether the source code should be highlighted
2661
    pub highlight_source: bool,
2662
}
2663

2664
impl LineAnnotation<'_> {
2665
    pub(crate) fn is_primary(&self) -> bool {
6✔
2666
        self.kind == AnnotationKind::Primary
6✔
2667
    }
2668

2669
    /// Whether this annotation is a vertical line placeholder.
2670
    pub(crate) fn is_line(&self) -> bool {
6✔
2671
        matches!(self.annotation_type, LineAnnotationType::MultilineLine(_))
10✔
2672
    }
2673

2674
    /// Length of this annotation as displayed in the stderr output
2675
    pub(crate) fn len(&self) -> usize {
3✔
2676
        // Account for usize underflows
2677
        self.end.display.abs_diff(self.start.display)
3✔
2678
    }
2679

2680
    pub(crate) fn has_label(&self) -> bool {
8✔
2681
        if let Some(label) = &self.label {
12✔
2682
            // Consider labels with no text as effectively not being there
2683
            // to avoid weird output with unnecessary vertical lines, like:
2684
            //
2685
            //     X | fn foo(x: u32) {
2686
            //       | -------^------
2687
            //       | |      |
2688
            //       | |
2689
            //       |
2690
            //
2691
            // Note that this would be the complete output users would see.
2692
            !label.is_empty()
4✔
2693
        } else {
2694
            false
7✔
2695
        }
2696
    }
2697

2698
    pub(crate) fn takes_space(&self) -> bool {
2✔
2699
        // Multiline annotations always have to keep vertical space.
2700
        matches!(
2✔
2701
            self.annotation_type,
2✔
2702
            LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
2703
        )
2704
    }
2705
}
2706

2707
#[derive(Clone, Copy, Debug)]
2708
pub(crate) enum DisplaySuggestion {
2709
    Underline,
2710
    Diff,
2711
    None,
2712
    Add,
2713
}
2714

2715
// We replace some characters so the CLI output is always consistent and underlines aligned.
2716
// Keep the following list in sync with `rustc_span::char_width`.
2717
const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[
2718
    // In terminals without Unicode support the following will be garbled, but in *all* terminals
2719
    // the underlying codepoint will be as well. We could gate this replacement behind a "unicode
2720
    // support" gate.
2721
    ('\0', "␀"),
2722
    ('\u{0001}', "␁"),
2723
    ('\u{0002}', "␂"),
2724
    ('\u{0003}', "␃"),
2725
    ('\u{0004}', "␄"),
2726
    ('\u{0005}', "␅"),
2727
    ('\u{0006}', "␆"),
2728
    ('\u{0007}', "␇"),
2729
    ('\u{0008}', "␈"),
2730
    ('\t', "    "), // We do our own tab replacement
2731
    ('\u{000b}', "␋"),
2732
    ('\u{000c}', "␌"),
2733
    ('\u{000d}', "␍"),
2734
    ('\u{000e}', "␎"),
2735
    ('\u{000f}', "␏"),
2736
    ('\u{0010}', "␐"),
2737
    ('\u{0011}', "␑"),
2738
    ('\u{0012}', "␒"),
2739
    ('\u{0013}', "␓"),
2740
    ('\u{0014}', "␔"),
2741
    ('\u{0015}', "␕"),
2742
    ('\u{0016}', "␖"),
2743
    ('\u{0017}', "␗"),
2744
    ('\u{0018}', "␘"),
2745
    ('\u{0019}', "␙"),
2746
    ('\u{001a}', "␚"),
2747
    ('\u{001b}', "␛"),
2748
    ('\u{001c}', "␜"),
2749
    ('\u{001d}', "␝"),
2750
    ('\u{001e}', "␞"),
2751
    ('\u{001f}', "␟"),
2752
    ('\u{007f}', "␡"),
2753
    ('\u{200d}', ""), // Replace ZWJ for consistent terminal output of grapheme clusters.
2754
    ('\u{202a}', "�"), // The following unicode text flow control characters are inconsistently
2755
    ('\u{202b}', "�"), // supported across CLIs and can cause confusion due to the bytes on disk
2756
    ('\u{202c}', "�"), // not corresponding to the visible source code, so we replace them always.
2757
    ('\u{202d}', "�"),
2758
    ('\u{202e}', "�"),
2759
    ('\u{2066}', "�"),
2760
    ('\u{2067}', "�"),
2761
    ('\u{2068}', "�"),
2762
    ('\u{2069}', "�"),
2763
];
2764

2765
pub(crate) fn normalize_whitespace(s: &str) -> String {
7✔
2766
    // Scan the input string for a character in the ordered table above.
2767
    // If it's present, replace it with its alternative string (it can be more than 1 char!).
2768
    // Otherwise, retain the input char.
2769
    s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
11✔
2770
        match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) {
32✔
2771
            Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1),
2✔
2772
            _ => s.push(c),
16✔
2773
        }
2774
        s
6✔
2775
    })
2776
}
2777

2778
#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)]
2779
pub(crate) enum ElementStyle {
2780
    MainHeaderMsg,
2781
    HeaderMsg,
2782
    LineAndColumn,
2783
    LineNumber,
2784
    Quotation,
2785
    UnderlinePrimary,
2786
    UnderlineSecondary,
2787
    LabelPrimary,
2788
    LabelSecondary,
2789
    NoStyle,
2790
    Level(LevelInner),
2791
    Addition,
2792
    Removal,
2793
}
2794

2795
impl ElementStyle {
2796
    fn color_spec(&self, level: &Level<'_>, stylesheet: &Stylesheet) -> Style {
3✔
2797
        match self {
3✔
2798
            ElementStyle::Addition => stylesheet.addition,
4✔
2799
            ElementStyle::Removal => stylesheet.removal,
3✔
2800
            ElementStyle::LineAndColumn => stylesheet.none,
4✔
2801
            ElementStyle::LineNumber => stylesheet.line_num,
4✔
2802
            ElementStyle::Quotation => stylesheet.none,
5✔
2803
            ElementStyle::MainHeaderMsg => stylesheet.emphasis,
4✔
2804
            ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary => level.style(stylesheet),
4✔
2805
            ElementStyle::UnderlineSecondary | ElementStyle::LabelSecondary => stylesheet.context,
3✔
2806
            ElementStyle::HeaderMsg | ElementStyle::NoStyle => stylesheet.none,
4✔
2807
            ElementStyle::Level(lvl) => lvl.style(stylesheet),
4✔
2808
        }
2809
    }
2810
}
2811

2812
#[derive(Debug, Clone, Copy)]
2813
struct UnderlineParts {
2814
    style: ElementStyle,
2815
    underline: char,
2816
    label_start: char,
2817
    vertical_text_line: char,
2818
    multiline_vertical: char,
2819
    multiline_horizontal: char,
2820
    multiline_whole_line: char,
2821
    multiline_start_down: char,
2822
    bottom_right: char,
2823
    top_left: char,
2824
    top_right_flat: char,
2825
    bottom_left: char,
2826
    multiline_end_up: char,
2827
    multiline_end_same_line: char,
2828
    multiline_bottom_right_with_text: char,
2829
}
2830

2831
/// Whether the original and suggested code are the same.
2832
pub(crate) fn is_different(sm: &SourceMap<'_>, suggested: &str, range: Range<usize>) -> bool {
3✔
2833
    match sm.span_to_snippet(range) {
4✔
2834
        Some(s) => s != suggested,
3✔
2835
        None => true,
×
2836
    }
2837
}
2838

2839
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2840
pub enum OutputTheme {
2841
    Ascii,
2842
    Unicode,
2843
}
2844

2845
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2846
enum TitleStyle {
2847
    MainHeader,
2848
    Header,
2849
    Secondary,
2850
}
2851

2852
fn max_line_number(groups: &[Group<'_>]) -> usize {
7✔
2853
    groups
8✔
2854
        .iter()
2855
        .map(|v| {
8✔
2856
            v.elements
7✔
2857
                .iter()
2858
                .map(|s| match s {
13✔
2859
                    Element::Title(_) | Element::Origin(_) | Element::Padding(_) => 0,
7✔
2860
                    Element::Cause(cause) => {
7✔
2861
                        let end = cause
23✔
2862
                            .markers
2863
                            .iter()
2864
                            .map(|a| a.span.end)
14✔
2865
                            .max()
2866
                            .unwrap_or(cause.source.len())
6✔
2867
                            .min(cause.source.len());
6✔
2868

2869
                        cause.line_start + newline_count(&cause.source[..end])
13✔
2870
                    }
2871
                    Element::Suggestion(suggestion) => {
3✔
2872
                        let end = suggestion
9✔
2873
                            .markers
2874
                            .iter()
2875
                            .map(|a| a.span.end)
6✔
2876
                            .max()
2877
                            .unwrap_or(suggestion.source.len())
3✔
2878
                            .min(suggestion.source.len());
3✔
2879

2880
                        suggestion.line_start + newline_count(&suggestion.source[..end])
6✔
2881
                    }
2882
                })
2883
                .max()
2884
                .unwrap_or(1)
2885
        })
2886
        .max()
2887
        .unwrap_or(1)
2888
}
2889

2890
fn newline_count(body: &str) -> usize {
8✔
2891
    #[cfg(feature = "simd")]
2892
    {
2893
        memchr::memchr_iter(b'\n', body.as_bytes())
2894
            .count()
2895
            .saturating_sub(1)
2896
    }
2897
    #[cfg(not(feature = "simd"))]
2898
    {
2899
        body.lines().count().saturating_sub(1)
6✔
2900
    }
2901
}
2902

2903
#[cfg(test)]
2904
mod test {
2905
    use super::OUTPUT_REPLACEMENTS;
2906
    use snapbox::IntoData;
2907

2908
    fn format_replacements(replacements: Vec<(char, &str)>) -> String {
2909
        replacements
2910
            .into_iter()
2911
            .map(|r| format!("    {r:?}"))
2912
            .collect::<Vec<_>>()
2913
            .join("\n")
2914
    }
2915

2916
    #[test]
2917
    /// The [`OUTPUT_REPLACEMENTS`] array must be sorted (for binary search to
2918
    /// work) and must contain no duplicate entries
2919
    fn ensure_output_replacements_is_sorted() {
2920
        let mut expected = OUTPUT_REPLACEMENTS.to_owned();
2921
        expected.sort_by_key(|r| r.0);
2922
        expected.dedup_by_key(|r| r.0);
2923
        let expected = format_replacements(expected);
2924
        let actual = format_replacements(OUTPUT_REPLACEMENTS.to_owned());
2925
        snapbox::assert_data_eq!(actual, expected.into_data().raw());
2926
    }
2927
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc