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

rust-lang / annotate-snippets-rs / 22150311874

18 Feb 2026 05:25PM UTC coverage: 90.018% (-0.06%) from 90.078%
22150311874

push

github

web-flow
chore(deps): Update Prek to v0.3.3 (#370)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

1497 of 1663 relevant lines covered (90.02%)

4.98 hits per line

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

95.72
/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 {
5✔
30
    if renderer.short_message {
5✔
31
        render_short_message(renderer, groups).unwrap()
2✔
32
    } else {
33
        let (max_line_num, og_primary_path, groups) = pre_process(groups);
4✔
34
        let max_line_num_len = if renderer.anonymized_line_numbers {
5✔
35
            ANONYMIZED_LINE_NUM.len()
×
36
        } else {
37
            num_decimal_digits(max_line_num)
11✔
38
        };
39
        let mut out_string = String::new();
4✔
40
        let group_len = groups.len();
10✔
41
        for (
4✔
42
            g,
5✔
43
            PreProcessedGroup {
44
                group,
3✔
45
                elements,
5✔
46
                primary_path,
3✔
47
                max_depth,
5✔
48
            },
49
        ) in groups.into_iter().enumerate()
8✔
50
        {
51
            let mut buffer = StyledBuffer::new();
4✔
52
            let level = group.primary_level.clone();
6✔
53
            let mut message_iter = elements.into_iter().enumerate().peekable();
10✔
54
            if let Some(title) = &group.title {
3✔
55
                let peek = message_iter.peek().map(|(_, s)| s);
20✔
56
                let title_style = if title.allows_styling {
14✔
57
                    TitleStyle::Header
3✔
58
                } else {
59
                    TitleStyle::MainHeader
3✔
60
                };
61
                let buffer_msg_line_offset = buffer.num_lines();
3✔
62
                render_title(
63
                    renderer,
64
                    &mut buffer,
65
                    title,
66
                    max_line_num_len,
6✔
67
                    title_style,
4✔
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(_))) {
4✔
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()
9✔
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;
5✔
95
            let mut last_suggestion_path = None;
4✔
96
            while let Some((i, section)) = message_iter.next() {
13✔
97
                let peek = message_iter.peek().map(|(_, s)| s);
15✔
98
                let is_first = i == 0;
5✔
99
                match section {
4✔
100
                    PreProcessedElement::Message(title) => {
4✔
101
                        let title_style = TitleStyle::Secondary;
4✔
102
                        let buffer_msg_line_offset = buffer.num_lines();
8✔
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)) => {
5✔
114
                        let is_primary = primary_path == cause.path.as_ref() && !seen_primary;
11✔
115
                        seen_primary |= is_primary;
5✔
116
                        render_snippet_annotations(
117
                            renderer,
118
                            &mut buffer,
119
                            max_line_num_len,
5✔
120
                            cause,
121
                            is_primary,
5✔
122
                            &source_map,
123
                            &annotated_lines,
5✔
124
                            max_depth,
125
                            peek.is_some() || (g == 0 && group_len > 1),
5✔
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,
5✔
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),
4✔
167
                            matches_previous_suggestion,
168
                            is_first,
169
                            //matches!(peek, Some(Element::Message(_) | Element::Padding(_))),
170
                            peek.is_some(),
3✔
171
                        );
172

173
                        if matches!(peek, Some(PreProcessedElement::Suggestion(_))) {
12✔
174
                            last_suggestion_path = Some(suggestion.path.as_ref());
6✔
175
                        } else {
176
                            last_suggestion_path = None;
5✔
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();
8✔
206
                        if peek.is_none() {
4✔
207
                            draw_col_separator_end(
208
                                renderer,
209
                                &mut buffer,
210
                                current_line,
211
                                max_line_num_len + 1,
2✔
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)
4✔
226
                .unwrap();
227
            if g != group_len - 1 {
5✔
228
                use core::fmt::Write;
229

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

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

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

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

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

273
            let source_map = SourceMap::new(&cause.source, cause.line_start);
4✔
274
            let (_depth, annotated_lines) =
4✔
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(
4✔
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 => (
4✔
329
            ElementStyle::Level(title.level().level),
4✔
330
            if renderer.short_message {
12✔
331
                ElementStyle::NoStyle
2✔
332
            } else {
333
                ElementStyle::MainHeaderMsg
4✔
334
            },
335
        ),
336
        TitleStyle::Header => (
3✔
337
            ElementStyle::Level(title.level().level),
3✔
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;
4✔
356

357
    if title.level().name != Some(None) {
10✔
358
        buffer.append(buffer_msg_line_offset, title.level().as_str(), label_style);
4✔
359
        label_width += title.level().as_str().len();
5✔
360
        if let Some(Id { id: Some(id), url }) = &title.id() {
17✔
361
            buffer.append(buffer_msg_line_offset, "[", label_style);
4✔
362
            if let Some(url) = url.as_ref() {
4✔
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);
4✔
370
            if url.is_some() {
4✔
371
                buffer.append(buffer_msg_line_offset, "\x1B]8;;\x1B\\", label_style);
×
372
            }
373
            buffer.append(buffer_msg_line_offset, "]", label_style);
4✔
374
            label_width += 2 + id.len();
8✔
375
        }
376
        buffer.append(buffer_msg_line_offset, ": ", title_element_style);
5✔
377
        label_width += 2;
9✔
378
    }
379

380
    let padding = " ".repeat(if title_style == TitleStyle::Secondary {
15✔
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
4✔
401
    });
402

403
    let (title_str, style) = if title.allows_styling() {
15✔
404
        (title.text().to_owned(), ElementStyle::NoStyle)
6✔
405
    } else {
406
        (normalize_whitespace(title.text()), title_element_style)
8✔
407
    };
408
    for (i, text) in title_str.split('\n').enumerate() {
9✔
409
        if i != 0 {
4✔
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);
12✔
435
    }
436
}
437

438
#[allow(clippy::too_many_arguments)]
439
fn render_origin(
8✔
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 {
14✔
450
        buffer.prepend(
7✔
451
            buffer_msg_line_offset,
452
            renderer.decor_style.file_start(is_first, alone),
6✔
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) {
14✔
484
        (Some(line), Some(col)) => {
8✔
485
            format!("{}:{}:{}", origin.path, line, col)
6✔
486
        }
487
        (Some(line), None) => format!("{}:{}", origin.path, line),
1✔
488
        _ => origin.path.to_string(),
1✔
489
    };
490

491
    buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn);
14✔
492
    if !renderer.short_message {
6✔
493
        for _ in 0..max_line_num_len {
14✔
494
            buffer.prepend(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
8✔
495
        }
496
    }
497
}
498

499
#[allow(clippy::too_many_arguments)]
500
fn render_snippet_annotations(
6✔
501
    renderer: &Renderer,
502
    buffer: &mut StyledBuffer,
503
    max_line_num_len: usize,
504
    snippet: &Snippet<'_, Annotation<'_>>,
505
    is_primary: bool,
506
    sm: &SourceMap<'_>,
507
    annotated_lines: &[AnnotatedLineInfo<'_>],
508
    multiline_depth: usize,
509
    is_cont: bool,
510
    is_first: bool,
511
) {
512
    if let Some(path) = &snippet.path {
5✔
513
        let mut origin = Origin::path(path.as_ref());
5✔
514
        // print out the span location and spacer before we print the annotated source
515
        // to do this, we need to know if this span will be primary
516
        //let is_primary = primary_path == Some(&origin.path);
517

518
        if is_primary {
5✔
519
            if let Some(primary_line) = annotated_lines
12✔
520
                .iter()
521
                .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
15✔
522
                .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
16✔
523
            {
524
                origin.line = Some(primary_line.line_index);
4✔
525
                if let Some(first_annotation) = primary_line
18✔
526
                    .annotations
527
                    .iter()
528
                    .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
15✔
529
                {
530
                    origin.char_column = Some(first_annotation.start.char + 1);
5✔
531
                }
532
            }
533
        } else {
534
            let buffer_msg_line_offset = buffer.num_lines();
6✔
535
            // Add spacing line, as shown:
536
            //   --> $DIR/file:54:15
537
            //    |
538
            // LL |         code
539
            //    |         ^^^^
540
            //    | (<- It prints *this* line)
541
            //   ::: $DIR/other_file.rs:15:5
542
            //    |
543
            // LL |     code
544
            //    |     ----
545
            draw_col_separator_no_space(
546
                renderer,
547
                buffer,
548
                buffer_msg_line_offset,
549
                max_line_num_len + 1,
3✔
550
            );
551
            if let Some(first_line) = annotated_lines.first() {
3✔
552
                origin.line = Some(first_line.line_index);
3✔
553
                if let Some(first_annotation) = first_line.annotations.first() {
8✔
554
                    origin.char_column = Some(first_annotation.start.char + 1);
2✔
555
                }
556
            }
557
        }
558
        let buffer_msg_line_offset = buffer.num_lines();
12✔
559
        render_origin(
560
            renderer,
561
            buffer,
562
            max_line_num_len,
563
            &origin,
564
            is_primary,
565
            is_first,
566
            false,
567
            buffer_msg_line_offset,
568
        );
569
        // Put in the spacer between the location and annotated source
570
        draw_col_separator_no_space(
571
            renderer,
572
            buffer,
573
            buffer_msg_line_offset + 1,
8✔
574
            max_line_num_len + 1,
6✔
575
        );
576
    } else {
577
        let buffer_msg_line_offset = buffer.num_lines();
2✔
578
        if is_primary {
2✔
579
            if renderer.decor_style == DecorStyle::Unicode {
2✔
580
                buffer.puts(
2✔
581
                    buffer_msg_line_offset,
582
                    max_line_num_len,
583
                    renderer.decor_style.file_start(is_first, false),
2✔
584
                    ElementStyle::LineNumber,
585
                );
586
            } else {
587
                draw_col_separator_no_space(
588
                    renderer,
589
                    buffer,
590
                    buffer_msg_line_offset,
591
                    max_line_num_len + 1,
2✔
592
                );
593
            }
594
        } else {
595
            // Add spacing line, as shown:
596
            //   --> $DIR/file:54:15
597
            //    |
598
            // LL |         code
599
            //    |         ^^^^
600
            //    | (<- It prints *this* line)
601
            //   ::: $DIR/other_file.rs:15:5
602
            //    |
603
            // LL |     code
604
            //    |     ----
605
            draw_col_separator_no_space(
606
                renderer,
607
                buffer,
608
                buffer_msg_line_offset,
609
                max_line_num_len + 1,
1✔
610
            );
611

612
            buffer.puts(
1✔
613
                buffer_msg_line_offset + 1,
1✔
614
                max_line_num_len,
615
                renderer.decor_style.secondary_file_start(),
1✔
616
                ElementStyle::LineNumber,
617
            );
618
        }
619
    }
620

621
    // Contains the vertical lines' positions for active multiline annotations
622
    let mut multilines = Vec::new();
8✔
623

624
    // Get the left-side margin to remove it
625
    let mut whitespace_margin = usize::MAX;
6✔
626
    for line_info in annotated_lines {
14✔
627
        let leading_whitespace = line_info
11✔
628
            .line
629
            .chars()
630
            .take_while(|c| c.is_whitespace())
20✔
631
            .map(|c| {
12✔
632
                match c {
3✔
633
                    // Tabs are displayed as 4 spaces
634
                    '\t' => 4,
2✔
635
                    _ => 1,
3✔
636
                }
637
            })
638
            .sum();
639
        if line_info.line.chars().any(|c| !c.is_whitespace()) {
26✔
640
            whitespace_margin = min(whitespace_margin, leading_whitespace);
5✔
641
        }
642
    }
643
    if whitespace_margin == usize::MAX {
8✔
644
        whitespace_margin = 0;
2✔
645
    }
646

647
    // Left-most column any visible span points at.
648
    let mut span_left_margin = usize::MAX;
8✔
649
    for line_info in annotated_lines {
14✔
650
        for ann in &line_info.annotations {
17✔
651
            span_left_margin = min(span_left_margin, ann.start.display);
4✔
652
            span_left_margin = min(span_left_margin, ann.end.display);
8✔
653
        }
654
    }
655
    if span_left_margin == usize::MAX {
9✔
656
        span_left_margin = 0;
1✔
657
    }
658

659
    // Right-most column any visible span points at.
660
    let mut span_right_margin = 0;
5✔
661
    let mut label_right_margin = 0;
6✔
662
    let mut max_line_len = 0;
5✔
663
    for line_info in annotated_lines {
10✔
664
        max_line_len = max(max_line_len, str_width(line_info.line));
11✔
665
        for ann in &line_info.annotations {
11✔
666
            span_right_margin = max(span_right_margin, ann.start.display);
4✔
667
            span_right_margin = max(span_right_margin, ann.end.display);
7✔
668
            // FIXME: account for labels not in the same line
669
            let label_right = ann.label.as_ref().map_or(0, |l| str_width(l) + 1);
11✔
670
            label_right_margin = max(label_right_margin, ann.end.display + label_right);
4✔
671
        }
672
    }
673
    let width_offset = 3 + max_line_num_len;
7✔
674
    let code_offset = if multiline_depth == 0 {
11✔
675
        width_offset
5✔
676
    } else {
677
        width_offset + multiline_depth + 1
6✔
678
    };
679

680
    let column_width = renderer.term_width.saturating_sub(code_offset);
3✔
681

682
    let margin = Margin::new(
683
        whitespace_margin,
6✔
684
        span_left_margin,
5✔
685
        span_right_margin,
6✔
686
        label_right_margin,
5✔
687
        column_width,
688
        max_line_len,
6✔
689
    );
690

691
    // Next, output the annotate source for this file
692
    for annotated_line_idx in 0..annotated_lines.len() {
10✔
693
        let previous_buffer_line = buffer.num_lines();
9✔
694

695
        let depths = render_source_line(
696
            renderer,
697
            &annotated_lines[annotated_line_idx],
5✔
698
            buffer,
699
            width_offset,
700
            code_offset,
5✔
701
            max_line_num_len,
702
            margin,
703
            !is_cont && annotated_line_idx + 1 == annotated_lines.len(),
11✔
704
        );
705

706
        let mut to_add = BTreeMap::new();
6✔
707

708
        for (depth, style) in depths {
14✔
709
            if let Some(index) = multilines.iter().position(|(d, _)| d == &depth) {
15✔
710
                multilines.swap_remove(index);
6✔
711
            } else {
712
                to_add.insert(depth, style);
8✔
713
            }
714
        }
715

716
        // Set the multiline annotation vertical lines to the left of
717
        // the code in this line.
718
        for (depth, style) in &multilines {
5✔
719
            for line in previous_buffer_line..buffer.num_lines() {
6✔
720
                draw_multiline_line(renderer, buffer, line, width_offset, *depth, *style);
3✔
721
            }
722
        }
723
        // check to see if we need to print out or elide lines that come between
724
        // this annotated line and the next one.
725
        if annotated_line_idx < (annotated_lines.len() - 1) {
5✔
726
            let line_idx_delta = annotated_lines[annotated_line_idx + 1].line_index
13✔
727
                - annotated_lines[annotated_line_idx].line_index;
9✔
728
            match line_idx_delta.cmp(&2) {
8✔
729
                Ordering::Greater => {
730
                    let last_buffer_line_num = buffer.num_lines();
6✔
731

732
                    draw_line_separator(renderer, buffer, last_buffer_line_num, width_offset);
3✔
733

734
                    // Set the multiline annotation vertical lines on `...` bridging line.
735
                    for (depth, style) in &multilines {
3✔
736
                        draw_multiline_line(
737
                            renderer,
738
                            buffer,
739
                            last_buffer_line_num,
740
                            width_offset,
741
                            *depth,
3✔
742
                            *style,
743
                        );
744
                    }
745
                    if let Some(line) = annotated_lines.get(annotated_line_idx) {
3✔
746
                        for ann in &line.annotations {
3✔
747
                            if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type {
3✔
748
                                // In the case where we have elided the entire start of the
749
                                // multispan because those lines were empty, we still need
750
                                // to draw the `|`s across the `...`.
751
                                draw_multiline_line(
752
                                    renderer,
753
                                    buffer,
754
                                    last_buffer_line_num,
755
                                    width_offset,
756
                                    pos,
757
                                    if ann.is_primary() {
1✔
758
                                        ElementStyle::UnderlinePrimary
1✔
759
                                    } else {
760
                                        ElementStyle::UnderlineSecondary
×
761
                                    },
762
                                );
763
                            }
764
                        }
765
                    }
766
                }
767

768
                Ordering::Equal => {
769
                    let unannotated_line = sm
3✔
770
                        .get_line(annotated_lines[annotated_line_idx].line_index + 1)
6✔
771
                        .unwrap_or("");
772

773
                    let last_buffer_line_num = buffer.num_lines();
3✔
774

775
                    draw_line(
776
                        renderer,
777
                        buffer,
778
                        &normalize_whitespace(unannotated_line),
3✔
779
                        annotated_lines[annotated_line_idx + 1].line_index - 1,
3✔
780
                        last_buffer_line_num,
781
                        width_offset,
782
                        code_offset,
3✔
783
                        max_line_num_len,
784
                        margin,
785
                    );
786

787
                    for (depth, style) in &multilines {
3✔
788
                        draw_multiline_line(
789
                            renderer,
790
                            buffer,
791
                            last_buffer_line_num,
792
                            width_offset,
793
                            *depth,
1✔
794
                            *style,
795
                        );
796
                    }
797
                    if let Some(line) = annotated_lines.get(annotated_line_idx) {
3✔
798
                        for ann in &line.annotations {
3✔
799
                            if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type {
3✔
800
                                draw_multiline_line(
801
                                    renderer,
802
                                    buffer,
803
                                    last_buffer_line_num,
804
                                    width_offset,
805
                                    pos,
806
                                    if ann.is_primary() {
2✔
807
                                        ElementStyle::UnderlinePrimary
2✔
808
                                    } else {
809
                                        ElementStyle::UnderlineSecondary
×
810
                                    },
811
                                );
812
                            }
813
                        }
814
                    }
815
                }
816
                Ordering::Less => {}
817
            }
818
        }
819

820
        multilines.extend(to_add);
4✔
821
    }
822
}
823

824
#[allow(clippy::too_many_arguments)]
825
fn render_source_line(
6✔
826
    renderer: &Renderer,
827
    line_info: &AnnotatedLineInfo<'_>,
828
    buffer: &mut StyledBuffer,
829
    width_offset: usize,
830
    code_offset: usize,
831
    max_line_num_len: usize,
832
    margin: Margin,
833
    close_window: bool,
834
) -> Vec<(usize, ElementStyle)> {
835
    // Draw:
836
    //
837
    //   LL | ... code ...
838
    //      |     ^^-^ span label
839
    //      |       |
840
    //      |       secondary span label
841
    //
842
    //   ^^ ^ ^^^ ^^^^ ^^^ we don't care about code too far to the right of a span, we trim it
843
    //   |  | |   |
844
    //   |  | |   actual code found in your source code and the spans we use to mark it
845
    //   |  | when there's too much wasted space to the left, trim it
846
    //   |  vertical divider between the column number and the code
847
    //   column number
848

849
    let source_string = normalize_whitespace(line_info.line);
6✔
850

851
    let line_offset = buffer.num_lines();
12✔
852

853
    let left = draw_line(
854
        renderer,
855
        buffer,
856
        &source_string,
6✔
857
        line_info.line_index,
7✔
858
        line_offset,
859
        width_offset,
860
        code_offset,
861
        max_line_num_len,
862
        margin,
863
    );
864

865
    // If there are no annotations, we are done
866
    if line_info.annotations.is_empty() {
3✔
867
        // `close_window` normally gets handled later, but we are early
868
        // returning, so it needs to be handled here
869
        if close_window {
2✔
870
            draw_col_separator_end(renderer, buffer, line_offset + 1, width_offset - 2);
2✔
871
        }
872
        return vec![];
4✔
873
    }
874

875
    // Special case when there's only one annotation involved, it is the start of a multiline
876
    // span and there's no text at the beginning of the code line. Instead of doing the whole
877
    // graph:
878
    //
879
    // 2 |   fn foo() {
880
    //   |  _^
881
    // 3 | |
882
    // 4 | | }
883
    //   | |_^ test
884
    //
885
    // we simplify the output to:
886
    //
887
    // 2 | / fn foo() {
888
    // 3 | |
889
    // 4 | | }
890
    //   | |_^ test
891
    let mut buffer_ops = vec![];
8✔
892
    let mut annotations = vec![];
3✔
893
    let mut short_start = true;
7✔
894
    for ann in &line_info.annotations {
11✔
895
        if let LineAnnotationType::MultilineStart(depth) = ann.annotation_type {
8✔
896
            if source_string
7✔
897
                .chars()
898
                .take(ann.start.display)
3✔
899
                .all(char::is_whitespace)
3✔
900
            {
901
                let uline = renderer.decor_style.underline(ann.is_primary());
3✔
902
                let chr = uline.multiline_whole_line;
3✔
903
                annotations.push((depth, uline.style));
3✔
904
                buffer_ops.push((line_offset, width_offset + depth - 1, chr, uline.style));
3✔
905
            } else {
906
                short_start = false;
3✔
907
                break;
908
            }
909
        } else if let LineAnnotationType::MultilineLine(_) = ann.annotation_type {
7✔
910
        } else {
911
            short_start = false;
4✔
912
            break;
6✔
913
        }
914
    }
915
    if short_start {
6✔
916
        for (y, x, c, s) in buffer_ops {
12✔
917
            buffer.putc(y, x, c, s);
9✔
918
        }
919
        return annotations;
3✔
920
    }
921

922
    // We want to display like this:
923
    //
924
    //      vec.push(vec.pop().unwrap());
925
    //      ---      ^^^               - previous borrow ends here
926
    //      |        |
927
    //      |        error occurs here
928
    //      previous borrow of `vec` occurs here
929
    //
930
    // But there are some weird edge cases to be aware of:
931
    //
932
    //      vec.push(vec.pop().unwrap());
933
    //      --------                    - previous borrow ends here
934
    //      ||
935
    //      |this makes no sense
936
    //      previous borrow of `vec` occurs here
937
    //
938
    // For this reason, we group the lines into "highlight lines"
939
    // and "annotations lines", where the highlight lines have the `^`.
940

941
    // Sort the annotations by (start, end col)
942
    // The labels are reversed, sort and then reversed again.
943
    // Consider a list of annotations (A1, A2, C1, C2, B1, B2) where
944
    // the letter signifies the span. Here we are only sorting by the
945
    // span and hence, the order of the elements with the same span will
946
    // not change. On reversing the ordering (|a, b| but b.cmp(a)), you get
947
    // (C1, C2, B1, B2, A1, A2). All the elements with the same span are
948
    // still ordered first to last, but all the elements with different
949
    // spans are ordered by their spans in last to first order. Last to
950
    // first order is important, because the jiggly lines and | are on
951
    // the left, so the rightmost span needs to be rendered first,
952
    // otherwise the lines would end up needing to go over a message.
953

954
    let mut annotations = line_info.annotations.clone();
7✔
955
    annotations.sort_by_key(|a| Reverse((a.start.display, a.start.char)));
16✔
956

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

1046
                // This annotation needs a new line in the output.
1047
                p += 1;
4✔
1048
                break;
1049
            }
1050
        }
1051
        annotations_position.push((p, annotation));
4✔
1052
        for (j, next) in annotations.iter().enumerate() {
3✔
1053
            if j > i {
7✔
1054
                let l = next.label.as_ref().map_or(0, |label| label.len() + 2);
9✔
1055
                if (overlaps(next, annotation, l) // Do not allow two labels to be in the same
3✔
1056
                        // line if they overlap including padding, to
1057
                        // avoid situations like:
1058
                        //
1059
                        //      fn foo(x: u32) {
1060
                        //      -------^------
1061
                        //      |      |
1062
                        //      fn_spanx_span
1063
                        //
1064
                        && annotation.has_label()    // Both labels must have some text, otherwise
3✔
1065
                        && next.has_label())         // they are not overlapping.
3✔
1066
                        // Do not add a new line if this annotation
1067
                        // or the next are vertical line placeholders.
1068
                        || (annotation.takes_space() // If either this or the next annotation is
6✔
1069
                        && next.has_label())     // multiline start/end, move it to a new line
2✔
1070
                        || (annotation.has_label()   // so as not to overlap the horizontal lines.
6✔
1071
                        && next.takes_space())
2✔
1072
                        || (annotation.takes_space() && next.takes_space())
8✔
1073
                        || (overlaps(next, annotation, l)
6✔
1074
                        && (next.end.display, next.end.char) <= (annotation.end.display, annotation.end.char)
1✔
1075
                        && next.has_label()
1✔
1076
                        && p == 0)
1✔
1077
                // Avoid #42595.
1078
                {
1079
                    // This annotation needs a new line in the output.
1080
                    p += 1;
6✔
1081
                    break;
1082
                }
1083
            }
1084
        }
1085
        line_len = max(line_len, p);
10✔
1086
    }
1087

1088
    if line_len != 0 {
9✔
1089
        line_len += 1;
3✔
1090
    }
1091

1092
    // If there are no annotations or the only annotations on this line are
1093
    // MultilineLine, then there's only code being shown, stop processing.
1094
    if line_info.annotations.iter().all(LineAnnotation::is_line) {
11✔
1095
        return vec![];
×
1096
    }
1097

1098
    if annotations_position
19✔
1099
        .iter()
1100
        .all(|(_, ann)| matches!(ann.annotation_type, LineAnnotationType::MultilineStart(_)))
20✔
1101
    {
1102
        if let Some(max_pos) = annotations_position.iter().map(|(pos, _)| *pos).max() {
18✔
1103
            // Special case the following, so that we minimize overlapping multiline spans.
1104
            //
1105
            // 3 │       X0 Y0 Z0
1106
            //   │ ┏━━━━━┛  │  │     < We are writing these lines
1107
            //   │ ┃┌───────┘  │     < by reverting the "depth" of
1108
            //   │ ┃│┌─────────┘     < their multiline spans.
1109
            // 4 │ ┃││   X1 Y1 Z1
1110
            // 5 │ ┃││   X2 Y2 Z2
1111
            //   │ ┃│└────╿──│──┘ `Z` label
1112
            //   │ ┃└─────│──┤
1113
            //   │ ┗━━━━━━┥  `Y` is a good letter too
1114
            //   ╰╴       `X` is a good letter
1115
            for (pos, _) in &mut annotations_position {
8✔
1116
                *pos = max_pos - *pos;
8✔
1117
            }
1118
            // We know then that we don't need an additional line for the span label, saving us
1119
            // one line of vertical space.
1120
            line_len = line_len.saturating_sub(1);
4✔
1121
        }
1122
    }
1123

1124
    // Write the column separator.
1125
    //
1126
    // After this we will have:
1127
    //
1128
    // 2 |   fn foo() {
1129
    //   |
1130
    //   |
1131
    //   |
1132
    // 3 |
1133
    // 4 |   }
1134
    //   |
1135
    for pos in 0..=line_len {
11✔
1136
        draw_col_separator_no_space(renderer, buffer, line_offset + pos + 1, width_offset - 2);
9✔
1137
    }
1138
    if close_window {
4✔
1139
        draw_col_separator_end(
1140
            renderer,
1141
            buffer,
1142
            line_offset + line_len + 1,
4✔
1143
            width_offset - 2,
4✔
1144
        );
1145
    }
1146
    // Write the horizontal lines for multiline annotations
1147
    // (only the first and last lines need this).
1148
    //
1149
    // After this we will have:
1150
    //
1151
    // 2 |   fn foo() {
1152
    //   |  __________
1153
    //   |
1154
    //   |
1155
    // 3 |
1156
    // 4 |   }
1157
    //   |  _
1158
    for &(pos, annotation) in &annotations_position {
8✔
1159
        let underline = renderer.decor_style.underline(annotation.is_primary());
9✔
1160
        let pos = pos + 1;
7✔
1161
        match annotation.annotation_type {
10✔
1162
            LineAnnotationType::MultilineStart(depth) | LineAnnotationType::MultilineEnd(depth) => {
8✔
1163
                draw_range(
1164
                    buffer,
1165
                    underline.multiline_horizontal,
4✔
1166
                    line_offset + pos,
4✔
1167
                    width_offset + depth,
4✔
1168
                    (code_offset + annotation.start.display).saturating_sub(left),
8✔
1169
                    underline.style,
1170
                );
1171
            }
1172
            _ if annotation.highlight_source => {
5✔
1173
                buffer.set_style_range(
1✔
1174
                    line_offset,
1175
                    (code_offset + annotation.start.display).saturating_sub(left),
1✔
1176
                    (code_offset + annotation.end.display).saturating_sub(left),
1✔
1177
                    underline.style,
1✔
1178
                    annotation.is_primary(),
1✔
1179
                );
1180
            }
1181
            _ => {}
1182
        }
1183
    }
1184

1185
    // Write the vertical lines for labels that are on a different line as the underline.
1186
    //
1187
    // After this we will have:
1188
    //
1189
    // 2 |   fn foo() {
1190
    //   |  __________
1191
    //   | |    |
1192
    //   | |
1193
    // 3 | |
1194
    // 4 | | }
1195
    //   | |_
1196
    for &(pos, annotation) in &annotations_position {
4✔
1197
        let underline = renderer.decor_style.underline(annotation.is_primary());
9✔
1198
        let pos = pos + 1;
5✔
1199

1200
        if pos > 1 && (annotation.has_label() || annotation.takes_space()) {
9✔
1201
            for p in line_offset + 1..=line_offset + pos {
6✔
1202
                buffer.putc(
3✔
1203
                    p,
1204
                    (code_offset + annotation.start.display).saturating_sub(left),
6✔
1205
                    match annotation.annotation_type {
3✔
1206
                        LineAnnotationType::MultilineLine(_) => underline.multiline_vertical,
×
1207
                        _ => underline.vertical_text_line,
3✔
1208
                    },
1209
                    underline.style,
1210
                );
1211
            }
1212
            if let LineAnnotationType::MultilineStart(_) = annotation.annotation_type {
3✔
1213
                buffer.putc(
3✔
1214
                    line_offset + pos,
2✔
1215
                    (code_offset + annotation.start.display).saturating_sub(left),
5✔
1216
                    underline.bottom_right,
3✔
1217
                    underline.style,
1218
                );
1219
            }
1220
            if matches!(
3✔
1221
                annotation.annotation_type,
1222
                LineAnnotationType::MultilineEnd(_)
1223
            ) && annotation.has_label()
2✔
1224
            {
1225
                buffer.putc(
2✔
1226
                    line_offset + pos,
2✔
1227
                    (code_offset + annotation.start.display).saturating_sub(left),
4✔
1228
                    underline.multiline_bottom_right_with_text,
2✔
1229
                    underline.style,
1230
                );
1231
            }
1232
        }
1233
        match annotation.annotation_type {
5✔
1234
            LineAnnotationType::MultilineStart(depth) => {
4✔
1235
                buffer.putc(
5✔
1236
                    line_offset + pos,
4✔
1237
                    width_offset + depth - 1,
10✔
1238
                    underline.top_left,
5✔
1239
                    underline.style,
1240
                );
1241
                for p in line_offset + pos + 1..line_offset + line_len + 2 {
5✔
1242
                    buffer.putc(
4✔
1243
                        p,
1244
                        width_offset + depth - 1,
2✔
1245
                        underline.multiline_vertical,
2✔
1246
                        underline.style,
1247
                    );
1248
                }
1249
            }
1250
            LineAnnotationType::MultilineEnd(depth) => {
4✔
1251
                for p in line_offset..line_offset + pos {
8✔
1252
                    buffer.putc(
7✔
1253
                        p,
1254
                        width_offset + depth - 1,
8✔
1255
                        underline.multiline_vertical,
4✔
1256
                        underline.style,
1257
                    );
1258
                }
1259
                buffer.putc(
6✔
1260
                    line_offset + pos,
3✔
1261
                    width_offset + depth - 1,
6✔
1262
                    underline.bottom_left,
4✔
1263
                    underline.style,
1264
                );
1265
            }
1266
            _ => (),
1267
        }
1268
    }
1269

1270
    // Write the labels on the annotations that actually have a label.
1271
    //
1272
    // After this we will have:
1273
    //
1274
    // 2 |   fn foo() {
1275
    //   |  __________
1276
    //   |      |
1277
    //   |      something about `foo`
1278
    // 3 |
1279
    // 4 |   }
1280
    //   |  _  test
1281
    for &(pos, annotation) in &annotations_position {
4✔
1282
        let style = if annotation.is_primary() {
13✔
1283
            ElementStyle::LabelPrimary
4✔
1284
        } else {
1285
            ElementStyle::LabelSecondary
3✔
1286
        };
1287
        let (pos, col) = if pos == 0 {
8✔
1288
            if annotation.end.display == 0 {
13✔
1289
                (pos + 1, (annotation.end.display + 2).saturating_sub(left))
6✔
1290
            } else {
1291
                (pos + 1, (annotation.end.display + 1).saturating_sub(left))
9✔
1292
            }
1293
        } else {
1294
            (pos + 2, annotation.start.display.saturating_sub(left))
6✔
1295
        };
1296
        if let Some(label) = &annotation.label {
9✔
1297
            buffer.puts(line_offset + pos, code_offset + col, label, style);
4✔
1298
        }
1299
    }
1300

1301
    // Sort from biggest span to smallest span so that smaller spans are
1302
    // represented in the output:
1303
    //
1304
    // x | fn foo()
1305
    //   | ^^^---^^
1306
    //   | |  |
1307
    //   | |  something about `foo`
1308
    //   | something about `fn foo()`
1309
    annotations_position.sort_by_key(|(_, ann)| {
10✔
1310
        // Decreasing order. When annotations share the same length, prefer `Primary`.
1311
        (Reverse(ann.len()), ann.is_primary())
3✔
1312
    });
1313

1314
    // Write the underlines.
1315
    //
1316
    // After this we will have:
1317
    //
1318
    // 2 |   fn foo() {
1319
    //   |  ____-_____^
1320
    //   |      |
1321
    //   |      something about `foo`
1322
    // 3 |
1323
    // 4 |   }
1324
    //   |  _^  test
1325
    for &(pos, annotation) in &annotations_position {
7✔
1326
        let uline = renderer.decor_style.underline(annotation.is_primary());
15✔
1327
        for p in annotation.start.display..annotation.end.display {
8✔
1328
            // The default span label underline.
1329
            buffer.putc(
6✔
1330
                line_offset + 1,
7✔
1331
                (code_offset + p).saturating_sub(left),
11✔
1332
                uline.underline,
5✔
1333
                uline.style,
1334
            );
1335
        }
1336

1337
        if pos == 0
4✔
1338
            && matches!(
4✔
1339
                annotation.annotation_type,
7✔
1340
                LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1341
            )
1342
        {
1343
            // The beginning of a multiline span with its leftward moving line on the same line.
1344
            buffer.putc(
4✔
1345
                line_offset + 1,
4✔
1346
                (code_offset + annotation.start.display).saturating_sub(left),
8✔
1347
                match annotation.annotation_type {
4✔
1348
                    LineAnnotationType::MultilineStart(_) => uline.top_right_flat,
4✔
1349
                    LineAnnotationType::MultilineEnd(_) => uline.multiline_end_same_line,
3✔
1350
                    _ => panic!("unexpected annotation type: {annotation:?}"),
×
1351
                },
1352
                uline.style,
1353
            );
1354
        } else if pos != 0
6✔
1355
            && matches!(
3✔
1356
                annotation.annotation_type,
3✔
1357
                LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1358
            )
1359
        {
1360
            // The beginning of a multiline span with its leftward moving line on another line,
1361
            // so we start going down first.
1362
            buffer.putc(
3✔
1363
                line_offset + 1,
2✔
1364
                (code_offset + annotation.start.display).saturating_sub(left),
4✔
1365
                match annotation.annotation_type {
2✔
1366
                    LineAnnotationType::MultilineStart(_) => uline.multiline_start_down,
3✔
1367
                    LineAnnotationType::MultilineEnd(_) => uline.multiline_end_up,
2✔
1368
                    _ => panic!("unexpected annotation type: {annotation:?}"),
×
1369
                },
1370
                uline.style,
1371
            );
1372
        } else if pos != 0 && annotation.has_label() {
8✔
1373
            // The beginning of a span label with an actual label, we'll point down.
1374
            buffer.putc(
3✔
1375
                line_offset + 1,
3✔
1376
                (code_offset + annotation.start.display).saturating_sub(left),
6✔
1377
                uline.label_start,
3✔
1378
                uline.style,
1379
            );
1380
        }
1381
    }
1382

1383
    // We look for individual *long* spans, and we trim the *middle*, so that we render
1384
    // LL | ...= [0, 0, 0, ..., 0, 0];
1385
    //    |      ^^^^^^^^^^...^^^^^^^ expected `&[u8]`, found `[{integer}; 1680]`
1386
    for (i, (_pos, annotation)) in annotations_position.iter().enumerate() {
5✔
1387
        // Skip cases where multiple spans overlap eachother.
1388
        if overlap[i] {
12✔
1389
            continue;
1390
        };
1391
        let LineAnnotationType::Singleline = annotation.annotation_type else {
6✔
1392
            continue;
1393
        };
1394
        let width = annotation.end.display - annotation.start.display;
4✔
1395
        if width > margin.term_width * 2 && width > 10 {
9✔
1396
            // If the terminal is *too* small, we keep at least a tiny bit of the span for
1397
            // display.
1398
            let pad = max(margin.term_width / 3, 5);
1✔
1399
            // Code line
1400
            buffer.replace(
1✔
1401
                line_offset,
1402
                annotation.start.display + pad,
1✔
1403
                annotation.end.display - pad,
1✔
1404
                renderer.decor_style.margin(),
1✔
1405
            );
1406
            // Underline line
1407
            buffer.replace(
1✔
1408
                line_offset + 1,
1✔
1409
                annotation.start.display + pad,
2✔
1410
                annotation.end.display - pad,
1✔
1411
                renderer.decor_style.margin(),
1✔
1412
            );
1413
        }
1414
    }
1415
    annotations_position
6✔
1416
        .iter()
1417
        .filter_map(|&(_, annotation)| match annotation.annotation_type {
24✔
1418
            LineAnnotationType::MultilineStart(p) | LineAnnotationType::MultilineEnd(p) => {
8✔
1419
                let style = if annotation.is_primary() {
8✔
1420
                    ElementStyle::LabelPrimary
4✔
1421
                } else {
1422
                    ElementStyle::LabelSecondary
3✔
1423
                };
1424
                Some((p, style))
4✔
1425
            }
1426
            _ => None,
6✔
1427
        })
1428
        .collect::<Vec<_>>()
1429
}
1430

1431
#[allow(clippy::too_many_arguments)]
1432
fn emit_suggestion_default(
4✔
1433
    renderer: &Renderer,
1434
    buffer: &mut StyledBuffer,
1435
    suggestion: &Snippet<'_, Patch<'_>>,
1436
    spliced_lines: SplicedLines<'_>,
1437
    show_code_change: DisplaySuggestion,
1438
    max_line_num_len: usize,
1439
    sm: &SourceMap<'_>,
1440
    primary_path: Option<&Cow<'_, str>>,
1441
    matches_previous_suggestion: bool,
1442
    is_first: bool,
1443
    is_cont: bool,
1444
) {
1445
    let buffer_offset = buffer.num_lines();
8✔
1446
    let mut row_num = buffer_offset + usize::from(!matches_previous_suggestion);
4✔
1447
    let (complete, parts, highlights) = spliced_lines;
5✔
1448
    let is_multiline = complete.lines().count() > 1;
8✔
1449

1450
    if matches_previous_suggestion {
4✔
1451
        buffer.puts(
4✔
1452
            row_num - 1,
4✔
1453
            max_line_num_len + 1,
4✔
1454
            renderer.decor_style.multi_suggestion_separator(),
4✔
1455
            ElementStyle::LineNumber,
1456
        );
1457
    } else {
1458
        draw_col_separator_start(renderer, buffer, row_num - 1, max_line_num_len + 1);
7✔
1459
    }
1460
    if suggestion.path.as_ref() != primary_path {
7✔
1461
        if let Some(path) = suggestion.path.as_ref() {
1✔
1462
            if !matches_previous_suggestion {
×
1463
                let (loc, _) = sm.span_to_locations(parts[0].span.clone());
×
1464
                // --> file.rs:line:col
1465
                //  |
1466
                let arrow = renderer.decor_style.file_start(is_first, false);
×
1467
                buffer.puts(row_num - 1, 0, arrow, ElementStyle::LineNumber);
×
1468
                let message = format!("{}:{}:{}", path, loc.line, loc.char + 1);
×
1469
                let col = usize::max(max_line_num_len + 1, arrow.len());
×
1470
                buffer.puts(row_num - 1, col, &message, ElementStyle::LineAndColumn);
×
1471
                for _ in 0..max_line_num_len {
×
1472
                    buffer.prepend(row_num - 1, " ", ElementStyle::NoStyle);
×
1473
                }
1474
                draw_col_separator_no_space(renderer, buffer, row_num, max_line_num_len + 1);
×
1475
                row_num += 1;
×
1476
            }
1477
        }
1478
    }
1479

1480
    if let DisplaySuggestion::Diff = show_code_change {
6✔
1481
        row_num += 1;
8✔
1482
    }
1483

1484
    let lo = parts.iter().map(|p| p.span.start).min().unwrap();
16✔
1485
    let hi = parts.iter().map(|p| p.span.end).max().unwrap();
12✔
1486

1487
    let file_lines = sm.span_to_lines(lo..hi);
4✔
1488
    let (line_start, line_end) = if suggestion.fold {
7✔
1489
        // We use the original span to get original line_start
1490
        sm.span_to_locations(parts[0].original_span.clone())
8✔
1491
    } else {
1492
        sm.span_to_locations(0..sm.source.len())
2✔
1493
    };
1494
    let mut lines = complete.lines();
8✔
1495
    if lines.clone().next().is_none() {
5✔
1496
        // Account for a suggestion to completely remove a line(s) with whitespace (#94192).
1497
        for line in line_start.line..=line_end.line {
1✔
1498
            buffer.puts(
1✔
1499
                row_num - 1 + line - line_start.line,
2✔
1500
                0,
1501
                &maybe_anonymized(renderer, line, max_line_num_len),
2✔
1502
                ElementStyle::LineNumber,
1503
            );
1504
            buffer.puts(
1✔
1505
                row_num - 1 + line - line_start.line,
1✔
1506
                max_line_num_len + 1,
1✔
1507
                "- ",
1508
                ElementStyle::Removal,
1509
            );
1510
            buffer.puts(
1✔
1511
                row_num - 1 + line - line_start.line,
1✔
1512
                max_line_num_len + 3,
1✔
1513
                &normalize_whitespace(sm.get_line(line).unwrap()),
2✔
1514
                ElementStyle::Removal,
1515
            );
1516
        }
1517
        row_num += line_end.line - line_start.line;
1✔
1518
    }
1519
    let mut unhighlighted_lines = Vec::new();
4✔
1520
    for (line_pos, (line, highlight_parts)) in lines.by_ref().zip(highlights).enumerate() {
11✔
1521
        // Remember lines that are not highlighted to hide them if needed
1522
        if highlight_parts.is_empty() && suggestion.fold {
9✔
1523
            unhighlighted_lines.push((line_pos, line));
1✔
1524
            continue;
1525
        }
1526

1527
        match unhighlighted_lines.len() {
9✔
1528
            0 => (),
1529
            // Since we show first line, "..." line and last line,
1530
            // There is no reason to hide if there are 3 or less lines
1531
            // (because then we just replace a line with ... which is
1532
            // not helpful)
1533
            n if n <= 3 => unhighlighted_lines.drain(..).for_each(|(p, l)| {
5✔
1534
                draw_code_line(
1✔
1535
                    renderer,
1✔
1536
                    buffer,
1✔
1537
                    &mut row_num,
1✔
1538
                    &[],
1539
                    p + line_start.line,
1✔
1540
                    l,
1541
                    show_code_change,
1✔
1542
                    max_line_num_len,
1✔
1543
                    &file_lines,
1✔
1544
                    is_multiline,
1✔
1545
                );
1546
            }),
1547
            // Print first unhighlighted line, "..." and last unhighlighted line, like so:
1548
            //
1549
            // LL | this line was highlighted
1550
            // LL | this line is just for context
1551
            // ...
1552
            // LL | this line is just for context
1553
            // LL | this line was highlighted
1554
            _ => {
1555
                let last_line = unhighlighted_lines.pop();
1✔
1556
                let first_line = unhighlighted_lines.drain(..).next();
1✔
1557

1558
                if let Some((p, l)) = first_line {
1✔
1559
                    draw_code_line(
1560
                        renderer,
1561
                        buffer,
1562
                        &mut row_num,
1563
                        &[],
1564
                        p + line_start.line,
1✔
1565
                        l,
1566
                        show_code_change,
1567
                        max_line_num_len,
1568
                        &file_lines,
1✔
1569
                        is_multiline,
1570
                    );
1571
                }
1572

1573
                let placeholder = renderer.decor_style.margin();
2✔
1574
                let padding = str_width(placeholder);
1✔
1575
                buffer.puts(
1✔
1576
                    row_num,
1✔
1577
                    max_line_num_len.saturating_sub(padding),
1✔
1578
                    placeholder,
1579
                    ElementStyle::LineNumber,
1580
                );
1581
                row_num += 1;
1✔
1582

1583
                if let Some((p, l)) = last_line {
2✔
1584
                    draw_code_line(
1585
                        renderer,
1586
                        buffer,
1587
                        &mut row_num,
1588
                        &[],
1589
                        p + line_start.line,
1✔
1590
                        l,
1591
                        show_code_change,
1592
                        max_line_num_len,
1593
                        &file_lines,
1✔
1594
                        is_multiline,
1595
                    );
1596
                }
1597
            }
1598
        }
1599
        draw_code_line(
1600
            renderer,
1601
            buffer,
1602
            &mut row_num,
1603
            &highlight_parts,
5✔
1604
            line_pos + line_start.line,
3✔
1605
            line,
1606
            show_code_change,
1607
            max_line_num_len,
1608
            &file_lines,
5✔
1609
            is_multiline,
1610
        );
1611
    }
1612

1613
    // This offset and the ones below need to be signed to account for replacement code
1614
    // that is shorter than the original code.
1615
    let mut offsets: Vec<(usize, isize)> = Vec::new();
5✔
1616
    // Only show an underline in the suggestions if the suggestion is not the
1617
    // entirety of the code being shown and the displayed code is not multiline.
1618
    if let DisplaySuggestion::Diff | DisplaySuggestion::Underline | DisplaySuggestion::Add =
4✔
1619
        show_code_change
1620
    {
1621
        let mut prev_lines: Option<(usize, usize)> = None;
6✔
1622
        for part in parts {
16✔
1623
            let snippet = sm.span_to_snippet(part.span.clone()).unwrap_or_default();
12✔
1624
            let (span_start, span_end) = sm.span_to_locations(part.span.clone());
6✔
1625
            let span_start_pos = span_start.display;
6✔
1626
            let span_end_pos = span_end.display;
6✔
1627

1628
            // If this addition is _only_ whitespace, then don't trim it,
1629
            // or else we're just not rendering anything.
1630
            let is_whitespace_addition = part.replacement.trim().is_empty();
6✔
1631

1632
            // Do not underline the leading...
1633
            let start = if is_whitespace_addition {
11✔
1634
                0
5✔
1635
            } else {
1636
                part.replacement
8✔
1637
                    .len()
1638
                    .saturating_sub(part.replacement.trim_start().len())
4✔
1639
            };
1640
            // ...or trailing spaces. Account for substitutions containing unicode
1641
            // characters.
1642
            let sub_len: usize = str_width(if is_whitespace_addition {
17✔
1643
                &part.replacement
10✔
1644
            } else {
1645
                part.replacement.trim()
8✔
1646
            });
1647

1648
            let offset: isize = offsets
13✔
1649
                .iter()
1650
                .filter_map(|(start, v)| {
13✔
1651
                    if span_start_pos < *start {
6✔
1652
                        None
1✔
1653
                    } else {
1654
                        Some(v)
3✔
1655
                    }
1656
                })
1657
                .sum();
1658
            let underline_start = (span_start_pos + start) as isize + offset;
7✔
1659
            let underline_end = (span_start_pos + start + sub_len) as isize + offset;
14✔
1660
            assert!(underline_start >= 0 && underline_end >= 0);
7✔
1661
            let padding: usize = max_line_num_len + 3;
7✔
1662
            for p in underline_start..underline_end {
14✔
1663
                if matches!(show_code_change, DisplaySuggestion::Underline) {
3✔
1664
                    // If this is a replacement, underline with `~`, if this is an addition
1665
                    // underline with `+`.
1666
                    buffer.putc(
2✔
1667
                        row_num,
2✔
1668
                        (padding as isize + p) as usize,
2✔
1669
                        if part.is_addition(sm) {
6✔
1670
                            '+'
2✔
1671
                        } else {
1672
                            renderer.decor_style.diff()
×
1673
                        },
1674
                        ElementStyle::Addition,
1675
                    );
1676
                }
1677
            }
1678
            if let DisplaySuggestion::Diff = show_code_change {
9✔
1679
                // Colorize removal with red in diff format.
1680

1681
                // Below, there's some tricky buffer indexing going on. `row_num` at this
1682
                // point corresponds to:
1683
                //
1684
                //    |
1685
                // LL | CODE
1686
                //    | ++++  <- `row_num`
1687
                //
1688
                // in the buffer. When we have a diff format output, we end up with
1689
                //
1690
                //    |
1691
                // LL - OLDER   <- row_num - 2
1692
                // LL + NEWER
1693
                //    |         <- row_num
1694
                //
1695
                // The `row_num - 2` is to select the buffer line that has the "old version
1696
                // of the diff" at that point. When the removal is a single line, `i` is
1697
                // `0`, `newlines` is `1` so `(newlines - i - 1)` ends up being `0`, so row
1698
                // points at `LL - OLDER`. When the removal corresponds to multiple lines,
1699
                // we end up with `newlines > 1` and `i` being `0..newlines - 1`.
1700
                //
1701
                //    |
1702
                // LL - OLDER   <- row_num - 2 - (newlines - last_i - 1)
1703
                // LL - CODE
1704
                // LL - BEING
1705
                // LL - REMOVED <- row_num - 2 - (newlines - first_i - 1)
1706
                // LL + NEWER
1707
                //    |         <- row_num
1708

1709
                let newlines = snippet.lines().count();
12✔
1710
                if newlines > 0 && row_num > newlines {
12✔
1711
                    let offset = match prev_lines {
6✔
1712
                        Some((start, end)) => {
3✔
1713
                            file_lines.len().saturating_sub(end.saturating_sub(start))
7✔
1714
                        }
1715
                        None => file_lines.len(),
6✔
1716
                    };
1717
                    // Account for removals where the part being removed spans multiple
1718
                    // lines.
1719
                    // FIXME: We check the number of rows because in some cases, like in
1720
                    // `tests/ui/lint/invalid-nan-comparison-suggestion.rs`, the rendered
1721
                    // suggestion will only show the first line of code being replaced. The
1722
                    // proper way of doing this would be to change the suggestion rendering
1723
                    // logic to show the whole prior snippet, but the current output is not
1724
                    // too bad to begin with, so we side-step that issue here.
1725
                    for (i, line) in snippet.lines().enumerate() {
12✔
1726
                        let tabs: usize = line
3✔
1727
                            .chars()
1728
                            .take(span_start.char)
6✔
1729
                            .map(|ch| match ch {
16✔
1730
                                '\t' => 3,
1✔
1731
                                _ => 0,
5✔
1732
                            })
1733
                            .sum();
1734
                        let line = normalize_whitespace(line);
3✔
1735
                        // Going lower than buffer_offset (+ 1) would mean
1736
                        // overwriting existing content in the buffer
1737
                        let min_row = buffer_offset + usize::from(!matches_previous_suggestion);
10✔
1738
                        let row = (row_num - 2 - (offset - i - 1)).max(min_row);
13✔
1739
                        // On the first line, we highlight between the start of the part
1740
                        // span, and the end of that line.
1741
                        // On the last line, we highlight between the start of the line, and
1742
                        // the column of the part span end.
1743
                        // On all others, we highlight the whole line.
1744
                        let start = if i == 0 {
15✔
1745
                            (padding as isize + (span_start.char + tabs) as isize) as usize
12✔
1746
                        } else {
1747
                            padding
5✔
1748
                        };
1749
                        let end = if i == 0 {
15✔
1750
                            (padding as isize
18✔
1751
                                + (span_start.char + tabs) as isize
9✔
1752
                                + line.chars().count() as isize)
18✔
1753
                                as usize
1754
                        } else if i == newlines - 1 {
15✔
1755
                            (padding as isize + (span_end.char + tabs) as isize) as usize
7✔
1756
                        } else {
1757
                            (padding as isize + line.chars().count() as isize) as usize
7✔
1758
                        };
1759
                        buffer.set_style_range(row, start, end, ElementStyle::Removal, true);
6✔
1760
                    }
1761
                } else {
1762
                    let tabs: usize = snippet
3✔
1763
                        .chars()
1764
                        .take(span_start.char)
3✔
1765
                        .map(|ch| match ch {
3✔
1766
                            '\t' => 3,
×
1767
                            _ => 0,
×
1768
                        })
1769
                        .sum();
1770
                    // The removed code fits all in one line.
1771
                    buffer.set_style_range(
6✔
1772
                        row_num - 2,
3✔
1773
                        (padding as isize + (span_start.char + tabs) as isize) as usize,
6✔
1774
                        (padding as isize + (span_end.char + tabs) as isize) as usize,
6✔
1775
                        ElementStyle::Removal,
1776
                        true,
1777
                    );
1778
                }
1779
                prev_lines = Some((span_start.line, span_end.line));
3✔
1780
            }
1781

1782
            // length of the code after substitution
1783
            let full_sub_len = str_width(&part.replacement) as isize;
8✔
1784

1785
            // length of the code to be substituted
1786
            let snippet_len = span_end_pos as isize - span_start_pos as isize;
5✔
1787
            // For multiple substitutions, use the position *after* the previous
1788
            // substitutions have happened, only when further substitutions are
1789
            // located strictly after.
1790
            offsets.push((span_end_pos, full_sub_len - snippet_len));
9✔
1791
        }
1792
        row_num += 1;
5✔
1793
    }
1794

1795
    // if we elided some lines, add an ellipsis
1796
    if lines.next().is_some() {
10✔
1797
        let placeholder = renderer.decor_style.margin();
×
1798
        let padding = str_width(placeholder);
×
1799
        buffer.puts(
×
1800
            row_num,
×
1801
            max_line_num_len.saturating_sub(padding),
×
1802
            placeholder,
1803
            ElementStyle::LineNumber,
1804
        );
1805
    } else {
1806
        let row = match show_code_change {
5✔
1807
            DisplaySuggestion::Diff | DisplaySuggestion::Add | DisplaySuggestion::Underline => {
1808
                row_num - 1
12✔
1809
            }
1810
            DisplaySuggestion::None => row_num,
2✔
1811
        };
1812
        if is_cont {
7✔
1813
            draw_col_separator_no_space(renderer, buffer, row, max_line_num_len + 1);
6✔
1814
        } else {
1815
            draw_col_separator_end(renderer, buffer, row, max_line_num_len + 1);
9✔
1816
        }
1817
    }
1818
}
1819

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

1981
    // Colorize addition/replacements with green.
1982
    for &SubstitutionHighlight { start, end } in highlight_parts {
8✔
1983
        // This is a no-op for empty ranges
1984
        if start != end {
4✔
1985
            // Account for tabs when highlighting (#87972).
1986
            let tabs: usize = line_to_add
3✔
1987
                .chars()
1988
                .take(start)
3✔
1989
                .map(|ch| match ch {
10✔
1990
                    '\t' => 3,
×
1991
                    _ => 0,
3✔
1992
                })
1993
                .sum();
1994
            buffer.set_style_range(
3✔
1995
                *row_num,
4✔
1996
                max_line_num_len + 3 + start + tabs,
4✔
1997
                max_line_num_len + 3 + end + tabs,
7✔
1998
                ElementStyle::Addition,
1999
                true,
2000
            );
2001
        }
2002
    }
2003
    *row_num += 1;
4✔
2004
}
2005

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

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

2062
            if width_taken >= padding {
3✔
2063
                break;
2064
            }
2065
        }
2066

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

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

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

2093
            if width_taken_inner >= padding {
3✔
2094
                break;
2095
            }
2096
        }
2097

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

2106
    buffer.puts(
8✔
2107
        line_offset,
2108
        0,
2109
        &maybe_anonymized(renderer, line_index, max_line_num_len),
6✔
2110
        ElementStyle::LineNumber,
2111
    );
2112

2113
    draw_col_separator_no_space(renderer, buffer, line_offset, width_offset - 2);
3✔
2114

2115
    left
8✔
2116
}
2117

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2313
    #[cfg(target_pointer_width = "32")]
2314
    const MAX_DIGITS: usize = 10;
2315

2316
    #[cfg(target_pointer_width = "16")]
2317
    const MAX_DIGITS: usize = 5;
2318

2319
    let mut lim = 10;
3✔
2320
    for num_digits in 1..MAX_DIGITS {
8✔
2321
        if num < lim {
4✔
2322
            return num_digits;
5✔
2323
        }
2324
        lim = lim.wrapping_mul(10);
3✔
2325
    }
2326
    MAX_DIGITS
×
2327
}
2328

2329
fn str_width(s: &str) -> usize {
4✔
2330
    s.chars().map(char_width).sum()
7✔
2331
}
2332

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

2352
pub(crate) fn num_overlap(
5✔
2353
    a_start: usize,
2354
    a_end: usize,
2355
    b_start: usize,
2356
    b_end: usize,
2357
    inclusive: bool,
2358
) -> bool {
2359
    let extra = usize::from(inclusive);
5✔
2360
    (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start)
5✔
2361
}
2362

2363
fn overlaps(a1: &LineAnnotation<'_>, a2: &LineAnnotation<'_>, padding: usize) -> bool {
6✔
2364
    num_overlap(
2365
        a1.start.display,
5✔
2366
        a1.end.display + padding,
7✔
2367
        a2.start.display,
5✔
2368
        a2.end.display,
7✔
2369
        false,
2370
    )
2371
}
2372

2373
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2374
pub(crate) enum LineAnnotationType {
2375
    /// Annotation under a single line of code
2376
    Singleline,
2377

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

2399
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2400
pub(crate) struct LineAnnotation<'a> {
2401
    /// Start column.
2402
    /// Note that it is important that this field goes
2403
    /// first, so that when we sort, we sort orderings by start
2404
    /// column.
2405
    pub start: Loc,
2406

2407
    /// End column within the line (exclusive)
2408
    pub end: Loc,
2409

2410
    /// level
2411
    pub kind: AnnotationKind,
2412

2413
    /// Optional label to display adjacent to the annotation.
2414
    pub label: Option<Cow<'a, str>>,
2415

2416
    /// Is this a single line, multiline or multiline span minimized down to a
2417
    /// smaller span.
2418
    pub annotation_type: LineAnnotationType,
2419

2420
    /// Whether the source code should be highlighted
2421
    pub highlight_source: bool,
2422
}
2423

2424
impl LineAnnotation<'_> {
2425
    pub(crate) fn is_primary(&self) -> bool {
6✔
2426
        self.kind == AnnotationKind::Primary
5✔
2427
    }
2428

2429
    /// Whether this annotation is a vertical line placeholder.
2430
    pub(crate) fn is_line(&self) -> bool {
5✔
2431
        matches!(self.annotation_type, LineAnnotationType::MultilineLine(_))
6✔
2432
    }
2433

2434
    /// Length of this annotation as displayed in the stderr output
2435
    pub(crate) fn len(&self) -> usize {
3✔
2436
        // Account for usize underflows
2437
        self.end.display.abs_diff(self.start.display)
3✔
2438
    }
2439

2440
    pub(crate) fn has_label(&self) -> bool {
8✔
2441
        if let Some(label) = &self.label {
10✔
2442
            // Consider labels with no text as effectively not being there
2443
            // to avoid weird output with unnecessary vertical lines, like:
2444
            //
2445
            //     X | fn foo(x: u32) {
2446
            //       | -------^------
2447
            //       | |      |
2448
            //       | |
2449
            //       |
2450
            //
2451
            // Note that this would be the complete output users would see.
2452
            !label.is_empty()
6✔
2453
        } else {
2454
            false
5✔
2455
        }
2456
    }
2457

2458
    pub(crate) fn takes_space(&self) -> bool {
3✔
2459
        // Multiline annotations always have to keep vertical space.
2460
        matches!(
3✔
2461
            self.annotation_type,
3✔
2462
            LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
2463
        )
2464
    }
2465
}
2466

2467
#[derive(Clone, Copy, Debug)]
2468
pub(crate) enum DisplaySuggestion {
2469
    Underline,
2470
    Diff,
2471
    None,
2472
    Add,
2473
}
2474

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

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

2550
pub(crate) fn normalize_whitespace(s: &str) -> String {
6✔
2551
    // Scan the input string for a character in the ordered table above.
2552
    // If it's present, replace it with its alternative string (it can be more than 1 char!).
2553
    // Otherwise, retain the input char.
2554
    s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
9✔
2555
        match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) {
20✔
2556
            Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1),
2✔
2557
            _ => s.push(c),
7✔
2558
        }
2559
        s
5✔
2560
    })
