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

rust-lang / annotate-snippets-rs / 15861679393

24 Jun 2025 09:16PM UTC coverage: 86.51% (-0.1%) from 86.621%
15861679393

Pull #221

github

web-flow
Merge 156604040 into a81dc31d2
Pull Request #221: refactor: Use Origin for Snippet::origin

40 of 49 new or added lines in 2 files covered. (81.63%)

6 existing lines in 1 file now uncovered.

1398 of 1616 relevant lines covered (86.51%)

4.44 hits per line

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

87.39
/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 [`Message`]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
//! Level::ERROR
22
//!     .header("unresolved import `baz::zed`")
23
//!     .id("E0432")
24
//!     .group(
25
//!         Group::new().element(
26
//!             Snippet::source(source)
27
//!                 .origin("temp.rs")
28
//!                 .line_start(1)
29
//!                 .fold(true)
30
//!                 .annotation(
31
//!                     AnnotationKind::Primary
32
//!                         .span(10..13)
33
//!                          .label("could not find `zed` in `baz`"),
34
//!                 )
35
//!         )
36
//!     );
37
//! ```
38

39
mod margin;
40
pub(crate) mod source_map;
41
mod styled_buffer;
42
pub(crate) mod stylesheet;
43

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

59
const ANONYMIZED_LINE_NUM: &str = "LL";
60
pub const DEFAULT_TERM_WIDTH: usize = 140;
61

62
/// A renderer for [`Message`]s
63
#[derive(Clone, Debug)]
64
pub struct Renderer {
65
    anonymized_line_numbers: bool,
66
    term_width: usize,
67
    theme: OutputTheme,
68
    stylesheet: Stylesheet,
69
    short_message: bool,
70
}
71

