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

rust-lang / annotate-snippets-rs / 22874293285

09 Mar 2026 08:53PM UTC coverage: 90.674% (-0.1%) from 90.782%
22874293285

Pull #386

github

web-flow
Merge b1da999d4 into aa7907cce
Pull Request #386: feat: Add support for hyperlinks in snippet origin

10 of 13 new or added lines in 2 files covered. (76.92%)

22 existing lines in 1 file now uncovered.

1507 of 1662 relevant lines covered (90.67%)

5.41 hits per line

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

97.13
/src/renderer/render.rs
1
// Most of this file is adapted from https://github.com/rust-lang/rust/blob/160905b6253f42967ed4aef4b98002944c7df24c/compiler/rustc_errors/src/emitter.rs
2

3
use alloc::borrow::{Cow, ToOwned};
4
use alloc::collections::BTreeMap;
5
use alloc::string::{String, ToString};
6
use alloc::{format, vec, vec::Vec};
7
use core::cmp::{max, min, Ordering, Reverse};
8
use core::fmt;
9

10
use anstyle::Style;
11

12
use super::margin::Margin;
13
use super::stylesheet::Stylesheet;
14
use super::DecorStyle;
15
use super::Renderer;
16
use crate::level::{Level, LevelInner};
17
use crate::renderer::source_map::{
18
    AnnotatedLineInfo, LineInfo, Loc, SourceMap, SplicedLines, SubstitutionHighlight, TrimmedPatch,
19
};
20
use crate::renderer::styled_buffer::StyledBuffer;
21
use crate::snippet::Id;
22
use crate::{
23
    Annotation, AnnotationKind, Element, Group, Message, Origin, Padding, Patch, Report, Snippet,
24
    Title,
25
};
26

27
const ANONYMIZED_LINE_NUM: &str = "LL";
28

29
pub(crate) fn render(renderer: &Renderer, groups: Report<'_>) -> String {
8✔
30
    if renderer.short_message {
8✔
31
        render_short_message(renderer, groups).unwrap()
1✔
32
    } else {
33
        let (max_line_num, og_primary_path, groups) = pre_process(groups);
8✔
34
        let max_line_num_len = if renderer.anonymized_line_numbers {
4✔
35
            ANONYMIZED_LINE_NUM.len()
×
36
        } else {
37
            num_decimal_digits(max_line_num)
9✔
38
        };
39
        let mut out_string = String::new();
5✔
40
        let group_len = groups.len();
10✔
41
        for (
5✔
42
            g,
5✔
43
            PreProcessedGroup {
44
                group,
5✔
45
                elements,
5✔
46
                primary_path,
5✔
47
                max_depth,
5✔
48
            },
49
        ) in groups.into_iter().enumerate()
10✔
50
        {
51
            let mut buffer = StyledBuffer::new();
5✔
52
            let level = group.primary_level.clone();
6✔
53
            let mut message_iter = elements.into_iter().enumerate().peekable();
11✔
54
            if let Some(title) = &group.title {
7✔
55
                let peek = message_iter.peek().map(|(_, s)| s);
25✔
56
                let title_style = if title.allows_styling {
10✔
57
                    TitleStyle::Header
3✔
58
                } else {
59
                    TitleStyle::MainHeader
7✔
60
                };
61
                let buffer_msg_line_offset = buffer.num_lines();
7✔
62
                render_title(
63
                    renderer,
64
                    &mut buffer,
65
                    title,
66
                    max_line_num_len,
6✔
67
                    title_style,
6✔
68
                    matches!(peek, Some(PreProcessedElement::Message(_))),
6✔
69
                    buffer_msg_line_offset,
70
                );
71
                let buffer_msg_line_offset = buffer.num_lines();
5✔
72

73
                if matches!(peek, Some(PreProcessedElement::Message(_))) {
5✔
74
                    draw_col_separator_no_space(
75
                        renderer,
76
                        &mut buffer,
77
                        buffer_msg_line_offset,
78
                        max_line_num_len + 1,
2✔
79
                    );
80
                }
81
                if peek.is_none()
12✔
82
                    && title_style == TitleStyle::MainHeader
2✔
83
                    && g == 0
2✔
84
                    && group_len > 1
1✔
85
                {
86
                    draw_col_separator_end(
87
                        renderer,
88
                        &mut buffer,
89
                        buffer_msg_line_offset,
90
                        max_line_num_len + 1,
1✔
91
                    );
92
                }
93
            }
94
            let mut seen_primary = false;
7✔
95
            let mut last_suggestion_path = None;
7✔
96
            while let Some((i, section)) = message_iter.next() {
19✔
97
                let peek = message_iter.peek().map(|(_, s)| s);
22✔
98
                let is_first = i == 0;
7✔
99
                match section {
6✔
100
                    PreProcessedElement::Message(title) => {
3✔
101
                        let title_style = TitleStyle::Secondary;
3✔
102
                        let buffer_msg_line_offset = buffer.num_lines();
6✔
103
                        render_title(
104
                            renderer,
105
                            &mut buffer,
106
                            title,
107
                            max_line_num_len,
3✔
108
                            title_style,
109
                            peek.is_some(),
3✔
110
                            buffer_msg_line_offset,
111
                        );
112
                    }
113
                    PreProcessedElement::Cause((cause, source_map, annotated_lines)) => {
7✔
114
                        let is_primary = primary_path == cause.path.as_ref() && !seen_primary;
15✔
115
                        seen_primary |= is_primary;
8✔
116
                        render_snippet_annotations(
117
                            renderer,
118
                            &mut buffer,
119
                            max_line_num_len,
8✔
120
                            cause,
121
                            is_primary,
7✔
122
                            &source_map,
123
                            &annotated_lines,
9✔
124
                            max_depth,
125
                            peek.is_some() || (g == 0 && group_len > 1),
7✔
126
                            is_first,
127
                        );
128

129
                        if g == 0 {
5✔
130
                            let current_line = buffer.num_lines();
5✔
131
                            match peek {
5✔
132
                                Some(PreProcessedElement::Message(_)) => {
133
                                    draw_col_separator_no_space(
134
                                        renderer,
135
                                        &mut buffer,
136
                                        current_line,
137
                                        max_line_num_len + 1,
3✔
138
                                    );
139
                                }
140
                                None if group_len > 1 => draw_col_separator_end(
141
                                    renderer,
142
                                    &mut buffer,
143
                                    current_line,
144
                                    max_line_num_len + 1,
3✔
145
                                ),
146
                                _ => {}
147
                            }
148
                        }
149
                    }
150
                    PreProcessedElement::Suggestion((
3✔
151
                        suggestion,
152
                        source_map,
153
                        spliced_lines,
154
                        display_suggestion,
155
                    )) => {
156
                        let matches_previous_suggestion =
6✔
157
                            last_suggestion_path == Some(suggestion.path.as_ref());
158
                        emit_suggestion_default(
159
                            renderer,
160
                            &mut buffer,
161
                            suggestion,
162
                            spliced_lines,
4✔
163
                            display_suggestion,
164
                            max_line_num_len,
3✔
165
                            &source_map,
166
                            primary_path.or(og_primary_path),
3✔
167
                            matches_previous_suggestion,
168
                            is_first,
169
                            //matches!(peek, Some(Element::Message(_) | Element::Padding(_))),
170
                            peek.is_some(),
5✔
171
                        );
172

173
                        if matches!(peek, Some(PreProcessedElement::Suggestion(_))) {
14✔
174
                            last_suggestion_path = Some(suggestion.path.as_ref());
8✔
175
                        } else {
176
                            last_suggestion_path = None;
3✔
177
                        }
178
                    }
179

180
                    PreProcessedElement::Origin(origin) => {
2✔
181
                        let buffer_msg_line_offset = buffer.num_lines();
4✔
182
                        let is_primary = primary_path == Some(&origin.path) && !seen_primary;
2✔
183
                        seen_primary |= is_primary;
2✔
184
                        render_origin(
185
                            renderer,
186
                            &mut buffer,
187
                            max_line_num_len,
2✔
188
                            origin,
189
                            is_primary,
2✔
190
                            is_first,
191
                            peek.is_none(),
2✔
192
                            buffer_msg_line_offset,
193
                        );
194
                        let current_line = buffer.num_lines();
2✔
195
                        if g == 0 && peek.is_none() && group_len > 1 {
2✔
196
                            draw_col_separator_end(
197
                                renderer,
198
                                &mut buffer,
199
                                current_line,
200
                                max_line_num_len + 1,
×
201
                            );
202
                        }
203
                    }
204
                    PreProcessedElement::Padding(_) => {
205
                        let current_line = buffer.num_lines();
6✔
206
                        if peek.is_none() {
3✔
207
                            draw_col_separator_end(
208
                                renderer,
209
                                &mut buffer,
210
                                current_line,
211
                                max_line_num_len + 1,
3✔
212
                            );
213
                        } else {
214
                            draw_col_separator_no_space(
215
                                renderer,
216
                                &mut buffer,
217
                                current_line,
218
                                max_line_num_len + 1,
2✔
219
                            );
220
                        }
221
                    }
222
                }
223
            }
224
            buffer
225
                .render(&level, &renderer.stylesheet, &mut out_string)
5✔
226
                .unwrap();
227
            if g != group_len - 1 {
6✔
228
                use core::fmt::Write;
229

230
                writeln!(out_string).unwrap();
3✔
231
            }
232
        }
233
        out_string
7✔
234
    }
235
}
236

