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

rust-lang / annotate-snippets-rs / 13903310413

17 Mar 2025 03:26PM UTC coverage: 85.793%. Remained the same
13903310413

Pull #185

github

web-flow
Merge 34a69b497 into 5c6ce17f3
Pull Request #185: refactor(display-list): split into separate modules (#184)

636 of 729 new or added lines in 6 files covered. (87.24%)

779 of 908 relevant lines covered (85.79%)

4.62 hits per line

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

95.8
/src/renderer/display/mod.rs
1
//! `display_list` module stores the output model for the snippet.
2
//!
3
//! `DisplayList` is a central structure in the crate, which contains
4
//! the structured list of lines to be displayed.
5
//!
6
//! It is made of two types of lines: `Source` and `Raw`. All `Source` lines
7
//! are structured using four columns:
8
//!
9
//! ```text
10
//!  /------------ (1) Line number column.
11
//!  |  /--------- (2) Line number column delimiter.
12
//!  |  | /------- (3) Inline marks column.
13
//!  |  | |   /--- (4) Content column with the source and annotations for slices.
14
//!  |  | |   |
15
//! =============================================================================
16
//! error[E0308]: mismatched types
17
//!    --> src/format.rs:51:5
18
//!     |
19
//! 151 | /   fn test() -> String {
20
//! 152 | |       return "test";
21
//! 153 | |   }
22
//!     | |___^ error: expected `String`, for `&str`.
23
//! ```
24
//!
25
//! The first two lines of the example above are `Raw` lines, while the rest
26
//! are `Source` lines.
27
//!
28
//! `DisplayList` does not store column alignment information, and those are
29
//! only calculated by the implementation of `std::fmt::Display` using information such as
30
//! styling.
31
//!
32
//! The above snippet has been built out of the following structure:
33

34
mod constants;
35
mod cursor_line;
36
mod display_annotations;
37
mod display_header;
38
mod display_line;
39
mod display_list;
40
mod display_mark;
41
mod display_set;
42
mod display_text;
43
mod end_line;
44

45
use crate::snippet;
46
use std::cmp::{max, min};
47
use std::collections::HashMap;
48
use std::fmt;
49
use std::ops::Range;
50

51
use crate::renderer::styled_buffer::StyledBuffer;
52
use crate::renderer::{stylesheet::Stylesheet, Margin, DEFAULT_TERM_WIDTH};
53

54
use constants::*;
55
use cursor_line::CursorLines;
56
use display_annotations::{
57
    get_annotation_style, Annotation, DisplayAnnotationPart, DisplayAnnotationType,
58
    DisplaySourceAnnotation,
59
};
60
use display_header::DisplayHeaderType;
61
use display_line::{DisplayLine, DisplayRawLine, DisplaySourceLine};
62
use display_mark::{DisplayMark, DisplayMarkType};
63
use display_set::DisplaySet;
64
use display_text::{DisplayTextFragment, DisplayTextStyle};
65

66
pub(crate) use display_list::DisplayList;
67

68
pub(crate) fn format_message(
6✔
69
    message: snippet::Message<'_>,
70
    term_width: usize,
71
    anonymized_line_numbers: bool,
72
    primary: bool,
73
) -> Vec<DisplaySet<'_>> {
74
    let snippet::Message {
4✔
75
        level,
6✔
76
        id,
4✔
77
        title,
6✔
78
        footer,
4✔
79
        snippets,
6✔
80
    } = message;
81

82
    let mut sets = vec![];
4✔
83
    let body = if !snippets.is_empty() || primary {
15✔
84
        vec![format_title(level, id, title)]
10✔
85
    } else {
86
        format_footer(level, id, title)
1✔
87
    };
88

89
    for (idx, snippet) in snippets.into_iter().enumerate() {
20✔
90
        let snippet = fold_prefix_suffix(snippet);
6✔
91
        sets.push(format_snippet(
4✔
92
            snippet,
93
            idx == 0,
6✔
94
            term_width,
95
            anonymized_line_numbers,
96
        ));
97
    }
98

99
    if let Some(first) = sets.first_mut() {
4✔
100
        for line in body {
20✔
101
            first.display_lines.insert(0, line);
10✔
102
        }
103
    } else {
104
        sets.push(DisplaySet {
1✔
105
            display_lines: body,
1✔
106
            margin: Margin::new(0, 0, 0, 0, DEFAULT_TERM_WIDTH, 0),
1✔
107
        });
108
    }