72
impl Renderer {
73
    /// No terminal styling
74
    pub const fn plain() -> Self {
8✔
75
        Self {
76
            anonymized_line_numbers: false,
77
            term_width: DEFAULT_TERM_WIDTH,
78
            theme: OutputTheme::Ascii,
79
            stylesheet: Stylesheet::plain(),
8✔
80
            short_message: false,
81
        }
82
    }
83

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

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

142
    pub const fn short_message(mut self, short_message: bool) -> Self {
1✔
143
        self.short_message = short_message;
1✔
144
        self
1✔
145
    }
146

147
    // Set the terminal width
148
    pub const fn term_width(mut self, term_width: usize) -> Self {
2✔
149
        self.term_width = term_width;
2✔
150
        self
2✔
151
    }
152

153
    pub const fn theme(mut self, output_theme: OutputTheme) -> Self {
2✔
154
        self.theme = output_theme;
2✔
155
        self
2✔
156
    }
157

158
    /// Set the output style for `error`
159
    pub const fn error(mut self, style: Style) -> Self {
×
160
        self.stylesheet.error = style;
×
161
        self
×
162
    }
163

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

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

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

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

188
    /// Set the output style for line numbers
189
    pub const fn line_num(mut self, style: Style) -> Self {
×
190
        self.stylesheet.line_num = style;
×
191
        self
×
192
    }
193

194
    /// Set the output style for emphasis
195
    pub const fn emphasis(mut self, style: Style) -> Self {
×
196
        self.stylesheet.emphasis = style;
×
197
        self
×
198
    }
199

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

207
impl Renderer {
208
    pub fn render(&self, mut message: Message<'_>) -> String {
5✔
209
        if self.short_message {
8✔
210
            self.render_short_message(message).unwrap()
3✔
211
        } else {
212
            let max_line_num_len = if self.anonymized_line_numbers {
4✔
213
                ANONYMIZED_LINE_NUM.len()
8✔
214
            } else {
215
                let n = message.max_line_number();
15✔
216
                num_decimal_digits(n)
5✔
217
            };
218
            let title = message.groups.remove(0).elements.remove(0);
12✔
219
            if let Some(first) = message.groups.first_mut() {
6✔
220
                first.elements.insert(0, title);
14✔
221
            } else {
222
                message.groups.push(Group::new().element(title));
2✔
223
            }
224
            self.render_message(message, max_line_num_len).unwrap()
13✔
225
        }
226
    }
227

228
    fn render_message(
7✔
229
        &self,
230
        message: Message<'_>,
231
        max_line_num_len: usize,
232
    ) -> Result<String, fmt::Error> {
233
        let mut out_string = String::new();
8✔
234

235
        let og_primary_origin = message
21✔
236
            .groups
237
            .iter()
238
            .find_map(|group| {
7✔
239
                group.elements.iter().find_map(|s| match &s {
24✔
240
                    Element::Cause(cause) => {
8✔
241
                        if cause.markers.iter().any(|m| m.kind.is_primary()) {
22✔
242
                            Some(cause.origin.as_ref())
7✔
243
                        } else {
244
                            None
2✔
245
                        }
246
                    }
247
                    Element::Origin(origin) => {
×
248
                        if origin.primary {
×
NEW
249
                            Some(Some(origin))
×
250
                        } else {
251
                            None
×
252
                        }
253
                    }
254
                    _ => None,
6✔
255
                })
256
            })
257
            .unwrap_or(
258
                message
3✔
259
                    .groups
260
                    .iter()
261
                    .find_map(|group| {
7✔
262
                        group.elements.iter().find_map(|s| match &s {
13✔
263
                            Element::Cause(cause) => Some(cause.origin.as_ref()),
3✔
NEW
264
                            Element::Origin(origin) => Some(Some(origin)),
×
265
                            _ => None,
7✔
266
                        })
267
                    })
268
                    .unwrap_or_default(),
269
            );
270
        let group_len = message.groups.len();
3✔
271
        for (g, group) in message.groups.iter().enumerate() {
7✔
272
            let mut buffer = StyledBuffer::new();
4✔
273
            let primary_origin = group
16✔
274
                .elements
275
                .iter()
276
                .find_map(|s| match &s {
12✔
277
                    Element::Cause(cause) => {
6✔
278
                        if cause.markers.iter().any(|m| m.kind.is_primary()) {
23✔
279
                            Some(cause.origin.as_ref())
6✔
280
                        } else {
281
                            None
3✔
282
                        }
283
                    }
284
                    Element::Origin(origin) => {
1✔
285
                        if origin.primary {
2✔
286
                            Some(Some(origin))
1✔
287
                        } else {
288
                            None
×
289
                        }
290
                    }
291
                    _ => None,
6✔
292
                })
293
                .unwrap_or(
294
                    group
6✔
295
                        .elements
296
                        .iter()
297
                        .find_map(|s| match &s {
12✔
298
                            Element::Cause(cause) => Some(cause.origin.as_ref()),
7✔
299
                            Element::Origin(origin) => Some(Some(origin)),
1✔
300
                            _ => None,
5✔
301
                        })
302
                        .unwrap_or_default(),
303
                );
304
            let level = group
7✔
305
                .elements
306
                .iter()
307
                .find_map(|s| match &s {
12✔
308
                    Element::Title(title) => Some(title.level.clone()),
5✔
309
                    _ => None,
×
310
                })
311
                .unwrap_or(Level::ERROR);
312
            let mut source_map_annotated_lines = VecDeque::new();
7✔
313
            let mut max_depth = 0;
6✔
314
            for e in &group.elements {
11✔
315
                if let Element::Cause(cause) = e {
16✔
316
                    let source_map = SourceMap::new(cause.source, cause.line_start);
6✔
317
                    let (depth, annotated_lines) =
11✔
318
                        source_map.annotated_lines(cause.markers.clone(), cause.fold);
319
                    max_depth = max(max_depth, depth);
6✔
320
                    source_map_annotated_lines.push_back((source_map, annotated_lines));
3✔
321
                }
322
            }
323
            let mut message_iter = group.elements.iter().enumerate().peekable();
4✔
324
            let mut last_was_suggestion = false;
4✔
325
            while let Some((i, section)) = message_iter.next() {
3✔
326
                let peek = message_iter.peek().map(|(_, s)| s).copied();
12✔
327
                match &section {
3✔
328
                    Element::Title(title) => {
3✔
329
                        let title_style = match (i == 0, g == 0) {
8✔
330
                            (true, true) => TitleStyle::MainHeader,
4✔
331
                            (true, false) => TitleStyle::Header,
2✔
332
                            (false, _) => TitleStyle::Secondary,
2✔
333
                        };
334
                        let buffer_msg_line_offset = buffer.num_lines();
7✔
335
                        self.render_title(
4✔
336
                            &mut buffer,
337
                            title,
338
                            max_line_num_len,
339
                            title_style,
3✔
340
                            message.id.as_ref().and_then(|id| {
7✔
341
                                if g == 0 && i == 0 {
6✔
342
                                    Some(id)
4✔
343
                                } else {
344
                                    None
2✔
345
                                }
346
                            }),
347
                            matches!(peek, Some(Element::Title(_))),
3✔
348
                            buffer_msg_line_offset,
349
                        );
350
                        last_was_suggestion = false;
4✔
351
                    }
352
                    Element::Cause(cause) => {
4✔
353
                        if let Some((source_map, annotated_lines)) =
6✔
354
                            source_map_annotated_lines.pop_front()
355
                        {
356
                            self.render_snippet_annotations(
6✔
357
                                &mut buffer,
358
                                max_line_num_len,
359
                                cause,
360
                                primary_origin,
361
                                &source_map,
362
                                &annotated_lines,
4✔
363
                                max_depth,
6✔
364
                                peek.is_some() || (g == 0 && group_len > 1),
4✔
365
                            );
366

367
                            if g == 0 {
3✔
368
                                let current_line = buffer.num_lines();
6✔
369
                                match peek {
3✔
370
                                    Some(Element::Title(level))
2✔
371
                                        if level.level.name != Some(None) =>
2✔
372
                                    {
373
                                        self.draw_col_separator_no_space(
4✔
374
                                            &mut buffer,
375
                                            current_line,
376
                                            max_line_num_len + 1,
2✔
377
                                        );
378
                                    }
379

380
                                    None if group_len > 1 => self.draw_col_separator_end(
7✔
381
                                        &mut buffer,
382
                                        current_line,
383
                                        max_line_num_len + 1,
2✔
384
                                    ),
385
                                    _ => {}
386
                                }
387
                            }
388
                        }
389

390
                        last_was_suggestion = false;
3✔
391
                    }
392
                    Element::Suggestion(suggestion) => {
2✔
393
                        let source_map = SourceMap::new(suggestion.source, suggestion.line_start);
2✔
394
                        self.emit_suggestion_default(
2✔
395
                            &mut buffer,
396
                            suggestion,
397
                            max_line_num_len,
398
                            &source_map,
399
                            primary_origin.or(og_primary_origin),
2✔
400
                            last_was_suggestion,
2✔
401
                        );
402
                        last_was_suggestion = true;
2✔
403
                    }
404

405
                    Element::Origin(origin) => {
1✔
406
                        let buffer_msg_line_offset = buffer.num_lines();
2✔
407
                        self.render_origin(
1✔
408
                            &mut buffer,
409
                            max_line_num_len,
410
                            origin,
411
                            buffer_msg_line_offset,
412
                        );
413
                        last_was_suggestion = false;
1✔
414
                    }
415
                    Element::Padding(_) => {
416
                        let current_line = buffer.num_lines();
2✔
417
                        self.draw_col_separator_no_space(
2✔
418
                            &mut buffer,
419
                            current_line,
420
                            max_line_num_len + 1,
1✔
421
                        );
422
                    }
423
                }
424
                if g == 0
4✔
425
                    && (matches!(section, Element::Origin(_))
4✔
426
                        || (matches!(section, Element::Title(_)) && i == 0)
4✔
427
                        || matches!(section, Element::Title(level) if level.level.name == Some(None)))
3✔
428
                {
429
                    let current_line = buffer.num_lines();
8✔
430
                    if peek.is_none() && group_len > 1 {
4✔
431
                        self.draw_col_separator_end(
2✔
432
                            &mut buffer,
433
                            current_line,
434
                            max_line_num_len + 1,
1✔
435
                        );
436
                    } else if matches!(peek, Some(Element::Title(level)) if level.level.name != Some(None))
8✔
437
                    {
438
                        self.draw_col_separator_no_space(
2✔
439
                            &mut buffer,
440
                            current_line,
441
                            max_line_num_len + 1,
1✔
442
                        );
443
                    }
444
                }
445
            }
446
            buffer.render(level, &self.stylesheet, &mut out_string)?;
7✔
447
            if g != group_len - 1 {
5✔
448
                use std::fmt::Write;
449

450
                writeln!(out_string)?;
2✔
451
            }
452
        }
453
        Ok(out_string)
5✔
454
    }
455

456
    fn render_short_message(&self, mut message: Message<'_>) -> Result<String, fmt::Error> {
1✔
457
        let mut buffer = StyledBuffer::new();
1✔
458

459
        let Element::Title(title) = message.groups.remove(0).elements.remove(0) else {
3✔
460
            panic!(
×
461
                "Expected first element to be a Title, got: {:?}",
462
                message.groups
463
            );
464
        };
465

466
        let mut labels = None;
1✔
467

468
        if let Some(Element::Cause(cause)) = message.groups.first().and_then(|group| {
5✔
469
            group
1✔
470
                .elements
471
                .iter()
472
                .find(|e| matches!(e, Element::Cause(_)))
2✔
473
        }) {
474
            let labels_inner = cause
2✔
475
                .markers
476
                .iter()
477
                .filter_map(|ann| match ann.label {
2✔
478
                    Some(msg) if ann.kind.is_primary() => {
2✔
479
                        if !msg.trim().is_empty() {
2✔
480
                            Some(msg.to_owned())
1✔
481
                        } else {
482
                            None
×
483
                        }
484
                    }
485
                    _ => None,
1✔
486
                })
487
                .collect::<Vec<_>>()
488
                .join(", ");
489
            if !labels_inner.is_empty() {
2✔
490
                labels = Some(labels_inner);
1✔
491
            }
492

493
            if let Some(mut origin) = cause.origin.clone() {
2✔
494
                origin.primary = true;
1✔
495

496
                if origin.line.is_none() || origin.char_column.is_none() {
2✔
497
                    let source_map = SourceMap::new(cause.source, cause.line_start);
1✔
498
                    let (_depth, annotated_lines) =
2✔
499
                        source_map.annotated_lines(cause.markers.clone(), cause.fold);
500

501
                    if let Some(primary_line) = annotated_lines
3✔
502
                        .iter()
503
                        .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
2✔
504
                        .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
3✔
505
                    {
506
                        origin.line = Some(primary_line.line_index);
1✔
507
                        if let Some(first_annotation) = primary_line
3✔
508
                            .annotations
509
                            .iter()
510
                            .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
2✔
511
                        {
512
                            origin.char_column = Some(first_annotation.start.char + 1);
1✔
513
                        }
514
                    }
515
                }
516

517
                self.render_origin(&mut buffer, 0, &origin, 0);
1✔
518
                buffer.append(0, ": ", ElementStyle::LineAndColumn);
1✔
519
            }
520
        }
521

522
        self.render_title(
2✔
523
            &mut buffer,
524
            &title,
525
            0, // No line numbers in short messages
526
            TitleStyle::MainHeader,
1✔
527
            message.id.as_ref(),
2✔
528
            false,
529
            0,
530
        );
531

532
        if let Some(labels) = labels {
2✔
533
            buffer.append(0, &format!(": {labels}"), ElementStyle::NoStyle);
4✔
534
        }
535

536
        let mut out_string = String::new();
2✔
537
        buffer.render(title.level, &self.stylesheet, &mut out_string)?;
4✔
538

539
        Ok(out_string)
2✔
540
    }
541

542
    #[allow(clippy::too_many_arguments)]
543
    fn render_title(
3✔
544
        &self,
545
        buffer: &mut StyledBuffer,
546
        title: &Title<'_>,
547
        max_line_num_len: usize,
548
        title_style: TitleStyle,
549
        id: Option<&&str>,
550
        is_cont: bool,
551
        buffer_msg_line_offset: usize,
552
    ) {
553
        if title_style == TitleStyle::Secondary {
4✔
554
            // This is a secondary message with no span info
555
            for _ in 0..max_line_num_len {
4✔
556
                buffer.prepend(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
2✔
557
            }
558

559
            if title.level.name != Some(None) {
2✔
560
                self.draw_note_separator(
2✔
561
                    buffer,
562
                    buffer_msg_line_offset,
563
                    max_line_num_len + 1,
2✔
564
                    is_cont,
565
                );
566
                buffer.append(
2✔
567
                    buffer_msg_line_offset,
568
                    title.level.as_str(),
2✔
569
                    ElementStyle::MainHeaderMsg,
2✔
570
                );
571
                buffer.append(buffer_msg_line_offset, ": ", ElementStyle::NoStyle);
2✔
572
            }
573

574
            let printed_lines =
2✔
575
                self.msgs_to_buffer(buffer, title.title, max_line_num_len, "note", None);
576
            if is_cont && matches!(self.theme, OutputTheme::Unicode) {
6✔
577
                // There's another note after this one, associated to the subwindow above.
578
                // We write additional vertical lines to join them:
579
                //   ╭▸ test.rs:3:3
580
                //   │
581
                // 3 │   code
582
                //   │   ━━━━
583
                //   │
584
                //   ├ note: foo
585
                //   │       bar
586
                //   ╰ note: foo
587
                //           bar
588
                for i in buffer_msg_line_offset + 1..=printed_lines {
2✔
589
                    self.draw_col_separator_no_space(buffer, i, max_line_num_len + 1);
1✔
590
                }
591
            }
592
        } else {
593
            let mut label_width = 0;
5✔
594

595
            if title.level.name != Some(None) {
5✔
596
                buffer.append(
4✔
597
                    buffer_msg_line_offset,
598
                    title.level.as_str(),
5✔
599
                    ElementStyle::Level(title.level.level),
5✔
600
                );
601
            }
602
            label_width += title.level.as_str().len();
8✔
603
            if let Some(id) = id {
15✔
604
                buffer.append(
4✔
605
                    buffer_msg_line_offset,
606
                    "[",
607
                    ElementStyle::Level(title.level.level),
4✔
608
                );
609
                buffer.append(
4✔
610
                    buffer_msg_line_offset,
611
                    id,
4✔
612
                    ElementStyle::Level(title.level.level),
4✔
613
                );
614
                buffer.append(
4✔
615
                    buffer_msg_line_offset,
616
                    "]",
617
                    ElementStyle::Level(title.level.level),
4✔
618
                );
619
                label_width += 2 + id.len();
8✔
620
            }
621
            let header_style = match title_style {
4✔
622
                TitleStyle::MainHeader => {
623
                    if self.short_message {
10✔
624
                        ElementStyle::NoStyle
2✔
625
                    } else {
626
                        ElementStyle::MainHeaderMsg
5✔
627
                    }
628
                }
629
                TitleStyle::Header => ElementStyle::HeaderMsg,
2✔
630
                TitleStyle::Secondary => unreachable!(),
631
            };
632
            if title.level.name != Some(None) {
8✔
633
                buffer.append(buffer_msg_line_offset, ": ", header_style);
5✔
634
                label_width += 2;
4✔
635
            }
636
            if !title.title.is_empty() {
4✔
637
                for (line, text) in normalize_whitespace(title.title).lines().enumerate() {
14✔
638
                    buffer.append(
7✔
639
                        buffer_msg_line_offset + line,
6✔
640
                        &format!(
7✔
641
                            "{}{}",
642
                            if line == 0 {
8✔
643
                                String::new()
13✔
644
                            } else {
645
                                " ".repeat(label_width)
2✔
646
                            },
647
                            text
648
                        ),
649
                        header_style,
650
                    );
651
                }
652
            }
653
        }
654
    }
655

656
    /// Adds a left margin to every line but the first, given a padding length and the label being
657
    /// displayed, keeping the provided highlighting.
658
    fn msgs_to_buffer(
2✔
659
        &self,
660
        buffer: &mut StyledBuffer,
661
        title: &str,
662
        padding: usize,
663
        label: &str,
664
        override_style: Option<ElementStyle>,
665
    ) -> usize {
666
        // The extra 5 ` ` is padding that's always needed to align to the `note: `:
667
        //
668
        //   error: message
669
        //     --> file.rs:13:20
670
        //      |
671
        //   13 |     <CODE>
672
        //      |      ^^^^
673
        //      |
674
        //      = note: multiline
675
        //              message
676
        //   ++^^^----xx
677
        //    |  |   | |
678
        //    |  |   | magic `2`
679
        //    |  |   length of label
680
        //    |  magic `3`
681
        //    `max_line_num_len`
682
        let padding = " ".repeat(padding + label.len() + 5);
2✔
683

684
        let mut line_number = buffer.num_lines().saturating_sub(1);
4✔
685

686
        // Provided the following diagnostic message:
687
        //
688
        //     let msgs = vec![
689
        //       ("
690
        //       ("highlighted multiline\nstring to\nsee how it ", Style::NoStyle),
691
        //       ("looks", Style::Highlight),
692
        //       ("with\nvery ", Style::NoStyle),
693
        //       ("weird", Style::Highlight),
694
        //       (" formats\n", Style::NoStyle),
695
        //       ("see?", Style::Highlight),
696
        //     ];
697
        //
698
        // the expected output on a note is (* surround the highlighted text)
699
        //
700
        //        = note: highlighted multiline
701
        //                string to
702
        //                see how it *looks* with
703
        //                very *weird* formats
704
        //                see?
705
        let style = if let Some(override_style) = override_style {
3✔
706
            override_style
×
707
        } else {
708
            ElementStyle::NoStyle
4✔
709
        };
710
        let lines = title.split('\n').collect::<Vec<_>>();
4✔
711
        if lines.len() > 1 {
8✔
712
            for (i, line) in lines.iter().enumerate() {
4✔
713
                if i != 0 {
2✔
714
                    line_number += 1;
2✔
715
                    buffer.append(line_number, &padding, ElementStyle::NoStyle);
4✔
716
                }
717
                buffer.append(line_number, line, style);
4✔
718
            }
719
        } else {
720
            buffer.append(line_number, title, style);
8✔
721
        }
722
        line_number
4✔
723
    }
724

725
    fn render_origin(
7✔
726
        &self,
727
        buffer: &mut StyledBuffer,
728
        max_line_num_len: usize,
729
        origin: &Origin<'_>,
730
        buffer_msg_line_offset: usize,
731
    ) {
732
        if origin.primary && !self.short_message {
16✔
733
            buffer.prepend(
4✔
734
                buffer_msg_line_offset,
735
                self.file_start(),
7✔
736
                ElementStyle::LineNumber,
8✔
737
            );
738
        } else if !self.short_message {
3✔
739
            // if !origin.standalone {
740
            //     // Add spacing line, as shown:
741
            //     //   --> $DIR/file:54:15
742
            //     //    |
743
            //     // LL |         code
744
            //     //    |         ^^^^
745
            //     //    | (<- It prints *this* line)
746
            //     //   ::: $DIR/other_file.rs:15:5
747
            //     //    |
748
            //     // LL |     code
749
            //     //    |     ----
750
            //     self.draw_col_separator_no_space(
751
            //         buffer,
752
            //         buffer_msg_line_offset,
753
            //         max_line_num_len + 1,
754
            //     );
755
            //
756
            //     buffer_msg_line_offset += 1;
757
            // }
758
            // Then, the secondary file indicator
759
            buffer.prepend(
3✔
760
                buffer_msg_line_offset,
761
                self.secondary_file_start(),
3✔
762
                ElementStyle::LineNumber,
3✔
763
            );
764
        }
765

766
        let str = match (&origin.line, &origin.char_column) {
10✔
767
            (Some(line), Some(col)) => {
5✔
768
                format!("{}:{}:{}", origin.origin, line, col)
7✔
769
            }
770
            (Some(line), None) => format!("{}:{}", origin.origin, line),
1✔
771
            _ => origin.origin.to_owned(),
1✔
772
        };
773

774
        buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn);
12✔
775
        if !self.short_message {
5✔
776
            for _ in 0..max_line_num_len {
11✔
777
                buffer.prepend(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
7✔
778
            }
779
        }
780
    }
781

782
    #[allow(clippy::too_many_arguments)]
783
    fn render_snippet_annotations(
5✔
784
        &self,
785
        buffer: &mut StyledBuffer,
786
        max_line_num_len: usize,
787
        snippet: &Snippet<'_, Annotation<'_>>,
788
        primary_origin: Option<&Origin<'_>>,
789
        sm: &SourceMap<'_>,
790
        annotated_lines: &[AnnotatedLineInfo<'_>],
791
        multiline_depth: usize,
792
        is_cont: bool,
793
    ) {
794
        if let Some(mut origin) = snippet.origin.clone() {
4✔
795
            // print out the span location and spacer before we print the annotated source
796
            // to do this, we need to know if this span will be primary
797
            let is_primary = origin.primary || primary_origin == Some(&origin);
7✔
798

799
            if is_primary {
4✔
800
                origin.primary = true;
4✔
801

802
                if origin.line.is_none() || origin.char_column.is_none() {
4✔
803
                    if let Some(primary_line) = annotated_lines
8✔
804
                        .iter()
805
                        .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
7✔
806
                        .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
12✔
807
                    {
808
                        if origin.line.is_none() {
11✔
809
                            origin.line = Some(primary_line.line_index);
5✔
810
                        }
811

812
                        // Always set the char column, as it is either `None`
813
                        // or we set the line and now need to set the column on
814
                        // that line.
815
                        if let Some(first_annotation) = primary_line
17✔
816
                            .annotations
817
                            .iter()
818
                            .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
11✔
819
                        {
820
                            origin.char_column = Some(first_annotation.start.char + 1);
6✔
821
                        }
822
                    }
823
                }
824
            } else {
825
                let buffer_msg_line_offset = buffer.num_lines();
3✔
826
                // Add spacing line, as shown:
827
                //   --> $DIR/file:54:15
828
                //    |
829
                // LL |         code
830
                //    |         ^^^^
831
                //    | (<- It prints *this* line)
832
                //   ::: $DIR/other_file.rs:15:5
833
                //    |
834
                // LL |     code
835
                //    |     ----
836
                self.draw_col_separator_no_space(
3✔
837
                    buffer,
838
                    buffer_msg_line_offset,
839
                    max_line_num_len + 1,
3✔
840
                );
841
                if origin.line.is_none() {
3✔
842
                    if let Some(first_line) = annotated_lines.first() {
3✔
843
                        origin.line = Some(first_line.line_index);
3✔
844
                        if let Some(first_annotation) = first_line.annotations.first() {
5✔
845
                            origin.char_column = Some(first_annotation.start.char + 1);
2✔
846
                        }
847
                    }
848
                }
849
            }
850
            let buffer_msg_line_offset = buffer.num_lines();
7✔
851
            self.render_origin(buffer, max_line_num_len, &origin, buffer_msg_line_offset);
5✔
852
        }
853

854
        // Put in the spacer between the location and annotated source
855
        let buffer_msg_line_offset = buffer.num_lines();
9✔
856
        self.draw_col_separator_no_space(buffer, buffer_msg_line_offset, max_line_num_len + 1);
14✔
857

858
        // Contains the vertical lines' positions for active multiline annotations
859
        let mut multilines = Vec::new();
8✔
860

861
        // Get the left-side margin to remove it
862
        let mut whitespace_margin = usize::MAX;
4✔
863
        for line_info in annotated_lines {
12✔
864
            // Whitespace can only be removed (aka considered leading)
865
            // if the lexer considers it whitespace.
866
            // non-rustc_lexer::is_whitespace() chars are reported as an
867
            // error (ex. no-break-spaces \u{a0}), and thus can't be considered
868
            // for removal during error reporting.
869
            let leading_whitespace = line_info
12✔
870
                .line
871
                .chars()
872
                .take_while(|c| c.is_whitespace())
12✔
873
                .map(|c| {
6✔
874
                    match c {
5✔
875
                        // Tabs are displayed as 4 spaces
876
                        '\t' => 4,
1✔
877
                        _ => 1,
7✔
878
                    }
879
                })
880
                .sum();
881
            if line_info.line.chars().any(|c| !c.is_whitespace()) {
21✔
882
                whitespace_margin = min(whitespace_margin, leading_whitespace);
7✔
883
            }
884
        }
885
        if whitespace_margin == usize::MAX {
7✔
886
            whitespace_margin = 0;
2✔
887
        }
888

889
        // Left-most column any visible span points at.
890
        let mut span_left_margin = usize::MAX;
7✔
891
        for line_info in annotated_lines {
12✔
892
            for ann in &line_info.annotations {
16✔
893
                span_left_margin = min(span_left_margin, ann.start.display);
4✔
894
                span_left_margin = min(span_left_margin, ann.end.display);
8✔
895
            }
896
        }
897
        if span_left_margin == usize::MAX {
9✔
898
            span_left_margin = 0;
1✔
899
        }
900

901
        // Right-most column any visible span points at.
902
        let mut span_right_margin = 0;
4✔
903
        let mut label_right_margin = 0;
8✔
904
        let mut max_line_len = 0;
4✔
905
        for line_info in annotated_lines {
12✔
906
            max_line_len = max(max_line_len, line_info.line.len());
12✔
907
            for ann in &line_info.annotations {
11✔
908
                span_right_margin = max(span_right_margin, ann.start.display);
6✔
909
                span_right_margin = max(span_right_margin, ann.end.display);
6✔
910
                // FIXME: account for labels not in the same line
911
                let label_right = ann.label.as_ref().map_or(0, |l| l.len() + 1);
21✔
912
                label_right_margin = max(label_right_margin, ann.end.display + label_right);
4✔
913
            }
914
        }
915
        let width_offset = 3 + max_line_num_len;
3✔
916
        let code_offset = if multiline_depth == 0 {
17✔
917
            width_offset
5✔
918
        } else {
919
            width_offset + multiline_depth + 1
7✔
920
        };
921

922
        let column_width = self.term_width.saturating_sub(code_offset);
7✔
923

924
        let margin = Margin::new(
925
            whitespace_margin,
8✔
926
            span_left_margin,
4✔
927
            span_right_margin,
8✔
928
            label_right_margin,
4✔
929
            column_width,
930
            max_line_len,
8✔
931
        );
932

933
        // Next, output the annotate source for this file
934
        for annotated_line_idx in 0..annotated_lines.len() {
8✔
935
            let previous_buffer_line = buffer.num_lines();
11✔
936

937
            let depths = self.render_source_line(
5✔
938
                &annotated_lines[annotated_line_idx],
6✔
939
                buffer,
940
                width_offset,
941
                code_offset,
5✔
942
                max_line_num_len,
943
                margin,
944
                !is_cont && annotated_line_idx + 1 == annotated_lines.len(),
13✔
945
            );
946

947
            let mut to_add = HashMap::new();
4✔
948

949
            for (depth, style) in depths {
14✔
950
                if let Some(index) = multilines.iter().position(|(d, _)| d == &depth) {
13✔
951
                    multilines.swap_remove(index);
6✔
952
                } else {
953
                    to_add.insert(depth, style);
7✔
954
                }
955
            }
956

957
            // Set the multiline annotation vertical lines to the left of
958
            // the code in this line.
959
            for (depth, style) in &multilines {
3✔
960
                for line in previous_buffer_line..buffer.num_lines() {
6✔
961
                    self.draw_multiline_line(buffer, line, width_offset, *depth, *style);
3✔
962
                }
963
            }
964
            // check to see if we need to print out or elide lines that come between
965
            // this annotated line and the next one.
966
            if annotated_line_idx < (annotated_lines.len() - 1) {
3✔
967
                let line_idx_delta = annotated_lines[annotated_line_idx + 1].line_index
12✔
968
                    - annotated_lines[annotated_line_idx].line_index;
8✔
969
                match line_idx_delta.cmp(&2) {
9✔
970
                    Ordering::Greater => {
971
                        let last_buffer_line_num = buffer.num_lines();
6✔
972

973
                        self.draw_line_separator(buffer, last_buffer_line_num, width_offset);
3✔
974

975
                        // Set the multiline annotation vertical lines on `...` bridging line.
976
                        for (depth, style) in &multilines {
3✔
977
                            self.draw_multiline_line(
6✔
978
                                buffer,
979
                                last_buffer_line_num,
980
                                width_offset,
981
                                *depth,
3✔
982
                                *style,
983
                            );
984
                        }
985
                        if let Some(line) = annotated_lines.get(annotated_line_idx) {
3✔
986
                            for ann in &line.annotations {
3✔
987
                                if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type
3✔
988
                                {
989
                                    // In the case where we have elided the entire start of the
990
                                    // multispan because those lines were empty, we still need
991
                                    // to draw the `|`s across the `...`.
992
                                    self.draw_multiline_line(
1✔
993
                                        buffer,
994
                                        last_buffer_line_num,
995
                                        width_offset,
996
                                        pos,
997
                                        if ann.is_primary() {
1✔
998
                                            ElementStyle::UnderlinePrimary
1✔
999
                                        } else {
1000
                                            ElementStyle::UnderlineSecondary
×
1001
                                        },
1002
                                    );
1003
                                }
1004
                            }
1005
                        }
1006
                    }
1007

1008
                    Ordering::Equal => {
1009
                        let unannotated_line = sm
4✔
1010
                            .get_line(annotated_lines[annotated_line_idx].line_index + 1)
4✔
1011
                            .unwrap_or("");
1012

1013
                        let last_buffer_line_num = buffer.num_lines();
2✔
1014

1015
                        self.draw_line(
2✔
1016
                            buffer,
1017
                            &normalize_whitespace(unannotated_line),
2✔
1018
                            annotated_lines[annotated_line_idx + 1].line_index - 1,
2✔
1019
                            last_buffer_line_num,
1020
                            width_offset,
1021
                            code_offset,
2✔
1022
                            max_line_num_len,
1023
                            margin,
1024
                        );
1025

1026
                        for (depth, style) in &multilines {
2✔
1027
                            self.draw_multiline_line(
2✔
1028
                                buffer,
1029
                                last_buffer_line_num,
1030
                                width_offset,
1031
                                *depth,
1✔
1032
                                *style,
1033
                            );
1034
                        }
1035
                        if let Some(line) = annotated_lines.get(annotated_line_idx) {
2✔
1036
                            for ann in &line.annotations {
2✔
1037
                                if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type
2✔
1038
                                {
1039
                                    self.draw_multiline_line(
1✔
1040
                                        buffer,
1041
                                        last_buffer_line_num,
1042
                                        width_offset,
1043
                                        pos,
1044
                                        if ann.is_primary() {
1✔
1045
                                            ElementStyle::UnderlinePrimary
1✔
1046
                                        } else {
1047
                                            ElementStyle::UnderlineSecondary
×
1048
                                        },
1049
                                    );
1050
                                }
1051
                            }
1052
                        }
1053
                    }
1054
                    Ordering::Less => {}
1055
                }
1056
            }
1057

1058
            multilines.extend(to_add);
5✔
1059
        }
1060
    }
1061

1062
    #[allow(clippy::too_many_arguments)]
1063
    fn render_source_line(
7✔
1064
        &self,
1065
        line_info: &AnnotatedLineInfo<'_>,
1066
        buffer: &mut StyledBuffer,
1067
        width_offset: usize,
1068
        code_offset: usize,
1069
        max_line_num_len: usize,
1070
        margin: Margin,
1071
        close_window: bool,
1072
    ) -> Vec<(usize, ElementStyle)> {
1073
        // Draw:
1074
        //
1075
        //   LL | ... code ...
1076
        //      |     ^^-^ span label
1077
        //      |       |
1078
        //      |       secondary span label
1079
        //
1080
        //   ^^ ^ ^^^ ^^^^ ^^^ we don't care about code too far to the right of a span, we trim it
1081
        //   |  | |   |
1082
        //   |  | |   actual code found in your source code and the spans we use to mark it
1083
        //   |  | when there's too much wasted space to the left, trim it
1084
        //   |  vertical divider between the column number and the code
1085
        //   column number
1086

1087
        if line_info.line_index == 0 {
5✔
1088
            return Vec::new();
×
1089
        }
1090

1091
        let source_string = normalize_whitespace(line_info.line);
7✔
1092

1093
        let line_offset = buffer.num_lines();
11✔
1094

1095
        let left = self.draw_line(
4✔
1096
            buffer,
1097
            &source_string,
4✔
1098
            line_info.line_index,
7✔
1099
            line_offset,
1100
            width_offset,
1101
            code_offset,
1102
            max_line_num_len,
1103
            margin,
1104
        );
1105

1106
        // Special case when there's only one annotation involved, it is the start of a multiline
1107
        // span and there's no text at the beginning of the code line. Instead of doing the whole
1108
        // graph:
1109
        //
1110
        // 2 |   fn foo() {
1111
        //   |  _^
1112
        // 3 | |
1113
        // 4 | | }
1114
        //   | |_^ test
1115
        //
1116
        // we simplify the output to:
1117
        //
1118
        // 2 | / fn foo() {
1119
        // 3 | |
1120
        // 4 | | }
1121
        //   | |_^ test
1122
        let mut buffer_ops = vec![];
5✔
1123
        let mut annotations = vec![];
9✔
1124
        let mut short_start = true;
4✔
1125
        for ann in &line_info.annotations {
12✔
1126
            if let LineAnnotationType::MultilineStart(depth) = ann.annotation_type {
12✔
1127
                if source_string
11✔
1128
                    .chars()
1129
                    .take(ann.start.display)
3✔
1130
                    .all(char::is_whitespace)
1131
                {
1132
                    let uline = self.underline(ann.is_primary());
3✔
1133
                    let chr = uline.multiline_whole_line;
3✔
1134
                    annotations.push((depth, uline.style));
3✔
1135
                    buffer_ops.push((line_offset, width_offset + depth - 1, chr, uline.style));
3✔
1136
                } else {
1137
                    short_start = false;
4✔
1138
                    break;
1139
                }
1140
            } else if let LineAnnotationType::MultilineLine(_) = ann.annotation_type {
5✔
1141
            } else {
1142
                short_start = false;
5✔
1143
                break;
5✔
1144
            }
1145
        }
1146
        if short_start {
4✔
1147
            for (y, x, c, s) in buffer_ops {
13✔
1148
                buffer.putc(y, x, c, s);
9✔
1149
            }
1150
            return annotations;
3✔
1151
        }
1152

1153
        // We want to display like this:
1154
        //
1155
        //      vec.push(vec.pop().unwrap());
1156
        //      ---      ^^^               - previous borrow ends here
1157
        //      |        |
1158
        //      |        error occurs here
1159
        //      previous borrow of `vec` occurs here
1160
        //
1161
        // But there are some weird edge cases to be aware of:
1162
        //
1163
        //      vec.push(vec.pop().unwrap());
1164
        //      --------                    - previous borrow ends here
1165
        //      ||
1166
        //      |this makes no sense
1167
        //      previous borrow of `vec` occurs here
1168
        //
1169
        // For this reason, we group the lines into "highlight lines"
1170
        // and "annotations lines", where the highlight lines have the `^`.
1171

1172
        // Sort the annotations by (start, end col)
1173
        // The labels are reversed, sort and then reversed again.
1174
        // Consider a list of annotations (A1, A2, C1, C2, B1, B2) where
1175
        // the letter signifies the span. Here we are only sorting by the
1176
        // span and hence, the order of the elements with the same span will
1177
        // not change. On reversing the ordering (|a, b| but b.cmp(a)), you get
1178
        // (C1, C2, B1, B2, A1, A2). All the elements with the same span are
1179
        // still ordered first to last, but all the elements with different
1180
        // spans are ordered by their spans in last to first order. Last to
1181
        // first order is important, because the jiggly lines and | are on
1182
        // the left, so the rightmost span needs to be rendered first,
1183
        // otherwise the lines would end up needing to go over a message.
1184

1185
        let mut annotations = line_info.annotations.clone();
8✔
1186
        annotations.sort_by_key(|a| Reverse(a.start.display));
18✔
1187

1188
        // First, figure out where each label will be positioned.
1189
        //
1190
        // In the case where you have the following annotations:
1191
        //
1192
        //      vec.push(vec.pop().unwrap());
1193
        //      --------                    - previous borrow ends here [C]
1194
        //      ||
1195
        //      |this makes no sense [B]
1196
        //      previous borrow of `vec` occurs here [A]
1197
        //
1198
        // `annotations_position` will hold [(2, A), (1, B), (0, C)].
1199
        //
1200
        // We try, when possible, to stick the rightmost annotation at the end
1201
        // of the highlight line:
1202
        //
1203
        //      vec.push(vec.pop().unwrap());
1204
        //      ---      ---               - previous borrow ends here
1205
        //
1206
        // But sometimes that's not possible because one of the other
1207
        // annotations overlaps it. For example, from the test
1208
        // `span_overlap_label`, we have the following annotations
1209
        // (written on distinct lines for clarity):
1210
        //
1211
        //      fn foo(x: u32) {
1212
        //      --------------
1213
        //             -
1214
        //
1215
        // In this case, we can't stick the rightmost-most label on
1216
        // the highlight line, or we would get:
1217
        //
1218
        //      fn foo(x: u32) {
1219
        //      -------- x_span
1220
        //      |
1221
        //      fn_span
1222
        //
1223
        // which is totally weird. Instead we want:
1224
        //
1225
        //      fn foo(x: u32) {
1226
        //      --------------
1227
        //      |      |
1228
        //      |      x_span
1229
        //      fn_span
1230
        //
1231
        // which is...less weird, at least. In fact, in general, if
1232
        // the rightmost span overlaps with any other span, we should
1233
        // use the "hang below" version, so we can at least make it
1234
        // clear where the span *starts*. There's an exception for this
1235
        // logic, when the labels do not have a message:
1236
        //
1237
        //      fn foo(x: u32) {
1238
        //      --------------
1239
        //             |
1240
        //             x_span
1241
        //
1242
        // instead of:
1243
        //
1244
        //      fn foo(x: u32) {
1245
        //      --------------
1246
        //      |      |
1247
        //      |      x_span
1248
        //      <EMPTY LINE>
1249
        //
1250
        let mut overlap = vec![false; annotations.len()];
4✔
1251
        let mut annotations_position = vec![];
8✔
1252
        let mut line_len: usize = 0;
3✔
1253
        let mut p = 0;
7✔
1254
        for (i, annotation) in annotations.iter().enumerate() {
16✔
1255
            for (j, next) in annotations.iter().enumerate() {
12✔
1256
                if overlaps(next, annotation, 0) && j > 1 {
20✔
1257
                    overlap[i] = true;
2✔
1258
                    overlap[j] = true;
3✔
1259
                }
1260
                if overlaps(next, annotation, 0)  // This label overlaps with another one and both
10✔
1261
                    && annotation.has_label()     // take space (they have text and are not
8✔
1262
                    && j > i                      // multiline lines).
3✔
1263
                    && p == 0
2✔
1264
                // We're currently on the first line, move the label one line down
1265
                {
1266
                    // If we're overlapping with an un-labelled annotation with the same span
1267
                    // we can just merge them in the output
1268
                    if next.start.display == annotation.start.display
2✔
1269
                        && next.end.display == annotation.end.display
2✔
1270
                        && !next.has_label()
2✔
1271
                    {
1272
                        continue;
1273
                    }
1274

1275
                    // This annotation needs a new line in the output.
1276
                    p += 1;
4✔
1277
                    break;
1278
                }
1279
            }
1280
            annotations_position.push((p, annotation));
6✔
1281
            for (j, next) in annotations.iter().enumerate() {
5✔
1282
                if j > i {
5✔
1283
                    let l = next.label.as_ref().map_or(0, |label| label.len() + 2);
9✔
1284
                    if (overlaps(next, annotation, l) // Do not allow two labels to be in the same
3✔
1285
                        // line if they overlap including padding, to
1286
                        // avoid situations like:
1287
                        //
1288
                        //      fn foo(x: u32) {
1289
                        //      -------^------
1290
                        //      |      |
1291
                        //      fn_spanx_span
1292
                        //
1293
                        && annotation.has_label()    // Both labels must have some text, otherwise
3✔
1294
                        && next.has_label())         // they are not overlapping.
3✔
1295
                        // Do not add a new line if this annotation
1296
                        // or the next are vertical line placeholders.
1297
                        || (annotation.takes_space() // If either this or the next annotation is
4✔
1298
                        && next.has_label())     // multiline start/end, move it to a new line
2✔
1299
                        || (annotation.has_label()   // so as not to overlap the horizontal lines.
4✔
1300
                        && next.takes_space())
2✔
1301
                        || (annotation.takes_space() && next.takes_space())
6✔
1302
                        || (overlaps(next, annotation, l)
4✔
1303
                        && next.end.display <= annotation.end.display
1✔
1304
                        && next.has_label()
1✔
1305
                        && p == 0)
1✔
1306
                    // Avoid #42595.
1307
                    {
1308
                        // This annotation needs a new line in the output.
1309
                        p += 1;
6✔
1310
                        break;
1311
                    }
1312
                }
1313
            }
1314
            line_len = max(line_len, p);
11✔
1315
        }
1316

1317
        if line_len != 0 {
11✔
1318
            line_len += 1;
3✔
1319
        }
1320

1321
        // If there are no annotations or the only annotations on this line are
1322
        // MultilineLine, then there's only code being shown, stop processing.
1323
        if line_info.annotations.iter().all(LineAnnotation::is_line) {
12✔
1324
            return vec![];
×
1325
        }
1326

1327
        if annotations_position
10✔
1328
            .iter()
1329
            .all(|(_, ann)| matches!(ann.annotation_type, LineAnnotationType::MultilineStart(_)))
12✔
1330
        {
1331
            if let Some(max_pos) = annotations_position.iter().map(|(pos, _)| *pos).max() {
15✔
1332
                // Special case the following, so that we minimize overlapping multiline spans.
1333
                //
1334
                // 3 │       X0 Y0 Z0
1335
                //   │ ┏━━━━━┛  │  │     < We are writing these lines
1336
                //   │ ┃┌───────┘  │     < by reverting the "depth" of
1337
                //   │ ┃│┌─────────┘     < their multiline spans.
1338
                // 4 │ ┃││   X1 Y1 Z1
1339
                // 5 │ ┃││   X2 Y2 Z2
1340
                //   │ ┃│└────╿──│──┘ `Z` label
1341
                //   │ ┃└─────│──┤
1342
                //   │ ┗━━━━━━┥  `Y` is a good letter too
1343
                //   ╰╴       `X` is a good letter
1344
                for (pos, _) in &mut annotations_position {
8✔
1345
                    *pos = max_pos - *pos;
9✔
1346
                }
1347
                // We know then that we don't need an additional line for the span label, saving us
1348
                // one line of vertical space.
1349
                line_len = line_len.saturating_sub(1);
3✔
1350
            }
1351
        }
1352

1353
        // Write the column separator.
1354
        //
1355
        // After this we will have:
1356
        //
1357
        // 2 |   fn foo() {
1358
        //   |
1359
        //   |
1360
        //   |
1361
        // 3 |
1362
        // 4 |   }
1363
        //   |
1364
        for pos in 0..=line_len {
10✔
1365
            self.draw_col_separator_no_space(buffer, line_offset + pos + 1, width_offset - 2);
10✔
1366
        }
1367
        if close_window {
7✔
1368
            self.draw_col_separator_end(buffer, line_offset + line_len + 1, width_offset - 2);
4✔
1369
        }
1370
        // Write the horizontal lines for multiline annotations
1371
        // (only the first and last lines need this).
1372
        //
1373
        // After this we will have:
1374
        //
1375
        // 2 |   fn foo() {
1376
        //   |  __________
1377
        //   |
1378
        //   |
1379
        // 3 |
1380
        // 4 |   }
1381
        //   |  _
1382
        for &(pos, annotation) in &annotations_position {
9✔
1383
            let underline = self.underline(annotation.is_primary());
9✔
1384
            let pos = pos + 1;
4✔
1385
            match annotation.annotation_type {
8✔
1386
                LineAnnotationType::MultilineStart(depth)
4✔
1387
                | LineAnnotationType::MultilineEnd(depth) => {
1388
                    self.draw_range(
3✔
1389
                        buffer,
1390
                        underline.multiline_horizontal,
4✔
1391
                        line_offset + pos,
3✔
1392
                        width_offset + depth,
4✔
1393
                        (code_offset + annotation.start.display).saturating_sub(left),
7✔
1394
                        underline.style,
1395
                    );
1396
                }
1397
                _ if annotation.highlight_source => {
5✔
1398
                    buffer.set_style_range(
×
1399
                        line_offset,
1400
                        (code_offset + annotation.start.display).saturating_sub(left),
×
1401
                        (code_offset + annotation.end.display).saturating_sub(left),
×
1402
                        underline.style,
×
1403
                        annotation.is_primary(),
×
1404
                    );
1405
                }
1406
                _ => {}
1407
            }
1408
        }
1409

1410
        // Write the vertical lines for labels that are on a different line as the underline.
1411
        //
1412
        // After this we will have:
1413
        //
1414
        // 2 |   fn foo() {
1415
        //   |  __________
1416
        //   | |    |
1417
        //   | |
1418
        // 3 | |
1419
        // 4 | | }
1420
        //   | |_
1421
        for &(pos, annotation) in &annotations_position {
5✔
1422
            let underline = self.underline(annotation.is_primary());
9✔
1423
            let pos = pos + 1;
4✔
1424

1425
            if pos > 1 && (annotation.has_label() || annotation.takes_space()) {
11✔
1426
                for p in line_offset + 1..=line_offset + pos {
9✔
1427
                    buffer.putc(
5✔
1428
                        p,
1429
                        (code_offset + annotation.start.display).saturating_sub(left),
10✔
1430
                        match annotation.annotation_type {
5✔
1431
                            LineAnnotationType::MultilineLine(_) => underline.multiline_vertical,
×
1432
                            _ => underline.vertical_text_line,
5✔
1433
                        },
1434
                        underline.style,
1435
                    );
1436
                }
1437
                if let LineAnnotationType::MultilineStart(_) = annotation.annotation_type {
5✔
1438
                    buffer.putc(
3✔
1439
                        line_offset + pos,
2✔
1440
                        (code_offset + annotation.start.display).saturating_sub(left),
6✔
1441
                        underline.bottom_right,
3✔
1442
                        underline.style,
1443
                    );
1444
                }
1445
                if matches!(
5✔
1446
                    annotation.annotation_type,
1447
                    LineAnnotationType::MultilineEnd(_)
1448
                ) && annotation.has_label()
2✔
1449
                {
1450
                    buffer.putc(
2✔
1451
                        line_offset + pos,
2✔
1452
                        (code_offset + annotation.start.display).saturating_sub(left),
4✔
1453
                        underline.multiline_bottom_right_with_text,
2✔
1454
                        underline.style,
1455
                    );
1456
                }
1457
            }
1458
            match annotation.annotation_type {
4✔
1459
                LineAnnotationType::MultilineStart(depth) => {
4✔
1460
                    buffer.putc(
3✔
1461
                        line_offset + pos,
3✔
1462
                        width_offset + depth - 1,
7✔
1463
                        underline.top_left,
4✔
1464
                        underline.style,
1465
                    );
1466
                    for p in line_offset + pos + 1..line_offset + line_len + 2 {
4✔
1467
                        buffer.putc(
4✔
1468
                            p,
1469
                            width_offset + depth - 1,
2✔
1470
                            underline.multiline_vertical,
2✔
1471
                            underline.style,
1472
                        );
1473
                    }
1474
                }
1475
                LineAnnotationType::MultilineEnd(depth) => {
3✔
1476
                    for p in line_offset..line_offset + pos {
6✔
1477
                        buffer.putc(
6✔
1478
                            p,
1479
                            width_offset + depth - 1,
6✔
1480
                            underline.multiline_vertical,
3✔
1481
                            underline.style,
1482
                        );
1483
                    }
1484
                    buffer.putc(
8✔
1485
                        line_offset + pos,
4✔
1486
                        width_offset + depth - 1,
8✔
1487
                        underline.bottom_left,
4✔
1488
                        underline.style,
1489
                    );
1490
                }
1491
                _ => (),
1492
            }
1493
        }
1494

1495
        // Write the labels on the annotations that actually have a label.
1496
        //
1497
        // After this we will have:
1498
        //
1499
        // 2 |   fn foo() {
1500
        //   |  __________
1501
        //   |      |
1502
        //   |      something about `foo`
1503
        // 3 |
1504
        // 4 |   }
1505
        //   |  _  test
1506
        for &(pos, annotation) in &annotations_position {
4✔
1507
            let style = if annotation.is_primary() {
12✔
1508
                ElementStyle::LabelPrimary
5✔
1509
            } else {
1510
                ElementStyle::LabelSecondary
4✔
1511
            };
1512
            let (pos, col) = if pos == 0 {
14✔
1513
                if annotation.end.display == 0 {
11✔
1514
                    (pos + 1, (annotation.end.display + 2).saturating_sub(left))
7✔
1515
                } else {
1516
                    (pos + 1, (annotation.end.display + 1).saturating_sub(left))
10✔
1517
                }
1518
            } else {
1519
                (pos + 2, annotation.start.display.saturating_sub(left))
6✔
1520
            };
1521
            if let Some(label) = annotation.label {
8✔
1522
                buffer.puts(line_offset + pos, code_offset + col, label, style);
3✔
1523
            }
1524
        }
1525

1526
        // Sort from biggest span to smallest span so that smaller spans are
1527
        // represented in the output:
1528
        //
1529
        // x | fn foo()
1530
        //   | ^^^---^^
1531
        //   | |  |
1532
        //   | |  something about `foo`
1533
        //   | something about `fn foo()`
1534
        annotations_position.sort_by_key(|(_, ann)| {
11✔
1535
            // Decreasing order. When annotations share the same length, prefer `Primary`.
1536
            (Reverse(ann.len()), ann.is_primary())
3✔
1537
        });
1538

1539
        // Write the underlines.
1540
        //
1541
        // After this we will have:
1542
        //
1543
        // 2 |   fn foo() {
1544
        //   |  ____-_____^
1545
        //   |      |
1546
        //   |      something about `foo`
1547
        // 3 |
1548
        // 4 |   }
1549
        //   |  _^  test
1550
        for &(pos, annotation) in &annotations_position {
4✔
1551
            let uline = self.underline(annotation.is_primary());
11✔
1552
            for p in annotation.start.display..annotation.end.display {
7✔
1553
                // The default span label underline.
1554
                buffer.putc(
5✔
1555
                    line_offset + 1,
5✔
1556
                    (code_offset + p).saturating_sub(left),
12✔
1557
                    uline.underline,
7✔
1558
                    uline.style,
1559
                );
1560
            }
1561

1562
            if pos == 0
7✔
1563
                && matches!(
11✔
1564
                    annotation.annotation_type,
5✔
1565
                    LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1566
                )
1567
            {
1568
                // The beginning of a multiline span with its leftward moving line on the same line.
1569
                buffer.putc(
3✔
1570
                    line_offset + 1,
4✔
1571
                    (code_offset + annotation.start.display).saturating_sub(left),
7✔
1572
                    match annotation.annotation_type {
3✔
1573
                        LineAnnotationType::MultilineStart(_) => uline.top_right_flat,
4✔
1574
                        LineAnnotationType::MultilineEnd(_) => uline.multiline_end_same_line,
3✔
1575
                        _ => panic!("unexpected annotation type: {annotation:?}"),
×
1576
                    },
1577
                    uline.style,
1578
                );
1579
            } else if pos != 0
4✔
1580
                && matches!(
3✔
1581
                    annotation.annotation_type,
3✔
1582
                    LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1583
                )
1584
            {
1585
                // The beginning of a multiline span with its leftward moving line on another line,
1586
                // so we start going down first.
1587
                buffer.putc(
2✔
1588
                    line_offset + 1,
2✔
1589
                    (code_offset + annotation.start.display).saturating_sub(left),
4✔
1590
                    match annotation.annotation_type {
2✔
1591
                        LineAnnotationType::MultilineStart(_) => uline.multiline_start_down,
3✔
1592
                        LineAnnotationType::MultilineEnd(_) => uline.multiline_end_up,
2✔
1593
                        _ => panic!("unexpected annotation type: {annotation:?}"),
×
1594
                    },
1595
                    uline.style,
1596
                );
1597
            } else if pos != 0 && annotation.has_label() {
7✔
1598
                // The beginning of a span label with an actual label, we'll point down.
1599
                buffer.putc(
4✔
1600
                    line_offset + 1,
4✔
1601
                    (code_offset + annotation.start.display).saturating_sub(left),
8✔
1602
                    uline.label_start,
4✔
1603
                    uline.style,
1604
                );
1605
            }
1606
        }
1607

1608
        // We look for individual *long* spans, and we trim the *middle*, so that we render
1609
        // LL | ...= [0, 0, 0, ..., 0, 0];
1610
        //    |      ^^^^^^^^^^...^^^^^^^ expected `&[u8]`, found `[{integer}; 1680]`
1611
        for (i, (_pos, annotation)) in annotations_position.iter().enumerate() {
4✔
1612
            // Skip cases where multiple spans overlap eachother.
1613
            if overlap[i] {
10✔
1614
                continue;
1615
            };
1616
            let LineAnnotationType::Singleline = annotation.annotation_type else {
7✔
1617
                continue;
1618
            };
1619
            let width = annotation.end.display - annotation.start.display;
4✔
1620
            if width > margin.term_width * 2 && width > 10 {
6✔
1621
                // If the terminal is *too* small, we keep at least a tiny bit of the span for
1622
                // display.
1623
                let pad = max(margin.term_width / 3, 5);
1✔
1624
                // Code line
1625
                buffer.replace(
1✔
1626
                    line_offset,
1627
                    annotation.start.display + pad,
1✔
1628
                    annotation.end.display - pad,
1✔
1629
                    self.margin(),
1✔
1630
                );
1631
                // Underline line
1632
                buffer.replace(
1✔
1633
                    line_offset + 1,
1✔
1634
                    annotation.start.display + pad,
1✔
1635
                    annotation.end.display - pad,
1✔
1636
                    self.margin(),
1✔
1637
                );
1638
            }
1639
        }
1640
        annotations_position
4✔
1641
            .iter()
1642
            .filter_map(|&(_, annotation)| match annotation.annotation_type {
11✔
1643
                LineAnnotationType::MultilineStart(p) | LineAnnotationType::MultilineEnd(p) => {
6✔
1644
                    let style = if annotation.is_primary() {
6✔
1645
                        ElementStyle::LabelPrimary
4✔
1646
                    } else {
1647
                        ElementStyle::LabelSecondary
3✔
1648
                    };
1649
                    Some((p, style))
3✔
1650
                }
1651
                _ => None,
3✔
1652
            })
1653
            .collect::<Vec<_>>()
1654
    }
1655

1656
    fn emit_suggestion_default(
2✔
1657
        &self,
1658
        buffer: &mut StyledBuffer,
1659
        suggestion: &Snippet<'_, Patch<'_>>,
1660
        max_line_num_len: usize,
1661
        sm: &SourceMap<'_>,
1662
        primary_origin: Option<&Origin<'_>>,
1663
        is_cont: bool,
1664
    ) {
1665
        let suggestions = sm.splice_lines(suggestion.markers.clone());
2✔
1666

1667
        let buffer_offset = buffer.num_lines();
12✔
1668
        let mut row_num = buffer_offset + usize::from(!is_cont);
6✔
1669
        for (i, (complete, parts, highlights)) in suggestions.iter().enumerate() {
12✔
1670
            let has_deletion = parts
12✔
1671
                .iter()
1672
                .any(|p| p.is_deletion(sm) || p.is_destructive_replacement(sm));
12✔
1673
            let is_multiline = complete.lines().count() > 1;
5✔
1674

1675
            if i == 0 {
5✔
1676
                self.draw_col_separator_start(buffer, row_num - 1, max_line_num_len + 1);
11✔
1677
            } else {
1678
                buffer.puts(
×
1679
                    row_num - 1,
×
1680
                    max_line_num_len + 1,
×
1681
                    self.multi_suggestion_separator(),
×
1682
                    ElementStyle::LineNumber,
×
1683
                );
1684
            }
1685

1686
            if suggestion.origin.as_ref() != primary_origin && primary_origin.is_some() {
15✔
1687
                if let Some(origin) = suggestion.origin.clone() {
1✔
NEW
1688
                    let (line, char_col) =
×
1689
                        if let (Some(line), Some(char_col)) = (origin.line, origin.char_column) {
NEW
1690
                            (line, char_col)
×
1691
                        } else {
NEW
1692
                            let (loc, _) = sm.span_to_locations(parts[0].span.clone());
×
NEW
1693
                            (loc.line, loc.char + 1)
×
1694
                        };
1695

1696
                    // --> file.rs:line:col
1697
                    //  |
1698
                    let arrow = self.file_start();
×
1699
                    buffer.puts(row_num - 1, 0, arrow, ElementStyle::LineNumber);
×
NEW
1700
                    let message = format!("{}:{}:{}", origin.origin, line, char_col);
×
1701
                    if is_cont {
×
1702
                        buffer.append(row_num - 1, &message, ElementStyle::LineAndColumn);
×
1703
                    } else {
1704
                        let col = usize::max(max_line_num_len + 1, arrow.len());
×
1705
                        buffer.puts(row_num - 1, col, &message, ElementStyle::LineAndColumn);
×
1706
                    }
1707
                    for _ in 0..max_line_num_len {
×
1708
                        buffer.prepend(row_num - 1, " ", ElementStyle::NoStyle);
×
1709
                    }
1710
                    self.draw_col_separator_no_space(buffer, row_num, max_line_num_len + 1);
×
1711
                    row_num += 1;
×
1712
                }
1713
            }
1714
            let show_code_change = if has_deletion && !is_multiline {
10✔
1715
                DisplaySuggestion::Diff
3✔
1716
            } else if parts.len() == 1
9✔
1717
                && parts.first().map_or(false, |p| {
12✔
1718
                    p.replacement.ends_with('\n') && p.replacement.trim() == complete.trim()
4✔
1719
                })
1720
            {
1721
                // We are adding a line(s) of code before code that was already there.
1722
                DisplaySuggestion::Add
1✔
1723
            } else if (parts.len() != 1 || parts[0].replacement.trim() != complete.trim())
14✔
1724
                && !is_multiline
3✔
1725
            {
1726
                DisplaySuggestion::Underline
3✔
1727
            } else {
1728
                DisplaySuggestion::None
1✔
1729
            };
1730

1731
            if let DisplaySuggestion::Diff = show_code_change {
6✔
1732
                row_num += 1;
6✔
1733
            }
1734

1735
            let file_lines = sm.span_to_lines(parts[0].span.clone());
6✔
1736
            let (line_start, line_end) = sm.span_to_locations(parts[0].span.clone());
6✔
1737
            let mut lines = complete.lines();
3✔
1738
            if lines.clone().next().is_none() {
4✔
1739
                // Account for a suggestion to completely remove a line(s) with whitespace (#94192).
1740
                for line in line_start.line..=line_end.line {
×
1741
                    buffer.puts(
×
1742
                        row_num - 1 + line - line_start.line,
×
1743
                        0,
1744
                        &self.maybe_anonymized(line),
×
1745
                        ElementStyle::LineNumber,
×
1746
                    );
1747
                    buffer.puts(
×
1748
                        row_num - 1 + line - line_start.line,
×
1749
                        max_line_num_len + 1,
×
1750
                        "- ",
1751
                        ElementStyle::Removal,
×
1752
                    );
1753
                    buffer.puts(
×
1754
                        row_num - 1 + line - line_start.line,
×
1755
                        max_line_num_len + 3,
×
1756
                        &normalize_whitespace(sm.get_line(line).unwrap()),
×
1757
                        ElementStyle::Removal,
×
1758
                    );
1759
                }
1760
                row_num += line_end.line - line_start.line;
×
1761
            }
1762
            let mut last_pos = 0;
5✔
1763
            let mut is_item_attribute = false;
7✔
1764
            let mut unhighlighted_lines = Vec::new();
6✔
1765
            for (line_pos, (line, highlight_parts)) in lines.by_ref().zip(highlights).enumerate() {
14✔
1766
                last_pos = line_pos;
6✔
1767

1768
                // Remember lines that are not highlighted to hide them if needed
1769
                if highlight_parts.is_empty() {
11✔
1770
                    unhighlighted_lines.push((line_pos, line));
2✔
1771
                    continue;
1772
                }
1773
                if highlight_parts.len() == 1
11✔
1774
                    && line.trim().starts_with("#[")
9✔
1775
                    && line.trim().ends_with(']')
×
1776
                {
1777
                    is_item_attribute = true;
×
1778
                }
1779

1780
                match unhighlighted_lines.len() {
5✔
1781
                    0 => (),
1782
                    // Since we show first line, "..." line and last line,
1783
                    // There is no reason to hide if there are 3 or less lines
1784
                    // (because then we just replace a line with ... which is
1785
                    // not helpful)
1786
                    n if n <= 3 => unhighlighted_lines.drain(..).for_each(|(p, l)| {
5✔
1787
                        self.draw_code_line(
2✔
1788
                            buffer,
1✔
1789
                            &mut row_num,
1✔
1790
                            &[],
1791
                            p + line_start.line,
1✔
1792
                            l,
1793
                            show_code_change,
1✔
1794
                            max_line_num_len,
1✔
1795
                            &file_lines,
1✔
1796
                            is_multiline,
1✔
1797
                        );
1798
                    }),
1799
                    // Print first unhighlighted line, "..." and last unhighlighted line, like so:
1800
                    //
1801
                    // LL | this line was highlighted
1802
                    // LL | this line is just for context
1803
                    // ...
1804
                    // LL | this line is just for context
1805
                    // LL | this line was highlighted
1806
                    _ => {
1807
                        let last_line = unhighlighted_lines.pop();
×
1808
                        let first_line = unhighlighted_lines.drain(..).next();
×
1809

1810
                        if let Some((p, l)) = first_line {
×
1811
                            self.draw_code_line(
×
1812
                                buffer,
1813
                                &mut row_num,
1814
                                &[],
1815
                                p + line_start.line,
×
1816
                                l,
1817
                                show_code_change,
×
1818
                                max_line_num_len,
1819
                                &file_lines,
×
1820
                                is_multiline,
1821
                            );
1822
                        }
1823

1824
                        let placeholder = self.margin();
×
1825
                        let padding = str_width(placeholder);
×
1826
                        buffer.puts(
×
1827
                            row_num,
×
1828
                            max_line_num_len.saturating_sub(padding),
×
1829
                            placeholder,
1830
                            ElementStyle::LineNumber,
×
1831
                        );
1832
                        row_num += 1;
×
1833

1834
                        if let Some((p, l)) = last_line {
×
1835
                            self.draw_code_line(
×
1836
                                buffer,
1837
                                &mut row_num,
1838
                                &[],
1839
                                p + line_start.line,
×
1840
                                l,
1841
                                show_code_change,
×
1842
                                max_line_num_len,
1843
                                &file_lines,
×
1844
                                is_multiline,
1845
                            );
1846
                        }
1847
                    }
1848
                }
1849
                self.draw_code_line(
3✔
1850
                    buffer,
1851
                    &mut row_num,
1852
                    highlight_parts,
4✔
1853
                    line_pos + line_start.line,
3✔
1854
                    line,
1855
                    show_code_change,
4✔
1856
                    max_line_num_len,
1857
                    &file_lines,
3✔
1858
                    is_multiline,
1859
                );
1860
            }
1861

1862
            if matches!(show_code_change, DisplaySuggestion::Add) && is_item_attribute {
3✔
1863
                // The suggestion adds an entire line of code, ending on a newline, so we'll also
1864
                // print the *following* line, to provide context of what we're advising people to
1865
                // do. Otherwise you would only see contextless code that can be confused for
1866
                // already existing code, despite the colors and UI elements.
1867
                // We special case `#[derive(_)]\n` and other attribute suggestions, because those
1868
                // are the ones where context is most useful.
1869
                let file_lines = sm.span_to_lines(parts[0].span.end..parts[0].span.end);
×
1870
                let (lo, _) = sm.span_to_locations(parts[0].span.clone());
×
1871
                let line_num = lo.line;
×
1872
                if let Some(line) = sm.get_line(line_num) {
×
1873
                    let line = normalize_whitespace(line);
×
1874
                    self.draw_code_line(
×
1875
                        buffer,
1876
                        &mut row_num,
1877
                        &[],
1878
                        line_num + last_pos + 1,
×
1879
                        &line,
×
1880
                        DisplaySuggestion::None,
×
1881
                        max_line_num_len,
1882
                        &file_lines,
×
1883
                        is_multiline,
1884
                    );
1885
                }
1886
            }
1887
            // This offset and the ones below need to be signed to account for replacement code
1888
            // that is shorter than the original code.
1889
            let mut offsets: Vec<(usize, isize)> = Vec::new();
3✔
1890
            // Only show an underline in the suggestions if the suggestion is not the
1891
            // entirety of the code being shown and the displayed code is not multiline.
1892
            if let DisplaySuggestion::Diff | DisplaySuggestion::Underline | DisplaySuggestion::Add =
2✔
1893
                show_code_change
1894
            {
1895
                for part in parts {
4✔
1896
                    let (span_start, span_end) = sm.span_to_locations(part.span.clone());
6✔
1897
                    let span_start_pos = span_start.display;
3✔
1898
                    let span_end_pos = span_end.display;
2✔
1899

1900
                    // If this addition is _only_ whitespace, then don't trim it,
1901
                    // or else we're just not rendering anything.
1902
                    let is_whitespace_addition = part.replacement.trim().is_empty();
3✔
1903

1904
                    // Do not underline the leading...
1905
                    let start = if is_whitespace_addition {
4✔
1906
                        0
1✔
1907
                    } else {
1908
                        part.replacement
6✔
1909
                            .len()
1910
                            .saturating_sub(part.replacement.trim_start().len())
3✔
1911
                    };
1912
                    // ...or trailing spaces. Account for substitutions containing unicode
1913
                    // characters.
1914
                    let sub_len: usize = str_width(if is_whitespace_addition {
5✔
1915
                        part.replacement
1✔
1916
                    } else {
1917
                        part.replacement.trim()
6✔
1918
                    });
1919

1920
                    let offset: isize = offsets
2✔
1921
                        .iter()
1922
                        .filter_map(|(start, v)| {
2✔
1923
                            if span_start_pos < *start {
2✔
1924
                                None
×
1925
                            } else {
1926
                                Some(v)
1✔
1927
                            }
1928
                        })
1929
                        .sum();
1930
                    let underline_start = (span_start_pos + start) as isize + offset;
4✔
1931
                    let underline_end = (span_start_pos + start + sub_len) as isize + offset;
6✔
1932
                    assert!(underline_start >= 0 && underline_end >= 0);
3✔
1933
                    let padding: usize = max_line_num_len + 3;
4✔
1934
                    for p in underline_start..underline_end {
7✔
1935
                        if matches!(show_code_change, DisplaySuggestion::Underline)
5✔
1936
                            && is_different(sm, part.replacement, part.span.clone())
2✔
1937
                        {
1938
                            // If this is a replacement, underline with `~`, if this is an addition
1939
                            // underline with `+`.
1940
                            buffer.putc(
2✔
1941
                                row_num,
2✔
1942
                                (padding as isize + p) as usize,
2✔
1943
                                if part.is_addition(sm) {
6✔
1944
                                    '+'
2✔
1945
                                } else {
1946
                                    self.diff()
×
1947
                                },
1948
                                ElementStyle::Addition,
2✔
1949
                            );
1950
                        }
1951
                    }
1952
                    if let DisplaySuggestion::Diff = show_code_change {
3✔
1953
                        // Colorize removal with red in diff format.
1954
                        buffer.set_style_range(
6✔
1955
                            row_num - 2,
3✔
1956
                            (padding as isize + span_start_pos as isize) as usize,
3✔
1957
                            (padding as isize + span_end_pos as isize) as usize,
3✔
1958
                            ElementStyle::Removal,
3✔
1959
                            true,
1960
                        );
1961
                    }
1962

1963
                    // length of the code after substitution
1964
                    let full_sub_len = str_width(part.replacement) as isize;
4✔
1965

1966
                    // length of the code to be substituted
1967
                    let snippet_len = span_end_pos as isize - span_start_pos as isize;
5✔
1968
                    // For multiple substitutions, use the position *after* the previous
1969
                    // substitutions have happened, only when further substitutions are
1970
                    // located strictly after.
1971
                    offsets.push((span_end_pos, full_sub_len - snippet_len));
7✔
1972
                }
1973
                row_num += 1;
2✔
1974
            }
1975

1976
            // if we elided some lines, add an ellipsis
1977
            if lines.next().is_some() {
12✔
1978
                let placeholder = self.margin();
×
1979
                let padding = str_width(placeholder);
×
1980
                buffer.puts(
×
1981
                    row_num,
×
1982
                    max_line_num_len.saturating_sub(padding),
×
1983
                    placeholder,
1984
                    ElementStyle::LineNumber,
×
1985
                );
1986
            } else {
1987
                let row = match show_code_change {
5✔
1988
                    DisplaySuggestion::Diff
9✔
1989
                    | DisplaySuggestion::Add
1990
                    | DisplaySuggestion::Underline => row_num - 1,
1991
                    DisplaySuggestion::None => row_num,
1✔
1992
                };
1993
                self.draw_col_separator_end(buffer, row, max_line_num_len + 1);
6✔
1994
                row_num = row + 1;
2✔
1995
            }
1996
        }
1997
    }
1998

1999
    #[allow(clippy::too_many_arguments)]
2000
    fn draw_code_line(
4✔
2001
        &self,
2002
        buffer: &mut StyledBuffer,
2003
        row_num: &mut usize,
2004
        highlight_parts: &[SubstitutionHighlight],
2005
        line_num: usize,
2006
        line_to_add: &str,
2007
        show_code_change: DisplaySuggestion,
2008
        max_line_num_len: usize,
2009
        file_lines: &[&LineInfo<'_>],
2010
        is_multiline: bool,
2011
    ) {
2012
        if let DisplaySuggestion::Diff = show_code_change {
3✔
2013
            // We need to print more than one line if the span we need to remove is multiline.
2014
            // For more info: https://github.com/rust-lang/rust/issues/92741
2015
            let lines_to_remove = file_lines.iter().take(file_lines.len() - 1);
4✔
2016
            for (index, line_to_remove) in lines_to_remove.enumerate() {
4✔
2017
                buffer.puts(
1✔
2018
                    *row_num - 1,
1✔
2019
                    0,
2020
                    &self.maybe_anonymized(line_num + index),
2✔
2021
                    ElementStyle::LineNumber,
1✔
2022
                );
2023
                buffer.puts(
1✔
2024
                    *row_num - 1,
1✔
2025
                    max_line_num_len + 1,
1✔
2026
                    "- ",
2027
                    ElementStyle::Removal,
1✔
2028
                );
2029
                let line = normalize_whitespace(line_to_remove.line);
2✔
2030
                buffer.puts(
2✔
2031
                    *row_num - 1,
2✔
2032
                    max_line_num_len + 3,
2✔
2033
                    &line,
2✔
2034
                    ElementStyle::NoStyle,
2✔
2035
                );
2036
                *row_num += 1;
2✔
2037
            }
2038
            // If the last line is exactly equal to the line we need to add, we can skip both of
2039
            // them. This allows us to avoid output like the following:
2040
            // 2 - &
2041
            // 2 + if true { true } else { false }
2042
            // 3 - if true { true } else { false }
2043
            // If those lines aren't equal, we print their diff
2044
            let last_line = &file_lines.last().unwrap();
3✔
2045
            if last_line.line == line_to_add {
3✔
2046
                *row_num -= 2;
×
2047
            } else {
2048
                buffer.puts(
3✔
2049
                    *row_num - 1,
3✔
2050
                    0,
2051
                    &self.maybe_anonymized(line_num + file_lines.len() - 1),
6✔
2052
                    ElementStyle::LineNumber,
3✔
2053
                );
2054
                buffer.puts(
3✔
2055
                    *row_num - 1,
3✔
2056
                    max_line_num_len + 1,
3✔
2057
                    "- ",
2058
                    ElementStyle::Removal,
3✔
2059
                );
2060
                buffer.puts(
3✔
2061
                    *row_num - 1,
3✔
2062
                    max_line_num_len + 3,
3✔
2063
                    &normalize_whitespace(last_line.line),
3✔
2064
                    ElementStyle::NoStyle,
3✔
2065
                );
2066
                if line_to_add.trim().is_empty() {
3✔
2067
                    *row_num -= 1;
×
2068
                } else {
2069
                    // Check if after the removal, the line is left with only whitespace. If so, we
2070
                    // will not show an "addition" line, as removing the whole line is what the user
2071
                    // would really want.
2072
                    // For example, for the following:
2073
                    //   |
2074
                    // 2 -     .await
2075
                    // 2 +     (note the left over whitespace)
2076
                    //   |
2077
                    // We really want
2078
                    //   |
2079
                    // 2 -     .await
2080
                    //   |
2081
                    // *row_num -= 1;
2082
                    buffer.puts(
2✔
2083
                        *row_num,
2✔
2084
                        0,
2085
                        &self.maybe_anonymized(line_num),
2✔
2086
                        ElementStyle::LineNumber,
2✔
2087
                    );
2088
                    buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
2✔
2089
                    buffer.append(
2✔
2090
                        *row_num,
2✔
2091
                        &normalize_whitespace(line_to_add),
2✔
2092
                        ElementStyle::NoStyle,
2✔
2093
                    );
2094
                }
2095
            }
2096
        } else if is_multiline {
2✔
2097
            buffer.puts(
1✔
2098
                *row_num,
1✔
2099
                0,
2100
                &self.maybe_anonymized(line_num),
1✔
2101
                ElementStyle::LineNumber,
1✔
2102
            );
2103
            match &highlight_parts {
1✔
2104
                [SubstitutionHighlight { start: 0, end }] if *end == line_to_add.len() => {
1✔
2105
                    buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
×
2106
                }
2107
                [] => {
1✔
2108
                    // FIXME: needed? Doesn't get exercised in any test.
2109
                    self.draw_col_separator_no_space(buffer, *row_num, max_line_num_len + 1);
1✔
2110
                }
2111
                _ => {
2112
                    let diff = self.diff();
1✔
2113
                    buffer.puts(
1✔
2114
                        *row_num,
1✔
2115
                        max_line_num_len + 1,
1✔
2116
                        &format!("{diff} "),
1✔
2117
                        ElementStyle::Addition,
1✔
2118
                    );
2119
                }
2120
            }
2121
            //   LL | line_to_add
2122
            //   ++^^^
2123
            //    |  |
2124
            //    |  magic `3`
2125
            //    `max_line_num_len`
2126
            buffer.puts(
1✔
2127
                *row_num,
1✔
2128
                max_line_num_len + 3,
1✔
2129
                &normalize_whitespace(line_to_add),
1✔
2130
                ElementStyle::NoStyle,
1✔
2131
            );
2132
        } else if let DisplaySuggestion::Add = show_code_change {
2✔
2133
            buffer.puts(
1✔
2134
                *row_num,
1✔
2135
                0,
2136
                &self.maybe_anonymized(line_num),
1✔
2137
                ElementStyle::LineNumber,
1✔
2138
            );
2139
            buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
1✔
2140
            buffer.append(
1✔
2141
                *row_num,
1✔
2142
                &normalize_whitespace(line_to_add),
1✔
2143
                ElementStyle::NoStyle,
1✔
2144
            );
2145
        } else {
2146
            buffer.puts(
2✔
2147
                *row_num,
2✔
2148
                0,
2149
                &self.maybe_anonymized(line_num),
2✔
2150
                ElementStyle::LineNumber,
2✔
2151
            );
2152
            self.draw_col_separator(buffer, *row_num, max_line_num_len + 1);
3✔
2153
            buffer.append(
2✔
2154
                *row_num,
2✔
2155
                &normalize_whitespace(line_to_add),
2✔
2156
                ElementStyle::NoStyle,
2✔
2157
            );
2158
        }
2159

2160
        // Colorize addition/replacements with green.
2161
        for &SubstitutionHighlight { start, end } in highlight_parts {
5✔
2162
            // This is a no-op for empty ranges
2163
            if start != end {
2✔
2164
                // Account for tabs when highlighting (#87972).
2165
                let tabs: usize = line_to_add
2✔
2166
                    .chars()
2167
                    .take(start)
2168
                    .map(|ch| match ch {
4✔
2169
                        '\t' => 3,
×
2170
                        _ => 0,
2✔
2171
                    })
2172
                    .sum();
2173
                buffer.set_style_range(
2✔
2174
                    *row_num,
2✔
2175
                    max_line_num_len + 3 + start + tabs,
2✔
2176
                    max_line_num_len + 3 + end + tabs,
6✔
2177
                    ElementStyle::Addition,
2✔
2178
                    true,
2179
                );
2180
            }
2181
        }
2182
        *row_num += 1;
3✔
2183
    }
2184

2185
    #[allow(clippy::too_many_arguments)]
2186
    fn draw_line(
7✔
2187
        &self,
2188
        buffer: &mut StyledBuffer,
2189
        source_string: &str,
2190
        line_index: usize,
2191
        line_offset: usize,
2192
        width_offset: usize,
2193
        code_offset: usize,
2194
        max_line_num_len: usize,
2195
        margin: Margin,
2196
    ) -> usize {
2197
        // Tabs are assumed to have been replaced by spaces in calling code.
2198
        debug_assert!(!source_string.contains('\t'));
4✔
2199
        let line_len = str_width(source_string);
8✔
2200
        // Create the source line we will highlight.
2201
        let mut left = margin.left(line_len);
4✔
2202
        let right = margin.right(line_len);
8✔
2203
        // FIXME: The following code looks fishy. See #132860.
2204
        // On long lines, we strip the source line, accounting for unicode.
2205
        let mut taken = 0;
8✔
2206
        let mut skipped = 0;
4✔
2207
        let code: String = source_string
16✔
2208
            .chars()
2209
            .skip_while(|ch| {
4✔
2210
                skipped += char_width(*ch);
8✔
2211
                skipped <= left
4✔
2212
            })
2213
            .take_while(|ch| {
12✔
2214
                // Make sure that the trimming on the right will fall within the terminal width.
2215
                taken += char_width(*ch);
4✔
2216
                taken <= (right - left)
12✔
2217
            })
2218
            .collect();
2219

2220
        let placeholder = self.margin();
16✔
2221
        let padding = str_width(placeholder);
6✔
2222
        let (width_taken, bytes_taken) = if margin.was_cut_left() {
23✔
2223
            // We have stripped some code/whitespace from the beginning, make it clear.
2224
            let mut bytes_taken = 0;
4✔
2225
            let mut width_taken = 0;
4✔
2226
            for ch in code.chars() {
10✔
2227
                width_taken += char_width(ch);
10✔
2228
                bytes_taken += ch.len_utf8();
8✔
2229

2230
                if width_taken >= padding {
4✔
2231
                    break;
2232
                }
2233
            }
2234

2235
            if width_taken > padding {
6✔
2236
                left -= width_taken - padding;
1✔
2237
            }
2238

2239
            buffer.puts(
4✔
2240
                line_offset,
2241
                code_offset,
2242
                placeholder,
2243
                ElementStyle::LineNumber,
4✔
2244
            );
2245
            (width_taken, bytes_taken)
4✔
2246
        } else {
2247
            (0, 0)
5✔
2248
        };
2249

2250
        buffer.puts(
3✔
2251
            line_offset,
2252
            code_offset + width_taken,
8✔
2253
            &code[bytes_taken..],
3✔
2254
            ElementStyle::Quotation,
8✔
2255
        );
2256

2257
        if line_len > right {
8✔
2258
            // We have stripped some code/whitespace from the beginning, make it clear.
2259
            let mut char_taken = 0;
2✔
2260
            let mut width_taken_inner = 0;
2✔
2261
            for ch in code.chars().rev() {
2✔
2262
                width_taken_inner += char_width(ch);
4✔
2263
                char_taken += 1;
4✔
2264

2265
                if width_taken_inner >= padding {
2✔
2266
                    break;
2267
                }
2268
            }
2269

2270
            buffer.puts(
4✔
2271
                line_offset,
2272
                code_offset + width_taken + code[bytes_taken..].chars().count() - char_taken,
4✔
2273
                placeholder,
2274
                ElementStyle::LineNumber,
2✔
2275
            );
2276
        }
2277

2278
        buffer.puts(
8✔
2279
            line_offset,
2280
            0,
2281
            &format!("{:>max_line_num_len$}", self.maybe_anonymized(line_index)),
11✔
2282
            ElementStyle::LineNumber,
3✔
2283
        );
2284

2285
        self.draw_col_separator_no_space(buffer, line_offset, width_offset - 2);
3✔
2286

2287
        left
9✔
2288
    }
2289

2290
    fn draw_range(
4✔
2291
        &self,
2292
        buffer: &mut StyledBuffer,
2293
        symbol: char,
2294
        line: usize,
2295
        col_from: usize,
2296
        col_to: usize,
2297
        style: ElementStyle,
2298
    ) {
2299
        for col in col_from..col_to {
7✔
2300
            buffer.putc(line, col, symbol, style);
3✔
2301
        }
2302
    }
2303

2304
    fn draw_multiline_line(
3✔
2305
        &self,
2306
        buffer: &mut StyledBuffer,
2307
        line: usize,
2308
        offset: usize,
2309
        depth: usize,
2310
        style: ElementStyle,
2311
    ) {
2312
        let chr = match (style, self.theme) {
3✔
2313
            (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, OutputTheme::Ascii) => {
2314
                '|'
3✔
2315
            }
2316
            (_, OutputTheme::Ascii) => '|',
1✔
2317
            (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, OutputTheme::Unicode) => {
2318
                '┃'
1✔
2319
            }
2320
            (_, OutputTheme::Unicode) => '│',
1✔
2321
        };
2322
        buffer.putc(line, offset + depth - 1, chr, style);
6✔
2323
    }
2324

2325
    fn col_separator(&self) -> char {
5✔
2326
        match self.theme {
7✔
2327
            OutputTheme::Ascii => '|',
5✔
2328
            OutputTheme::Unicode => '│',
2✔
2329
        }
2330
    }
2331

2332
    fn multi_suggestion_separator(&self) -> &'static str {
×
2333
        match self.theme {
×
2334
            OutputTheme::Ascii => "|",
×
2335
            OutputTheme::Unicode => "├╴",
×
2336
        }
2337
    }
2338

2339
    fn draw_col_separator(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
3✔
2340
        let chr = self.col_separator();
3✔
2341
        buffer.puts(line, col, &format!("{chr} "), ElementStyle::LineNumber);
3✔
2342
    }
2343

2344
    fn draw_col_separator_no_space(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
5✔
2345
        let chr = self.col_separator();
7✔
2346
        self.draw_col_separator_no_space_with_style(
4✔
2347
            buffer,
2348
            chr,
2349
            line,
2350
            col,
2351
            ElementStyle::LineNumber,
7✔
2352
        );
2353
    }
2354

2355
    fn draw_col_separator_start(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
6✔
2356
        match self.theme {
6✔
2357
            OutputTheme::Ascii => {
2358
                self.draw_col_separator_no_space_with_style(
7✔
2359
                    buffer,
2360
                    '|',
2361
                    line,
2362
                    col,
2363
                    ElementStyle::LineNumber,
6✔
2364
                );
2365
            }
2366
            OutputTheme::Unicode => {
2367
                self.draw_col_separator_no_space_with_style(
1✔
2368
                    buffer,
2369
                    '╭',
2370
                    line,
2371
                    col,
2372
                    ElementStyle::LineNumber,
1✔
2373
                );
2374
                self.draw_col_separator_no_space_with_style(
1✔
2375
                    buffer,
2376
                    '╴',
2377
                    line,
2378
                    col + 1,
1✔
2379
                    ElementStyle::LineNumber,
1✔
2380
                );
2381
            }
2382
        }
2383
    }
2384

2385
    fn draw_col_separator_end(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
3✔
2386
        match self.theme {
3✔
2387
            OutputTheme::Ascii => {
2388
                self.draw_col_separator_no_space_with_style(
4✔
2389
                    buffer,
2390
                    '|',
2391
                    line,
2392
                    col,
2393
                    ElementStyle::LineNumber,
4✔
2394
                );
2395
            }
2396
            OutputTheme::Unicode => {
2397
                self.draw_col_separator_no_space_with_style(
2✔
2398
                    buffer,
2399
                    '╰',
2400
                    line,
2401
                    col,
2402
                    ElementStyle::LineNumber,
2✔
2403
                );
2404
                self.draw_col_separator_no_space_with_style(
2✔
2405
                    buffer,
2406
                    '╴',
2407
                    line,
2408
                    col + 1,
2✔
2409
                    ElementStyle::LineNumber,
2✔
2410
                );
2411
            }
2412
        }
2413
    }
2414

2415
    fn draw_col_separator_no_space_with_style(
8✔
2416
        &self,
2417
        buffer: &mut StyledBuffer,
2418
        chr: char,
2419
        line: usize,
2420
        col: usize,
2421
        style: ElementStyle,
2422
    ) {
2423
        buffer.putc(line, col, chr, style);
4✔
2424
    }
2425

2426
    fn maybe_anonymized(&self, line_num: usize) -> Cow<'static, str> {
8✔
2427
        if self.anonymized_line_numbers {
6✔
2428
            Cow::Borrowed(ANONYMIZED_LINE_NUM)
4✔
2429
        } else {
2430
            Cow::Owned(line_num.to_string())
7✔
2431
        }
2432
    }
2433

2434
    fn file_start(&self) -> &'static str {
4✔
2435
        match self.theme {
8✔
2436
            OutputTheme::Ascii => "--> ",
4✔
2437
            OutputTheme::Unicode => " ╭▸ ",
2✔
2438
        }
2439
    }
2440

2441
    fn secondary_file_start(&self) -> &'static str {
3✔
2442
        match self.theme {
3✔
2443
            OutputTheme::Ascii => "::: ",
3✔
2444
            OutputTheme::Unicode => " ⸬  ",
×
2445
        }
2446
    }
2447

2448
    fn draw_note_separator(
2✔
2449
        &self,
2450
        buffer: &mut StyledBuffer,
2451
        line: usize,
2452
        col: usize,
2453
        is_cont: bool,
2454
    ) {
2455
        let chr = match self.theme {
2✔
2456
            OutputTheme::Ascii => "= ",
2✔
2457
            OutputTheme::Unicode if is_cont => "├ ",
2✔
2458
            OutputTheme::Unicode => "╰ ",
1✔
2459
        };
2460
        buffer.puts(line, col, chr, ElementStyle::LineNumber);
2✔
2461
    }
2462

2463
    fn diff(&self) -> char {
1✔
2464
        match self.theme {
1✔
2465
            OutputTheme::Ascii => '~',
1✔
2466
            OutputTheme::Unicode => '±',
×
2467
        }
2468
    }
2469

2470
    fn draw_line_separator(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
3✔
2471
        let (column, dots) = match self.theme {
6✔
2472
            OutputTheme::Ascii => (0, "..."),
3✔
2473
            OutputTheme::Unicode => (col - 2, "‡"),
2✔
2474
        };
2475
        buffer.puts(line, column, dots, ElementStyle::LineNumber);
3✔
2476
    }
2477

2478
    fn margin(&self) -> &'static str {
4✔
2479
        match self.theme {
8✔
2480
            OutputTheme::Ascii => "...",
4✔
2481
            OutputTheme::Unicode => "…",
2✔
2482
        }
2483
    }