237
fn render_short_message(renderer: &Renderer, groups: &[Group<'_>]) -> Result<String, fmt::Error> {
1✔
238
    let mut buffer = StyledBuffer::new();
1✔
239
    let mut labels = None;
1✔
240
    let group = groups.first().expect("Expected at least one group");
2✔
241

242
    let Some(title) = &group.title else {
1✔
243
        panic!("Expected a Title");
×
244
    };
245

246
    if let Some(Element::Cause(cause)) = group
6✔
247
        .elements
248
        .iter()
249
        .find(|e| matches!(e, Element::Cause(_)))
3✔
250
    {
251
        let labels_inner = cause
2✔
252
            .markers
253
            .iter()
254
            .filter_map(|ann| match &ann.label {
3✔
255
                Some(msg) if ann.kind.is_primary() => {
1✔
256
                    if !msg.trim().is_empty() {
2✔
257
                        Some(msg.to_string())
1✔
258
                    } else {
259
                        None
×
260
                    }
261
                }
262
                _ => None,
1✔
263
            })
264
            .collect::<Vec<_>>()
265
            .join(", ");
266
        if !labels_inner.is_empty() {
2✔
267
            labels = Some(labels_inner);
1✔
268
        }
269

270
        if let Some(path) = &cause.path {
2✔
271
            let mut origin = Origin::path(path.as_ref());
3✔
272

273
            let source_map = SourceMap::new(&cause.source, cause.line_start);
4✔
274
            let (_depth, annotated_lines) =
2✔
275
                source_map.annotated_lines(cause.markers.clone(), cause.fold);
276

277
            if let Some(primary_line) = annotated_lines
6✔
278
                .iter()
279
                .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
6✔
280
                .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
6✔
281
            {
282
                origin.line = Some(primary_line.line_index);
2✔
283
                if let Some(first_annotation) = primary_line
6✔
284
                    .annotations
285
                    .iter()
286
                    .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
6✔
287
                {
288
                    origin.char_column = Some(first_annotation.start.char + 1);
2✔
289
                }
290
            }
291

292
            render_origin(renderer, &mut buffer, 0, &origin, true, true, true, 0);
2✔
293
            buffer.append(0, ": ", ElementStyle::LineAndColumn);
2✔
294
        }
295
    }
296

297
    render_title(
298
        renderer,
299
        &mut buffer,
300
        title,
301
        0, // No line numbers in short messages
302
        TitleStyle::MainHeader,
303
        false,
304
        0,
305
    );
306

307
    if let Some(labels) = labels {
2✔
308
        buffer.append(0, &format!(": {labels}"), ElementStyle::NoStyle);
4✔
309
    }
310

311
    let mut out_string = String::new();
2✔
312
    buffer.render(&title.level, &renderer.stylesheet, &mut out_string)?;
4✔
313

314
    Ok(out_string)
2✔
315
}
316

317
#[allow(clippy::too_many_arguments)]
318
fn render_title(
6✔
319
    renderer: &Renderer,
320
    buffer: &mut StyledBuffer,
321
    title: &dyn MessageOrTitle,
322
    max_line_num_len: usize,
323
    title_style: TitleStyle,
324
    is_cont: bool,
325
    buffer_msg_line_offset: usize,
326
) {
327
    let (label_style, title_element_style) = match title_style {
12✔
328
        TitleStyle::MainHeader => (
6✔
329
            ElementStyle::Level(title.level().level),
7✔
330
            if renderer.short_message {
12✔
331
                ElementStyle::NoStyle
2✔
332
            } else {
333
                ElementStyle::MainHeaderMsg
6✔
334
            },
335
        ),
336
        TitleStyle::Header => (
4✔
337
            ElementStyle::Level(title.level().level),
4✔
338
            ElementStyle::HeaderMsg,
339
        ),
340
        TitleStyle::Secondary => {
341
            for _ in 0..max_line_num_len {
6✔
342
                buffer.prepend(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
3✔
343
            }
344

345
            draw_note_separator(
346
                renderer,
347
                buffer,
348
                buffer_msg_line_offset,
349
                max_line_num_len + 1,
3✔
350
                is_cont,
351
            );
352
            (ElementStyle::MainHeaderMsg, ElementStyle::NoStyle)
3✔
353
        }
354
    };
355
    let mut label_width = 0;
6✔
356

357
    if title.level().name != Some(None) {
13✔
358
        buffer.append(buffer_msg_line_offset, title.level().as_str(), label_style);
6✔
359
        label_width += title.level().as_str().len();
6✔
360
        if let Some(Id { id: Some(id), url }) = &title.id() {
19✔
361
            buffer.append(buffer_msg_line_offset, "[", label_style);
4✔
362
            if let Some(url) = url.as_ref() {
5✔
363
                buffer.append(
×
364
                    buffer_msg_line_offset,
365
                    &format!("\x1B]8;;{url}\x1B\\"),
×
366
                    label_style,
367
                );
368
            }
369
            buffer.append(buffer_msg_line_offset, id, label_style);
5✔
370
            if url.is_some() {
5✔
UNCOV
371
                buffer.append(buffer_msg_line_offset, "\x1B]8;;\x1B\\", label_style);
×
372
            }
373
            buffer.append(buffer_msg_line_offset, "]", label_style);
5✔
374
            label_width += 2 + id.len();
8✔
375
        }
376
        buffer.append(buffer_msg_line_offset, ": ", title_element_style);
5✔
377
        label_width += 2;
13✔
378
    }
379

380
    let padding = " ".repeat(if title_style == TitleStyle::Secondary {
22✔
381
        // The extra 3 ` ` is padding that's always needed to align to the
382
        // label i.e. `note: `:
383
        //
384
        //   error: message
385
        //     --> file.rs:13:20
386
        //      |
387
        //   13 |     <CODE>
388
        //      |      ^^^^
389
        //      |
390
        //      = note: multiline
391
        //              message
392
        //   ++^^^------
393
        //    |  |     |
394
        //    |  |     |
395
        //    |  |     width of label
396
        //    |  magic `3`
397
        //    `max_line_num_len`
398
        max_line_num_len + 3 + label_width
6✔
399
    } else {
400
        label_width
7✔
401
    });
402

403
    let (title_str, style) = if title.allows_styling() {
23✔
404
        (title.text().to_owned(), ElementStyle::NoStyle)
8✔
405
    } else {
406
        (normalize_whitespace(title.text()), title_element_style)
12✔
407
    };
408
    for (i, text) in title_str.split('\n').enumerate() {
11✔
409
        if i != 0 {
6✔
410
            buffer.append(buffer_msg_line_offset + i, &padding, ElementStyle::NoStyle);
3✔
411
            if title_style == TitleStyle::Secondary
3✔
412
                && is_cont
3✔
413
                && matches!(renderer.decor_style, DecorStyle::Unicode)
1✔
414
            {
415
                // There's another note after this one, associated to the subwindow above.
416
                // We write additional vertical lines to join them:
417
                //   ╭▸ test.rs:3:3
418
                //   │
419
                // 3 │   code
420
                //   │   ━━━━
421
                //   │
422
                //   ├ note: foo
423
                //   │       bar
424
                //   ╰ note: foo
425
                //           bar
426
                draw_col_separator_no_space(
427
                    renderer,
428
                    buffer,
429
                    buffer_msg_line_offset + i,
1✔
430
                    max_line_num_len + 1,
1✔
431
                );
432
            }
433
        }
434
        buffer.append(buffer_msg_line_offset + i, text, style);
11✔
435
    }
436
}
437

438
#[allow(clippy::too_many_arguments)]
439
fn render_origin(
7✔
440
    renderer: &Renderer,
441
    buffer: &mut StyledBuffer,
442
    max_line_num_len: usize,
443
    origin: &Origin<'_>,
444
    is_primary: bool,
445
    is_first: bool,
446
    alone: bool,
447
    buffer_msg_line_offset: usize,
448
) {
449
    if is_primary && !renderer.short_message {
16✔
450
        buffer.prepend(
8✔
451
            buffer_msg_line_offset,
452
            renderer.decor_style.file_start(is_first, alone),
9✔
453
            ElementStyle::LineNumber,
454
        );
455
    } else if !renderer.short_message {
3✔
456
        // if !origin.standalone {
457
        //     // Add spacing line, as shown:
458
        //     //   --> $DIR/file:54:15
459
        //     //    |
460
        //     // LL |         code
461
        //     //    |         ^^^^
462
        //     //    | (<- It prints *this* line)
463
        //     //   ::: $DIR/other_file.rs:15:5
464
        //     //    |
465
        //     // LL |     code
466
        //     //    |     ----
467
        //     draw_col_separator_no_space(renderer,
468
        //         buffer,
469
        //         buffer_msg_line_offset,
470
        //         max_line_num_len + 1,
471
        //     );
472
        //
473
        //     buffer_msg_line_offset += 1;
474
        // }
475
        // Then, the secondary file indicator
476
        buffer.prepend(
3✔
477
            buffer_msg_line_offset,
478
            renderer.decor_style.secondary_file_start(),
3✔
479
            ElementStyle::LineNumber,
480
        );
481
    }
482

483
    let str = match (&origin.line, &origin.char_column) {
16✔
484
        (Some(line), Some(col)) => {
8✔
485
            format!("{}:{}:{}", origin.path, line, col)
8✔
486
        }
487
        (Some(line), None) => format!("{}:{}", origin.path, line),
1✔
488
        _ => origin.path.to_string(),
1✔
489
    };
490

491
    if let Some(url) = &origin.url {
7✔
492
        buffer.append(
1✔
493
            buffer_msg_line_offset,
494
            &format!("\x1B]8;;{url}\x1B\\"),
2✔
495
            ElementStyle::LineAndColumn,
496
        );
497
    }
498
    buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn);
16✔
499
    if origin.url.is_some() {
9✔
500
        buffer.append(
1✔
501
            buffer_msg_line_offset,
502
            "\x1B]8;;\x1B\\",
503
            ElementStyle::LineAndColumn,
504
        );
505
    }
506

507
    if !renderer.short_message {
7✔
508
        for _ in 0..max_line_num_len {
16✔
509
            buffer.prepend(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
8✔
510
        }
511
    }
512
}
513

514
#[allow(clippy::too_many_arguments)]
515
fn render_snippet_annotations(
9✔
516
    renderer: &Renderer,
517
    buffer: &mut StyledBuffer,
518
    max_line_num_len: usize,
519
    snippet: &Snippet<'_, Annotation<'_>>,
520
    is_primary: bool,
521
    sm: &SourceMap<'_>,
522
    annotated_lines: &[AnnotatedLineInfo<'_>],
523
    multiline_depth: usize,
524
    is_cont: bool,
525
    is_first: bool,
526
) {
527
    if let Some(path) = &snippet.path {
7✔
528
        let mut origin = Origin::path(path.as_ref());
9✔
529
        // print out the span location and spacer before we print the annotated source
530
        // to do this, we need to know if this span will be primary
531
        //let is_primary = primary_path == Some(&origin.path);
532

533
        if is_primary {
8✔
534
            if let Some(primary_line) = annotated_lines
16✔
535
                .iter()
536
                .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
23✔
537
                .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
24✔
538
            {
539
                origin.line = Some(primary_line.line_index);
8✔
540
                if let Some(first_annotation) = primary_line
29✔
541
                    .annotations
542
                    .iter()
543
                    .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
25✔
544
                {
545
                    origin.char_column = Some(first_annotation.start.char + 1);
9✔
546
                }
547
            }
548
        } else {
549
            let buffer_msg_line_offset = buffer.num_lines();
6✔
550
            // Add spacing line, as shown:
551
            //   --> $DIR/file:54:15
552
            //    |
553
            // LL |         code
554
            //    |         ^^^^
555
            //    | (<- It prints *this* line)
556
            //   ::: $DIR/other_file.rs:15:5
557
            //    |
558
            // LL |     code
559
            //    |     ----
560
            draw_col_separator_no_space(
561
                renderer,
562
                buffer,
563
                buffer_msg_line_offset,
564
                max_line_num_len + 1,
3✔
565
            );
566
            if let Some(first_line) = annotated_lines.first() {
3✔
567
                origin.line = Some(first_line.line_index);
3✔
568
                if let Some(first_annotation) = first_line.annotations.first() {
9✔
569
                    origin.char_column = Some(first_annotation.start.char + 1);
2✔
570
                }
571
            }
572
        }
573

574
        if let Some(url) = &snippet.url {
8✔
575
            origin.url = Some(Cow::Borrowed(url));
2✔
576
        }
577

578
        let buffer_msg_line_offset = buffer.num_lines();
16✔
579
        render_origin(
580
            renderer,
581
            buffer,
582
            max_line_num_len,
583
            &origin,
584
            is_primary,
585
            is_first,
586
            false,
587
            buffer_msg_line_offset,
588
        );
589
        // Put in the spacer between the location and annotated source
590
        draw_col_separator_no_space(
591
            renderer,
592
            buffer,
593
            buffer_msg_line_offset + 1,
7✔
594
            max_line_num_len + 1,
6✔
595
        );
596
    } else {
597
        let buffer_msg_line_offset = buffer.num_lines();
2✔
598
        if is_primary {
2✔
599
            if renderer.decor_style == DecorStyle::Unicode {
2✔
600
                buffer.puts(
2✔
601
                    buffer_msg_line_offset,
602
                    max_line_num_len,
603
                    renderer.decor_style.file_start(is_first, false),
2✔
604
                    ElementStyle::LineNumber,
605
                );
606
            } else {
607
                draw_col_separator_no_space(
608
                    renderer,
609
                    buffer,
610
                    buffer_msg_line_offset,
611
                    max_line_num_len + 1,
2✔
612
                );
613
            }
614
        } else {
615
            // Add spacing line, as shown:
616
            //   --> $DIR/file:54:15
617
            //    |
618
            // LL |         code
619
            //    |         ^^^^
620
            //    | (<- It prints *this* line)
621
            //   ::: $DIR/other_file.rs:15:5
622
            //    |
623
            // LL |     code
624
            //    |     ----
625
            draw_col_separator_no_space(
626
                renderer,
627
                buffer,
628
                buffer_msg_line_offset,
629
                max_line_num_len + 1,
1✔
630
            );
631

632
            buffer.puts(
1✔
633
                buffer_msg_line_offset + 1,
1✔
634
                max_line_num_len,
635
                renderer.decor_style.secondary_file_start(),
1✔
636
                ElementStyle::LineNumber,
637
            );
638
        }
639
    }
640

641
    // Contains the vertical lines' positions for active multiline annotations
642
    let mut multilines = Vec::new();
8✔
643

644
    // Get the left-side margin to remove it
645
    let mut whitespace_margin = usize::MAX;
5✔
646
    for line_info in annotated_lines {
13✔
647
        let leading_whitespace = line_info
13✔
648
            .line
649
            .chars()
650
            .take_while(|c| c.is_whitespace())
22✔
651
            .map(|c| {
12✔
652
                match c {
5✔
653
                    // Tabs are displayed as 4 spaces
654
                    '\t' => 4,
2✔
655
                    _ => 1,
5✔
656
                }
657
            })
658
            .sum();
659
        if line_info.line.chars().any(|c| !c.is_whitespace()) {
26✔
660
            whitespace_margin = min(whitespace_margin, leading_whitespace);
7✔
661
        }
662
    }
663
    if whitespace_margin == usize::MAX {
8✔
664
        whitespace_margin = 0;
2✔
665
    }
666

667
    // Left-most column any visible span points at.
668
    let mut span_left_margin = usize::MAX;
7✔
669
    for line_info in annotated_lines {
12✔
670
        for ann in &line_info.annotations {
19✔
671
            span_left_margin = min(span_left_margin, ann.start.display);
6✔
672
            span_left_margin = min(span_left_margin, ann.end.display);
7✔
673
        }
674
    }
675
    if span_left_margin == usize::MAX {
8✔
676
        span_left_margin = 0;
1✔
677
    }
678

679
    // Right-most column any visible span points at.
680
    let mut span_right_margin = 0;
5✔
681
    let mut label_right_margin = 0;
7✔
682
    let mut max_line_len = 0;
8✔
683
    for line_info in annotated_lines {
16✔
684
        max_line_len = max(max_line_len, str_width(line_info.line));
13✔
685
        for ann in &line_info.annotations {
11✔
686
            span_right_margin = max(span_right_margin, ann.start.display);
6✔
687
            span_right_margin = max(span_right_margin, ann.end.display);
7✔
688
            // FIXME: account for labels not in the same line
689
            let label_right = ann.label.as_ref().map_or(0, |l| str_width(l) + 1);
16✔
690
            label_right_margin = max(label_right_margin, ann.end.display + label_right);
6✔
691
        }
692
    }
693
    let width_offset = 3 + max_line_num_len;
6✔
694
    let code_offset = if multiline_depth == 0 {
15✔
695
        width_offset
6✔
696
    } else {
697
        width_offset + multiline_depth + 1
8✔
698
    };
699

700
    let column_width = renderer.term_width.saturating_sub(code_offset);
5✔
701

702
    let margin = Margin::new(
703
        whitespace_margin,
7✔
704
        span_left_margin,
6✔
705
        span_right_margin,
6✔
706
        label_right_margin,
6✔
707
        column_width,
708
        max_line_len,
6✔
709
    );
710

711
    // Next, output the annotate source for this file
712
    for annotated_line_idx in 0..annotated_lines.len() {
13✔
713
        let previous_buffer_line = buffer.num_lines();
14✔
714

715
        let depths = render_source_line(
716
            renderer,
717
            &annotated_lines[annotated_line_idx],
6✔
718
            buffer,
719
            width_offset,
720
            code_offset,
9✔
721
            max_line_num_len,
722
            margin,
723
            !is_cont && annotated_line_idx + 1 == annotated_lines.len(),
13✔
724
        );
725

726
        let mut to_add = BTreeMap::new();
6✔
727

728
        for (depth, style) in depths {
19✔
729
            if let Some(index) = multilines.iter().position(|(d, _)| d == &depth) {
12✔
730
                multilines.swap_remove(index);
8✔
731
            } else {
732
                to_add.insert(depth, style);
6✔
733
            }
734
        }
735

736
        // Set the multiline annotation vertical lines to the left of
737
        // the code in this line.
738
        for (depth, style) in &multilines {
5✔
739
            for line in previous_buffer_line..buffer.num_lines() {
6✔
740
                draw_multiline_line(renderer, buffer, line, width_offset, *depth, *style);
3✔
741
            }
742
        }
743
        // check to see if we need to print out or elide lines that come between
744
        // this annotated line and the next one.
745
        if annotated_line_idx < (annotated_lines.len() - 1) {
7✔
746
            let line_idx_delta = annotated_lines[annotated_line_idx + 1].line_index
14✔
747
                - annotated_lines[annotated_line_idx].line_index;
9✔
748
            match line_idx_delta.cmp(&2) {
9✔
749
                Ordering::Greater => {
750
                    let last_buffer_line_num = buffer.num_lines();
8✔
751

752
                    draw_line_separator(renderer, buffer, last_buffer_line_num, width_offset);
4✔
753

754
                    // Set the multiline annotation vertical lines on `...` bridging line.
755
                    for (depth, style) in &multilines {
4✔
756
                        draw_multiline_line(
757
                            renderer,
758
                            buffer,
759
                            last_buffer_line_num,
760
                            width_offset,
761
                            *depth,
3✔
762
                            *style,
763
                        );
764
                    }
765
                    if let Some(line) = annotated_lines.get(annotated_line_idx) {
4✔
766
                        for ann in &line.annotations {
4✔
767
                            if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type {
4✔
768
                                // In the case where we have elided the entire start of the
769
                                // multispan because those lines were empty, we still need
770
                                // to draw the `|`s across the `...`.
771
                                draw_multiline_line(
772
                                    renderer,
773
                                    buffer,
774
                                    last_buffer_line_num,
775
                                    width_offset,
776
                                    pos,
777
                                    if ann.is_primary() {
1✔
778
                                        ElementStyle::UnderlinePrimary
1✔
779
                                    } else {
UNCOV
780
                                        ElementStyle::UnderlineSecondary
×
781
                                    },
782
                                );
783
                            }
784
                        }
785
                    }
786
                }
787

788
                Ordering::Equal => {
789
                    let unannotated_line = sm
3✔
790
                        .get_line(annotated_lines[annotated_line_idx].line_index + 1)
6✔
791
                        .unwrap_or("");
792

793
                    let last_buffer_line_num = buffer.num_lines();
3✔
794

795
                    draw_line(
796
                        renderer,
797
                        buffer,
798
                        &normalize_whitespace(unannotated_line),
3✔
799
                        annotated_lines[annotated_line_idx + 1].line_index - 1,
3✔
800
                        last_buffer_line_num,
801
                        width_offset,
802
                        code_offset,
3✔
803
                        max_line_num_len,
804
                        margin,
805
                    );
806

807
                    for (depth, style) in &multilines {
3✔
808
                        draw_multiline_line(
809
                            renderer,
810
                            buffer,
811
                            last_buffer_line_num,
812
                            width_offset,
813
                            *depth,
1✔
814
                            *style,
815
                        );
816
                    }
817
                    if let Some(line) = annotated_lines.get(annotated_line_idx) {
3✔
818
                        for ann in &line.annotations {
3✔
819
                            if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type {
3✔
820
                                draw_multiline_line(
821
                                    renderer,
822
                                    buffer,
823
                                    last_buffer_line_num,
824
                                    width_offset,
825
                                    pos,
826
                                    if ann.is_primary() {
2✔
827
                                        ElementStyle::UnderlinePrimary
2✔
828
                                    } else {
UNCOV
829
                                        ElementStyle::UnderlineSecondary
×
830
                                    },
831
                                );
832
                            }
833
                        }
834
                    }
835
                }
836
                Ordering::Less => {}
837
            }
838
        }
839

840
        multilines.extend(to_add);
6✔
841
    }
842
}
843

844
#[allow(clippy::too_many_arguments)]
845
fn render_source_line(
7✔
846
    renderer: &Renderer,
847
    line_info: &AnnotatedLineInfo<'_>,
848
    buffer: &mut StyledBuffer,
849
    width_offset: usize,
850
    code_offset: usize,
851
    max_line_num_len: usize,
852
    margin: Margin,
853
    close_window: bool,
854
) -> Vec<(usize, ElementStyle)> {
855
    // Draw:
856
    //
857
    //   LL | ... code ...
858
    //      |     ^^-^ span label
859
    //      |       |
860
    //      |       secondary span label
861
    //
862
    //   ^^ ^ ^^^ ^^^^ ^^^ we don't care about code too far to the right of a span, we trim it
863
    //   |  | |   |
864
    //   |  | |   actual code found in your source code and the spans we use to mark it
865
    //   |  | when there's too much wasted space to the left, trim it
866
    //   |  vertical divider between the column number and the code
867
    //   column number
868

869
    let source_string = normalize_whitespace(line_info.line);
7✔
870

871
    let line_offset = buffer.num_lines();
16✔
872

873
    let left = draw_line(
874
        renderer,
875
        buffer,
876
        &source_string,
8✔
877
        line_info.line_index,
5✔
878
        line_offset,
879
        width_offset,
880
        code_offset,
881
        max_line_num_len,
882
        margin,
883
    );
884

885
    // If there are no annotations, we are done
886
    if line_info.annotations.is_empty() {
8✔
887
        // `close_window` normally gets handled later, but we are early
888
        // returning, so it needs to be handled here
889
        if close_window {
2✔
890
            draw_col_separator_end(renderer, buffer, line_offset + 1, width_offset - 2);
2✔
891
        }
892
        return vec![];
4✔
893
    }
894

895
    // Special case when there's only one annotation involved, it is the start of a multiline
896
    // span and there's no text at the beginning of the code line. Instead of doing the whole
897
    // graph:
898
    //
899
    // 2 |   fn foo() {
900
    //   |  _^
901
    // 3 | |
902
    // 4 | | }
903
    //   | |_^ test
904
    //
905
    // we simplify the output to:
906
    //
907
    // 2 | / fn foo() {
908
    // 3 | |
909
    // 4 | | }
910
    //   | |_^ test
911
    let mut buffer_ops = vec![];
5✔
912
    let mut annotations = vec![];
8✔
913
    let mut short_start = true;
6✔
914
    for ann in &line_info.annotations {
14✔
915
        if let LineAnnotationType::MultilineStart(depth) = ann.annotation_type {
11✔
916
            if source_string
6✔
917
                .chars()
918
                .take(ann.start.display)
3✔
919
                .all(char::is_whitespace)
3✔
920
            {
921
                let uline = renderer.decor_style.underline(ann.is_primary());
3✔
922
                let chr = uline.multiline_whole_line;
3✔
923
                annotations.push((depth, uline.style));
3✔
924
                buffer_ops.push((line_offset, width_offset + depth - 1, chr, uline.style));
3✔
925
            } else {
926
                short_start = false;
3✔
927
                break;
928
            }
929
        } else if let LineAnnotationType::MultilineLine(_) = ann.annotation_type {
6✔
930
        } else {
931
            short_start = false;
7✔
932
            break;
7✔
933
        }
934
    }
935
    if short_start {
7✔
936
        for (y, x, c, s) in buffer_ops {
12✔
937
            buffer.putc(y, x, c, s);
9✔
938
        }
939
        return annotations;
3✔
940
    }
941

942
    // We want to display like this:
943
    //
944
    //      vec.push(vec.pop().unwrap());
945
    //      ---      ^^^               - previous borrow ends here
946
    //      |        |
947
    //      |        error occurs here
948
    //      previous borrow of `vec` occurs here
949
    //
950
    // But there are some weird edge cases to be aware of:
951
    //
952
    //      vec.push(vec.pop().unwrap());
953
    //      --------                    - previous borrow ends here
954
    //      ||
955
    //      |this makes no sense
956
    //      previous borrow of `vec` occurs here
957
    //
958
    // For this reason, we group the lines into "highlight lines"
959
    // and "annotations lines", where the highlight lines have the `^`.
960

961
    // Sort the annotations by (start, end col)
962
    // The labels are reversed, sort and then reversed again.
963
    // Consider a list of annotations (A1, A2, C1, C2, B1, B2) where
964
    // the letter signifies the span. Here we are only sorting by the
965
    // span and hence, the order of the elements with the same span will
966
    // not change. On reversing the ordering (|a, b| but b.cmp(a)), you get
967
    // (C1, C2, B1, B2, A1, A2). All the elements with the same span are
968
    // still ordered first to last, but all the elements with different
969
    // spans are ordered by their spans in last to first order. Last to
970
    // first order is important, because the jiggly lines and | are on
971
    // the left, so the rightmost span needs to be rendered first,
972
    // otherwise the lines would end up needing to go over a message.
973

974
    let mut annotations = line_info.annotations.clone();
7✔
975
    annotations.sort_by_key(|a| Reverse((a.start.display, a.start.char)));
20✔
976

977
    // First, figure out where each label will be positioned.
978
    //
979
    // In the case where you have the following annotations:
980
    //
981
    //      vec.push(vec.pop().unwrap());
982
    //      --------                    - previous borrow ends here [C]
983
    //      ||
984
    //      |this makes no sense [B]
985
    //      previous borrow of `vec` occurs here [A]
986
    //
987
    // `annotations_position` will hold [(2, A), (1, B), (0, C)].
988
    //
989
    // We try, when possible, to stick the rightmost annotation at the end
990
    // of the highlight line:
991
    //
992
    //      vec.push(vec.pop().unwrap());
993
    //      ---      ---               - previous borrow ends here
994
    //
995
    // But sometimes that's not possible because one of the other
996
    // annotations overlaps it. For example, from the test
997
    // `span_overlap_label`, we have the following annotations
998
    // (written on distinct lines for clarity):
999
    //
1000
    //      fn foo(x: u32) {
1001
    //      --------------
1002
    //             -
1003
    //
1004
    // In this case, we can't stick the rightmost-most label on
1005
    // the highlight line, or we would get:
1006
    //
1007
    //      fn foo(x: u32) {
1008
    //      -------- x_span
1009
    //      |
1010
    //      fn_span
1011
    //
1012
    // which is totally weird. Instead we want:
1013
    //
1014
    //      fn foo(x: u32) {
1015
    //      --------------
1016
    //      |      |
1017
    //      |      x_span
1018
    //      fn_span
1019
    //
1020
    // which is...less weird, at least. In fact, in general, if
1021
    // the rightmost span overlaps with any other span, we should
1022
    // use the "hang below" version, so we can at least make it
1023
    // clear where the span *starts*. There's an exception for this
1024
    // logic, when the labels do not have a message:
1025
    //
1026
    //      fn foo(x: u32) {
1027
    //      --------------
1028
    //             |
1029
    //             x_span
1030
    //
1031
    // instead of:
1032
    //
1033
    //      fn foo(x: u32) {
1034
    //      --------------
1035
    //      |      |
1036
    //      |      x_span
1037
    //      <EMPTY LINE>
1038
    //
1039
    let mut overlap = vec![false; annotations.len()];
6✔
1040
    let mut annotations_position = vec![];
7✔
1041
    let mut line_len: usize = 0;
6✔
1042
    let mut p = 0;
7✔
1043
    for (i, annotation) in annotations.iter().enumerate() {
21✔
1044
        for (j, next) in annotations.iter().enumerate() {
14✔
1045
            if overlaps(next, annotation, 0) && j > 1 {
22✔
1046
                overlap[i] = true;
2✔
1047
                overlap[j] = true;
2✔
1048
            }
1049
            if overlaps(next, annotation, 0)  // This label overlaps with another one and both
14✔
1050
                    && annotation.has_label()     // take space (they have text and are not
8✔
1051
                    && j > i                      // multiline lines).
5✔
1052
                    && p == 0
2✔
1053
            // We're currently on the first line, move the label one line down
1054
            {
1055
                // If we're overlapping with an un-labelled annotation with the same span
1056
                // we can just merge them in the output
1057
                if next.start.display == annotation.start.display
2✔
1058
                    && next.start.char == annotation.start.char
2✔
1059
                    && next.end.display == annotation.end.display
2✔
1060
                    && next.end.char == annotation.end.char
2✔
1061
                    && !next.has_label()
2✔
1062
                {
1063
                    continue;
1064
                }
1065

1066
                // This annotation needs a new line in the output.
1067
                p += 1;
4✔
1068
                break;
1069
            }
1070
        }
1071
        annotations_position.push((p, annotation));
5✔
1072
        for (j, next) in annotations.iter().enumerate() {
6✔
1073
            if j > i {
5✔
1074
                let l = next.label.as_ref().map_or(0, |label| label.len() + 2);
9✔
1075
                if (overlaps(next, annotation, l) // Do not allow two labels to be in the same
3✔
1076
                        // line if they overlap including padding, to
1077
                        // avoid situations like:
1078
                        //
1079
                        //      fn foo(x: u32) {
1080
                        //      -------^------
1081
                        //      |      |
1082
                        //      fn_spanx_span
1083
                        //
1084
                        && annotation.has_label()    // Both labels must have some text, otherwise
3✔
1085
                        && next.has_label())         // they are not overlapping.
3✔
1086
                        // Do not add a new line if this annotation
1087
                        // or the next are vertical line placeholders.
1088
                        || (annotation.takes_space() // If either this or the next annotation is
6✔
1089
                        && next.has_label())     // multiline start/end, move it to a new line
2✔
1090
                        || (annotation.has_label()   // so as not to overlap the horizontal lines.
6✔
1091
                        && next.takes_space())
2✔
1092
                        || (annotation.takes_space() && next.takes_space())
8✔
1093
                        || (overlaps(next, annotation, l)
6✔
1094
                        && (next.end.display, next.end.char) <= (annotation.end.display, annotation.end.char)
1✔
1095
                        && next.has_label()
1✔
1096
                        && p == 0)
1✔
1097
                // Avoid #42595.
1098
                {
1099
                    // This annotation needs a new line in the output.
1100
                    p += 1;
6✔
1101
                    break;
1102
                }
1103
            }
1104
        }
1105
        line_len = max(line_len, p);
10✔
1106
    }
1107

1108
    if line_len != 0 {
9✔
1109
        line_len += 1;
3✔
1110
    }
1111

1112
    // If there are no annotations or the only annotations on this line are
1113
    // MultilineLine, then there's only code being shown, stop processing.
1114
    if line_info.annotations.iter().all(LineAnnotation::is_line) {
13✔
UNCOV
1115
        return vec![];
×
1116
    }
1117

1118
    if annotations_position
19✔
1119
        .iter()
1120
        .all(|(_, ann)| matches!(ann.annotation_type, LineAnnotationType::MultilineStart(_)))
20✔
1121
    {
1122
        if let Some(max_pos) = annotations_position.iter().map(|(pos, _)| *pos).max() {
12✔
1123
            // Special case the following, so that we minimize overlapping multiline spans.
1124
            //
1125
            // 3 │       X0 Y0 Z0
1126
            //   │ ┏━━━━━┛  │  │     < We are writing these lines
1127
            //   │ ┃┌───────┘  │     < by reverting the "depth" of
1128
            //   │ ┃│┌─────────┘     < their multiline spans.
1129
            // 4 │ ┃││   X1 Y1 Z1
1130
            // 5 │ ┃││   X2 Y2 Z2
1131
            //   │ ┃│└────╿──│──┘ `Z` label
1132
            //   │ ┃└─────│──┤
1133
            //   │ ┗━━━━━━┥  `Y` is a good letter too
1134
            //   ╰╴       `X` is a good letter
1135
            for (pos, _) in &mut annotations_position {
6✔
1136
                *pos = max_pos - *pos;
6✔
1137
            }
1138
            // We know then that we don't need an additional line for the span label, saving us
1139
            // one line of vertical space.
1140
            line_len = line_len.saturating_sub(1);
3✔
1141
        }
1142
    }
1143

1144
    // Write the column separator.
1145
    //
1146
    // After this we will have:
1147
    //
1148
    // 2 |   fn foo() {
1149
    //   |
1150
    //   |
1151
    //   |
1152
    // 3 |
1153
    // 4 |   }
1154
    //   |
1155
    for pos in 0..=line_len {
13✔
1156
        draw_col_separator_no_space(renderer, buffer, line_offset + pos + 1, width_offset - 2);
13✔
1157
    }
1158
    if close_window {
8✔
1159
        draw_col_separator_end(
1160
            renderer,
1161
            buffer,
1162
            line_offset + line_len + 1,
5✔
1163
            width_offset - 2,
5✔
1164
        );
1165
    }
1166
    // Write the horizontal lines for multiline annotations
1167
    // (only the first and last lines need this).
1168
    //
1169
    // After this we will have:
1170
    //
1171
    // 2 |   fn foo() {
1172
    //   |  __________
1173
    //   |
1174
    //   |
1175
    // 3 |
1176
    // 4 |   }
1177
    //   |  _
1178
    for &(pos, annotation) in &annotations_position {
9✔
1179
        let underline = renderer.decor_style.underline(annotation.is_primary());
9✔
1180
        let pos = pos + 1;
4✔
1181
        match annotation.annotation_type {
8✔
1182
            LineAnnotationType::MultilineStart(depth) | LineAnnotationType::MultilineEnd(depth) => {
6✔
1183
                draw_range(
1184
                    buffer,
1185
                    underline.multiline_horizontal,
3✔
1186
                    line_offset + pos,
3✔
1187
                    width_offset + depth,
3✔
1188
                    (code_offset + annotation.start.display).saturating_sub(left),
6✔
1189
                    underline.style,
1190
                );
1191
            }
1192
            _ if annotation.highlight_source => {
6✔
1193
                buffer.set_style_range(
1✔
1194
                    line_offset,
1195
                    (code_offset + annotation.start.display).saturating_sub(left),
1✔
1196
                    (code_offset + annotation.end.display).saturating_sub(left),
1✔
1197
                    underline.style,
1✔
1198
                    annotation.is_primary(),
1✔
1199
                );
1200
            }
1201
            _ => {}
1202
        }
1203
    }
1204

1205
    // Write the vertical lines for labels that are on a different line as the underline.
1206
    //
1207
    // After this we will have:
1208
    //
1209
    // 2 |   fn foo() {
1210
    //   |  __________
1211
    //   | |    |
1212
    //   | |
1213
    // 3 | |
1214
    // 4 | | }
1215
    //   | |_
1216
    for &(pos, annotation) in &annotations_position {
7✔
1217
        let underline = renderer.decor_style.underline(annotation.is_primary());
13✔
1218
        let pos = pos + 1;
6✔
1219

1220
        if pos > 1 && (annotation.has_label() || annotation.takes_space()) {
14✔
1221
            for p in line_offset + 1..=line_offset + pos {
6✔
1222
                buffer.putc(
3✔
1223
                    p,
1224
                    (code_offset + annotation.start.display).saturating_sub(left),
6✔
1225
                    match annotation.annotation_type {
3✔
UNCOV
1226
                        LineAnnotationType::MultilineLine(_) => underline.multiline_vertical,
×
1227
                        _ => underline.vertical_text_line,
3✔
1228
                    },
1229
                    underline.style,
1230
                );
1231
            }
1232
            if let LineAnnotationType::MultilineStart(_) = annotation.annotation_type {
3✔
1233
                buffer.putc(
3✔
1234
                    line_offset + pos,
2✔
1235
                    (code_offset + annotation.start.display).saturating_sub(left),
4✔
1236
                    underline.bottom_right,
3✔
1237
                    underline.style,
1238
                );
1239
            }
1240
            if matches!(
3✔
1241
                annotation.annotation_type,
1242
                LineAnnotationType::MultilineEnd(_)
1243
            ) && annotation.has_label()
2✔
1244
            {
1245
                buffer.putc(
2✔
1246
                    line_offset + pos,
2✔
1247
                    (code_offset + annotation.start.display).saturating_sub(left),
4✔
1248
                    underline.multiline_bottom_right_with_text,
2✔
1249
                    underline.style,
1250
                );
1251
            }
1252
        }
1253
        match annotation.annotation_type {
4✔
1254
            LineAnnotationType::MultilineStart(depth) => {
3✔
1255
                buffer.putc(
3✔
1256
                    line_offset + pos,
3✔
1257
                    width_offset + depth - 1,
6✔
1258
                    underline.top_left,
3✔
1259
                    underline.style,
1260
                );
1261
                for p in line_offset + pos + 1..line_offset + line_len + 2 {
3✔
1262
                    buffer.putc(
4✔
1263
                        p,
1264
                        width_offset + depth - 1,
2✔
1265
                        underline.multiline_vertical,
2✔
1266
                        underline.style,
1267
                    );
1268
                }
1269
            }
1270
            LineAnnotationType::MultilineEnd(depth) => {
3✔
1271
                for p in line_offset..line_offset + pos {
6✔
1272
                    buffer.putc(
6✔
1273
                        p,
1274
                        width_offset + depth - 1,
6✔
1275
                        underline.multiline_vertical,
3✔
1276
                        underline.style,
1277
                    );
1278
                }
1279
                buffer.putc(
6✔
1280
                    line_offset + pos,
3✔
1281
                    width_offset + depth - 1,
6✔
1282
                    underline.bottom_left,
3✔
1283
                    underline.style,
1284
                );
1285
            }
1286
            _ => (),
1287
        }
1288
    }
1289

1290
    // Write the labels on the annotations that actually have a label.
1291
    //
1292
    // After this we will have:
1293
    //
1294
    // 2 |   fn foo() {
1295
    //   |  __________
1296
    //   |      |
1297
    //   |      something about `foo`
1298
    // 3 |
1299
    // 4 |   }
1300
    //   |  _  test
1301
    for &(pos, annotation) in &annotations_position {
7✔
1302
        let style = if annotation.is_primary() {
18✔
1303
            ElementStyle::LabelPrimary
7✔
1304
        } else {
1305
            ElementStyle::LabelSecondary
4✔
1306
        };
1307
        let (pos, col) = if pos == 0 {
13✔
1308
            if annotation.end.display == 0 {
16✔
1309
                (pos + 1, (annotation.end.display + 2).saturating_sub(left))
6✔
1310
            } else {
1311
                (pos + 1, (annotation.end.display + 1).saturating_sub(left))
13✔
1312
            }
1313
        } else {
1314
            (pos + 2, annotation.start.display.saturating_sub(left))
6✔
1315
        };
1316
        if let Some(label) = &annotation.label {
9✔
1317
            buffer.puts(line_offset + pos, code_offset + col, label, style);
4✔
1318
        }
1319
    }
1320

1321
    // Sort from biggest span to smallest span so that smaller spans are
1322
    // represented in the output:
1323
    //
1324
    // x | fn foo()
1325
    //   | ^^^---^^
1326
    //   | |  |
1327
    //   | |  something about `foo`
1328
    //   | something about `fn foo()`
1329
    annotations_position.sort_by_key(|(_, ann)| {
11✔
1330
        // Decreasing order. When annotations share the same length, prefer `Primary`.
1331
        (Reverse(ann.len()), ann.is_primary())
4✔
1332
    });
1333

1334
    // Write the underlines.
1335
    //
1336
    // After this we will have:
1337
    //
1338
    // 2 |   fn foo() {
1339
    //   |  ____-_____^
1340
    //   |      |
1341
    //   |      something about `foo`
1342
    // 3 |
1343
    // 4 |   }
1344
    //   |  _^  test
1345
    for &(pos, annotation) in &annotations_position {
4✔
1346
        let uline = renderer.decor_style.underline(annotation.is_primary());
12✔
1347
        for p in annotation.start.display..annotation.end.display {
8✔
1348
            // The default span label underline.
1349
            buffer.putc(
5✔
1350
                line_offset + 1,
5✔
1351
                (code_offset + p).saturating_sub(left),
13✔
1352
                uline.underline,
8✔
1353
                uline.style,
1354
            );
1355
        }
1356

1357
        if pos == 0
8✔
1358
            && matches!(
8✔
1359
                annotation.annotation_type,
5✔
1360
                LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1361
            )
1362
        {
1363
            // The beginning of a multiline span with its leftward moving line on the same line.
1364
            buffer.putc(
3✔
1365
                line_offset + 1,
3✔
1366
                (code_offset + annotation.start.display).saturating_sub(left),
6✔
1367
                match annotation.annotation_type {
3✔
1368
                    LineAnnotationType::MultilineStart(_) => uline.top_right_flat,
3✔
1369
                    LineAnnotationType::MultilineEnd(_) => uline.multiline_end_same_line,
3✔
UNCOV
1370
                    _ => panic!("unexpected annotation type: {annotation:?}"),
×
1371
                },
1372
                uline.style,
1373
            );
1374
        } else if pos != 0
6✔
1375
            && matches!(
3✔
1376
                annotation.annotation_type,
3✔
1377
                LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1378
            )
1379
        {
1380
            // The beginning of a multiline span with its leftward moving line on another line,
1381
            // so we start going down first.
1382
            buffer.putc(
3✔
1383
                line_offset + 1,
3✔
1384
                (code_offset + annotation.start.display).saturating_sub(left),
6✔
1385
                match annotation.annotation_type {
3✔
1386
                    LineAnnotationType::MultilineStart(_) => uline.multiline_start_down,
3✔
1387
                    LineAnnotationType::MultilineEnd(_) => uline.multiline_end_up,
2✔
UNCOV
1388
                    _ => panic!("unexpected annotation type: {annotation:?}"),
×
1389
                },
1390
                uline.style,
1391
            );
1392
        } else if pos != 0 && annotation.has_label() {
10✔
1393
            // The beginning of a span label with an actual label, we'll point down.
1394
            buffer.putc(
4✔
1395
                line_offset + 1,
3✔
1396
                (code_offset + annotation.start.display).saturating_sub(left),
6✔
1397
                uline.label_start,
3✔
1398
                uline.style,
1399
            );
1400
        }
1401
    }
1402

1403
    // We look for individual *long* spans, and we trim the *middle*, so that we render
1404
    // LL | ...= [0, 0, 0, ..., 0, 0];
1405
    //    |      ^^^^^^^^^^...^^^^^^^ expected `&[u8]`, found `[{integer}; 1680]`
1406
    for (i, (_pos, annotation)) in annotations_position.iter().enumerate() {
6✔
1407
        // Skip cases where multiple spans overlap eachother.
1408
        if overlap[i] {
13✔
1409
            continue;
1410
        };
1411
        let LineAnnotationType::Singleline = annotation.annotation_type else {
5✔
1412
            continue;
1413
        };
1414
        let width = annotation.end.display - annotation.start.display;
6✔
1415
        if width > margin.term_width * 2 && width > 10 {
12✔
1416
            // If the terminal is *too* small, we keep at least a tiny bit of the span for
1417
            // display.
1418
            let pad = max(margin.term_width / 3, 5);
1✔
1419
            // Code line
1420
            buffer.replace(
1✔
1421
                line_offset,
1422
                annotation.start.display + pad,
1✔
1423
                annotation.end.display - pad,
1✔
1424
                renderer.decor_style.margin(),
1✔
1425
            );
1426
            // Underline line
1427
            buffer.replace(
1✔
1428
                line_offset + 1,
1✔
1429
                annotation.start.display + pad,
2✔
1430
                annotation.end.display - pad,
1✔
1431
                renderer.decor_style.margin(),
2✔
1432
            );
1433
        }
1434
    }
1435
    annotations_position
5✔
1436
        .iter()
1437
        .filter_map(|&(_, annotation)| match annotation.annotation_type {
16✔
1438
            LineAnnotationType::MultilineStart(p) | LineAnnotationType::MultilineEnd(p) => {
6✔
1439
                let style = if annotation.is_primary() {
6✔
1440
                    ElementStyle::LabelPrimary
3✔
1441
                } else {
1442
                    ElementStyle::LabelSecondary
3✔
1443
                };
1444
                Some((p, style))
3✔
1445
            }
1446
            _ => None,
6✔
1447
        })
1448
        .collect::<Vec<_>>()
1449
}
1450

1451
#[allow(clippy::too_many_arguments)]
1452
fn emit_suggestion_default(
4✔
1453
    renderer: &Renderer,
1454
    buffer: &mut StyledBuffer,
1455
    suggestion: &Snippet<'_, Patch<'_>>,
1456
    spliced_lines: SplicedLines<'_>,
1457
    show_code_change: DisplaySuggestion,
1458
    max_line_num_len: usize,
1459
    sm: &SourceMap<'_>,
1460
    primary_path: Option<&Cow<'_, str>>,
1461
    matches_previous_suggestion: bool,
1462
    is_first: bool,
1463
    is_cont: bool,
1464
) {
1465
    let buffer_offset = buffer.num_lines();
7✔
1466
    let mut row_num = buffer_offset + usize::from(!matches_previous_suggestion);
4✔
1467
    let (complete, parts, highlights) = spliced_lines;
3✔
1468
    let is_multiline = complete.lines().count() > 1;
8✔
1469

1470
    if matches_previous_suggestion {
5✔
1471
        buffer.puts(
4✔
1472
            row_num - 1,
4✔
1473
            max_line_num_len + 1,
4✔
1474
            renderer.decor_style.multi_suggestion_separator(),
4✔
1475
            ElementStyle::LineNumber,
1476
        );
1477
    } else {
1478
        draw_col_separator_start(renderer, buffer, row_num - 1, max_line_num_len + 1);
9✔
1479
    }
1480
    if suggestion.path.as_ref() != primary_path {
9✔
1481
        if let Some(path) = suggestion.path.as_ref() {
2✔
1482
            if !matches_previous_suggestion {
1✔
1483
                let (loc, _) = sm.span_to_locations(parts[0].span.clone());
1✔
1484
                // --> file.rs:line:col
1485
                //  |
1486
                let arrow = renderer.decor_style.file_start(is_first, false);
1✔
1487
                buffer.puts(row_num - 1, 0, arrow, ElementStyle::LineNumber);
1✔
1488
                let message = format!("{}:{}:{}", path, loc.line, loc.char + 1);
1✔
1489
                let col = usize::max(max_line_num_len + 1, arrow.len());
2✔
1490
                buffer.puts(row_num - 1, col, &message, ElementStyle::LineAndColumn);
1✔
1491
                for _ in 0..max_line_num_len {
1✔
1492
                    buffer.prepend(row_num - 1, " ", ElementStyle::NoStyle);
2✔
1493
                }
1494
                draw_col_separator_no_space(renderer, buffer, row_num, max_line_num_len + 1);
1✔
1495
                row_num += 1;
1✔
1496
            }
1497
        }
1498
    }
1499

1500
    if let DisplaySuggestion::Diff = show_code_change {
10✔
1501
        row_num += 1;
10✔
1502
    }
1503

1504
    let lo = parts.iter().map(|p| p.span.start).min().unwrap();
20✔
1505
    let hi = parts.iter().map(|p| p.span.end).max().unwrap();
15✔
1506

1507
    let file_lines = sm.span_to_lines(lo..hi);
5✔
1508
    let (line_start, line_end) = if suggestion.fold {
10✔
1509
        // We use the original span to get original line_start
1510
        sm.span_to_locations(parts[0].original_span.clone())
9✔
1511
    } else {
1512
        sm.span_to_locations(0..sm.source.len())
2✔
1513
    };
1514
    let mut lines = complete.lines();
9✔
1515
    if lines.clone().next().is_none() {
6✔
1516
        // Account for a suggestion to completely remove a line(s) with whitespace (#94192).
1517
        for line in line_start.line..=line_end.line {
1✔
1518
            buffer.puts(
1✔
1519
                row_num - 1 + line - line_start.line,
2✔
1520
                0,
1521
                &maybe_anonymized(renderer, line, max_line_num_len),
2✔
1522
                ElementStyle::LineNumber,
1523
            );
1524
            buffer.puts(
1✔
1525
                row_num - 1 + line - line_start.line,
1✔
1526
                max_line_num_len + 1,
1✔
1527
                "- ",
1528
                ElementStyle::Removal,
1529
            );
1530
            buffer.puts(
1✔
1531
                row_num - 1 + line - line_start.line,
1✔
1532
                max_line_num_len + 3,
1✔
1533
                &normalize_whitespace(sm.get_line(line).unwrap()),
2✔
1534
                ElementStyle::Removal,
1535
            );
1536
        }
1537
        row_num += line_end.line - line_start.line;
1✔
1538
    }
1539
    let mut unhighlighted_lines = Vec::new();
5✔
1540
    for (line_pos, (line, highlight_parts)) in lines.by_ref().zip(highlights).enumerate() {
17✔
1541
        // Remember lines that are not highlighted to hide them if needed
1542
        if highlight_parts.is_empty() && suggestion.fold {
10✔
1543
            unhighlighted_lines.push((line_pos, line));
1✔
1544
            continue;
1545
        }
1546

1547
        match unhighlighted_lines.len() {
9✔
1548
            0 => (),
1549
            // Since we show first line, "..." line and last line,
1550
            // There is no reason to hide if there are 3 or less lines
1551
            // (because then we just replace a line with ... which is
1552
            // not helpful)
1553
            n if n <= 3 => unhighlighted_lines.drain(..).for_each(|(p, l)| {
5✔
1554
                draw_code_line(
1✔
1555
                    renderer,
1✔
1556
                    buffer,
1✔
1557
                    &mut row_num,
1✔
1558
                    &[],
1559
                    p + line_start.line,
1✔
1560
                    l,
1561
                    show_code_change,
1✔
1562
                    max_line_num_len,
1✔
1563
                    &file_lines,
1✔
1564
                    is_multiline,
1✔
1565
                );
1566
            }),
1567
            // Print first unhighlighted line, "..." and last unhighlighted line, like so:
1568
            //
1569
            // LL | this line was highlighted
1570
            // LL | this line is just for context
1571
            // ...
1572
            // LL | this line is just for context
1573
            // LL | this line was highlighted
1574
            _ => {
1575
                let last_line = unhighlighted_lines.pop();
1✔
1576
                let first_line = unhighlighted_lines.drain(..).next();
1✔
1577

1578
                if let Some((p, l)) = first_line {
1✔
1579
                    draw_code_line(
1580
                        renderer,
1581
                        buffer,
1582
                        &mut row_num,
1583
                        &[],
1584
                        p + line_start.line,
1✔
1585
                        l,
1586
                        show_code_change,
1587
                        max_line_num_len,
1588
                        &file_lines,
1✔
1589
                        is_multiline,
1590
                    );
1591
                }
1592

1593
                let placeholder = renderer.decor_style.margin();
2✔
1594
                let padding = str_width(placeholder);
1✔
1595
                buffer.puts(
1✔
1596
                    row_num,
1✔
1597
                    max_line_num_len.saturating_sub(padding),
1✔
1598
                    placeholder,
1599
                    ElementStyle::LineNumber,
1600
                );
1601
                row_num += 1;
1✔
1602

1603
                if let Some((p, l)) = last_line {
2✔
1604
                    draw_code_line(
1605
                        renderer,
1606
                        buffer,
1607
                        &mut row_num,
1608
                        &[],
1609
                        p + line_start.line,
1✔
1610
                        l,
1611
                        show_code_change,
1612
                        max_line_num_len,
1613
                        &file_lines,
1✔
1614
                        is_multiline,
1615
                    );
1616
                }
1617
            }
1618
        }
1619
        draw_code_line(
1620
            renderer,
1621
            buffer,
1622
            &mut row_num,
1623
            &highlight_parts,
5✔
1624
            line_pos + line_start.line,
4✔
1625
            line,
1626
            show_code_change,
1627
            max_line_num_len,
1628
            &file_lines,
5✔
1629
            is_multiline,
1630
        );
1631
    }
1632

1633
    // This offset and the ones below need to be signed to account for replacement code
1634
    // that is shorter than the original code.
1635
    let mut offsets: Vec<(usize, isize)> = Vec::new();
3✔
1636
    // Only show an underline in the suggestions if the suggestion is not the
1637
    // entirety of the code being shown and the displayed code is not multiline.
1638
    if let DisplaySuggestion::Diff | DisplaySuggestion::Underline | DisplaySuggestion::Add =
5✔
1639
        show_code_change
1640
    {
1641
        let mut prev_lines: Option<(usize, usize)> = None;
3✔
1642
        for part in parts {
13✔
1643
            let snippet = sm.span_to_snippet(part.span.clone()).unwrap_or_default();
8✔
1644
            let (span_start, span_end) = sm.span_to_locations(part.span.clone());
4✔
1645
            let span_start_pos = span_start.display;
4✔
1646
            let span_end_pos = span_end.display;
4✔
1647

1648
            // If this addition is _only_ whitespace, then don't trim it,
1649
            // or else we're just not rendering anything.
1650
            let is_whitespace_addition = part.replacement.trim().is_empty();
4✔
1651

1652
            // Do not underline the leading...
1653
            let start = if is_whitespace_addition {
8✔
1654
                0
3✔
1655
            } else {
1656
                part.replacement
10✔
1657
                    .len()
1658
                    .saturating_sub(part.replacement.trim_start().len())
3✔
1659
            };
1660
            // ...or trailing spaces. Account for substitutions containing unicode
1661
            // characters.
1662
            let sub_len: usize = str_width(if is_whitespace_addition {
12✔
1663
                &part.replacement
7✔
1664
            } else {
1665
                part.replacement.trim()
11✔
1666
            });
1667

1668
            let offset: isize = offsets
10✔
1669
                .iter()
1670
                .filter_map(|(start, v)| {
10✔
1671
                    if span_start_pos < *start {
6✔
1672
                        None
1✔
1673
                    } else {
1674
                        Some(v)
3✔
1675
                    }
1676
                })
1677
                .sum();
1678
            let underline_start = (span_start_pos + start) as isize + offset;
4✔
1679
            let underline_end = (span_start_pos + start + sub_len) as isize + offset;
9✔
1680
            assert!(underline_start >= 0 && underline_end >= 0);
5✔
1681
            let padding: usize = max_line_num_len + 3;
4✔
1682
            for p in underline_start..underline_end {
9✔
1683
                if matches!(show_code_change, DisplaySuggestion::Underline) {
4✔
1684
                    // If this is a replacement, underline with `~`, if this is an addition
1685
                    // underline with `+`.
1686
                    buffer.putc(
2✔
1687
                        row_num,
2✔
1688
                        (padding as isize + p) as usize,
2✔
1689
                        if part.is_addition(sm) {
6✔
1690
                            '+'
2✔
1691
                        } else {
UNCOV
1692
                            renderer.decor_style.diff()
×
1693
                        },
1694
                        ElementStyle::Addition,
1695
                    );
1696
                }
1697
            }
1698
            if let DisplaySuggestion::Diff = show_code_change {
8✔
1699
                // Colorize removal with red in diff format.
1700

1701
                // Below, there's some tricky buffer indexing going on. `row_num` at this
1702
                // point corresponds to:
1703
                //
1704
                //    |
1705
                // LL | CODE
1706
                //    | ++++  <- `row_num`
1707
                //
1708
                // in the buffer. When we have a diff format output, we end up with
1709
                //
1710
                //    |
1711
                // LL - OLDER   <- row_num - 2
1712
                // LL + NEWER
1713
                //    |         <- row_num
1714
                //
1715
                // The `row_num - 2` is to select the buffer line that has the "old version
1716
                // of the diff" at that point. When the removal is a single line, `i` is
1717
                // `0`, `newlines` is `1` so `(newlines - i - 1)` ends up being `0`, so row
1718
                // points at `LL - OLDER`. When the removal corresponds to multiple lines,
1719
                // we end up with `newlines > 1` and `i` being `0..newlines - 1`.
1720
                //
1721
                //    |
1722
                // LL - OLDER   <- row_num - 2 - (newlines - last_i - 1)
1723
                // LL - CODE
1724
                // LL - BEING
1725
                // LL - REMOVED <- row_num - 2 - (newlines - first_i - 1)
1726
                // LL + NEWER
1727
                //    |         <- row_num
1728

1729
                let newlines = snippet.lines().count();
8✔
1730
                let offset = match prev_lines {
3✔
1731
                    Some((start, end)) => {
4✔
1732
                        file_lines.len().saturating_sub(end.saturating_sub(start))
8✔
1733
                    }
1734
                    None => file_lines.len(),
5✔
1735
                };
1736
                // FIXME: We check the number of rows because in some cases, like in
1737
                // `tests/ui/lint/invalid-nan-comparison-suggestion.rs`, the rendered
1738
                // suggestion will only show the first line of code being replaced. The
1739
                // proper way of doing this would be to change the suggestion rendering
1740
                // logic to show the whole prior snippet, but the current output is not
1741
                // too bad to begin with, so we side-step that issue here.
1742
                for (i, line) in snippet.lines().enumerate() {
8✔
1743
                    let norm_line = normalize_whitespace(line);
3✔
1744
                    // Going lower than buffer_offset (+ 1) would mean
1745
                    // overwriting existing content in the buffer
1746
                    let min_row = buffer_offset + usize::from(!matches_previous_suggestion);
8✔
1747
                    let row = (row_num - 2 - (offset - i - 1)).max(min_row);
8✔
1748
                    let (start, end) = match i {
9✔
1749
                        0 if span_start.line == span_end.line => {
3✔
1750
                            // If the removed code fits all in one line, highlight between the
1751
                            // start and end columns of the part span.
1752
                            let full_line = sm.get_line(span_start.line).unwrap_or_default();
6✔
1753
                            // We calculate the extra width from tabs for both the start and end of
1754
                            // the span, as tabs could be present in the middle of the span
1755
                            (
1756
                                span_start.char + extra_width_from_tabs(full_line, span_start.char),
3✔
1757
                                span_end.char + extra_width_from_tabs(full_line, span_end.char),
6✔
1758
                            )
1759
                        }
1760
                        0 => {
1761
                            // On the first line, we highlight between the start of the part
1762
                            // span, and the end of that line.
1763
                            let full_line = sm.get_line(span_start.line).unwrap_or_default();
7✔
1764
                            let extra_width = extra_width_from_tabs(full_line, span_start.char);
4✔
1765
                            let start = span_start.char + extra_width;
3✔
1766
                            (start, start + norm_line.chars().count())
7✔
1767
                        }
1768
                        x if x == newlines - 1 => {
7✔
1769
                            // On the last line, we highlight between the start of the line, and
1770
                            // the column of the part span end.
1771
                            let extra_width = extra_width_from_tabs(line, span_end.char);
7✔
1772
                            (0, span_end.char + extra_width)
3✔
1773
                        }
1774
                        _ => {
1775
                            // On all others, we highlight the whole line.
1776
                            (0, norm_line.chars().count())
6✔
1777
                        }
1778
                    };
1779
                    buffer.set_style_range(
3✔
1780
                        row,
1781
                        padding + start,
3✔
1782
                        padding + end,
4✔
1783
                        ElementStyle::Removal,
1784
                        true,
1785
                    );
1786
                }
1787

1788
                prev_lines = Some((span_start.line, span_end.line));
3✔
1789
            }
1790

1791
            // length of the code after substitution
1792
            let full_sub_len = str_width(&part.replacement) as isize;
10✔
1793

1794
            // length of the code to be substituted
1795
            let snippet_len = span_end_pos as isize - span_start_pos as isize;
3✔
1796
            // For multiple substitutions, use the position *after* the previous
1797
            // substitutions have happened, only when further substitutions are
1798
            // located strictly after.
1799
            offsets.push((span_end_pos, full_sub_len - snippet_len));
8✔
1800
        }
1801
        row_num += 1;
5✔
1802
    }
1803

1804
    // if we elided some lines, add an ellipsis
1805
    if lines.next().is_some() {
8✔
UNCOV
1806
        let placeholder = renderer.decor_style.margin();
×
UNCOV
1807
        let padding = str_width(placeholder);
×
UNCOV
1808
        buffer.puts(
×
UNCOV
1809
            row_num,
×
UNCOV
1810
            max_line_num_len.saturating_sub(padding),
×
1811
            placeholder,
1812
            ElementStyle::LineNumber,
1813
        );
1814
    } else {
1815
        let row = match show_code_change {
5✔
1816
            DisplaySuggestion::Diff | DisplaySuggestion::Add | DisplaySuggestion::Underline => {
1817
                row_num - 1
10✔
1818
            }
1819
            DisplaySuggestion::None => row_num,
2✔
1820
        };
1821
        if is_cont {
4✔
1822
            draw_col_separator_no_space(renderer, buffer, row, max_line_num_len + 1);
6✔
1823
        } else {
1824
            draw_col_separator_end(renderer, buffer, row, max_line_num_len + 1);
8✔
1825
        }
1826
    }
1827
}
1828

1829
#[allow(clippy::too_many_arguments)]
1830
fn draw_code_line(
6✔
1831
    renderer: &Renderer,
1832
    buffer: &mut StyledBuffer,
1833
    row_num: &mut usize,
1834
    highlight_parts: &[SubstitutionHighlight],
1835
    line_num: usize,
1836
    line_to_add: &str,
1837
    show_code_change: DisplaySuggestion,
1838
    max_line_num_len: usize,
1839
    file_lines: &[&LineInfo<'_>],
1840
    is_multiline: bool,
1841
) {
1842
    if let DisplaySuggestion::Diff = show_code_change {
7✔
1843
        // We need to print more than one line if the span we need to remove is multiline.
1844
        // For more info: https://github.com/rust-lang/rust/issues/92741
1845
        let lines_to_remove = file_lines.iter().take(file_lines.len() - 1);
7✔
1846
        for (index, line_to_remove) in lines_to_remove.enumerate() {
7✔
1847
            buffer.puts(
3✔
1848
                *row_num - 1,
3✔
1849
                0,
1850
                &maybe_anonymized(renderer, line_num + index, max_line_num_len),
6✔
1851
                ElementStyle::LineNumber,
1852
            );
1853
            buffer.puts(
3✔
1854
                *row_num - 1,
3✔
1855
                max_line_num_len + 1,
3✔
1856
                "- ",
1857
                ElementStyle::Removal,
1858
            );
1859
            let line = normalize_whitespace(line_to_remove.line);
3✔
1860
            buffer.puts(
3✔
1861
                *row_num - 1,
3✔
1862
                max_line_num_len + 3,
3✔
1863
                &line,
3✔
1864
                ElementStyle::NoStyle,
1865
            );
1866
            *row_num += 1;
3✔
1867
        }
1868
        // If the last line is exactly equal to the line we need to add, we can skip both of
1869
        // them. This allows us to avoid output like the following:
1870
        // 2 - &
1871
        // 2 + if true { true } else { false }
1872
        // 3 - if true { true } else { false }
1873
        // If those lines aren't equal, we print their diff
1874
        let last_line = &file_lines.last().unwrap();
3✔
1875
        if last_line.line == line_to_add {
5✔
1876
            *row_num -= 2;
4✔
1877
        } else {
1878
            buffer.puts(
3✔
1879
                *row_num - 1,
3✔
1880
                0,
1881
                &maybe_anonymized(renderer, line_num + file_lines.len() - 1, max_line_num_len),
6✔
1882
                ElementStyle::LineNumber,
1883
            );
1884
            buffer.puts(
3✔
1885
                *row_num - 1,
3✔
1886
                max_line_num_len + 1,
3✔
1887
                "- ",
1888
                ElementStyle::Removal,
1889
            );
1890
            buffer.puts(
4✔
1891
                *row_num - 1,
4✔
1892
                max_line_num_len + 3,
4✔
1893
                &normalize_whitespace(last_line.line),
4✔
1894
                ElementStyle::NoStyle,
1895
            );
1896
            if line_to_add.trim().is_empty() {
4✔
1897
                *row_num -= 1;
2✔
1898
            } else {
1899
                // Check if after the removal, the line is left with only whitespace. If so, we
1900
                // will not show an "addition" line, as removing the whole line is what the user
1901
                // would really want.
1902
                // For example, for the following:
1903
                //   |
1904
                // 2 -     .await
1905
                // 2 +     (note the left over whitespace)
1906
                //   |
1907
                // We really want
1908
                //   |
1909
                // 2 -     .await
1910
                //   |
1911
                // *row_num -= 1;
1912
                buffer.puts(
3✔
1913
                    *row_num,
3✔
1914
                    0,
1915
                    &maybe_anonymized(renderer, line_num, max_line_num_len),
3✔
1916
                    ElementStyle::LineNumber,
1917
                );
1918
                buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
3✔
1919
                buffer.append(
3✔
1920
                    *row_num,
3✔
1921
                    &normalize_whitespace(line_to_add),
3✔
1922
                    ElementStyle::NoStyle,
1923
                );
1924
            }
1925
        }
1926
    } else if is_multiline {
4✔
1927
        buffer.puts(
3✔
1928
            *row_num,
2✔
1929
            0,
1930
            &maybe_anonymized(renderer, line_num, max_line_num_len),
3✔
1931
            ElementStyle::LineNumber,
1932
        );
1933
        match &highlight_parts {
4✔
1934
            [SubstitutionHighlight { start: 0, end }] if *end == line_to_add.len() => {
5✔
1935
                buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
4✔
1936
            }
1937
            [] | [SubstitutionHighlight { start: 0, end: 0 }] => {
1✔
1938
                // FIXME: needed? Doesn't get exercised in any test.
1939
                draw_col_separator_no_space(renderer, buffer, *row_num, max_line_num_len + 1);
4✔
1940
            }
1941
            _ => {
1942
                let diff = renderer.decor_style.diff();
3✔
1943
                buffer.puts(
3✔
1944
                    *row_num,
3✔
1945
                    max_line_num_len + 1,
3✔
1946
                    &format!("{diff} "),
3✔
1947
                    ElementStyle::Addition,
1948
                );
1949
            }
1950
        }
1951
        //   LL | line_to_add
1952
        //   ++^^^
1953
        //    |  |
1954
        //    |  magic `3`
1955
        //    `max_line_num_len`
1956
        buffer.puts(
3✔
1957
            *row_num,
3✔
1958
            max_line_num_len + 3,
3✔
1959
            &normalize_whitespace(line_to_add),
3✔
1960
            ElementStyle::NoStyle,
1961
        );
1962
    } else if let DisplaySuggestion::Add = show_code_change {
3✔
1963
        buffer.puts(
2✔
1964
            *row_num,
2✔
1965
            0,
1966
            &maybe_anonymized(renderer, line_num, max_line_num_len),
2✔
1967
            ElementStyle::LineNumber,
1968
        );
1969
        buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
2✔
1970
        buffer.append(
2✔
1971
            *row_num,
2✔
1972
            &normalize_whitespace(line_to_add),
2✔
1973
            ElementStyle::NoStyle,
1974
        );
1975
    } else {
1976
        buffer.puts(
3✔
1977
            *row_num,
3✔
1978
            0,
1979
            &maybe_anonymized(renderer, line_num, max_line_num_len),
3✔
1980
            ElementStyle::LineNumber,
1981
        );
1982
        draw_col_separator(renderer, buffer, *row_num, max_line_num_len + 1);
3✔
1983
        buffer.append(
3✔
1984
            *row_num,
3✔
1985
            &normalize_whitespace(line_to_add),
3✔
1986
            ElementStyle::NoStyle,
1987
        );
1988
    }
1989

1990
    // Colorize addition/replacements with green.
1991
    for &SubstitutionHighlight { start, end } in highlight_parts {
8✔
1992
        // This is a no-op for empty ranges
1993
        if start != end {
4✔
1994
            // Account for tabs when highlighting (#87972).
1995
            let extra_width: usize = extra_width_from_tabs(line_to_add, start);
4✔
1996
            buffer.set_style_range(
4✔
1997
                *row_num,
4✔
1998
                max_line_num_len + 3 + start + extra_width,
4✔
1999
                max_line_num_len + 3 + end + extra_width,
8✔
2000
                ElementStyle::Addition,
2001
                true,
2002
            );
2003
        }
2004
    }
2005
    *row_num += 1;
5✔
2006
}
2007

2008
#[allow(clippy::too_many_arguments)]
2009
fn draw_line(
6✔
2010
    renderer: &Renderer,
2011
    buffer: &mut StyledBuffer,
2012
    source_string: &str,
2013
    line_index: usize,
2014
    line_offset: usize,
2015
    width_offset: usize,
2016
    code_offset: usize,
2017
    max_line_num_len: usize,
2018
    margin: Margin,
2019
) -> usize {
2020
    // Tabs are assumed to have been replaced by spaces in calling code.
2021
    debug_assert!(!source_string.contains('\t'));
7✔
2022
    let line_len = str_width(source_string);
7✔
2023
    // Create the source line we will highlight.
2024
    let mut left = margin.left(line_len);
7✔
2025
    let right = margin.right(line_len);
7✔
2026

2027
    let mut taken = 0;
8✔
2028
    let mut skipped = 0;
6✔
2029
    let code: String = source_string
2030
        .chars()
2031
        .skip_while(|ch| {
17✔
2032
            let w = char_width(*ch);
8✔
2033
            // If `skipped` is less than `left`, always skip the next `ch`,
2034
            // even if `ch` is a multi-width char that would make `skipped`
2035
            // exceed `left`. This ensures that we do not exceed term width on
2036
            // source lines.
2037
            if skipped < left {
20✔
2038
                skipped += w;
6✔
2039
                true
3✔
2040
            } else {
2041
                false
7✔
2042
            }
2043
        })
2044
        .take_while(|ch| {
14✔
2045
            // Make sure that the trimming on the right will fall within the terminal width.
2046
            taken += char_width(*ch);
7✔
2047
            taken <= (right - left)
14✔
2048
        })
2049
        .collect();
2050
    // If we skipped more than `left`, adjust `left` to account for it.
2051
    if skipped > left {
8✔
2052
        left += skipped - left;
3✔
2053
    }
2054
    let placeholder = renderer.decor_style.margin();
17✔
2055
    let padding = str_width(placeholder);
5✔
2056
    let (width_taken, bytes_taken) = if margin.was_cut_left() {
29✔
2057
        // We have stripped some code/whitespace from the beginning, make it clear.
2058
        let mut bytes_taken = 0;
4✔
2059
        let mut width_taken = 0;
4✔
2060
        for ch in code.chars() {
8✔
2061
            width_taken += char_width(ch);
8✔
2062
            bytes_taken += ch.len_utf8();
8✔
2063

2064
            if width_taken >= padding {
4✔
2065
                break;
2066
            }
2067
        }
2068

2069
        buffer.puts(
4✔
2070
            line_offset,
2071
            code_offset,
2072
            placeholder,
2073
            ElementStyle::LineNumber,
2074
        );
2075
        (width_taken, bytes_taken)
4✔
2076
    } else {
2077
        (0, 0)
8✔
2078
    };
2079

2080
    buffer.puts(
8✔
2081
        line_offset,
2082
        code_offset + width_taken,
8✔
2083
        &code[bytes_taken..],
8✔
2084
        ElementStyle::Quotation,
2085
    );
2086

2087
    if line_len > right {
7✔
2088
        // We have stripped some code/whitespace from the beginning, make it clear.
2089
        let mut char_taken = 0;
3✔
2090
        let mut width_taken_inner = 0;
3✔
2091
        for ch in code.chars().rev() {
3✔
2092
            width_taken_inner += char_width(ch);
6✔
2093
            char_taken += 1;
6✔
2094

2095
            if width_taken_inner >= padding {
3✔
2096
                break;
2097
            }
2098
        }
2099

2100
        buffer.puts(
6✔
2101
            line_offset,
2102
            code_offset + width_taken + code[bytes_taken..].chars().count() - char_taken,
6✔
2103
            placeholder,
2104
            ElementStyle::LineNumber,
2105
        );
2106
    }
2107

2108
    buffer.puts(
5✔
2109
        line_offset,
2110
        0,
2111
        &maybe_anonymized(renderer, line_index, max_line_num_len),
17✔
2112
        ElementStyle::LineNumber,
2113
    );
2114

2115
    draw_col_separator_no_space(renderer, buffer, line_offset, width_offset - 2);
9✔
2116

2117
    left
5✔
2118
}
2119

2120
fn draw_range(
3✔
2121
    buffer: &mut StyledBuffer,
2122
    symbol: char,
2123
    line: usize,
2124
    col_from: usize,
2125
    col_to: usize,
2126
    style: ElementStyle,
2127
) {
2128
    for col in col_from..col_to {
6✔
2129
        buffer.putc(line, col, symbol, style);
3✔
2130
    }
2131
}
2132

2133
fn draw_multiline_line(
3✔
2134
    renderer: &Renderer,
2135
    buffer: &mut StyledBuffer,
2136
    line: usize,
2137
    offset: usize,
2138
    depth: usize,
2139
    style: ElementStyle,
2140
) {
2141
    let chr = match (style, renderer.decor_style) {
3✔
2142
        (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Ascii) => '|',
3✔
2143
        (_, DecorStyle::Ascii) => '|',
2✔
2144
        (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Unicode) => '┃',
3✔
2145
        (_, DecorStyle::Unicode) => '│',
2✔
2146
    };
2147
    buffer.putc(line, offset + depth - 1, chr, style);
6✔
2148
}
2149

2150
fn draw_col_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
3✔
2151
    let chr = renderer.decor_style.col_separator();
3✔
2152
    buffer.puts(line, col, &format!("{chr} "), ElementStyle::LineNumber);
3✔
2153
}
2154

2155
fn draw_col_separator_no_space(
7✔
2156
    renderer: &Renderer,
2157
    buffer: &mut StyledBuffer,
2158
    line: usize,
2159
    col: usize,
2160
) {
2161
    let chr = renderer.decor_style.col_separator();
5✔
2162
    draw_col_separator_no_space_with_style(buffer, chr, line, col, ElementStyle::LineNumber);
5✔
2163
}
2164

2165
fn draw_col_separator_start(
4✔
2166
    renderer: &Renderer,
2167
    buffer: &mut StyledBuffer,
2168
    line: usize,
2169
    col: usize,
2170
) {
2171
    match renderer.decor_style {
5✔
2172
        DecorStyle::Ascii => {
2173
            draw_col_separator_no_space_with_style(
2174
                buffer,
2175
                '|',
2176
                line,
2177
                col,
2178
                ElementStyle::LineNumber,
2179
            );
2180
        }
2181
        DecorStyle::Unicode => {
2182
            draw_col_separator_no_space_with_style(
2183
                buffer,
2184
                '╭',
2185
                line,
2186
                col,
2187
                ElementStyle::LineNumber,
2188
            );
2189
            draw_col_separator_no_space_with_style(
2190
                buffer,
2191
                '╴',
2192
                line,
2193
                col + 1,
5✔
2194
                ElementStyle::LineNumber,
2195
            );
2196
        }
2197
    }
2198
}
2199

2200
fn draw_col_separator_end(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
5✔
2201
    match renderer.decor_style {
5✔
2202
        DecorStyle::Ascii => {
2203
            draw_col_separator_no_space_with_style(
2204
                buffer,
2205
                '|',
2206
                line,
2207
                col,
2208
                ElementStyle::LineNumber,
2209
            );
2210
        }
2211
        DecorStyle::Unicode => {
2212
            draw_col_separator_no_space_with_style(
2213
                buffer,
2214
                '╰',
2215
                line,
2216
                col,
2217
                ElementStyle::LineNumber,
2218
            );
2219
            draw_col_separator_no_space_with_style(
2220
                buffer,
2221
                '╴',
2222
                line,
2223
                col + 1,
6✔
2224
                ElementStyle::LineNumber,
2225
            );
2226
        }
2227
    }
2228
}
2229

2230
fn draw_col_separator_no_space_with_style(
8✔
2231
    buffer: &mut StyledBuffer,
2232
    chr: char,
2233
    line: usize,
2234
    col: usize,
2235
    style: ElementStyle,
2236
) {
2237
    buffer.putc(line, col, chr, style);
5✔
2238
}
2239

2240
fn maybe_anonymized(renderer: &Renderer, line_num: usize, max_line_num_len: usize) -> String {
7✔
2241
    format!(
5✔
2242
        "{:>max_line_num_len$}",
2243
        if renderer.anonymized_line_numbers {
18✔
UNCOV
2244
            Cow::Borrowed(ANONYMIZED_LINE_NUM)
×
2245
        } else {
2246
            Cow::Owned(line_num.to_string())
7✔
2247
        }
2248
    )
2249
}
2250

2251
fn draw_note_separator(
3✔
2252
    renderer: &Renderer,
2253
    buffer: &mut StyledBuffer,
2254
    line: usize,
2255
    col: usize,
2256
    is_cont: bool,
2257
) {
2258
    let chr = renderer.decor_style.note_separator(is_cont);
3✔
2259
    buffer.puts(line, col, chr, ElementStyle::LineNumber);
3✔
2260
}
2261

2262
fn draw_line_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
4✔
2263
    let (column, dots) = match renderer.decor_style {
8✔
2264
        DecorStyle::Ascii => (0, "..."),
4✔
2265
        DecorStyle::Unicode => (col - 2, "‡"),
8✔
2266
    };
2267
    buffer.puts(line, column, dots, ElementStyle::LineNumber);
4✔
2268
}
2269

2270
trait MessageOrTitle {
2271
    fn level(&self) -> &Level<'_>;
2272
    fn id(&self) -> Option<&Id<'_>>;
2273
    fn text(&self) -> &str;
2274
    fn allows_styling(&self) -> bool;
2275
}
2276

2277
impl MessageOrTitle for Title<'_> {
2278
    fn level(&self) -> &Level<'_> {
6✔
2279
        &self.level
6✔
2280
    }
2281
    fn id(&self) -> Option<&Id<'_>> {
6✔
2282
        self.id.as_ref()
6✔
2283
    }
