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

rust-lang / annotate-snippets-rs / 18723365188

22 Oct 2025 04:42PM UTC coverage: 90.214% (-0.1%) from 90.325%
18723365188

Pull #327

github

web-flow
Merge 331e11fe5 into 0bae5bdfd
Pull Request #327: fix: Use straight arrows for standalone Origin

6 of 7 new or added lines in 2 files covered. (85.71%)

2 existing lines in 1 file now uncovered.

1475 of 1635 relevant lines covered (90.21%)

4.94 hits per line

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

95.82
/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 std::borrow::Cow;
4
use std::cmp::{max, min, Ordering, Reverse};
5
use std::collections::HashMap;
6
use std::fmt;
7

8
use anstyle::Style;
9

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

25
const ANONYMIZED_LINE_NUM: &str = "LL";
26

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

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

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

171
                        if matches!(peek, Some(PreProcessedElement::Suggestion(_))) {
16✔
172
                            last_suggestion_path = Some(suggestion.path.as_ref());
10✔
173
                        } else {
174
                            last_suggestion_path = None;
5✔
175
                        }
176
                    }
177

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

228
                writeln!(out_string).unwrap();
4✔
229
            }
230
        }
231
        out_string
6✔
232
    }
233
}
234

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

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

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

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

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

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

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

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

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

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

312
    Ok(out_string)
2✔
313
}
314

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

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

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

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

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

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

481
    let str = match (&origin.line, &origin.char_column) {
11✔
482
        (Some(line), Some(col)) => {
4✔
483
            format!("{}:{}:{}", origin.path, line, col)
7✔
484
        }
485
        (Some(line), None) => format!("{}:{}", origin.path, line),
1✔
486
        _ => origin.path.to_string(),
1✔
487
    };
488

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

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

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

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

619
    // Contains the vertical lines' positions for active multiline annotations
620
    let mut multilines = Vec::new();
6✔
621

622
    // Get the left-side margin to remove it
623
    let mut whitespace_margin = usize::MAX;
5✔
624
    for line_info in annotated_lines {
10✔
625
        // Whitespace can only be removed (aka considered leading)
626
        // if the lexer considers it whitespace.
627
        // non-rustc_lexer::is_whitespace() chars are reported as an
628
        // error (ex. no-break-spaces \u{a0}), and thus can't be considered
629
        // for removal during error reporting.
630
        let leading_whitespace = line_info
10✔
631
            .line
632
            .chars()
633
            .take_while(|c| c.is_whitespace())
16✔
634
            .map(|c| {
10✔
635
                match c {
5✔
636
                    // Tabs are displayed as 4 spaces
637
                    '\t' => 4,
2✔
638
                    _ => 1,
5✔
639
                }
640
            })
641
            .sum();
642
        if line_info.line.chars().any(|c| !c.is_whitespace()) {
18✔
643
            whitespace_margin = min(whitespace_margin, leading_whitespace);
5✔
644
        }
645
    }
646
    if whitespace_margin == usize::MAX {
8✔
647
        whitespace_margin = 0;
2✔
648
    }
649

650
    // Left-most column any visible span points at.
651
    let mut span_left_margin = usize::MAX;
5✔
652
    for line_info in annotated_lines {
11✔
653
        for ann in &line_info.annotations {
15✔
654
            span_left_margin = min(span_left_margin, ann.start.display);
4✔
655
            span_left_margin = min(span_left_margin, ann.end.display);
7✔
656
        }
657
    }
658
    if span_left_margin == usize::MAX {
8✔
659
        span_left_margin = 0;
1✔
660
    }
661

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

683
    let column_width = renderer.term_width.saturating_sub(code_offset);
10✔
684

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

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

698
        let depths = render_source_line(
699
            renderer,
700
            &annotated_lines[annotated_line_idx],
4✔
701
            buffer,
702
            width_offset,
703
            code_offset,
6✔
704
            max_line_num_len,
705
            margin,
706
            !is_cont && annotated_line_idx + 1 == annotated_lines.len(),
10✔
707
        );
708

709
        let mut to_add = HashMap::new();
3✔
710

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

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

735
                    draw_line_separator(renderer, buffer, last_buffer_line_num, width_offset);
3✔
736

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

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

776
                    let last_buffer_line_num = buffer.num_lines();
3✔
777

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

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

823
        multilines.extend(to_add);
3✔
824
    }
825
}
826

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

852
    let source_string = normalize_whitespace(line_info.line);
6✔
853

854
    let line_offset = buffer.num_lines();
9✔
855

856
    let left = draw_line(
857
        renderer,
858
        buffer,
859
        &source_string,
3✔
860
        line_info.line_index,
5✔
861
        line_offset,
862
        width_offset,
863
        code_offset,
864
        max_line_num_len,
865
        margin,
866
    );
867

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

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

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

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

957
    let mut annotations = line_info.annotations.clone();
5✔
958
    annotations.sort_by_key(|a| Reverse((a.start.display, a.start.char)));
15✔
959

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

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

1091
    if line_len != 0 {
9✔
1092
        line_len += 1;
3✔
1093
    }
1094

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

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

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

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

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

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

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

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

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

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

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

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

1483
    if let DisplaySuggestion::Diff = show_code_change {
8✔
1484
        row_num += 1;
7✔
1485
    }
1486

1487
    let file_lines = sm.span_to_lines(parts[0].span.clone());