2484

2485
    fn underline(&self, is_primary: bool) -> UnderlineParts {
4✔
2486
        //               X0 Y0
2487
        // label_start > ┯━━━━ < underline
2488
        //               │ < vertical_text_line
2489
        //               text
2490

2491
        //    multiline_start_down ⤷ X0 Y0
2492
        //            top_left > ┌───╿──┘ < top_right_flat
2493
        //           top_left > ┏│━━━┙ < top_right
2494
        // multiline_vertical > ┃│
2495
        //                      ┃│   X1 Y1
2496
        //                      ┃│   X2 Y2
2497
        //                      ┃└────╿──┘ < multiline_end_same_line
2498
        //        bottom_left > ┗━━━━━┥ < bottom_right_with_text
2499
        //   multiline_horizontal ^   `X` is a good letter
2500

2501
        // multiline_whole_line > ┏ X0 Y0
2502
        //                        ┃   X1 Y1
2503
        //                        ┗━━━━┛ < multiline_end_same_line
2504

2505
        // multiline_whole_line > ┏ X0 Y0
2506
        //                        ┃ X1 Y1
2507
        //                        ┃  ╿ < multiline_end_up
2508
        //                        ┗━━┛ < bottom_right
2509

2510
        match (self.theme, is_primary) {
5✔
2511
            (OutputTheme::Ascii, true) => UnderlineParts {
2512
                style: ElementStyle::UnderlinePrimary,
2513
                underline: '^',
2514
                label_start: '^',
2515
                vertical_text_line: '|',
2516
                multiline_vertical: '|',
2517
                multiline_horizontal: '_',
2518
                multiline_whole_line: '/',
2519
                multiline_start_down: '^',
2520
                bottom_right: '|',
2521
                top_left: ' ',
2522
                top_right_flat: '^',
2523
                bottom_left: '|',
2524
                multiline_end_up: '^',
2525
                multiline_end_same_line: '^',
2526
                multiline_bottom_right_with_text: '|',
2527
            },
2528
            (OutputTheme::Ascii, false) => UnderlineParts {
2529
                style: ElementStyle::UnderlineSecondary,
2530
                underline: '-',
2531
                label_start: '-',
2532
                vertical_text_line: '|',
2533
                multiline_vertical: '|',
2534
                multiline_horizontal: '_',
2535
                multiline_whole_line: '/',
2536
                multiline_start_down: '-',
2537
                bottom_right: '|',
2538
                top_left: ' ',
2539
                top_right_flat: '-',
2540
                bottom_left: '|',
2541
                multiline_end_up: '-',
2542
                multiline_end_same_line: '-',
2543
                multiline_bottom_right_with_text: '|',
2544
            },
2545
            (OutputTheme::Unicode, true) => UnderlineParts {
2546
                style: ElementStyle::UnderlinePrimary,
2547
                underline: '━',
2548
                label_start: '┯',
2549
                vertical_text_line: '│',
2550
                multiline_vertical: '┃',
2551
                multiline_horizontal: '━',
2552
                multiline_whole_line: '┏',
2553
                multiline_start_down: '╿',
2554
                bottom_right: '┙',
2555
                top_left: '┏',
2556
                top_right_flat: '┛',
2557
                bottom_left: '┗',
2558
                multiline_end_up: '╿',
2559
                multiline_end_same_line: '┛',
2560
                multiline_bottom_right_with_text: '┥',
2561
            },
2562
            (OutputTheme::Unicode, false) => UnderlineParts {
2563
                style: ElementStyle::UnderlineSecondary,
2564
                underline: '─',
2565
                label_start: '┬',
2566
                vertical_text_line: '│',
2567
                multiline_vertical: '│',
2568
                multiline_horizontal: '─',
2569
                multiline_whole_line: '┌',
2570
                multiline_start_down: '│',
2571
                bottom_right: '┘',
2572
                top_left: '┌',
2573
                top_right_flat: '┘',
2574
                bottom_left: '└',
2575
                multiline_end_up: '│',
2576
                multiline_end_same_line: '┘',
2577
                multiline_bottom_right_with_text: '┤',
2578
            },
2579
        }
2580
    }