2284
    fn text(&self) -> &str {
6✔
2285
        self.text.as_ref()
6✔
2286
    }
2287
    fn allows_styling(&self) -> bool {
6✔
2288
        self.allows_styling
6✔
2289
    }
2290
}
2291

2292
impl MessageOrTitle for Message<'_> {
2293
    fn level(&self) -> &Level<'_> {
3✔
2294
        &self.level
3✔
2295
    }
2296
    fn id(&self) -> Option<&Id<'_>> {
3✔
2297
        None
2298
    }
2299
    fn text(&self) -> &str {
3✔
2300
        self.text.as_ref()
3✔
2301
    }
2302
    fn allows_styling(&self) -> bool {
3✔
2303
        true
2304
    }
2305
}
2306

2307
/// Count extra display columns from tabs in the first `n` chars of `s`.
2308
/// Each tab is displayed as 4 spaces, so the extra width per tab is 3.
2309
fn extra_width_from_tabs(s: &str, n: usize) -> usize {
4✔
2310
    s.chars().take(n).filter(|&ch| ch == '\t').count() * 3
12✔
2311
}
2312

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

2321
    #[cfg(target_pointer_width = "32")]
2322
    const MAX_DIGITS: usize = 10;
2323

2324
    #[cfg(target_pointer_width = "16")]