9✔
1488
    let (line_start, line_end) = if suggestion.fold {
12✔
1489
        // We use the original span to get original line_start
1490
        sm.span_to_locations(parts[0].original_span.clone())
12✔
1491
    } else {
1492
        sm.span_to_locations(0..sm.source.len())
4✔
1493
    };
1494
    let mut lines = complete.lines();
12✔
1495
    if lines.clone().next().is_none() {
6✔
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();
5✔
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 {
11✔
1523
            unhighlighted_lines.push((line_pos, line));
1✔
1524
            continue;
1525
        }
1526

1527
        match unhighlighted_lines.len() {
10✔
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,
6✔
1604
            line_pos + line_start.line,
4✔
1605
            line,
1606
            show_code_change,
1607
            max_line_num_len,
1608
            &file_lines,
4✔
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();
3✔
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 =
12✔
1619
        show_code_change
1620
    {
1621
        for part in parts {
13✔
1622
            let snippet = sm.span_to_snippet(part.span.clone()).unwrap_or_default();
10✔
1623
            let (span_start, span_end) = sm.span_to_locations(part.span.clone());
6✔
1624
            let span_start_pos = span_start.display;
4✔
1625
            let span_end_pos = span_end.display;
6✔
1626

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

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

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

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

1708
                let newlines = snippet.lines().count();
7✔
1709
                if newlines > 0 && row_num > newlines {
7✔
1710
                    // Account for removals where the part being removed spans multiple
1711
                    // lines.
1712
                    // FIXME: We check the number of rows because in some cases, like in
1713
                    // `tests/ui/lint/invalid-nan-comparison-suggestion.rs`, the rendered
1714
                    // suggestion will only show the first line of code being replaced. The
1715
                    // proper way of doing this would be to change the suggestion rendering
1716
                    // logic to show the whole prior snippet, but the current output is not
1717
                    // too bad to begin with, so we side-step that issue here.
1718
                    for (i, line) in snippet.lines().enumerate() {
7✔
1719
                        let line = normalize_whitespace(line);
3✔
1720
                        // Going lower than buffer_offset (+ 1) would mean
1721
                        // overwriting existing content in the buffer
1722
                        let min_row = buffer_offset + usize::from(!matches_previous_suggestion);
7✔
1723
                        let row = (row_num - 2 - (newlines - i - 1)).max(min_row);
7✔
1724
                        // On the first line, we highlight between the start of the part
1725
                        // span, and the end of that line.
1726
                        // On the last line, we highlight between the start of the line, and
1727
                        // the column of the part span end.
1728
                        // On all others, we highlight the whole line.
1729
                        let start = if i == 0 {
11✔
1730
                            (padding as isize + span_start_pos as isize) as usize
7✔
1731
                        } else {
1732
                            padding
4✔
1733
                        };
1734
                        let end = if i == 0 {
8✔
1735
                            (padding as isize + span_start_pos as isize + line.len() as isize)
8✔
1736
                                as usize
1737
                        } else if i == newlines - 1 {
14✔
1738
                            (padding as isize + span_end_pos as isize) as usize
7✔
1739
                        } else {
1740
                            (padding as isize + line.len() as isize) as usize
6✔
1741
                        };
1742
                        buffer.set_style_range(row, start, end, ElementStyle::Removal, true);
5✔
1743
                    }
1744
                } else {
1745
                    // The removed code fits all in one line.
1746
                    buffer.set_style_range(
6✔
1747
                        row_num - 2,
3✔
1748
                        (padding as isize + span_start_pos as isize) as usize,
3✔
1749
                        (padding as isize + span_end_pos as isize) as usize,
3✔
1750
                        ElementStyle::Removal,
1751
                        true,
1752
                    );
1753
                }
1754
            }
1755

1756
            // length of the code after substitution
1757
            let full_sub_len = str_width(&part.replacement) as isize;
10✔
1758

1759
            // length of the code to be substituted
1760
            let snippet_len = span_end_pos as isize - span_start_pos as isize;
5✔
1761
            // For multiple substitutions, use the position *after* the previous
1762
            // substitutions have happened, only when further substitutions are
1763
            // located strictly after.
1764
            offsets.push((span_end_pos, full_sub_len - snippet_len));
14✔
1765
        }
1766
        row_num += 1;
7✔
1767
    }
1768

1769
    // if we elided some lines, add an ellipsis
1770
    if lines.next().is_some() {
14✔
1771
        let placeholder = renderer.decor_style.margin();
×
1772
        let padding = str_width(placeholder);
×
1773
        buffer.puts(
×
1774
            row_num,
×
1775
            max_line_num_len.saturating_sub(padding),
×
1776
            placeholder,
1777
            ElementStyle::LineNumber,
1778
        );
1779
    } else {
1780
        let row = match show_code_change {
7✔
1781
            DisplaySuggestion::Diff | DisplaySuggestion::Add | DisplaySuggestion::Underline => {
1782
                row_num - 1
12✔
1783
            }
1784
            DisplaySuggestion::None => row_num,
2✔
1785
        };
1786
        if is_cont {
5✔
1787
            draw_col_separator_no_space(renderer, buffer, row, max_line_num_len + 1);
8✔
1788
        } else {
1789
            draw_col_separator_end(renderer, buffer, row, max_line_num_len + 1);
8✔
1790
        }
1791
    }
1792
}
1793

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

1955
    // Colorize addition/replacements with green.