2581
}
2582

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

2591
    #[cfg(target_pointer_width = "32")]
2592
    const MAX_DIGITS: usize = 10;
2593

2594
    #[cfg(target_pointer_width = "16")]
2595
    const MAX_DIGITS: usize = 5;
2596

2597
    let mut lim = 10;
5✔
2598
    for num_digits in 1..MAX_DIGITS {
11✔
2599
        if num < lim {
5✔
2600
            return num_digits;
6✔
2601
        }
2602
        lim = lim.wrapping_mul(10);
4✔
2603
    }
2604
    MAX_DIGITS
×
2605
}
2606

2607
pub fn str_width(s: &str) -> usize {
4✔
2608
    s.chars().map(char_width).sum()
8✔
2609
}
2610

2611
pub fn char_width(ch: char) -> usize {
6✔
2612
    // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` is. For now,
2613
    // just accept that sometimes the code line will be longer than desired.
2614
    match ch {
6✔
2615
        '\t' => 4,
1✔
2616
        // Keep the following list in sync with `rustc_errors::emitter::OUTPUT_REPLACEMENTS`. These
2617
        // are control points that we replace before printing with a visible codepoint for the sake
2618
        // of being able to point at them with underlines.
2619
        '\u{0000}' | '\u{0001}' | '\u{0002}' | '\u{0003}' | '\u{0004}' | '\u{0005}'
×
2620
        | '\u{0006}' | '\u{0007}' | '\u{0008}' | '\u{000B}' | '\u{000C}' | '\u{000D}'
2621
        | '\u{000E}' | '\u{000F}' | '\u{0010}' | '\u{0011}' | '\u{0012}' | '\u{0013}'
2622
        | '\u{0014}' | '\u{0015}' | '\u{0016}' | '\u{0017}' | '\u{0018}' | '\u{0019}'
2623
        | '\u{001A}' | '\u{001B}' | '\u{001C}' | '\u{001D}' | '\u{001E}' | '\u{001F}'
2624
        | '\u{007F}' | '\u{202A}' | '\u{202B}' | '\u{202D}' | '\u{202E}' | '\u{2066}'
2625
        | '\u{2067}' | '\u{2068}' | '\u{202C}' | '\u{2069}' => 1,
2626
        _ => unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1),
6✔
2627
    }