109

110
    for annotation in footer {
17✔
111
        sets.extend(format_message(
2✔
112
            annotation,
113
            term_width,
114
            anonymized_line_numbers,
115
            false,
116
        ));
117
    }
118

119
    sets
3✔
120
}
121

122
fn format_title<'a>(level: crate::Level, id: Option<&'a str>, label: &'a str) -> DisplayLine<'a> {
6✔
123
    DisplayLine::Raw(DisplayRawLine::Annotation {
6✔
124
        annotation: Annotation {
4✔
125
            annotation_type: DisplayAnnotationType::from(level),
4✔
NEW
126
            id,
×
127
            label: format_label(Some(label), Some(DisplayTextStyle::Emphasis)),
4✔
128
        },
NEW
129
        source_aligned: false,
×
NEW
130
        continuation: false,
×
131
    })
132
}
133

134
fn format_footer<'a>(
1✔
135
    level: crate::Level,
136
    id: Option<&'a str>,
137
    label: &'a str,
138
) -> Vec<DisplayLine<'a>> {
139
    let mut result = vec![];
1✔
140
    for (i, line) in label.lines().enumerate() {
3✔
141
        result.push(DisplayLine::Raw(DisplayRawLine::Annotation {
1✔
142
            annotation: Annotation {
1✔
143
                annotation_type: DisplayAnnotationType::from(level),
1✔
NEW
144
                id,
×
145
                label: format_label(Some(line), None),
1✔
146
            },
NEW
147
            source_aligned: true,
×
148
            continuation: i != 0,
1✔
149
        }));
150
    }
151
    result
1✔
152
}
153

154
fn format_label(
6✔
155
    label: Option<&str>,
156
    style: Option<DisplayTextStyle>,
157
) -> Vec<DisplayTextFragment<'_>> {
158
    let mut result = vec![];
4✔
159
    if let Some(label) = label {
6✔
160
        let element_style = style.unwrap_or(DisplayTextStyle::Regular);
10✔
161
        result.push(DisplayTextFragment {
4✔
162
            content: label,
163
            style: element_style,
164
        });
165
    }
166
    result
6✔
167
}
168

169
fn format_snippet(
6✔
170
    snippet: snippet::Snippet<'_>,
171
    is_first: bool,
172
    term_width: usize,
173
    anonymized_line_numbers: bool,
174
) -> DisplaySet<'_> {
175
    let main_range = snippet.annotations.first().map(|x| x.range.start);
20✔
176
    let origin = snippet.origin;
4✔
177
    let need_empty_header = origin.is_some() || is_first;
6✔
178
    let mut body = format_body(
179
        snippet,
4✔
180
        need_empty_header,
181
        term_width,
182
        anonymized_line_numbers,
183
    );
184
    let header = format_header(origin, main_range, &body.display_lines, is_first);
10✔
185

186
    if let Some(header) = header {
4✔
187
        body.display_lines.insert(0, header);
10✔
188
    }
189

190
    body
6✔
191
}
192

193
#[inline]
194
// TODO: option_zip
195
fn zip_opt<A, B>(a: Option<A>, b: Option<B>) -> Option<(A, B)> {
6✔
196
    a.and_then(|a| b.map(|b| (a, b)))
24✔
197
}
198

199
fn format_header<'a>(
6✔
200
    origin: Option<&'a str>,
201
    main_range: Option<usize>,
202
    body: &[DisplayLine<'_>],
203
    is_first: bool,
204
) -> Option<DisplayLine<'a>> {
205
    let display_header = if is_first {
6✔
206
        DisplayHeaderType::Initial
6✔
207
    } else {
208
        DisplayHeaderType::Continuation
2✔
209
    };
210