1956
    for &SubstitutionHighlight { start, end } in highlight_parts {
8✔
1957
        // This is a no-op for empty ranges
1958
        if start != end {
4✔
1959
            // Account for tabs when highlighting (#87972).
1960
            let tabs: usize = line_to_add
4✔
1961
                .chars()
1962
                .take(start)
5✔
1963
                .map(|ch| match ch {
11✔
1964
                    '\t' => 3,
×
1965
                    _ => 0,
3✔
1966
                })
1967
                .sum();
1968
            buffer.set_style_range(
5✔
1969
                *row_num,
4✔
1970
                max_line_num_len + 3 + start + tabs,
4✔
1971
                max_line_num_len + 3 + end + tabs,
9✔
1972
                ElementStyle::Addition,
1973
                true,
1974
            );
1975
        }
1976
    }
1977
    *row_num += 1;
5✔
1978
}
1979

1980
#[allow(clippy::too_many_arguments)]
1981
fn draw_line(
5✔
1982
    renderer: &Renderer,
1983
    buffer: &mut StyledBuffer,
1984
    source_string: &str,
1985
    line_index: usize,
1986
    line_offset: usize,
1987
    width_offset: usize,
1988
    code_offset: usize,
1989
    max_line_num_len: usize,
1990
    margin: Margin,
1991
) -> usize {
1992
    // Tabs are assumed to have been replaced by spaces in calling code.
1993
    debug_assert!(!source_string.contains('\t'));
5✔
1994
    let line_len = str_width(source_string);
5✔
1995
    // Create the source line we will highlight.
1996
    let mut left = margin.left(line_len);
5✔
1997
    let right = margin.right(line_len);
5✔
1998
    // FIXME: The following code looks fishy. See #132860.
1999
    // On long lines, we strip the source line, accounting for unicode.
2000
    let mut taken = 0;
4✔
2001
    let mut skipped = 0;
6✔
2002
    let code: String = source_string
2003
        .chars()
2004
        .skip_while(|ch| {
8✔
2005
            skipped += char_width(*ch);
8✔
2006
            skipped <= left
3✔
2007
        })
2008
        .take_while(|ch| {
15✔
2009
            // Make sure that the trimming on the right will fall within the terminal width.
2010
            taken += char_width(*ch);
6✔
2011
            taken <= (right - left)
11✔
2012
        })
2013
        .collect();
2014

2015
    let placeholder = renderer.decor_style.margin();
11✔
2016
    let padding = str_width(placeholder);
5✔
2017
    let (width_taken, bytes_taken) = if margin.was_cut_left() {
20✔
2018
        // We have stripped some code/whitespace from the beginning, make it clear.
2019
        let mut bytes_taken = 0;
4✔
2020
        let mut width_taken = 0;
4✔
2021
        for ch in code.chars() {
8✔
2022
            width_taken += char_width(ch);
6✔
2023
            bytes_taken += ch.len_utf8();
6✔
2024

2025
            if width_taken >= padding {
3✔
2026
                break;
2027
            }
2028
        }
2029

2030
        if width_taken > padding {
4✔
2031
            left -= width_taken - padding;
1✔
2032
        }
2033

2034
        buffer.puts(
4✔
2035
            line_offset,
2036
            code_offset,
2037
            placeholder,
2038
            ElementStyle::LineNumber,
2039
        );
2040
        (width_taken, bytes_taken)
3✔
2041
    } else {
2042
        (0, 0)
5✔
2043
    };
2044

2045
    buffer.puts(
6✔
2046
        line_offset,
2047
        code_offset + width_taken,
6✔
2048
        &code[bytes_taken..],
4✔
2049
        ElementStyle::Quotation,
2050
    );
2051

2052
    if line_len > right {
6✔
2053
        // We have stripped some code/whitespace from the beginning, make it clear.
2054
        let mut char_taken = 0;
3✔
2055
        let mut width_taken_inner = 0;
3✔
2056
        for ch in code.chars().rev() {
3✔
2057
            width_taken_inner += char_width(ch);
6✔
2058
            char_taken += 1;
6✔
2059

2060
            if width_taken_inner >= padding {
3✔
2061
                break;
2062
            }
2063
        }
2064

2065
        buffer.puts(
6✔
2066
            line_offset,
2067
            code_offset + width_taken + code[bytes_taken..].chars().count() - char_taken,
6✔
2068
            placeholder,
2069
            ElementStyle::LineNumber,
2070
        );
2071
    }
2072

2073
    buffer.puts(
5✔
2074
        line_offset,
2075
        0,
2076
        &maybe_anonymized(renderer, line_index, max_line_num_len),
11✔
2077
        ElementStyle::LineNumber,
2078
    );
2079

2080
    draw_col_separator_no_space(renderer, buffer, line_offset, width_offset - 2);
7✔
2081

2082
    left
5✔
2083
}
2084

2085
fn draw_range(
3✔
2086
    buffer: &mut StyledBuffer,
2087
    symbol: char,
2088
    line: usize,
2089
    col_from: usize,
2090
    col_to: usize,
2091
    style: ElementStyle,
2092
) {
2093
    for col in col_from..col_to {
6✔
2094
        buffer.putc(line, col, symbol, style);
3✔
2095
    }
2096
}
2097