2325
    const MAX_DIGITS: usize = 5;
2326

2327
    let mut lim = 10;
5✔
2328
    for num_digits in 1..MAX_DIGITS {
10✔
2329
        if num < lim {
5✔
2330
            return num_digits;
5✔
2331
        }
2332
        lim = lim.wrapping_mul(10);
5✔
2333
    }
UNCOV
2334
    MAX_DIGITS
×
2335
}
2336

2337
fn str_width(s: &str) -> usize {
6✔
2338
    s.chars().map(char_width).sum()
6✔
2339
}
2340

2341
pub(crate) fn char_width(ch: char) -> usize {
6✔
2342
    // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` is. For now,
2343
    // just accept that sometimes the code line will be longer than desired.
2344
    match ch {
7✔
2345
        '\t' => 4,
2✔
2346
        // Keep the following list in sync with `rustc_errors::emitter::OUTPUT_REPLACEMENTS`. These
2347
        // are control points that we replace before printing with a visible codepoint for the sake
2348
        // of being able to point at them with underlines.
2349
        '\u{0000}' | '\u{0001}' | '\u{0002}' | '\u{0003}' | '\u{0004}' | '\u{0005}'
1✔
2350
        | '\u{0006}' | '\u{0007}' | '\u{0008}' | '\u{000B}' | '\u{000C}' | '\u{000D}'
2351
        | '\u{000E}' | '\u{000F}' | '\u{0010}' | '\u{0011}' | '\u{0012}' | '\u{0013}'
2352
        | '\u{0014}' | '\u{0015}' | '\u{0016}' | '\u{0017}' | '\u{0018}' | '\u{0019}'
2353
        | '\u{001A}' | '\u{001B}' | '\u{001C}' | '\u{001D}' | '\u{001E}' | '\u{001F}'
2354
        | '\u{007F}' | '\u{202A}' | '\u{202B}' | '\u{202D}' | '\u{202E}' | '\u{2066}'
2355
        | '\u{2067}' | '\u{2068}' | '\u{202C}' | '\u{2069}' => 1,
2356
        _ => unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1),
6✔
2357
    }