211
    if let Some((main_range, path)) = zip_opt(main_range, origin) {
4✔
212
        let mut col = 1;
6✔
213
        let mut line_offset = 1;
4✔
214

215
        for item in body {
16✔
216
            if let DisplayLine::Source {
10✔
NEW
217
                line:
×
NEW
218
                    DisplaySourceLine::Content {
×
219
                        text,
4✔
220
                        range,
6✔
221
                        end_line,
4✔
222
                    },
223
                lineno,
6✔
NEW
224
                ..
×
NEW
225
            } = item
×
226
            {
227
                if main_range >= range.0 && main_range < range.1 + max(*end_line as usize, 1) {
4✔
228
                    let char_column = text[0..(main_range - range.0).min(text.len())]
6✔
229
                        .chars()
230
                        .count();
231
                    col = char_column + 1;
10✔
232
                    line_offset = lineno.unwrap_or(1);
4✔
NEW
233
                    break;
×
234
                }
235
            }
236
        }
237

238
        return Some(DisplayLine::Raw(DisplayRawLine::Origin {
6✔
NEW
239
            path,
×
240
            pos: Some((line_offset, col)),
6✔
241
            header_type: display_header,
4✔
242
        }));
243
    }
244

245
    if let Some(path) = origin {
3✔
246
        return Some(DisplayLine::Raw(DisplayRawLine::Origin {
1✔
NEW
247
            path,
×
248
            pos: None,
1✔
249
            header_type: display_header,
1✔
250
        }));
251
    }
252

253
    None
2✔
254
}
255