2628
}
2629

2630
fn num_overlap(
6✔
2631
    a_start: usize,
2632
    a_end: usize,
2633
    b_start: usize,
2634
    b_end: usize,
2635
    inclusive: bool,
2636
) -> bool {
2637
    let extra = usize::from(inclusive);
6✔
2638
    (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start)
4✔
2639
}
2640

2641
fn overlaps(a1: &LineAnnotation<'_>, a2: &LineAnnotation<'_>, padding: usize) -> bool {
7✔
2642
    num_overlap(
2643
        a1.start.display,
4✔
2644
        a1.end.display + padding,
8✔
2645
        a2.start.display,
4✔
2646
        a2.end.display,
8✔
2647
        false,
2648
    )
2649
}
2650

2651
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2652
pub(crate) enum LineAnnotationType {
2653
    /// Annotation under a single line of code
2654
    Singleline,
2655

2656
    // The Multiline type above is replaced with the following three in order
2657
    // to reuse the current label drawing code.
2658
    //
2659
    // Each of these corresponds to one part of the following diagram:
2660
    //
2661
    //     x |   foo(1 + bar(x,
2662
    //       |  _________^              < MultilineStart
2663
    //     x | |             y),        < MultilineLine
2664
    //       | |______________^ label   < MultilineEnd
2665
    //     x |       z);
2666
    /// Annotation marking the first character of a fully shown multiline span
2667
    MultilineStart(usize),
2668
    /// Annotation marking the last character of a fully shown multiline span
2669
    MultilineEnd(usize),
2670
    /// Line at the left enclosing the lines of a fully shown multiline span
2671
    // Just a placeholder for the drawing algorithm, to know that it shouldn't skip the first 4
2672
    // and last 2 lines of code. The actual line is drawn in `emit_message_default` and not in
2673
    // `draw_multiline_line`.
2674
    MultilineLine(usize),
2675
}
2676