2561
}
2562

2563
#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)]
2564
pub(crate) enum ElementStyle {
2565
    MainHeaderMsg,
2566
    HeaderMsg,
2567
    LineAndColumn,
2568
    LineNumber,
2569
    Quotation,
2570
    UnderlinePrimary,
2571
    UnderlineSecondary,
2572
    LabelPrimary,
2573
    LabelSecondary,
2574
    NoStyle,
2575
    Level(LevelInner),
2576
    Addition,
2577
    Removal,
2578
}
2579

2580
impl ElementStyle {
2581
    pub(crate) fn color_spec(&self, level: &Level<'_>, stylesheet: &Stylesheet) -> Style {
5✔
2582
        match self {
4✔
2583
            ElementStyle::Addition => stylesheet.addition,
4✔
2584
            ElementStyle::Removal => stylesheet.removal,
3✔
2585
            ElementStyle::LineAndColumn => stylesheet.none,
6✔
2586
            ElementStyle::LineNumber => stylesheet.line_num,
6✔
2587
            ElementStyle::Quotation => stylesheet.none,
5✔
2588
            ElementStyle::MainHeaderMsg => stylesheet.emphasis,
4✔
2589
            ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary => level.style(stylesheet),
4✔
2590
            ElementStyle::UnderlineSecondary | ElementStyle::LabelSecondary => stylesheet.context,
3✔
2591
            ElementStyle::HeaderMsg | ElementStyle::NoStyle => stylesheet.none,
6✔
2592
            ElementStyle::Level(lvl) => lvl.style(stylesheet),
4✔
2593
        }
2594
    }
2595
}
2596