256
fn fold_prefix_suffix(mut snippet: snippet::Snippet<'_>) -> snippet::Snippet<'_> {
4✔
257
    if !snippet.fold {
6✔
258
        return snippet;
3✔
259
    }
260

261
    let ann_start = snippet
8✔
262
        .annotations
263
        .iter()
264
        .map(|ann| ann.range.start)
8✔
265
        .min()
266
        .unwrap_or(0);
267
    if let Some(before_new_start) = snippet.source[0..ann_start].rfind('\n') {
3✔
268
        let new_start = before_new_start + 1;
8✔
269

270
        let line_offset = newline_count(&snippet.source[..new_start]);
8✔
271
        snippet.line_start += line_offset;
5✔
272

273
        snippet.source = &snippet.source[new_start..];
8✔
274

275
        for ann in &mut snippet.annotations {
8✔
276
            let range_start = ann.range.start - new_start;
5✔
277
            let range_end = ann.range.end - new_start;
8✔
278
            ann.range = range_start..range_end;
3✔
279
        }
280
    }
281

282
    let ann_end = snippet
13✔
283
        .annotations
284
        .iter()
285
        .map(|ann| ann.range.end)
8✔
286
        .max()
287
        .unwrap_or(snippet.source.len());
3✔
288
    if let Some(end_offset) = snippet.source[ann_end..].find('\n') {
7✔
289
        let new_end = ann_end + end_offset;
6✔
290
        snippet.source = &snippet.source[..new_end];
6✔
291
    }
292

293
    snippet
3✔
294
}
295

296
fn newline_count(body: &str) -> usize {
5✔
297
    #[cfg(feature = "simd")]
298
    {
299
        memchr::memchr_iter(b'\n', body.as_bytes()).count()
300
    }
301
    #[cfg(not(feature = "simd"))]
302
    {
303
        body.lines().count()
3✔
304
    }
305
}
306

307
fn fold_body(body: Vec<DisplayLine<'_>>) -> Vec<DisplayLine<'_>> {
3✔
308
    const INNER_CONTEXT: usize = 1;
309
    const INNER_UNFOLD_SIZE: usize = INNER_CONTEXT * 2 + 1;
310

311
    let mut lines = vec![];
3✔
312
    let mut unhighlighted_lines = vec![];
3✔
313
    for line in body {
16✔
314
        match &line {
3✔
315
            DisplayLine::Source { annotations, .. } => {
3✔
316
                if annotations.is_empty() {
6✔
317
                    unhighlighted_lines.push(line);
4✔
318
                } else {
319
                    if lines.is_empty() {
7✔
320
                        // Ignore leading unhighlighted lines
321
                        unhighlighted_lines.clear();
4✔
322
                    }
323
                    match unhighlighted_lines.len() {
8✔
324
                        0 => {}
325
                        n if n <= INNER_UNFOLD_SIZE => {
2✔
326
                            // Rather than render `...`, don't fold
327
                            lines.append(&mut unhighlighted_lines);
2✔
328
                        }
329
                        _ => {
330
                            lines.extend(unhighlighted_lines.drain(..INNER_CONTEXT));
5✔
331
                            let inline_marks = lines
3✔
332
                                .last()
333
                                .and_then(|line| {
3✔
334
                                    if let DisplayLine::Source {
5✔
335
                                        ref inline_marks, ..
3✔
336
                                    } = line
3✔
337
                                    {
338
                                        let inline_marks = inline_marks.clone();
3✔
339
                                        Some(inline_marks)
3✔
340
                                    } else {
NEW
341
                                        None
×
342
                                    }
343
                                })
344
                                .unwrap_or_default();
345
                            lines.push(DisplayLine::Fold {
2✔
346
                                inline_marks: inline_marks.clone(),
2✔
347
                            });
348
                            unhighlighted_lines
2✔
349
                                .drain(..unhighlighted_lines.len().saturating_sub(INNER_CONTEXT));
4✔
350
                            lines.append(&mut unhighlighted_lines);
2✔
351
                        }
352
                    }
353
                    lines.push(line);
8✔
354
                }
355
            }
356
            _ => {
NEW
357
                unhighlighted_lines.push(line);
×
358
            }
359
        }
360
    }
361

362
    lines
4✔
363
}
364

365
fn format_body(
7✔
366
    snippet: snippet::Snippet<'_>,
367
    need_empty_header: bool,
368
    term_width: usize,
369
    anonymized_line_numbers: bool,
370
) -> DisplaySet<'_> {
371
    let source_len = snippet.source.len();
10✔
372
    if let Some(bigger) = snippet.annotations.iter().find_map(|x| {
10✔
373
        // Allow highlighting one past the last character in the source.
374
        if source_len + 1 < x.range.end {
6✔
375
            Some(&x.range)
1✔
376
        } else {
377
            None
7✔
378
        }
379
    }) {
380
        panic!("SourceAnnotation range `{bigger:?}` is beyond the end of buffer `{source_len}`")
2✔
381
    }
382

383
    let mut body = vec![];
7✔
384
    let mut current_line = snippet.line_start;
3✔
385
    let mut current_index = 0;
7✔
386

387
    let mut whitespace_margin = usize::MAX;
3✔
388
    let mut span_left_margin = usize::MAX;
7✔
389
    let mut span_right_margin = 0;
3✔
390
    let mut label_right_margin = 0;
6✔
391
    let mut max_line_len = 0;
4✔
392

393
    let mut depth_map: HashMap<usize, usize> = HashMap::new();
6✔
394
    let mut current_depth = 0;
4✔
395
    let mut annotations = snippet.annotations;
6✔
396
    let ranges = annotations
10✔
397
        .iter()
398
        .map(|a| a.range.clone())
9✔
399
        .collect::<Vec<_>>();
400
    // We want to merge multiline annotations that have the same range into one
401
    // multiline annotation to save space. This is done by making any duplicate
402
    // multiline annotations into a single-line annotation pointing at the end
403
    // of the range.
404
    //
405
    // 3 |       X0 Y0 Z0
406
    //   |  _____^
407
    //   | | ____|
408
    //   | || ___|
409
    //   | |||
410
    // 4 | |||   X1 Y1 Z1
411
    // 5 | |||   X2 Y2 Z2
412
    //   | |||    ^
413
    //   | |||____|
414
    //   |  ||____`X` is a good letter
415
    //   |   |____`Y` is a good letter too
416
    //   |        `Z` label
417
    // Should be
418
    // error: foo
419
    //  --> test.rs:3:3
420
    //   |
421
    // 3 | /   X0 Y0 Z0
422
    // 4 | |   X1 Y1 Z1
423
    // 5 | |   X2 Y2 Z2
424
    //   | |    ^
425
    //   | |____|
426
    //   |      `X` is a good letter
427
    //   |      `Y` is a good letter too
428
    //   |      `Z` label
429
    //   |
430
    ranges.iter().enumerate().for_each(|(r_idx, range)| {
20✔
431
        annotations
10✔
432
            .iter_mut()
433
            .enumerate()
434
            .skip(r_idx + 1)
7✔
435
            .for_each(|(ann_idx, ann)| {
12✔
436
                // Skip if the annotation's index matches the range index
437
                if ann_idx != r_idx
4✔
438
                    // We only want to merge multiline annotations
439
                    && snippet.source[ann.range.clone()].lines().count() > 1
3✔
440
                    // We only want to merge annotations that have the same range
441
                    && ann.range.start == range.start
3✔
442
                    && ann.range.end == range.end
1✔
443
                {
444
                    ann.range.start = ann.range.end.saturating_sub(1);
2✔
445
                }
446
            });
447
    });
448
    annotations.sort_by_key(|a| a.range.start);
12✔
449
    let mut annotations = annotations.into_iter().enumerate().collect::<Vec<_>>();
5✔
450

451
    for (idx, (line, end_line)) in CursorLines::new(snippet.source).enumerate() {
12✔
452
        let line_length: usize = line.len();
8✔
453
        let line_range = (current_index, current_index + line_length);
6✔
454
        let end_line_size = end_line.len();
9✔
455
        body.push(DisplayLine::Source {
6✔
456
            lineno: Some(current_line),
4✔
457
            inline_marks: vec![],
6✔
458
            line: DisplaySourceLine::Content {
3✔
459
                text: line,
460
                range: line_range,
461
                end_line,
462
            },
463
            annotations: vec![],
7✔
464
        });
465

466
        let leading_whitespace = line
7✔
467
            .chars()
468
            .take_while(|c| c.is_whitespace())
10✔
469
            .map(|c| {
2✔
470
                match c {
2✔
471
                    // Tabs are displayed as 4 spaces
472
                    '\t' => 4,
1✔
473
                    _ => 1,
2✔
474
                }
475
            })
476
            .sum();
477
        if line.chars().any(|c| !c.is_whitespace()) {
18✔
478
            whitespace_margin = min(whitespace_margin, leading_whitespace);
6✔
479
        }
480
        max_line_len = max(max_line_len, line_length);
9✔
481

482
        let line_start_index = line_range.0;
7✔
483
        let line_end_index = line_range.1;
3✔
484
        current_line += 1;
7✔
485
        current_index += line_length + end_line_size;
10✔
486

487
        // It would be nice to use filter_drain here once it's stable.
488
        annotations.retain(|(key, annotation)| {
12✔
489
            let body_idx = idx;
6✔
490
            let annotation_type = match annotation.level {
4✔
491
                snippet::Level::Error => DisplayAnnotationType::None,
6✔
492
                snippet::Level::Warning => DisplayAnnotationType::None,
2✔
493
                _ => DisplayAnnotationType::from(annotation.level),
1✔
494
            };
495
            let label_right = annotation.label.map_or(0, |label| label.len() + 1);
13✔
496
            match annotation.range {
497
                // This handles if the annotation is on the next line. We add
498
                // the `end_line_size` to account for annotating the line end.
499
                Range { start, .. } if start > line_end_index + end_line_size => true,
8✔
500
                // This handles the case where an annotation is contained
501
                // within the current line including any line-end characters.
502
                Range { start, end }
10✔
503
                    if start >= line_start_index
5✔
504
                        // We add at least one to `line_end_index` to allow
505
                        // highlighting the end of a file
506
                        && end <= line_end_index + max(end_line_size, 1) =>
5✔
507
                {
508
                    if let DisplayLine::Source {
5✔
509
                        ref mut annotations,
3✔
510
                        ..
511
                    } = body[body_idx]
3✔
512
                    {
513
                        let annotation_start_col = line
8✔
514
                            [0..(start - line_start_index).min(line_length)]
8✔
515
                            .chars()
516
                            .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0))
8✔
517
                            .sum::<usize>();
518
                        let mut annotation_end_col = line
8✔
519
                            [0..(end - line_start_index).min(line_length)]
8✔
520
                            .chars()
521
                            .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0))
8✔
522
                            .sum::<usize>();
523
                        if annotation_start_col == annotation_end_col {
9✔
524
                            // At least highlight something
525
                            annotation_end_col += 1;
2✔
526
                        }
527

528
                        span_left_margin = min(span_left_margin, annotation_start_col);
3✔
529
                        span_right_margin = max(span_right_margin, annotation_end_col);
5✔
530
                        label_right_margin =
3✔
531
                            max(label_right_margin, annotation_end_col + label_right);
8✔
532

533
                        let range = (annotation_start_col, annotation_end_col);
5✔
534
                        annotations.push(DisplaySourceAnnotation {
3✔
535
                            annotation: Annotation {
5✔
536
                                annotation_type,
3✔
537
                                id: None,
5✔
538
                                label: format_label(annotation.label, None),
3✔
539
                            },
540
                            range,
541
                            annotation_type: DisplayAnnotationType::from(annotation.level),
3✔
542
                            annotation_part: DisplayAnnotationPart::Standalone,
5✔
543
                        });
544
                    }
545
                    false
5✔
546
                }
547
                // This handles the case where a multiline annotation starts
548
                // somewhere on the current line, including any line-end chars
549
                Range { start, end }
6✔
550
                    if start >= line_start_index
4✔
551
                        // The annotation can start on a line ending
552
                        && start <= line_end_index + end_line_size.saturating_sub(1)
7✔
553
                        && end > line_end_index =>
4✔
554
                {
555
                    if let DisplayLine::Source {
8✔
556
                        ref mut annotations,
4✔
557
                        ..
558
                    } = body[body_idx]
4✔
559
                    {
560
                        let annotation_start_col = line
7✔
561
                            [0..(start - line_start_index).min(line_length)]
7✔
562
                            .chars()
563
                            .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0))
7✔
564
                            .sum::<usize>();
565
                        let annotation_end_col = annotation_start_col + 1;
7✔
566

567
                        span_left_margin = min(span_left_margin, annotation_start_col);
4✔
568
                        span_right_margin = max(span_right_margin, annotation_end_col);
5✔
569
                        label_right_margin =
4✔
570
                            max(label_right_margin, annotation_end_col + label_right);
10✔
571

572
                        let range = (annotation_start_col, annotation_end_col);
3✔
573
                        annotations.push(DisplaySourceAnnotation {
4✔
574
                            annotation: Annotation {
3✔
575
                                annotation_type,
4✔
576
                                id: None,
3✔
577
                                label: vec![],
4✔
578
                            },
579
                            range,
580
                            annotation_type: DisplayAnnotationType::from(annotation.level),
4✔
581
                            annotation_part: DisplayAnnotationPart::MultilineStart(current_depth),
4✔
582
                        });
583
                        depth_map.insert(*key, current_depth);
4✔
584
                        current_depth += 1;
4✔
585
                    }
586
                    true
4✔
587
                }