2098
fn draw_multiline_line(
3✔
2099
    renderer: &Renderer,
2100
    buffer: &mut StyledBuffer,
2101
    line: usize,
2102
    offset: usize,
2103
    depth: usize,
2104
    style: ElementStyle,
2105
) {
2106
    let chr = match (style, renderer.decor_style) {
3✔
2107
        (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Ascii) => '|',
3✔
2108
        (_, DecorStyle::Ascii) => '|',
2✔
2109
        (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Unicode) => '┃',
3✔
2110
        (_, DecorStyle::Unicode) => '│',
2✔
2111
    };
2112
    buffer.putc(line, offset + depth - 1, chr, style);
6✔
2113
}
2114

2115
fn draw_col_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
3✔
2116
    let chr = renderer.decor_style.col_separator();
3✔
2117
    buffer.puts(line, col, &format!("{chr} "), ElementStyle::LineNumber);
3✔
2118
}
2119

2120
fn draw_col_separator_no_space(
6✔
2121
    renderer: &Renderer,
2122
    buffer: &mut StyledBuffer,
2123
    line: usize,
2124
    col: usize,
2125
) {
2126
    let chr = renderer.decor_style.col_separator();
5✔
2127
    draw_col_separator_no_space_with_style(buffer, chr, line, col, ElementStyle::LineNumber);
5✔
2128
}
2129

2130
fn draw_col_separator_start(
4✔
2131
    renderer: &Renderer,
2132
    buffer: &mut StyledBuffer,
2133
    line: usize,
2134
    col: usize,
2135
) {
2136
    match renderer.decor_style {
5✔
2137
        DecorStyle::Ascii => {
2138
            draw_col_separator_no_space_with_style(
2139
                buffer,
2140
                '|',
2141
                line,
2142
                col,
2143
                ElementStyle::LineNumber,
2144
            );
2145
        }
2146
        DecorStyle::Unicode => {
2147
            draw_col_separator_no_space_with_style(
2148
                buffer,
2149
                '╭',
2150
                line,
2151
                col,
2152
                ElementStyle::LineNumber,
2153
            );
2154
            draw_col_separator_no_space_with_style(
2155
                buffer,
2156
                '╴',
2157
                line,
2158
                col + 1,
3✔
2159
                ElementStyle::LineNumber,
2160
            );
2161
        }
2162
    }
2163
}
2164

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

2195
fn draw_col_separator_no_space_with_style(
6✔
2196
    buffer: &mut StyledBuffer,
2197
    chr: char,
2198
    line: usize,
2199
    col: usize,
2200
    style: ElementStyle,
2201
) {
2202
    buffer.putc(line, col, chr, style);
5✔
2203
}
2204

2205
fn maybe_anonymized(renderer: &Renderer, line_num: usize, max_line_num_len: usize) -> String {
6✔
2206
    format!(
7✔
2207
        "{:>max_line_num_len$}",
2208
        if renderer.anonymized_line_numbers {
8✔
2209
            Cow::Borrowed(ANONYMIZED_LINE_NUM)
4✔
2210
        } else {
2211
            Cow::Owned(line_num.to_string())
6✔
2212
        }
2213
    )
2214
}
2215

2216
fn draw_note_separator(
4✔
2217
    renderer: &Renderer,
2218
    buffer: &mut StyledBuffer,
2219
    line: usize,
2220
    col: usize,
2221
    is_cont: bool,
2222
) {
2223
    let chr = renderer.decor_style.note_separator(is_cont);
4✔
2224
    buffer.puts(line, col, chr, ElementStyle::LineNumber);
4✔
2225
}
2226

2227
fn draw_line_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
4✔
2228
    let (column, dots) = match renderer.decor_style {
8✔
2229
        DecorStyle::Ascii => (0, "..."),
4✔
2230
        DecorStyle::Unicode => (col - 2, "‡"),
6✔
2231
    };
2232
    buffer.puts(line, column, dots, ElementStyle::LineNumber);
4✔
2233
}
2234

2235
trait MessageOrTitle {
2236
    fn level(&self) -> &Level<'_>;
2237
    fn id(&self) -> Option<&Id<'_>>;
2238
    fn text(&self) -> &str;
2239
    fn allows_styling(&self) -> bool;
2240
}
2241

2242
impl MessageOrTitle for Title<'_> {
2243
    fn level(&self) -> &Level<'_> {
7✔
2244
        &self.level
4✔
2245
    }
2246
    fn id(&self) -> Option<&Id<'_>> {
8✔
2247
        self.id.as_ref()
4✔
2248
    }
2249
    fn text(&self) -> &str {
4✔
2250
        self.text.as_ref()
4✔
2251
    }
2252
    fn allows_styling(&self) -> bool {
5✔
2253
        self.allows_styling
4✔
2254
    }
2255
}
2256

2257
impl MessageOrTitle for Message<'_> {
2258
    fn level(&self) -> &Level<'_> {
4✔
2259
        &self.level
4✔
2260
    }
2261
    fn id(&self) -> Option<&Id<'_>> {
4✔
2262
        None
2263
    }
2264
    fn text(&self) -> &str {
5✔
2265
        self.text.as_ref()
5✔
2266
    }
2267
    fn allows_styling(&self) -> bool {
4✔
2268
        true
2269
    }
2270
}
2271

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

2280
    #[cfg(target_pointer_width = "32")]
2281
    const MAX_DIGITS: usize = 10;
2282

2283
    #[cfg(target_pointer_width = "16")]
2284
    const MAX_DIGITS: usize = 5;
2285

2286
    let mut lim = 10;