2597
#[derive(Debug, Clone, Copy)]
2598
pub(crate) struct UnderlineParts {
2599
    pub(crate) style: ElementStyle,
2600
    pub(crate) underline: char,
2601
    pub(crate) label_start: char,
2602
    pub(crate) vertical_text_line: char,
2603
    pub(crate) multiline_vertical: char,
2604
    pub(crate) multiline_horizontal: char,
2605
    pub(crate) multiline_whole_line: char,
2606
    pub(crate) multiline_start_down: char,
2607
    pub(crate) bottom_right: char,
2608
    pub(crate) top_left: char,
2609
    pub(crate) top_right_flat: char,
2610
    pub(crate) bottom_left: char,
2611
    pub(crate) multiline_end_up: char,
2612
    pub(crate) multiline_end_same_line: char,
2613
    pub(crate) multiline_bottom_right_with_text: char,
2614
}
2615

2616
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2617
enum TitleStyle {
2618
    MainHeader,
2619
    Header,
2620
    Secondary,
2621
}
2622

2623
struct PreProcessedGroup<'a> {
2624
    group: &'a Group<'a>,
2625
    elements: Vec<PreProcessedElement<'a>>,
2626
    primary_path: Option<&'a Cow<'a, str>>,
2627
    max_depth: usize,