588
                // This handles the case where a multiline annotation starts
589
                // somewhere before this line and ends after it as well
590
                Range { start, end }
7✔
591
                    if start < line_start_index && end > line_end_index + max(end_line_size, 1) =>
7✔
592
                {
593
                    if let DisplayLine::Source {
4✔
594
                        ref mut inline_marks,
4✔
595
                        ..
596
                    } = body[body_idx]
4✔
597
                    {
598
                        let depth = depth_map.get(key).cloned().unwrap_or_default();
4✔
599
                        inline_marks.push(DisplayMark {
4✔
600
                            mark_type: DisplayMarkType::AnnotationThrough(depth),
4✔
601
                            annotation_type: DisplayAnnotationType::from(annotation.level),
4✔
602
                        });
603
                    }
604
                    true
4✔
605
                }
606
                // This handles the case where a multiline annotation ends
607
                // somewhere on the current line, including any line-end chars
608
                Range { start, end }
6✔
609
                    if start < line_start_index
3✔
610
                        && end >= line_start_index
3✔
611
                        // We add at least one to `line_end_index` to allow
612
                        // highlighting the end of a file
613
                        && end <= line_end_index + max(end_line_size, 1) =>
3✔
614
                {
615
                    if let DisplayLine::Source {
3✔
616
                        ref mut annotations,
3✔
617
                        ..
618
                    } = body[body_idx]
3✔
619
                    {
620
                        let end_mark = line[0..(end - line_start_index).min(line_length)]
10✔
621
                            .chars()
622
                            .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0))