2358
}
2359

2360
pub(crate) fn num_overlap(
6✔
2361
    a_start: usize,
2362
    a_end: usize,
2363
    b_start: usize,
2364
    b_end: usize,
2365
    inclusive: bool,
2366
) -> bool {
2367
    let extra = usize::from(inclusive);
6✔
2368
    (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start)
6✔
2369
}
2370

2371
fn overlaps(a1: &LineAnnotation<'_>, a2: &LineAnnotation<'_>, padding: usize) -> bool {
6✔
2372
    num_overlap(
2373
        a1.start.display,
7✔
2374
        a1.end.display + padding,
7✔
2375
        a2.start.display,
7✔
2376
        a2.end.display,
7✔
2377
        false,
2378
    )
2379
}
2380

2381
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2382
pub(crate) enum LineAnnotationType {
2383
    /// Annotation under a single line of code
2384
    Singleline,
2385

2386
    // The Multiline type above is replaced with the following three in order
2387
    // to reuse the current label drawing code.
2388
    //
2389
    // Each of these corresponds to one part of the following diagram:
2390
    //
2391
    //     x |   foo(1 + bar(x,
2392
    //       |  _________^              < MultilineStart
2393
    //     x | |             y),        < MultilineLine
2394
    //       | |______________^ label   < MultilineEnd
2395
    //     x |       z);
2396
    /// Annotation marking the first character of a fully shown multiline span
2397
    MultilineStart(usize),
2398
    /// Annotation marking the last character of a fully shown multiline span
2399
    MultilineEnd(usize),
2400
    /// Line at the left enclosing the lines of a fully shown multiline span
2401
    // Just a placeholder for the drawing algorithm, to know that it shouldn't skip the first 4
2402
    // and last 2 lines of code. The actual line is drawn in `emit_message_default` and not in
2403
    // `draw_multiline_line`.
2404
    MultilineLine(usize),
2405
}
2406