2628
}
2629

2630
enum PreProcessedElement<'a> {
2631
    Message(&'a Message<'a>),
2632
    Cause(
2633
        (
2634
            &'a Snippet<'a, Annotation<'a>>,
2635
            SourceMap<'a>,
2636
            Vec<AnnotatedLineInfo<'a>>,
2637
        ),
2638
    ),
2639
    Suggestion(
2640
        (
2641
            &'a Snippet<'a, Patch<'a>>,
2642
            SourceMap<'a>,
2643
            SplicedLines<'a>,
2644
            DisplaySuggestion,
2645
        ),
2646
    ),
2647
    Origin(&'a Origin<'a>),
2648
    Padding(Padding),
2649
}
2650

2651
fn pre_process<'a>(
7✔
2652
    groups: &'a [Group<'a>],
2653
) -> (usize, Option<&'a Cow<'a, str>>, Vec<PreProcessedGroup<'a>>) {
2654
    let mut max_line_num = 0;
4✔
2655
    let mut og_primary_path = None;
6✔
2656
    let mut out = Vec::with_capacity(groups.len());
4✔
2657
    for group in groups {
15✔
2658
        let mut elements = Vec::with_capacity(group.elements.len());
10✔
2659
        let mut primary_path = None;
3✔
2660
        let mut max_depth = 0;
6✔
2661
        for element in &group.elements {
11✔
2662
            match element {
5✔
2663
                Element::Message(message) => {
3✔
2664
                    elements.push(PreProcessedElement::Message(message));
6✔
2665
                }
2666
                Element::Cause(cause) => {
6✔
2667
                    let sm = SourceMap::new(&cause.source, cause.line_start);
12✔
2668
                    let (depth, annotated_lines) =
11✔
2669
                        sm.annotated_lines(cause.markers.clone(), cause.fold);
×
2670

2671
                    if cause.fold {
11✔
2672
                        let end = cause
10✔
2673
                            .markers
×
2674
                            .iter()
2675
                            .map(|a| a.span.end)
15✔
2676
                            .max()
2677
                            .unwrap_or(cause.source.len())
4✔
2678
                            .min(cause.source.len());
5✔
2679

2680
                        max_line_num = max(
9✔
2681
                            cause.line_start + newline_count(&cause.source[..end]),
5✔
2682
                            max_line_num,
4✔
2683
                        );
2684
                    } else {
2685
                        max_line_num = max(
4✔
2686
                            cause.line_start + newline_count(&cause.source),
4✔
2687
                            max_line_num,
2✔
2688
                        );
2689
                    }
2690

2691
                    if primary_path.is_none() {
12✔
2692
                        primary_path = Some(cause.path.as_ref());
5✔
2693
                    }
2694
                    max_depth = max(depth, max_depth);
9✔
2695
                    elements.push(PreProcessedElement::Cause((cause, sm, annotated_lines)));
5✔
2696
                }
2697
                Element::Suggestion(suggestion) => {
4✔
2698
                    let sm = SourceMap::new(&suggestion.source, suggestion.line_start);
8✔
2699
                    if let Some((complete, patches, highlights)) =
11✔
2700
                        sm.splice_lines(suggestion.markers.clone(), suggestion.fold)
×
2701
                    {
2702
                        let display_suggestion = DisplaySuggestion::new(&complete, &patches, &sm);
8✔
2703

2704
                        if suggestion.fold {
5✔
2705
                            if let Some(first) = patches.first() {
18✔
2706
                                let (l_start, _) =
6✔
2707
                                    sm.span_to_locations(first.original_span.clone());
×
2708
                                let nc = newline_count(&complete);
6✔
2709
                                let sugg_max_line_num = match display_suggestion {
6✔
2710
                                    DisplaySuggestion::Underline => l_start.line,
2✔
2711
                                    DisplaySuggestion::Diff => {
×
2712
                                        let file_lines = sm.span_to_lines(first.span.clone());
12✔
2713
                                        file_lines
12✔
2714
                                            .last()
2715
                                            .map_or(l_start.line + nc, |line| line.line_index)
18✔
2716
                                    }
2717
                                    DisplaySuggestion::None => l_start.line + nc,
4✔
2718
                                    DisplaySuggestion::Add => l_start.line + nc,
4✔
2719
                                };
2720
                                max_line_num = max(sugg_max_line_num, max_line_num);
12✔
2721
                            }
2722
                        } else {
2723
                            max_line_num = max(
2✔
2724
                                suggestion.line_start + newline_count(&complete),
2✔
2725
                                max_line_num,
1✔
2726
                            );
2727
                        }
2728

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

2760
    (max_line_num, og_primary_path, out)
5✔
2761
}
2762

2763
fn newline_count(body: &str) -> usize {
5✔
2764
    #[cfg(feature = "simd")]
2765
    {
2766
        memchr::memchr_iter(b'\n', body.as_bytes()).count()
2767
    }
2768
    #[cfg(not(feature = "simd"))]
2769
    {
2770
        body.lines().count().saturating_sub(1)
6✔
2771
    }
2772
}
2773

2774
#[cfg(test)]
2775
mod test {
2776
    use super::{newline_count, OUTPUT_REPLACEMENTS};
2777
    use snapbox::IntoData;
2778

2779
    fn format_replacements(replacements: Vec<(char, &str)>) -> String {
2780
        replacements
2781
            .into_iter()
2782
            .map(|r| format!("    {r:?}"))
2783
            .collect::<Vec<_>>()
2784
            .join("\n")
2785
    }
2786

2787
    #[test]
2788
    /// The [`OUTPUT_REPLACEMENTS`] array must be sorted (for binary search to
2789
    /// work) and must contain no duplicate entries
2790
    fn ensure_output_replacements_is_sorted() {
2791
        let mut expected = OUTPUT_REPLACEMENTS.to_owned();
2792
        expected.sort_by_key(|r| r.0);
2793
        expected.dedup_by_key(|r| r.0);
2794
        let expected = format_replacements(expected);
2795
        let actual = format_replacements(OUTPUT_REPLACEMENTS.to_owned());
2796
        snapbox::assert_data_eq!(actual, expected.into_data().raw());
2797
    }
2798

2799
    #[test]
2800
    fn ensure_newline_count_correct() {
2801
        let source = r#"
2802
                cargo-features = ["path-bases"]
2803

2804
                [package]
2805
                name = "foo"
2806
                version = "0.5.0"
2807
                authors = ["wycats@example.com"]
2808

2809
                [dependencies]
2810
                bar = { base = '^^not-valid^^', path = 'bar' }
2811
            "#;
2812
        let actual_count = newline_count(source);
2813
        let expected_count = 10;
2814

2815
        assert_eq!(expected_count, actual_count);
2816
    }
2817
}
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