6✔
623
                            .sum::<usize>()
624
                            .saturating_sub(1);
625
                        // If the annotation ends on a line-end character, we
626
                        // need to annotate one past the end of the line
627
                        let (end_mark, end_plus_one) = if end > line_end_index
15✔
628
                            // Special case for highlighting the end of a file
629
                            || (end == line_end_index + 1 && end_line_size == 0)
7✔
630
                        {
631
                            (end_mark + 1, end_mark + 2)
3✔
632
                        } else {
633
                            (end_mark, end_mark + 1)
7✔
634
                        };
635

636
                        span_left_margin = min(span_left_margin, end_mark);
6✔
637
                        span_right_margin = max(span_right_margin, end_plus_one);
3✔
638
                        label_right_margin = max(label_right_margin, end_plus_one + label_right);
9✔
639

640
                        let range = (end_mark, end_plus_one);
6✔
641
                        let depth = depth_map.remove(key).unwrap_or(0);
3✔
642
                        annotations.push(DisplaySourceAnnotation {
6✔
643
                            annotation: Annotation {
3✔
644
                                annotation_type,
6✔
645
                                id: None,
3✔
646
                                label: format_label(annotation.label, None),
6✔
647
                            },
648
                            range,
649
                            annotation_type: DisplayAnnotationType::from(annotation.level),
6✔
650
                            annotation_part: DisplayAnnotationPart::MultilineEnd(depth),
3✔
651
                        });