2677
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2678
pub(crate) struct LineAnnotation<'a> {
2679
    /// Start column.
2680
    /// Note that it is important that this field goes
2681
    /// first, so that when we sort, we sort orderings by start
2682
    /// column.
2683
    pub start: Loc,
2684

2685
    /// End column within the line (exclusive)
2686
    pub end: Loc,
2687

2688
    /// level
2689
    pub kind: AnnotationKind,
2690

2691
    /// Optional label to display adjacent to the annotation.
2692
    pub label: Option<&'a str>,
2693

2694
    /// Is this a single line, multiline or multiline span minimized down to a
2695
    /// smaller span.
2696
    pub annotation_type: LineAnnotationType,
2697

2698
    /// Whether the source code should be highlighted
2699
    pub highlight_source: bool,
2700
}
2701

2702
impl LineAnnotation<'_> {
2703
    pub(crate) fn is_primary(&self) -> bool {
5✔
2704
        self.kind == AnnotationKind::Primary
4✔
2705
    }
2706

2707
    /// Whether this annotation is a vertical line placeholder.
2708
    pub(crate) fn is_line(&self) -> bool {
4✔
2709
        matches!(self.annotation_type, LineAnnotationType::MultilineLine(_))
8✔
2710
    }
2711

2712
    /// Length of this annotation as displayed in the stderr output