2407
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2408
pub(crate) struct LineAnnotation<'a> {
2409
    /// Start column.
2410
    /// Note that it is important that this field goes
2411
    /// first, so that when we sort, we sort orderings by start
2412
    /// column.
2413
    pub start: Loc,
2414

2415
    /// End column within the line (exclusive)
2416
    pub end: Loc,
2417

2418
    /// level
2419
    pub kind: AnnotationKind,
2420

2421
    /// Optional label to display adjacent to the annotation.
2422
    pub label: Option<Cow<'a, str>>,
2423

2424
    /// Is this a single line, multiline or multiline span minimized down to a
2425
    /// smaller span.
2426
    pub annotation_type: LineAnnotationType,
2427

2428
    /// Whether the source code should be highlighted
2429
    pub highlight_source: bool,
2430
}
2431

2432
impl LineAnnotation<'_> {
2433
    pub(crate) fn is_primary(&self) -> bool {
9✔
2434
        self.kind == AnnotationKind::Primary
7✔
2435
    }
2436

2437
    /// Whether this annotation is a vertical line placeholder.
2438
    pub(crate) fn is_line(&self) -> bool {
7✔
2439
        matches!(self.annotation_type, LineAnnotationType::MultilineLine(_))
6✔
2440
    }
2441

2442
    /// Length of this annotation as displayed in the stderr output
2443
    pub(crate) fn len(&self) -> usize {
4✔
2444
        // Account for usize underflows
2445
        self.end.display.abs_diff(self.start.display)
4✔
2446
    }
2447

2448
    pub(crate) fn has_label(&self) -> bool {
6✔
2449
        if let Some(label) = &self.label {
13✔
2450
            // Consider labels with no text as effectively not being there
2451
            // to avoid weird output with unnecessary vertical lines, like:
2452
            //
2453
            //     X | fn foo(x: u32) {
2454
            //       | -------^------
2455
            //       | |      |
2456
            //       | |
2457
            //       |
2458
            //
2459
            // Note that this would be the complete output users would see.
2460
            !label.is_empty()
5✔
2461
        } else {
2462
            false
4✔
2463
        }
2464
    }
2465

2466
    pub(crate) fn takes_space(&self) -> bool {
3✔
2467
        // Multiline annotations always have to keep vertical space.
2468
        matches!(
3✔
2469
            self.annotation_type,
3✔
2470
            LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
2471
        )
2472
    }
2473
}
2474

2475
#[derive(Clone, Copy, Debug)]
2476
pub(crate) enum DisplaySuggestion {
2477
    Underline,
2478
    Diff,
2479
    None,
2480
    Add,
2481
}
2482