3✔
2287
    for num_digits in 1..MAX_DIGITS {
9✔
2288
        if num < lim {
3✔
2289
            return num_digits;
5✔
2290
        }
2291
        lim = lim.wrapping_mul(10);
4✔
2292
    }
2293
    MAX_DIGITS
×
2294
}
2295

2296
fn str_width(s: &str) -> usize {
3✔
2297
    s.chars().map(char_width).sum()
5✔
2298
}
2299

2300
pub(crate) fn char_width(ch: char) -> usize {
4✔
2301
    // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` is. For now,
2302
    // just accept that sometimes the code line will be longer than desired.
2303
    match ch {
6✔
2304
        '\t' => 4,
2✔
2305
        // Keep the following list in sync with `rustc_errors::emitter::OUTPUT_REPLACEMENTS`. These
2306
        // are control points that we replace before printing with a visible codepoint for the sake
2307
        // of being able to point at them with underlines.
2308
        '\u{0000}' | '\u{0001}' | '\u{0002}' | '\u{0003}' | '\u{0004}' | '\u{0005}'
×
2309
        | '\u{0006}' | '\u{0007}' | '\u{0008}' | '\u{000B}' | '\u{000C}' | '\u{000D}'
2310
        | '\u{000E}' | '\u{000F}' | '\u{0010}' | '\u{0011}' | '\u{0012}' | '\u{0013}'
2311
        | '\u{0014}' | '\u{0015}' | '\u{0016}' | '\u{0017}' | '\u{0018}' | '\u{0019}'
2312
        | '\u{001A}' | '\u{001B}' | '\u{001C}' | '\u{001D}' | '\u{001E}' | '\u{001F}'
2313
        | '\u{007F}' | '\u{202A}' | '\u{202B}' | '\u{202D}' | '\u{202E}' | '\u{2066}'
2314
        | '\u{2067}' | '\u{2068}' | '\u{202C}' | '\u{2069}' => 1,
2315
        _ => unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1),
7✔
2316
    }
2317
}
2318

2319
pub(crate) fn num_overlap(
5✔
2320
    a_start: usize,
2321
    a_end: usize,
2322
    b_start: usize,
2323
    b_end: usize,
2324
    inclusive: bool,
2325
) -> bool {
2326
    let extra = usize::from(inclusive);
5✔
2327
    (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start)
5✔
2328
}
2329

2330
fn overlaps(a1: &LineAnnotation<'_>, a2: &LineAnnotation<'_>, padding: usize) -> bool {
5✔
2331
    num_overlap(
2332
        a1.start.display,
7✔
2333
        a1.end.display + padding,
5✔
2334
        a2.start.display,
7✔
2335
        a2.end.display,
5✔
2336
        false,
2337
    )
2338
}
2339

2340
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2341
pub(crate) enum LineAnnotationType {
2342
    /// Annotation under a single line of code
2343
    Singleline,
2344

2345
    // The Multiline type above is replaced with the following three in order
2346
    // to reuse the current label drawing code.
2347
    //
2348
    // Each of these corresponds to one part of the following diagram:
2349
    //
2350
    //     x |   foo(1 + bar(x,
2351
    //       |  _________^              < MultilineStart
2352
    //     x | |             y),        < MultilineLine
2353
    //       | |______________^ label   < MultilineEnd
2354
    //     x |       z);
2355
    /// Annotation marking the first character of a fully shown multiline span
2356
    MultilineStart(usize),
2357
    /// Annotation marking the last character of a fully shown multiline span
2358
    MultilineEnd(usize),
2359
    /// Line at the left enclosing the lines of a fully shown multiline span
2360
    // Just a placeholder for the drawing algorithm, to know that it shouldn't skip the first 4
2361
    // and last 2 lines of code. The actual line is drawn in `emit_message_default` and not in
2362
    // `draw_multiline_line`.
2363
    MultilineLine(usize),
2364
}
2365

2366
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2367
pub(crate) struct LineAnnotation<'a> {
2368
    /// Start column.
2369
    /// Note that it is important that this field goes
2370
    /// first, so that when we sort, we sort orderings by start
2371
    /// column.
2372
    pub start: Loc,
2373

2374
    /// End column within the line (exclusive)
2375
    pub end: Loc,
2376

2377
    /// level
2378
    pub kind: AnnotationKind,
2379

2380
    /// Optional label to display adjacent to the annotation.
2381
    pub label: Option<Cow<'a, str>>,
2382

2383
    /// Is this a single line, multiline or multiline span minimized down to a
2384
    /// smaller span.
2385
    pub annotation_type: LineAnnotationType,
2386

2387
    /// Whether the source code should be highlighted
2388
    pub highlight_source: bool,
2389
}
2390

2391
impl LineAnnotation<'_> {
2392
    pub(crate) fn is_primary(&self) -> bool {
6✔
2393
        self.kind == AnnotationKind::Primary
5✔
2394
    }
2395

2396
    /// Whether this annotation is a vertical line placeholder.
2397
    pub(crate) fn is_line(&self) -> bool {
6✔
2398
        matches!(self.annotation_type, LineAnnotationType::MultilineLine(_))
5✔
2399
    }
2400

2401
    /// Length of this annotation as displayed in the stderr output
2402
    pub(crate) fn len(&self) -> usize {
4✔
2403
        // Account for usize underflows
2404
        self.end.display.abs_diff(self.start.display)
4✔
2405
    }