2713
    pub(crate) fn len(&self) -> usize {
3✔
2714
        // Account for usize underflows
2715
        self.end.display.abs_diff(self.start.display)
3✔
2716
    }
2717

2718
    pub(crate) fn has_label(&self) -> bool {
4✔
2719
        if let Some(label) = self.label {
11✔
2720
            // Consider labels with no text as effectively not being there
2721
            // to avoid weird output with unnecessary vertical lines, like:
2722
            //
2723
            //     X | fn foo(x: u32) {
2724
            //       | -------^------
2725
            //       | |      |
2726
            //       | |
2727
            //       |
2728
            //
2729
            // Note that this would be the complete output users would see.
2730
            !label.is_empty()
3✔
2731
        } else {
2732
            false
3✔
2733
        }
2734
    }
2735

2736
    pub(crate) fn takes_space(&self) -> bool {
2✔
2737
        // Multiline annotations always have to keep vertical space.
2738
        matches!(
2✔
2739
            self.annotation_type,
2✔
2740
            LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
2741
        )
2742
    }
2743
}
2744

2745
#[derive(Clone, Copy, Debug)]
2746
pub(crate) enum DisplaySuggestion {
2747
    Underline,
2748
    Diff,
2749
    None,
2750
    Add,
2751
}
2752