2483
impl DisplaySuggestion {
2484
    fn new(complete: &str, patches: &[TrimmedPatch<'_>], sm: &SourceMap<'_>) -> Self {
4✔
2485
        let has_deletion = patches
2486
            .iter()
2487
            .any(|p| p.is_deletion(sm) || p.is_destructive_replacement(sm));
14✔
2488
        let is_multiline = complete.lines().count() > 1;
4✔
2489
        if has_deletion && !is_multiline {
7✔
2490
            DisplaySuggestion::Diff
3✔
2491
        } else if patches.len() == 1
5✔
2492
            && patches.first().map_or(false, |p| {
6✔
2493
                p.replacement.ends_with('\n') && p.replacement.trim() == complete.trim()
3✔
2494
            })
2495
        {
2496
            // We are adding a line(s) of code before code that was already there.
2497
            DisplaySuggestion::Add
2✔
2498
        } else if (patches.len() != 1 || patches[0].replacement.trim() != complete.trim())
14✔
2499
            && !is_multiline
2✔
2500
        {
2501
            DisplaySuggestion::Underline
3✔
2502
        } else {
2503
            DisplaySuggestion::None
2✔
2504
        }
2505
    }
2506
}
2507

2508
// We replace some characters so the CLI output is always consistent and underlines aligned.
2509
// Keep the following list in sync with `rustc_span::char_width`.
2510
const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[
2511
    // In terminals without Unicode support the following will be garbled, but in *all* terminals
2512
    // the underlying codepoint will be as well. We could gate this replacement behind a "unicode
2513
    // support" gate.
2514
    ('\0', "␀"),
2515
    ('\u{0001}', "␁"),
2516
    ('\u{0002}', "␂"),
2517
    ('\u{0003}', "␃"),
2518
    ('\u{0004}', "␄"),
2519
    ('\u{0005}', "␅"),
2520
    ('\u{0006}', "␆"),
2521
    ('\u{0007}', "␇"),
2522
    ('\u{0008}', "␈"),
2523
    ('\t', "    "), // We do our own tab replacement
2524
    ('\u{000b}', "␋"),
2525
    ('\u{000c}', "␌"),
2526
    ('\u{000d}', "␍"),
2527
    ('\u{000e}', "␎"),
2528
    ('\u{000f}', "␏"),
2529
    ('\u{0010}', "␐"),
2530
    ('\u{0011}', "␑"),
2531
    ('\u{0012}', "␒"),
2532
    ('\u{0013}', "␓"),
2533
    ('\u{0014}', "␔"),
2534
    ('\u{0015}', "␕"),
2535
    ('\u{0016}', "␖"),
2536
    ('\u{0017}', "␗"),
2537
    ('\u{0018}', "␘"),
2538
    ('\u{0019}', "␙"),
2539
    ('\u{001a}', "␚"),
2540
    ('\u{001b}', "␛"),
2541
    ('\u{001c}', "␜"),
2542
    ('\u{001d}', "␝"),
2543
    ('\u{001e}', "␞"),
2544
    ('\u{001f}', "␟"),
2545
    ('\u{007f}', "␡"),
2546
    ('\u{200d}', ""), // Replace ZWJ for consistent terminal output of grapheme clusters.
2547
    ('\u{202a}', "�"), // The following unicode text flow control characters are inconsistently
2548
    ('\u{202b}', "�"), // supported across CLIs and can cause confusion due to the bytes on disk
2549
    ('\u{202c}', "�"), // not corresponding to the visible source code, so we replace them always.
2550
    ('\u{202d}', "�"),
2551
    ('\u{202e}', "�"),
2552
    ('\u{2066}', "�"),
2553
    ('\u{2067}', "�"),
2554
    ('\u{2068}', "�"),
2555
    ('\u{2069}', "�"),
2556
];
2557

2558
pub(crate) fn normalize_whitespace(s: &str) -> String {
6✔
2559
    // Scan the input string for a character in the ordered table above.
2560
    // If it's present, replace it with its alternative string (it can be more than 1 char!).
2561
    // Otherwise, retain the input char.
2562
    s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
11✔
2563
        match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) {
21✔
2564
            Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1),
2✔
2565
            _ => s.push(c),
10✔
2566
        }
2567
        s
5✔
2568
    })
2569
}
2570

2571
#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)]
2572
pub(crate) enum ElementStyle {
2573
    MainHeaderMsg,
2574
    HeaderMsg,
2575
    LineAndColumn,
2576
    LineNumber,
2577
    Quotation,
2578
    UnderlinePrimary,
2579
    UnderlineSecondary,
2580
    LabelPrimary,
2581
    LabelSecondary,
2582
    NoStyle,
2583
    Level(LevelInner),
2584
    Addition,
2585
    Removal,
2586
}
2587

2588
impl ElementStyle {
2589
    pub(crate) fn color_spec(&self, level: &Level<'_>, stylesheet: &Stylesheet) -> Style {
6✔
2590
        match self {
5✔
2591
            ElementStyle::Addition => stylesheet.addition,
3✔
2592
            ElementStyle::Removal => stylesheet.removal,
3✔
2593
            ElementStyle::LineAndColumn => stylesheet.none,
4✔
2594
            ElementStyle::LineNumber => stylesheet.line_num,
7✔
2595
            ElementStyle::Quotation => stylesheet.none,
6✔
2596
            ElementStyle::MainHeaderMsg => stylesheet.emphasis,
6✔
2597
            ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary => level.style(stylesheet),
6✔
2598
            ElementStyle::UnderlineSecondary | ElementStyle::LabelSecondary => stylesheet.context,
4✔
2599
            ElementStyle::HeaderMsg | ElementStyle::NoStyle => stylesheet.none,
4✔
2600
            ElementStyle::Level(lvl) => lvl.style(stylesheet),
4✔
2601
        }
2602
    }
2603
}
2604

2605
#[derive(Debug, Clone, Copy)]
2606
pub(crate) struct UnderlineParts {
2607
    pub(crate) style: ElementStyle,
2608
    pub(crate) underline: char,
2609
    pub(crate) label_start: char,
2610
    pub(crate) vertical_text_line: char,
2611
    pub(crate) multiline_vertical: char,
2612
    pub(crate) multiline_horizontal: char,
2613
    pub(crate) multiline_whole_line: char,
2614
    pub(crate) multiline_start_down: char,
2615
    pub(crate) bottom_right: char,
2616
    pub(crate) top_left: char,
2617
    pub(crate) top_right_flat: char,
2618
    pub(crate) bottom_left: char,
2619
    pub(crate) multiline_end_up: char,
2620
    pub(crate) multiline_end_same_line: char,
2621
    pub(crate) multiline_bottom_right_with_text: char,
2622
}
2623

2624
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2625
enum TitleStyle {
2626
    MainHeader,
2627
    Header,
2628
    Secondary,
2629
}
2630

2631
struct PreProcessedGroup<'a> {
2632
    group: &'a Group<'a>,
2633
    elements: Vec<PreProcessedElement<'a>>,
2634
    primary_path: Option<&'a Cow<'a, str>>,
2635
    max_depth: usize,
2636
}
2637

2638
enum PreProcessedElement<'a> {
2639
    Message(&'a Message<'a>),
2640
    Cause(
2641
        (
2642
            &'a Snippet<'a, Annotation<'a>>,
2643
            SourceMap<'a>,
2644
            Vec<AnnotatedLineInfo<'a>>,
2645
        ),
2646
    ),
2647
    Suggestion(
2648
        (
2649
            &'a Snippet<'a, Patch<'a>>,
2650
            SourceMap<'a>,
2651
            SplicedLines<'a>,
2652
            DisplaySuggestion,
2653
        ),
2654
    ),
2655
    Origin(&'a Origin<'a>),
2656
    Padding(Padding),
2657
}
2658

2659
fn pre_process<'a>(
10✔
2660
    groups: &'a [Group<'a>],
2661
) -> (usize, Option<&'a Cow<'a, str>>, Vec<PreProcessedGroup<'a>>) {
2662
    let mut max_line_num = 0;
7✔
2663
    let mut og_primary_path = None;
6✔
2664
    let mut out = Vec::with_capacity(groups.len());
8✔
2665
    for group in groups {
16✔
2666
        let mut elements = Vec::with_capacity(group.elements.len());
11✔
2667
        let mut primary_path = None;
5✔
2668
        let mut max_depth = 0;
8✔
2669
        for element in &group.elements {
14✔
2670
            match element {
7✔
2671
                Element::Message(message) => {
3✔
2672
                    elements.push(PreProcessedElement::Message(message));
6✔
2673
                }
2674
                Element::Cause(cause) => {
6✔
2675
                    let sm = SourceMap::new(&cause.source, cause.line_start);
12✔
2676
                    let (depth, annotated_lines) =
10✔
UNCOV
2677
                        sm.annotated_lines(cause.markers.clone(), cause.fold);
×
2678

2679
                    if cause.fold {
12✔
2680
                        let end = cause
10✔
UNCOV
2681
                            .markers
×
2682
                            .iter()
2683
                            .map(|a| a.span.end)
17✔
2684
                            .max()
2685
                            .unwrap_or(cause.source.len())
5✔
2686
                            .min(cause.source.len());
6✔
2687

2688
                        max_line_num = max(
11✔
2689
                            cause.line_start + newline_count(&cause.source[..end]),
6✔
2690
                            max_line_num,
5✔
2691
                        );
2692
                    } else {
2693
                        max_line_num = max(
6✔
2694
                            cause.line_start + newline_count(&cause.source),
5✔
2695
                            max_line_num,
3✔
2696
                        );
2697
                    }
2698

2699
                    if primary_path.is_none() {
16✔
2700
                        primary_path = Some(cause.path.as_ref());
5✔
2701
                    }
2702
                    max_depth = max(depth, max_depth);
9✔
2703
                    elements.push(PreProcessedElement::Cause((cause, sm, annotated_lines)));
4✔
2704
                }
2705
                Element::Suggestion(suggestion) => {
3✔
2706
                    let sm = SourceMap::new(&suggestion.source, suggestion.line_start);
6✔
2707
                    if let Some((complete, patches, highlights)) =
8✔
UNCOV
2708
                        sm.splice_lines(suggestion.markers.clone(), suggestion.fold)
×
2709
                    {
2710
                        let display_suggestion = DisplaySuggestion::new(&complete, &patches, &sm);
9✔
2711

2712
                        if suggestion.fold {
3✔
2713
                            if let Some(first) = patches.first() {
11✔
2714
                                let (l_start, _) =
3✔
UNCOV
2715
                                    sm.span_to_locations(first.original_span.clone());
×
2716
                                let nc = newline_count(&complete);
4✔
2717
                                let sugg_max_line_num = match display_suggestion {
3✔
2718
                                    DisplaySuggestion::Underline => l_start.line,
3✔
UNCOV
2719
                                    DisplaySuggestion::Diff => {
×
2720
                                        let file_lines = sm.span_to_lines(first.span.clone());
6✔
2721
                                        file_lines
8✔
2722
                                            .last()
2723
                                            .map_or(l_start.line + nc, |line| line.line_index)
10✔
2724
                                    }
2725
                                    DisplaySuggestion::None => l_start.line + nc,
4✔
2726
                                    DisplaySuggestion::Add => l_start.line + nc,
4✔
2727
                                };
2728
                                max_line_num = max(sugg_max_line_num, max_line_num);
9✔
2729
                            }
2730
                        } else {
2731
                            max_line_num = max(
2✔
2732
                                suggestion.line_start + newline_count(&complete),
4✔
2733
                                max_line_num,
2✔
2734
                            );
2735
                        }
2736

2737
                        elements.push(PreProcessedElement::Suggestion((
5✔
UNCOV
2738
                            suggestion,
×
2739
                            sm,
5✔
2740
                            (complete, patches, highlights),
4✔
UNCOV
2741
                            display_suggestion,
×
2742
                        )));
2743
                    }
2744
                }
2745
                Element::Origin(origin) => {
2✔
2746
                    if primary_path.is_none() {
6✔
2747
                        primary_path = Some(Some(&origin.path));
2✔
2748
                    }
2749
                    elements.push(PreProcessedElement::Origin(origin));
4✔
2750
                }
2751
                Element::Padding(padding) => {
3✔
2752
                    elements.push(PreProcessedElement::Padding(padding.clone()));
6✔
2753
                }
2754
            }
2755
        }
2756
        let group = PreProcessedGroup {
2757
            group,
2758
            elements,
2759
            primary_path: primary_path.unwrap_or_default(),
4✔
2760
            max_depth,
2761
        };
2762
        if og_primary_path.is_none() && group.primary_path.is_some() {
16✔
2763
            og_primary_path = group.primary_path;
4✔
2764
        }
2765
        out.push(group);
5✔
2766
    }
2767

2768
    (max_line_num, og_primary_path, out)
4✔
2769
}
2770

2771
fn newline_count(body: &str) -> usize {
6✔
2772
    #[cfg(feature = "simd")]
2773
    {
2774
        memchr::memchr_iter(b'\n', body.as_bytes()).count()
2775
    }
2776
    #[cfg(not(feature = "simd"))]
2777
    {
2778
        body.lines().count().saturating_sub(1)
7✔
2779
    }
2780
}
2781

2782
#[cfg(test)]
2783
mod test {
2784
    use super::{newline_count, OUTPUT_REPLACEMENTS};
2785
    use snapbox::IntoData;
2786

2787
    fn format_replacements(replacements: Vec<(char, &str)>) -> String {
2788
        replacements
2789
            .into_iter()
2790
            .map(|r| format!("    {r:?}"))
2791
            .collect::<Vec<_>>()
2792
            .join("\n")
2793
    }
2794

2795
    #[test]
2796
    /// The [`OUTPUT_REPLACEMENTS`] array must be sorted (for binary search to
2797
    /// work) and must contain no duplicate entries
2798
    fn ensure_output_replacements_is_sorted() {
2799
        let mut expected = OUTPUT_REPLACEMENTS.to_owned();
2800
        expected.sort_by_key(|r| r.0);
2801
        expected.dedup_by_key(|r| r.0);
2802
        let expected = format_replacements(expected);
2803
        let actual = format_replacements(OUTPUT_REPLACEMENTS.to_owned());
2804
        snapbox::assert_data_eq!(actual, expected.into_data().raw());
2805
    }
2806

2807
    #[test]
2808
    fn ensure_newline_count_correct() {
2809
        let source = r#"
2810
                cargo-features = ["path-bases"]
2811

2812
                [package]
2813
                name = "foo"
2814
                version = "0.5.0"
2815
                authors = ["wycats@example.com"]
2816

2817
                [dependencies]
2818
                bar = { base = '^^not-valid^^', path = 'bar' }
2819
            "#;
2820
        let actual_count = newline_count(source);
2821
        let expected_count = 10;
2822

2823
        assert_eq!(expected_count, actual_count);
2824
    }
2825
}
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

© 2026 Coveralls, Inc