2406

2407
    pub(crate) fn has_label(&self) -> bool {
5✔
2408
        if let Some(label) = &self.label {
12✔
2409
            // Consider labels with no text as effectively not being there
2410
            // to avoid weird output with unnecessary vertical lines, like:
2411
            //
2412
            //     X | fn foo(x: u32) {
2413
            //       | -------^------
2414
            //       | |      |
2415
            //       | |
2416
            //       |
2417
            //
2418
            // Note that this would be the complete output users would see.
2419
            !label.is_empty()
5✔
2420
        } else {
2421
            false
4✔
2422
        }
2423
    }
2424

2425
    pub(crate) fn takes_space(&self) -> bool {
3✔
2426
        // Multiline annotations always have to keep vertical space.
2427
        matches!(
3✔
2428
            self.annotation_type,
3✔
2429
            LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
2430
        )
2431
    }
2432
}
2433

2434
#[derive(Clone, Copy, Debug)]
2435
pub(crate) enum DisplaySuggestion {
2436
    Underline,
2437
    Diff,
2438
    None,
2439
    Add,
2440
}
2441

2442
impl DisplaySuggestion {
2443
    fn new(complete: &str, patches: &[TrimmedPatch<'_>], sm: &SourceMap<'_>) -> Self {
5✔
2444
        let has_deletion = patches
2445
            .iter()
2446
            .any(|p| p.is_deletion(sm) || p.is_destructive_replacement(sm));
13✔
2447
        let is_multiline = complete.lines().count() > 1;
4✔
2448
        if has_deletion && !is_multiline {
8✔
2449
            DisplaySuggestion::Diff
3✔
2450
        } else if patches.len() == 1
5✔
2451
            && patches.first().map_or(false, |p| {
6✔
2452
                p.replacement.ends_with('\n') && p.replacement.trim() == complete.trim()
3✔
2453
            })
2454
        {
2455
            // We are adding a line(s) of code before code that was already there.
2456
            DisplaySuggestion::Add
2✔
2457
        } else if (patches.len() != 1 || patches[0].replacement.trim() != complete.trim())
14✔
2458
            && !is_multiline
3✔
2459
        {
2460
            DisplaySuggestion::Underline
3✔
2461
        } else {
2462
            DisplaySuggestion::None
2✔
2463
        }
2464
    }
2465
}
2466

2467
// We replace some characters so the CLI output is always consistent and underlines aligned.
2468
// Keep the following list in sync with `rustc_span::char_width`.
2469
const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[
2470
    // In terminals without Unicode support the following will be garbled, but in *all* terminals
2471
    // the underlying codepoint will be as well. We could gate this replacement behind a "unicode
2472
    // support" gate.
2473
    ('\0', "␀"),
2474
    ('\u{0001}', "␁"),
2475
    ('\u{0002}', "␂"),
2476
    ('\u{0003}', "␃"),
2477
    ('\u{0004}', "␄"),
2478
    ('\u{0005}', "␅"),
2479
    ('\u{0006}', "␆"),
2480
    ('\u{0007}', "␇"),
2481
    ('\u{0008}', "␈"),
2482
    ('\t', "    "), // We do our own tab replacement
2483
    ('\u{000b}', "␋"),
2484
    ('\u{000c}', "␌"),
2485
    ('\u{000d}', "␍"),
2486
    ('\u{000e}', "␎"),
2487
    ('\u{000f}', "␏"),
2488
    ('\u{0010}', "␐"),
2489
    ('\u{0011}', "␑"),
2490
    ('\u{0012}', "␒"),
2491
    ('\u{0013}', "␓"),
2492
    ('\u{0014}', "␔"),
2493
    ('\u{0015}', "␕"),
2494
    ('\u{0016}', "␖"),
2495
    ('\u{0017}', "␗"),
2496
    ('\u{0018}', "␘"),
2497
    ('\u{0019}', "␙"),
2498
    ('\u{001a}', "␚"),
2499
    ('\u{001b}', "␛"),
2500
    ('\u{001c}', "␜"),
2501
    ('\u{001d}', "␝"),
2502
    ('\u{001e}', "␞"),
2503
    ('\u{001f}', "␟"),
2504
    ('\u{007f}', "␡"),
2505
    ('\u{200d}', ""), // Replace ZWJ for consistent terminal output of grapheme clusters.
2506
    ('\u{202a}', "�"), // The following unicode text flow control characters are inconsistently
2507
    ('\u{202b}', "�"), // supported across CLIs and can cause confusion due to the bytes on disk
2508
    ('\u{202c}', "�"), // not corresponding to the visible source code, so we replace them always.
2509
    ('\u{202d}', "�"),
2510
    ('\u{202e}', "�"),
2511
    ('\u{2066}', "�"),
2512
    ('\u{2067}', "�"),
2513
    ('\u{2068}', "�"),
2514
    ('\u{2069}', "�"),
2515
];
2516

2517
pub(crate) fn normalize_whitespace(s: &str) -> String {
4✔
2518
    // Scan the input string for a character in the ordered table above.
2519
    // If it's present, replace it with its alternative string (it can be more than 1 char!).
2520
    // Otherwise, retain the input char.
2521
    s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
10✔
2522
        match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) {
18✔
2523
            Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1),
2✔
2524
            _ => s.push(c),
9✔
2525
        }
2526
        s
4✔
2527
    })
2528
}
2529

2530
#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)]
2531
pub(crate) enum ElementStyle {
2532
    MainHeaderMsg,
2533
    HeaderMsg,
2534
    LineAndColumn,
2535
    LineNumber,
2536
    Quotation,
2537
    UnderlinePrimary,
2538
    UnderlineSecondary,
2539
    LabelPrimary,
2540
    LabelSecondary,
2541
    NoStyle,
2542
    Level(LevelInner),
2543
    Addition,
2544
    Removal,
2545
}
2546

2547
impl ElementStyle {
2548
    pub(crate) fn color_spec(&self, level: &Level<'_>, stylesheet: &Stylesheet) -> Style {
4✔
2549
        match self {
3✔
2550
            ElementStyle::Addition => stylesheet.addition,
5✔
2551
            ElementStyle::Removal => stylesheet.removal,
3✔
2552
            ElementStyle::LineAndColumn => stylesheet.none,
6✔
2553
            ElementStyle::LineNumber => stylesheet.line_num,
5✔
2554
            ElementStyle::Quotation => stylesheet.none,
4✔
2555
            ElementStyle::MainHeaderMsg => stylesheet.emphasis,
5✔
2556
            ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary => level.style(stylesheet),
6✔
2557
            ElementStyle::UnderlineSecondary | ElementStyle::LabelSecondary => stylesheet.context,
3✔
2558
            ElementStyle::HeaderMsg | ElementStyle::NoStyle => stylesheet.none,
7✔
2559
            ElementStyle::Level(lvl) => lvl.style(stylesheet),
3✔
2560
        }
2561
    }
2562
}
2563

2564
#[derive(Debug, Clone, Copy)]
2565
pub(crate) struct UnderlineParts {
2566
    pub(crate) style: ElementStyle,
2567
    pub(crate) underline: char,
2568
    pub(crate) label_start: char,
2569
    pub(crate) vertical_text_line: char,
2570
    pub(crate) multiline_vertical: char,
2571
    pub(crate) multiline_horizontal: char,
2572
    pub(crate) multiline_whole_line: char,
2573
    pub(crate) multiline_start_down: char,
2574
    pub(crate) bottom_right: char,
2575
    pub(crate) top_left: char,
2576
    pub(crate) top_right_flat: char,
2577
    pub(crate) bottom_left: char,
2578
    pub(crate) multiline_end_up: char,
2579
    pub(crate) multiline_end_same_line: char,
2580
    pub(crate) multiline_bottom_right_with_text: char,
2581
}
2582

2583
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2584
enum TitleStyle {
2585
    MainHeader,
2586
    Header,
2587
    Secondary,
2588
}
2589

2590
struct PreProcessedGroup<'a> {
2591
    group: &'a Group<'a>,
2592
    elements: Vec<PreProcessedElement<'a>>,
2593
    primary_path: Option<&'a Cow<'a, str>>,
2594
    max_depth: usize,
2595
}
2596

2597
enum PreProcessedElement<'a> {
2598
    Message(&'a Message<'a>),
2599
    Cause(
2600
        (
2601
            &'a Snippet<'a, Annotation<'a>>,
2602
            SourceMap<'a>,
2603
            Vec<AnnotatedLineInfo<'a>>,
2604
        ),
2605
    ),
2606
    Suggestion(
2607
        (
2608
            &'a Snippet<'a, Patch<'a>>,
2609
            SourceMap<'a>,
2610
            SplicedLines<'a>,
2611
            DisplaySuggestion,
2612
        ),
2613
    ),
2614
    Origin(&'a Origin<'a>),
2615
    Padding(Padding),
2616
}
2617

2618
fn pre_process<'a>(
9✔
2619
    groups: &'a [Group<'a>],