2753
// We replace some characters so the CLI output is always consistent and underlines aligned.
2754
// Keep the following list in sync with `rustc_span::char_width`.
2755
const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[
2756
    // In terminals without Unicode support the following will be garbled, but in *all* terminals
2757
    // the underlying codepoint will be as well. We could gate this replacement behind a "unicode
2758
    // support" gate.
2759
    ('\0', "␀"),
2760
    ('\u{0001}', "␁"),
2761
    ('\u{0002}', "␂"),
2762
    ('\u{0003}', "␃"),
2763
    ('\u{0004}', "␄"),
2764
    ('\u{0005}', "␅"),
2765
    ('\u{0006}', "␆"),
2766
    ('\u{0007}', "␇"),
2767
    ('\u{0008}', "␈"),
2768
    ('\t', "    "), // We do our own tab replacement
2769
    ('\u{000b}', "␋"),
2770
    ('\u{000c}', "␌"),
2771
    ('\u{000d}', "␍"),
2772
    ('\u{000e}', "␎"),
2773
    ('\u{000f}', "␏"),
2774
    ('\u{0010}', "␐"),
2775
    ('\u{0011}', "␑"),
2776
    ('\u{0012}', "␒"),
2777
    ('\u{0013}', "␓"),
2778
    ('\u{0014}', "␔"),
2779
    ('\u{0015}', "␕"),
2780
    ('\u{0016}', "␖"),
2781
    ('\u{0017}', "␗"),
2782
    ('\u{0018}', "␘"),
2783
    ('\u{0019}', "␙"),
2784
    ('\u{001a}', "␚"),
2785
    ('\u{001b}', "␛"),
2786
    ('\u{001c}', "␜"),
2787
    ('\u{001d}', "␝"),
2788
    ('\u{001e}', "␞"),
2789
    ('\u{001f}', "␟"),
2790
    ('\u{007f}', "␡"),
2791
    ('\u{200d}', ""), // Replace ZWJ for consistent terminal output of grapheme clusters.
2792
    ('\u{202a}', "�"), // The following unicode text flow control characters are inconsistently
2793
    ('\u{202b}', "�"), // supported across CLIs and can cause confusion due to the bytes on disk
2794
    ('\u{202c}', "�"), // not corresponding to the visible source code, so we replace them always.
2795
    ('\u{202d}', "�"),
2796
    ('\u{202e}', "�"),
2797
    ('\u{2066}', "�"),
2798
    ('\u{2067}', "�"),
2799
    ('\u{2068}', "�"),
2800
    ('\u{2069}', "�"),
2801
];
2802

2803
pub(crate) fn normalize_whitespace(s: &str) -> String {
4✔
2804
    // Scan the input string for a character in the ordered table above.
2805
    // If it's present, replace it with its alternative string (it can be more than 1 char!).
2806
    // Otherwise, retain the input char.
2807
    s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
9✔
2808
        match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) {
21✔
2809
            Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1),
2✔
2810
            _ => s.push(c),
12✔
2811
        }
2812
        s
5✔
2813
    })
2814
}
2815

2816
#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)]
2817
pub(crate) enum ElementStyle {
2818
    MainHeaderMsg,
2819
    HeaderMsg,
2820
    LineAndColumn,
2821
    LineNumber,
2822
    Quotation,
2823
    UnderlinePrimary,
2824
    UnderlineSecondary,
2825
    LabelPrimary,
2826
    LabelSecondary,
2827
    NoStyle,
2828
    Level(LevelInner),
2829
    Addition,
2830
    Removal,
2831
}
2832

2833
impl ElementStyle {
2834
    fn color_spec(&self, level: &Level<'_>, stylesheet: &Stylesheet) -> Style {
5✔
2835
        match self {
5✔
2836
            ElementStyle::Addition => stylesheet.addition,
2✔
2837
            ElementStyle::Removal => stylesheet.removal,
3✔
2838
            ElementStyle::LineAndColumn => stylesheet.none,
4✔
2839
            ElementStyle::LineNumber => stylesheet.line_num,
4✔
2840
            ElementStyle::Quotation => stylesheet.none,
5✔
2841
            ElementStyle::MainHeaderMsg => stylesheet.emphasis,
4✔
2842
            ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary => level.style(stylesheet),
4✔
2843
            ElementStyle::UnderlineSecondary | ElementStyle::LabelSecondary => stylesheet.context,
3✔
2844
            ElementStyle::HeaderMsg | ElementStyle::NoStyle => stylesheet.none,
5✔
2845
            ElementStyle::Level(lvl) => lvl.style(stylesheet),
4✔
2846
        }
2847
    }
2848
}
2849

2850
#[derive(Debug, Clone, Copy)]
2851
struct UnderlineParts {
2852
    style: ElementStyle,
2853
    underline: char,
2854
    label_start: char,
2855
    vertical_text_line: char,
2856
    multiline_vertical: char,
2857
    multiline_horizontal: char,
2858
    multiline_whole_line: char,
2859
    multiline_start_down: char,
2860
    bottom_right: char,
2861
    top_left: char,
2862
    top_right_flat: char,
2863
    bottom_left: char,
2864
    multiline_end_up: char,
2865
    multiline_end_same_line: char,
2866
    multiline_bottom_right_with_text: char,
2867
}
2868

2869
/// Whether the original and suggested code are the same.
2870
pub(crate) fn is_different(sm: &SourceMap<'_>, suggested: &str, range: Range<usize>) -> bool {
3✔
2871
    match sm.span_to_snippet(range) {
3✔
2872
        Some(s) => s != suggested,
3✔
2873
        None => true,
×
2874
    }
2875
}
2876

2877
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2878
pub enum OutputTheme {
2879
    Ascii,
2880
    Unicode,
2881
}
2882

2883
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2884
enum TitleStyle {
2885
    MainHeader,
2886
    Header,
2887
    Secondary,
2888
}
2889

2890
#[cfg(test)]
2891
mod test {
2892
    use super::OUTPUT_REPLACEMENTS;
2893
    use snapbox::IntoData;
2894

2895
    fn format_replacements(replacements: Vec<(char, &str)>) -> String {
2896
        replacements
2897
            .into_iter()
2898
            .map(|r| format!("    {r:?}"))
2899
            .collect::<Vec<_>>()
2900
            .join("\n")
2901
    }
2902

2903
    #[test]
2904
    /// The [`OUTPUT_REPLACEMENTS`] array must be sorted (for binary search to
2905
    /// work) and must contain no duplicate entries
2906
    fn ensure_output_replacements_is_sorted() {
2907
        let mut expected = OUTPUT_REPLACEMENTS.to_owned();
2908
        expected.sort_by_key(|r| r.0);
2909
        expected.dedup_by_key(|r| r.0);
2910
        let expected = format_replacements(expected);
2911
        let actual = format_replacements(OUTPUT_REPLACEMENTS.to_owned());
2912
        snapbox::assert_data_eq!(actual, expected.into_data().raw());
2913
    }
2914
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc