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

rust-lang / annotate-snippets-rs / 15862044996

24 Jun 2025 09:37PM UTC coverage: 86.621%. Remained the same
15862044996

Pull #222

github

web-flow
Merge a0d26c6aa into a81dc31d2
Pull Request #222: test: Ensure all examples have a test

1392 of 1607 relevant lines covered (86.62%)

4.5 hits per line

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

84.54
/src/renderer/source_map.rs
1
use crate::renderer::{char_width, is_different, num_overlap, LineAnnotation, LineAnnotationType};
2
use crate::{Annotation, AnnotationKind, Patch};
3
use std::cmp::{max, min};
4
use std::ops::Range;
5

6
#[derive(Debug)]
7
pub(crate) struct SourceMap<'a> {
8
    lines: Vec<LineInfo<'a>>,
9
    pub(crate) source: &'a str,
10
}
11

12
impl<'a> SourceMap<'a> {
13
    pub(crate) fn new(source: &'a str, line_start: usize) -> Self {
5✔
14
        // Empty sources do have a "line", but it is empty, so we need to add
15
        // a line with an empty string to the source map.
16
        if source.is_empty() {
7✔
17
            return Self {
2✔
18
                lines: vec![LineInfo {
4✔
19
                    line: "",
×
20
                    line_index: line_start,
×
21
                    start_byte: 0,
×
22
                    end_byte: 0,
×
23
                    end_line_size: 0,
×
24
                }],
25
                source,
×
26
            };
27
        }
28

29
        let mut current_index = 0;
4✔
30

31
        let mut mapping = vec![];
8✔
32
        for (idx, (line, end_line)) in CursorLines::new(source).enumerate() {
14✔
33
            let line_length = line.len();
8✔
34
            let line_range = current_index..current_index + line_length;
3✔
35
            let end_line_size = end_line.len();
11✔
36

37
            mapping.push(LineInfo {
6✔
38
                line,
×
39
                line_index: line_start + idx,
6✔
40
                start_byte: line_range.start,
×
41
                end_byte: line_range.end + end_line_size,
6✔
42
                end_line_size,
×
43
            });
44

45
            current_index += line_length + end_line_size;
6✔
46
        }
47
        Self {
48
            lines: mapping,
49
            source,
50
        }
51
    }
52

53
    pub(crate) fn get_line(&self, idx: usize) -> Option<&'a str> {
3✔
54
        self.lines
4✔
55
            .iter()
56
            .find(|l| l.line_index == idx)
7✔
57
            .map(|info| info.line)
7✔
58
    }
59

60
    pub(crate) fn span_to_locations(&self, span: Range<usize>) -> (Loc, Loc) {
8✔
61
        let start_info = self
16✔
62
            .lines
×
63
            .iter()
64
            .find(|info| span.start >= info.start_byte && span.start < info.end_byte)
12✔
65
            .unwrap_or(self.lines.last().unwrap());
7✔
66
        let (mut start_char_pos, start_display_pos) = start_info.line
11✔
67
            [0..(span.start - start_info.start_byte).min(start_info.line.len())]
8✔
68
            .chars()
69
            .fold((0, 0), |(char_pos, byte_pos), c| {
12✔
70
                let display = char_width(c);
8✔
71
                (char_pos + 1, byte_pos + display)
8✔
72
            });
73
        // correct the char pos if we are highlighting the end of a line
74
        if (span.start - start_info.start_byte).saturating_sub(start_info.line.len()) > 0 {
15✔
75
            start_char_pos += 1;
3✔
76
        }
77
        let start = Loc {
78
            line: start_info.line_index,
4✔
79
            char: start_char_pos,
80
            display: start_display_pos,
81
            byte: span.start,
6✔
82
        };
83

84
        if span.start == span.end {
4✔
85
            return (start, start);
3✔
86
        }
87

88
        let end_info = self
14✔
89
            .lines
×
90
            .iter()
91
            .find(|info| span.end >= info.start_byte && span.end < info.end_byte)
13✔
92
            .unwrap_or(self.lines.last().unwrap());
4✔
93
        let (end_char_pos, end_display_pos) = end_info.line
8✔
94
            [0..(span.end - end_info.start_byte).min(end_info.line.len())]
9✔
95
            .chars()
96
            .fold((0, 0), |(char_pos, byte_pos), c| {
7✔
97
                let display = char_width(c);
3✔
98
                (char_pos + 1, byte_pos + display)
4✔
99
            });
100

101
        let mut end = Loc {
102
            line: end_info.line_index,
4✔
103
            char: end_char_pos,
104
            display: end_display_pos,
105
            byte: span.end,
4✔
106
        };
107
        if start.line != end.line && end.byte > end_info.end_byte - end_info.end_line_size {
13✔
108
            end.char += 1;
2✔
109
            end.display += 1;
4✔
110
        }
111

112
        (start, end)
4✔
113
    }
114

115
    pub(crate) fn span_to_snippet(&self, span: Range<usize>) -> Option<&str> {
3✔
116
        self.source.get(span)
3✔
117
    }
118

119
    pub(crate) fn span_to_lines(&self, span: Range<usize>) -> Vec<&LineInfo<'a>> {
2✔
120
        let mut lines = vec![];
2✔
121
        let start = span.start;
2✔
122
        let end = span.end;
2✔
123
        for line_info in &self.lines {
4✔
124
            if start >= line_info.end_byte {
2✔
125
                continue;
×
126
            }
127
            if end <= line_info.start_byte {
2✔
128
                break;
×
129
            }
130
            lines.push(line_info);
2✔
131
        }
132
        lines
2✔
133
    }
134

135
    pub(crate) fn annotated_lines(
8✔
136
        &self,
137
        annotations: Vec<Annotation<'a>>,
138
        fold: bool,
139
    ) -> (usize, Vec<AnnotatedLineInfo<'a>>) {
140
        let source_len = self.source.len();
16✔
141
        if let Some(bigger) = annotations.iter().find_map(|x| {
16✔
142
            // Allow highlighting one past the last character in the source.
143
            if source_len + 1 < x.span.end {
18✔
144
                Some(&x.span)
1✔
145
            } else {
146
                None
7✔
147
            }
148
        }) {
149
            panic!("Annotation range `{bigger:?}` is beyond the end of buffer `{source_len}`")
2✔
150
        }
151

152
        let mut annotated_line_infos = self
16✔
153
            .lines
×
154
            .iter()
155
            .map(|info| AnnotatedLineInfo {
14✔
156
                line: info.line,
9✔
157
                line_index: info.line_index,
7✔
158
                annotations: vec![],
9✔
159
            })
160
            .collect::<Vec<_>>();
161
        let mut multiline_annotations = vec![];
9✔
162

163
        for Annotation {
7✔
164
            span,
8✔
165
            label,
8✔
166
            kind,
8✔
167
            highlight_source,
8✔
168
        } in annotations
24✔
169
        {
170
            let (lo, mut hi) = self.span_to_locations(span.clone());
16✔
171

172
            // Watch out for "empty spans". If we get a span like 6..6, we
173
            // want to just display a `^` at 6, so convert that to
174
            // 6..7. This is degenerate input, but it's best to degrade
175
            // gracefully -- and the parser likes to supply a span like
176
            // that for EOF, in particular.
177

178
            if lo.display == hi.display && lo.line == hi.line {
10✔
179
                hi.display += 1;
3✔
180
            }
181

182
            if lo.line == hi.line {
4✔
183
                let line_ann = LineAnnotation {
184
                    start: lo,
185
                    end: hi,
186
                    kind,
187
                    label,
188
                    annotation_type: LineAnnotationType::Singleline,
189
                    highlight_source,
190
                };
191
                self.add_annotation_to_file(&mut annotated_line_infos, lo.line, line_ann);
6✔
192
            } else {
193
                multiline_annotations.push(MultilineAnnotation {
11✔
194
                    depth: 1,
×
195
                    start: lo,
×
196
                    end: hi,
4✔
197
                    kind,
×
198
                    label,
×
199
                    overlaps_exactly: false,
×
200
                    highlight_source,
×
201
                });
202
            }
203
        }
204

205
        let mut primary_spans = vec![];
3✔
206

207
        // Find overlapping multiline annotations, put them at different depths
208
        multiline_annotations.sort_by_key(|ml| (ml.start.line, usize::MAX - ml.end.line));
13✔
209
        for ann in multiline_annotations.clone() {
8✔
210
            if ann.kind.is_primary() {
9✔
211
                primary_spans.push((ann.start, ann.end));
5✔
212
            }
213
            for a in &mut multiline_annotations {
9✔
214
                // Move all other multiline annotations overlapping with this one
215
                // one level to the right.
216
                if !ann.same_span(a)
5✔
217
                    && num_overlap(ann.start.line, ann.end.line, a.start.line, a.end.line, true)
4✔
218
                {
219
                    a.increase_depth();
2✔
220
                } else if ann.same_span(a) && &ann != a {
14✔
221
                    a.overlaps_exactly = true;
1✔
222
                } else {
223
                    if primary_spans
19✔
224
                        .iter()
225
                        .any(|(s, e)| a.start == *s && a.end == *e)
13✔
226
                    {
227
                        a.kind = AnnotationKind::Primary;
4✔
228
                    }
229
                    break;
×
230
                }
231
            }
232
        }
233

234
        let mut max_depth = 0; // max overlapping multiline spans
3✔
235
        for ann in &multiline_annotations {
7✔
236
            max_depth = max(max_depth, ann.depth);
8✔
237
        }
238
        // Change order of multispan depth to minimize the number of overlaps in the ASCII art.
239
        for a in &mut multiline_annotations {
7✔
240
            a.depth = max_depth - a.depth + 1;
8✔
241
        }
242
        for ann in multiline_annotations {
6✔
243
            let mut end_ann = ann.as_end();
4✔
244
            if ann.overlaps_exactly {
5✔
245
                end_ann.annotation_type = LineAnnotationType::Singleline;
1✔
246
            } else {
247
                // avoid output like
248
                //
249
                //  |        foo(
250
                //  |   _____^
251
                //  |  |_____|
252
                //  | ||         bar,
253
                //  | ||     );
254
                //  | ||      ^
255
                //  | ||______|
256
                //  |  |______foo
257
                //  |         baz
258
                //
259
                // and instead get
260
                //
261
                //  |       foo(
262
                //  |  _____^
263
                //  | |         bar,
264
                //  | |     );
265
                //  | |      ^
266
                //  | |      |
267
                //  | |______foo
268
                //  |        baz
269
                self.add_annotation_to_file(
5✔
270
                    &mut annotated_line_infos,
×
271
                    ann.start.line,
4✔
272
                    ann.as_start(),
3✔
273
                );
274
                // 4 is the minimum vertical length of a multiline span when presented: two lines
275
                // of code and two lines of underline. This is not true for the special case where
276
                // the beginning doesn't have an underline, but the current logic seems to be
277
                // working correctly.
278
                let middle = min(ann.start.line + 4, ann.end.line);
3✔
279
                // We'll show up to 4 lines past the beginning of the multispan start.
280
                // We will *not* include the tail of lines that are only whitespace, a comment or
281
                // a bare delimiter.
282
                let filter = |s: &str| {
3✔
283
                    let s = s.trim();
5✔
284
                    // Consider comments as empty, but don't consider docstrings to be empty.
285
                    !(s.starts_with("//") && !(s.starts_with("///") || s.starts_with("//!")))
9✔
286
                        // Consider lines with nothing but whitespace, a single delimiter as empty.
287
                        && !["", "{", "}", "(", ")", "[", "]"].contains(&s)
5✔
288
                };
289
                let until = (ann.start.line..middle)
9✔
290
                    .rev()
291
                    .filter_map(|line| self.get_line(line).map(|s| (line + 1, s)))
14✔
292
                    .find(|(_, s)| filter(s))
7✔
293
                    .map_or(ann.start.line, |(line, _)| line);
12✔
294
                for line in ann.start.line + 1..until {
4✔
295
                    // Every `|` that joins the beginning of the span (`___^`) to the end (`|__^`).
296
                    self.add_annotation_to_file(&mut annotated_line_infos, line, ann.as_line());
6✔
297
                }
298
                let line_end = ann.end.line - 1;
3✔
299
                let end_is_empty = self.get_line(line_end).map_or(false, |s| !filter(s));
12✔
300
                if middle < line_end && !end_is_empty {
6✔
301
                    self.add_annotation_to_file(&mut annotated_line_infos, line_end, ann.as_line());
2✔
302
                }
303
            }
304
            self.add_annotation_to_file(&mut annotated_line_infos, end_ann.end.line, end_ann);
7✔
305
        }
306

307
        if fold {
3✔
308
            annotated_line_infos.retain(|l| !l.annotations.is_empty());
10✔
309
        }
310

311
        (max_depth, annotated_line_infos)
3✔
312
    }
313

314
    fn add_annotation_to_file(
3✔
315
        &self,
316
        annotated_line_infos: &mut Vec<AnnotatedLineInfo<'a>>,
317
        line_index: usize,
318
        line_ann: LineAnnotation<'a>,
319
    ) {
320
        if let Some(line_info) = annotated_line_infos
4✔
321
            .iter_mut()
322
            .find(|line_info| line_info.line_index == line_index)
7✔
323
        {
324
            line_info.annotations.push(line_ann);
3✔
325
        } else {
326
            let info = self
×
327
                .lines
×
328
                .iter()
329
                .find(|l| l.line_index == line_index)
×
330
                .unwrap();
331
            annotated_line_infos.push(AnnotatedLineInfo {
×
332
                line: info.line,
×
333
                line_index,
×
334
                annotations: vec![line_ann],
×
335
            });
336
            annotated_line_infos.sort_by_key(|l| l.line_index);
×
337
        }
338
    }
339

340
    pub(crate) fn splice_lines<'b>(
2✔
341
        &'b self,
342
        mut patches: Vec<Patch<'b>>,
343
    ) -> Vec<(String, Vec<Patch<'b>>, Vec<Vec<SubstitutionHighlight>>)> {
344
        fn push_trailing(
3✔
345
            buf: &mut String,
346
            line_opt: Option<&str>,
347
            lo: &Loc,
348
            hi_opt: Option<&Loc>,
349
        ) -> usize {
350
            let mut line_count = 0;
3✔
351
            // Convert CharPos to Usize, as CharPose is character offset
352
            // Extract low index and high index
353
            let (lo, hi_opt) = (lo.char, hi_opt.map(|hi| hi.char));
9✔
354
            if let Some(line) = line_opt {
3✔
355
                if let Some(lo) = line.char_indices().map(|(i, _)| i).nth(lo) {
12✔
356
                    // Get high index while account for rare unicode and emoji with char_indices
357
                    let hi_opt = hi_opt.and_then(|hi| line.char_indices().map(|(i, _)| i).nth(hi));
12✔
358
                    match hi_opt {
3✔
359
                        // If high index exist, take string from low to high index
360
                        Some(hi) if hi > lo => {
5✔
361
                            // count how many '\n' exist
362
                            line_count = line[lo..hi].matches('\n').count();
3✔
363
                            buf.push_str(&line[lo..hi]);
2✔
364
                        }
365
                        Some(_) => (),
×
366
                        // If high index absence, take string from low index till end string.len
367
                        None => {
×
368
                            // count how many '\n' exist
369
                            line_count = line[lo..].matches('\n').count();
2✔
370
                            buf.push_str(&line[lo..]);
3✔
371
                        }
372
                    }
373
                }
374
                // If high index is None
375
                if hi_opt.is_none() {
2✔
376
                    buf.push('\n');
2✔
377
                }
378
            }
379
            line_count
2✔
380
        }
381
        // Assumption: all spans are in the same file, and all spans
382
        // are disjoint. Sort in ascending order.
383
        patches.sort_by_key(|p| p.span.start);
6✔
384

385
        // Find the bounding span.
386
        let Some(lo) = patches.iter().map(|p| p.span.start).min() else {
6✔
387
            return Vec::new();
×
388
        };
389
        let Some(hi) = patches.iter().map(|p| p.span.end).max() else {
8✔
390
            return Vec::new();
×
391
        };
392

393
        let lines = self.span_to_lines(lo..hi);
2✔
394

395
        let mut highlights = vec![];
2✔
396
        // To build up the result, we do this for each span:
397
        // - push the line segment trailing the previous span
398
        //   (at the beginning a "phantom" span pointing at the start of the line)
399
        // - push lines between the previous and current span (if any)
400
        // - if the previous and current span are not on the same line
401
        //   push the line segment leading up to the current span
402
        // - splice in the span substitution
403
        //
404
        // Finally push the trailing line segment of the last span
405
        let (mut prev_hi, _) = self.span_to_locations(lo..hi);
4✔
406
        prev_hi.char = 0;
2✔
407
        let mut prev_line = lines.first().map(|line| line.line);
6✔
408
        let mut buf = String::new();
2✔
409

410
        let mut line_highlight = vec![];
2✔
411
        // We need to keep track of the difference between the existing code and the added
412
        // or deleted code in order to point at the correct column *after* substitution.
413
        let mut acc = 0;
2✔
414
        for part in &mut patches {
4✔
415
            // If this is a replacement of, e.g. `"a"` into `"ab"`, adjust the
416
            // suggestion and snippet to look as if we just suggested to add
417
            // `"b"`, which is typically much easier for the user to understand.
418
            part.trim_trivial_replacements(self);
3✔
419
            let (cur_lo, cur_hi) = self.span_to_locations(part.span.clone());
3✔
420
            if prev_hi.line == cur_lo.line {
3✔
421
                let mut count = push_trailing(&mut buf, prev_line, &prev_hi, Some(&cur_lo));
5✔
422
                while count > 0 {
3✔
423
                    highlights.push(std::mem::take(&mut line_highlight));
×
424
                    acc = 0;
×
425
                    count -= 1;
×
426
                }
427
            } else {
428
                acc = 0;
1✔
429
                highlights.push(std::mem::take(&mut line_highlight));
2✔
430
                let mut count = push_trailing(&mut buf, prev_line, &prev_hi, None);
2✔
431
                while count > 0 {
2✔
432
                    highlights.push(std::mem::take(&mut line_highlight));
×
433
                    count -= 1;
×
434
                }
435
                // push lines between the previous and current span (if any)
436
                for idx in prev_hi.line + 1..(cur_lo.line) {
4✔
437
                    if let Some(line) = self.get_line(idx) {
2✔
438
                        buf.push_str(line.as_ref());
1✔
439
                        buf.push('\n');
1✔
440
                        highlights.push(std::mem::take(&mut line_highlight));
1✔
441
                    }
442
                }
443
                if let Some(cur_line) = self.get_line(cur_lo.line) {
2✔
444
                    let end = match cur_line.char_indices().nth(cur_lo.char) {
4✔
445
                        Some((i, _)) => i,
2✔
446
                        None => cur_line.len(),
×
447
                    };
448
                    buf.push_str(&cur_line[..end]);
2✔
449
                }
450
            }
451
            // Add a whole line highlight per line in the snippet.
452
            let len: isize = part
9✔
453
                .replacement
×
454
                .split('\n')
455
                .next()
456
                .unwrap_or(part.replacement)
2✔
457
                .chars()
458
                .map(|c| match c {
4✔
459
                    '\t' => 4,
×
460
                    _ => 1,
2✔
461
                })
462
                .sum();
463
            if !is_different(self, part.replacement, part.span.clone()) {
2✔
464
                // Account for cases where we are suggesting the same code that's already
465
                // there. This shouldn't happen often, but in some cases for multipart
466
                // suggestions it's much easier to handle it here than in the origin.
467
            } else {
468
                line_highlight.push(SubstitutionHighlight {
5✔
469
                    start: (cur_lo.char as isize + acc) as usize,
2✔
470
                    end: (cur_lo.char as isize + acc + len) as usize,
5✔
471
                });
472
            }
473
            buf.push_str(part.replacement);
3✔
474
            // Account for the difference between the width of the current code and the
475
            // snippet being suggested, so that the *later* suggestions are correctly
476
            // aligned on the screen. Note that cur_hi and cur_lo can be on different
477
            // lines, so cur_hi.col can be smaller than cur_lo.col
478
            acc += len - (cur_hi.char as isize - cur_lo.char as isize);
2✔
479
            prev_hi = cur_hi;
3✔
480
            prev_line = self.get_line(prev_hi.line);
5✔
481
            for line in part.replacement.split('\n').skip(1) {
2✔
482
                acc = 0;
1✔
483
                highlights.push(std::mem::take(&mut line_highlight));
1✔
484
                let end: usize = line
1✔
485
                    .chars()
486
                    .map(|c| match c {
2✔
487
                        '\t' => 4,
×
488
                        _ => 1,
1✔
489
                    })
490
                    .sum();
491
                line_highlight.push(SubstitutionHighlight { start: 0, end });
1✔
492
            }
493
        }
494
        highlights.push(std::mem::take(&mut line_highlight));
3✔
495
        // if the replacement already ends with a newline, don't print the next line
496
        if !buf.ends_with('\n') {
2✔
497
            push_trailing(&mut buf, prev_line, &prev_hi, None);
5✔
498
        }
499
        // remove trailing newlines
500
        while buf.ends_with('\n') {
5✔
501
            buf.pop();
5✔
502
        }
503
        if highlights.iter().all(|parts| parts.is_empty()) {
10✔
504
            Vec::new()
×
505
        } else {
506
            vec![(buf, patches, highlights)]
5✔
507
        }
508
    }
509
}
510

511
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
512
pub(crate) struct MultilineAnnotation<'a> {
513
    pub depth: usize,
514
    pub start: Loc,
515
    pub end: Loc,
516
    pub kind: AnnotationKind,
517
    pub label: Option<&'a str>,
518
    pub overlaps_exactly: bool,
519
    pub highlight_source: bool,
520
}
521

522
impl<'a> MultilineAnnotation<'a> {
523
    pub(crate) fn increase_depth(&mut self) {
2✔
524
        self.depth += 1;
2✔
525
    }
526

527
    /// Compare two `MultilineAnnotation`s considering only the `Span` they cover.
528
    pub(crate) fn same_span(&self, other: &MultilineAnnotation<'_>) -> bool {
5✔
529
        self.start == other.start && self.end == other.end
4✔
530
    }
531

532
    pub(crate) fn as_start(&self) -> LineAnnotation<'a> {
4✔
533
        LineAnnotation {
534
            start: self.start,
4✔
535
            end: Loc {
5✔
536
                line: self.start.line,
537
                char: self.start.char + 1,
538
                display: self.start.display + 1,
539
                byte: self.start.byte + 1,
540
            },
541
            kind: self.kind,
4✔
542
            label: None,
543
            annotation_type: LineAnnotationType::MultilineStart(self.depth),
5✔
544
            highlight_source: self.highlight_source,
4✔
545
        }
546
    }
547

548
    pub(crate) fn as_end(&self) -> LineAnnotation<'a> {
4✔
549
        LineAnnotation {
550
            start: Loc {
4✔
551
                line: self.end.line,
552
                char: self.end.char.saturating_sub(1),
553
                display: self.end.display.saturating_sub(1),
554
                byte: self.end.byte.saturating_sub(1),
555
            },
556
            end: self.end,
4✔
557
            kind: self.kind,
4✔
558
            label: self.label,
4✔
559
            annotation_type: LineAnnotationType::MultilineEnd(self.depth),
4✔
560
            highlight_source: self.highlight_source,
4✔
561
        }
562
    }
563

564
    pub(crate) fn as_line(&self) -> LineAnnotation<'a> {
3✔
565
        LineAnnotation {
566
            start: Loc::default(),
3✔
567
            end: Loc::default(),
3✔
568
            kind: self.kind,
3✔
569
            label: None,
570
            annotation_type: LineAnnotationType::MultilineLine(self.depth),
3✔
571
            highlight_source: self.highlight_source,
3✔
572
        }
573
    }
574
}
575

576
#[derive(Debug)]
577
pub(crate) struct LineInfo<'a> {
578
    pub(crate) line: &'a str,
579
    pub(crate) line_index: usize,
580
    pub(crate) start_byte: usize,
581
    pub(crate) end_byte: usize,
582
    end_line_size: usize,
583
}
584

585
#[derive(Debug)]
586
pub(crate) struct AnnotatedLineInfo<'a> {
587
    pub(crate) line: &'a str,
588
    pub(crate) line_index: usize,
589
    pub(crate) annotations: Vec<LineAnnotation<'a>>,
590
}
591

592
/// A source code location used for error reporting.
593
#[derive(Clone, Copy, Debug, Default, PartialOrd, Ord, PartialEq, Eq)]
594
pub(crate) struct Loc {
595
    /// The (1-based) line number.
596
    pub(crate) line: usize,
597
    /// The (0-based) column offset.
598
    pub(crate) char: usize,
599
    /// The (0-based) column offset when displayed.
600
    pub(crate) display: usize,
601
    /// The (0-based) byte offset.
602
    pub(crate) byte: usize,
603
}
604

605
struct CursorLines<'a>(&'a str);
606

607
impl CursorLines<'_> {
608
    fn new(src: &str) -> CursorLines<'_> {
8✔
609
        CursorLines(src)
610
    }
611
}
612

613
#[derive(Copy, Clone, Debug, PartialEq)]
614
enum EndLine {
615
    Eof,
616
    Lf,
617
    Crlf,
618
}
619

620
impl EndLine {
621
    /// The number of characters this line ending occupies in bytes.
622
    pub(crate) fn len(self) -> usize {
3✔
623
        match self {
7✔
624
            EndLine::Eof => 0,
625
            EndLine::Lf => 1,
626
            EndLine::Crlf => 2,
627
        }
628
    }
629
}
630

631
impl<'a> Iterator for CursorLines<'a> {
632
    type Item = (&'a str, EndLine);
633

634
    fn next(&mut self) -> Option<Self::Item> {
8✔
635
        if self.0.is_empty() {
4✔
636
            None
8✔
637
        } else {
638
            self.0
7✔
639
                .find('\n')
640
                .map(|x| {
4✔
641
                    let ret = if 0 < x {
9✔
642
                        if self.0.as_bytes()[x - 1] == b'\r' {
14✔
643
                            (&self.0[..x - 1], EndLine::Crlf)
4✔
644
                        } else {
645
                            (&self.0[..x], EndLine::Lf)
4✔
646
                        }
647
                    } else {
648
                        ("", EndLine::Lf)
3✔
649
                    };
650
                    self.0 = &self.0[x + 1..];
8✔
651
                    ret
5✔
652
                })
653
                .or_else(|| {
3✔
654
                    let ret = Some((self.0, EndLine::Eof));
5✔
655
                    self.0 = "";
3✔
656
                    ret
×
657
                })
658
        }
659
    }
660
}
661

662
/// Used to translate between `Span`s and byte positions within a single output line in highlighted
663
/// code of structured suggestions.
664
#[derive(Debug, Clone, Copy)]
665
pub(crate) struct SubstitutionHighlight {
666
    pub(crate) start: usize,
667
    pub(crate) end: usize,
668
}
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