652
                    }
653
                    false
3✔
654
                }
655
                _ => true,
2✔
656
            }
657
        });
658
        // Reset the depth counter, but only after we've processed all
659
        // annotations for a given line.
660
        let max = depth_map.len();
3✔
661
        if current_depth > max {
8✔
662
            current_depth = max;
6✔
663
        }
664
    }
665

666
    if snippet.fold {
6✔
667
        body = fold_body(body);
3✔
668
    }
669

670
    if need_empty_header {
6✔
671
        body.insert(
6✔
672
            0,
673
            DisplayLine::Source {
3✔
674
                lineno: None,
3✔
675
                inline_marks: vec![],
6✔
676
                line: DisplaySourceLine::Empty,
3✔
677
                annotations: vec![],
6✔
678
            },
679
        );
680
    }
681

682
    let max_line_num_len = if anonymized_line_numbers {
3✔
683
        ANONYMIZED_LINE_NUM.len()
4✔
684
    } else {
685
        current_line.to_string().len()
16✔
686
    };
687

688
    let width_offset = 3 + max_line_num_len;
10✔
689

690
    if span_left_margin == usize::MAX {
5✔
691
        span_left_margin = 0;
1✔
692
    }
693

694
    let margin = Margin::new(
695
        whitespace_margin,
6✔
696
        span_left_margin,
4✔
697
        span_right_margin,
6✔
698
        label_right_margin,
4✔
699
        term_width.saturating_sub(width_offset),
700
        max_line_len,
6✔
701
    );
702

703
    DisplaySet {
704
        display_lines: body,
705
        margin,
706
    }
707
}
708

709
// We replace some characters so the CLI output is always consistent and underlines aligned.
710
const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[
711
    ('\t', "    "),   // We do our own tab replacement
712
    ('\u{200D}', ""), // Replace ZWJ with nothing for consistent terminal output of grapheme clusters.
713
    ('\u{202A}', ""), // The following unicode text flow control characters are inconsistently
714
    ('\u{202B}', ""), // supported across CLIs and can cause confusion due to the bytes on disk
715
    ('\u{202D}', ""), // not corresponding to the visible source code, so we replace them always.
716
    ('\u{202E}', ""),
717
    ('\u{2066}', ""),
718
    ('\u{2067}', ""),
719
    ('\u{2068}', ""),
720
    ('\u{202C}', ""),
721
    ('\u{2069}', ""),
722
];
723

724
fn normalize_whitespace(str: &str) -> String {
3✔
725
    let mut s = str.to_owned();
5✔
726
    for (c, replacement) in OUTPUT_REPLACEMENTS {
21✔
727
        s = s.replace(*c, replacement);
4✔
728
    }
729
    s
3✔
730
}
731

732
fn overlaps(
6✔
733
    a1: &DisplaySourceAnnotation<'_>,
734
    a2: &DisplaySourceAnnotation<'_>,
735
    padding: usize,
736
) -> bool {
737
    (a2.range.0..a2.range.1).contains(&a1.range.0)
11✔
738
        || (a1.range.0..a1.range.1 + padding).contains(&a2.range.0)
6✔
739
}
740

741
fn format_inline_marks(
5✔
742
    line: usize,
743
    inline_marks: &[DisplayMark],
744
    lineno_width: usize,
745
    stylesheet: &Stylesheet,
746
    buf: &mut StyledBuffer,
747
) -> fmt::Result {
748
    for mark in inline_marks.iter() {
7✔
749
        let annotation_style = get_annotation_style(&mark.annotation_type, stylesheet);
3✔
750
        match mark.mark_type {
751
            DisplayMarkType::AnnotationThrough(depth) => {
3✔
752
                buf.putc(line, 3 + lineno_width + depth, '|', *annotation_style);
3✔
753
            }
754
        };
755
    }
756
    Ok(())
5✔
757
}
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