2620
) -> (usize, Option<&'a Cow<'a, str>>, Vec<PreProcessedGroup<'a>>) {
2621
    let mut max_line_num = 0;
5✔
2622
    let mut og_primary_path = None;
7✔
2623
    let mut out = Vec::with_capacity(groups.len());
6✔
2624
    for group in groups {
19✔
2625
        let mut elements = Vec::with_capacity(group.elements.len());
14✔
2626
        let mut primary_path = None;
8✔
2627
        let mut max_depth = 0;
6✔
2628
        for element in &group.elements {
16✔
2629
            match element {
8✔
2630
                Element::Message(message) => {
3✔
2631
                    elements.push(PreProcessedElement::Message(message));
6✔
2632
                }
2633
                Element::Cause(cause) => {
8✔
2634
                    let sm = SourceMap::new(&cause.source, cause.line_start);
16✔
2635
                    let (depth, annotated_lines) =
7✔
2636
                        sm.annotated_lines(cause.markers.clone(), cause.fold);
×
2637

2638
                    if cause.fold {
11✔
2639
                        let end = cause
8✔
2640
                            .markers
×
2641
                            .iter()
2642
                            .map(|a| a.span.end)
13✔
2643
                            .max()
2644
                            .unwrap_or(cause.source.len())
4✔
2645
                            .min(cause.source.len());
5✔
2646

2647
                        max_line_num = max(
10✔
2648
                            cause.line_start + newline_count(&cause.source[..end]),
5✔
2649
                            max_line_num,
4✔
2650
                        );
2651
                    } else {
2652
                        max_line_num = max(
5✔
2653
                            cause.line_start + newline_count(&cause.source),
4✔
2654
                            max_line_num,
2✔
2655
                        );
2656
                    }
2657

2658
                    if primary_path.is_none() {
15✔
2659
                        primary_path = Some(cause.path.as_ref());
5✔
2660
                    }
2661
                    max_depth = max(depth, max_depth);
9✔
2662
                    elements.push(PreProcessedElement::Cause((cause, sm, annotated_lines)));
5✔
2663
                }
2664
                Element::Suggestion(suggestion) => {
3✔
2665
                    let sm = SourceMap::new(&suggestion.source, suggestion.line_start);
6✔
2666
                    if let Some((complete, patches, highlights)) =
6✔
2667
                        sm.splice_lines(suggestion.markers.clone(), suggestion.fold)
×
2668
                    {
2669
                        let display_suggestion = DisplaySuggestion::new(&complete, &patches, &sm);
9✔
2670

2671
                        if suggestion.fold {
5✔
2672
                            if let Some(first) = patches.first() {
13✔
2673
                                let (l_start, _) =
4✔
2674
                                    sm.span_to_locations(first.original_span.clone());
×
2675
                                let nc = newline_count(&complete);
5✔
2676
                                let sugg_max_line_num = match display_suggestion {
4✔
2677
                                    DisplaySuggestion::Underline => l_start.line,
3✔
2678
                                    DisplaySuggestion::Diff => {
×
2679
                                        let file_lines = sm.span_to_lines(first.span.clone());
7✔
2680
                                        file_lines
8✔
2681
                                            .last()
2682
                                            .map_or(l_start.line + nc, |line| line.line_index)
10✔
2683
                                    }
2684
                                    DisplaySuggestion::None => l_start.line + nc,
4✔
2685
                                    DisplaySuggestion::Add => l_start.line + nc,
4✔
2686
                                };
2687
                                max_line_num = max(sugg_max_line_num, max_line_num);
9✔
2688
                            }
2689
                        } else {
2690
                            max_line_num = max(
4✔
2691
                                suggestion.line_start + newline_count(&complete),
4✔
2692
                                max_line_num,
2✔
2693
                            );
2694
                        }
2695

2696
                        elements.push(PreProcessedElement::Suggestion((
5✔
2697
                            suggestion,
×
2698
                            sm,
5✔
2699
                            (complete, patches, highlights),
4✔
2700
                            display_suggestion,
×
2701
                        )));
2702
                    }
2703
                }
2704
                Element::Origin(origin) => {
2✔
2705
                    if primary_path.is_none() {
6✔
2706
                        primary_path = Some(Some(&origin.path));
2✔
2707
                    }
2708
                    elements.push(PreProcessedElement::Origin(origin));
4✔
2709
                }
2710
                Element::Padding(padding) => {
3✔
2711
                    elements.push(PreProcessedElement::Padding(padding.clone()));
6✔
2712
                }
2713
            }
2714
        }
2715
        let group = PreProcessedGroup {
2716
            group,
2717
            elements,
2718
            primary_path: primary_path.unwrap_or_default(),
4✔
2719
            max_depth,
2720
        };
2721
        if og_primary_path.is_none() && group.primary_path.is_some() {
19✔
2722
            og_primary_path = group.primary_path;
5✔
2723
        }
2724
        out.push(group);
4✔
2725
    }
2726

2727
    (max_line_num, og_primary_path, out)
4✔
2728
}
2729

2730
fn newline_count(body: &str) -> usize {
5✔
2731
    #[cfg(feature = "simd")]
2732
    {
2733
        memchr::memchr_iter(b'\n', body.as_bytes()).count()
2734
    }
2735
    #[cfg(not(feature = "simd"))]
2736
    {
2737
        body.lines().count().saturating_sub(1)
6✔
2738
    }
2739
}
2740

2741
#[cfg(test)]
2742
mod test {
2743
    use super::{newline_count, OUTPUT_REPLACEMENTS};
2744
    use snapbox::IntoData;
2745

2746
    fn format_replacements(replacements: Vec<(char, &str)>) -> String {
2747
        replacements
2748
            .into_iter()
2749
            .map(|r| format!("    {r:?}"))
2750
            .collect::<Vec<_>>()
2751
            .join("\n")
2752
    }
2753

2754
    #[test]
2755
    /// The [`OUTPUT_REPLACEMENTS`] array must be sorted (for binary search to
2756
    /// work) and must contain no duplicate entries
2757
    fn ensure_output_replacements_is_sorted() {
2758
        let mut expected = OUTPUT_REPLACEMENTS.to_owned();
2759
        expected.sort_by_key(|r| r.0);
2760
        expected.dedup_by_key(|r| r.0);
2761
        let expected = format_replacements(expected);
2762
        let actual = format_replacements(OUTPUT_REPLACEMENTS.to_owned());
2763
        snapbox::assert_data_eq!(actual, expected.into_data().raw());
2764
    }
2765

2766
    #[test]
2767
    fn ensure_newline_count_correct() {
2768
        let source = r#"
2769
                cargo-features = ["path-bases"]
2770

2771
                [package]
2772
                name = "foo"
2773
                version = "0.5.0"
2774
                authors = ["wycats@example.com"]
2775

2776
                [dependencies]
2777
                bar = { base = '^^not-valid^^', path = 'bar' }
2778
            "#;
2779
        let actual_count = newline_count(source);
2780
        let expected_count = 10;
2781

2782
        assert_eq!(expected_count, actual_count);
2783
    }
2784
}
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