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

rust-lang / annotate-snippets-rs / 15600187927

12 Jun 2025 02:30AM UTC coverage: 86.521% (-0.8%) from 87.308%
15600187927

Pull #216

github

web-flow
Merge 47e7ec6a8 into ab692e676
Pull Request #216: fix: Match how rustc handles annotating newlines

2 of 2 new or added lines in 1 file covered. (100.0%)

15 existing lines in 1 file now uncovered.

1348 of 1558 relevant lines covered (86.52%)

4.62 hits per line

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

86.27
/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 {
8✔
14
        let mut current_index = 0;
8✔
15

16
        let mut mapping = vec![];
8✔
17
        for (idx, (line, end_line)) in CursorLines::new(source).enumerate() {
26✔
18
            let line_length = line.len();
14✔
19
            let line_range = current_index..current_index + line_length;
6✔
20
            let end_line_size = end_line.len();
15✔
21

22
            mapping.push(LineInfo {
11✔
23
                line,
×
24
                line_index: line_start + idx,
8✔
25
                start_byte: line_range.start,
×
26
                end_byte: line_range.end + end_line_size,
9✔
27
                end_line_size,
×
28
            });
29

30
            current_index += line_length + end_line_size;
9✔
31
        }
32
        Self {
33
            lines: mapping,
34
            source,
35
        }
36
    }
37

38
    pub(crate) fn get_line(&self, idx: usize) -> Option<&'a str> {
5✔
39
        self.lines
6✔
40
            .iter()
41
            .find(|l| l.line_index == idx)
11✔
42
            .map(|info| info.line)
11✔
43
    }
44

45
    pub(crate) fn span_to_locations(&self, span: Range<usize>) -> (Loc, Loc) {
9✔
46
        let start_info = self
14✔
47
            .lines
×
48
            .iter()
49
            .find(|info| span.start >= info.start_byte && span.start < info.end_byte)
16✔
50
            .unwrap_or(self.lines.last().unwrap());
9✔
51
        let (mut start_char_pos, start_display_pos) = start_info.line
16✔
52
            [0..(span.start - start_info.start_byte).min(start_info.line.len())]
8✔
53
            .chars()
54
            .fold((0, 0), |(char_pos, byte_pos), c| {
16✔
55
                let display = char_width(c);
9✔
56
                (char_pos + 1, byte_pos + display)
8✔
57
            });
58
        // correct the char pos if we are highlighting the end of a line
59
        if (span.start - start_info.start_byte).saturating_sub(start_info.line.len()) > 0 {
18✔
60
            start_char_pos += 1;
3✔
61
        }
62
        let start = Loc {
63
            line: start_info.line_index,
7✔
64
            char: start_char_pos,
65
            display: start_display_pos,
66
            byte: span.start,
6✔
67
        };
68

69
        if span.start == span.end {
8✔
70
            return (start, start);
4✔
71
        }
72

73
        let end_info = self
20✔
74
            .lines
×
75
            .iter()
76
            .find(|info| span.end >= info.start_byte && span.end < info.end_byte)
20✔
77
            .unwrap_or(self.lines.last().unwrap());
6✔
78
        let (end_char_pos, end_display_pos) = end_info.line
13✔
79
            [0..(span.end - end_info.start_byte).min(end_info.line.len())]
15✔
80
            .chars()
81
            .fold((0, 0), |(char_pos, byte_pos), c| {
10✔
82
                let display = char_width(c);
5✔
83
                (char_pos + 1, byte_pos + display)
5✔
84
            });
85

86
        let mut end = Loc {
87
            line: end_info.line_index,
3✔
88
            char: end_char_pos,
89
            display: end_display_pos,
90
            byte: span.end,
3✔
91
        };
92
        if start.line != end.line && end.byte > end_info.end_byte - end_info.end_line_size {
8✔
93
            end.char += 1;
2✔
94
            end.display += 1;
4✔
95
        }
96

97
        (start, end)
6✔
98
    }
99

100
    pub(crate) fn span_to_snippet(&self, span: Range<usize>) -> Option<&str> {
2✔
101
        self.source.get(span)
2✔
102
    }
103

104
    pub(crate) fn span_to_lines(&self, span: Range<usize>) -> Vec<&LineInfo<'a>> {
2✔
105
        let mut lines = vec![];
2✔
106
        let start = span.start;
2✔
107
        let end = span.end;
2✔
108
        for line_info in &self.lines {
4✔
109
            if start >= line_info.end_byte {
2✔
110
                continue;
×
111
            }
112
            if end <= line_info.start_byte {
2✔
113
                break;
×
114
            }
115
            lines.push(line_info);
2✔
116
        }
117
        lines
2✔
118
    }
119

120
    pub(crate) fn annotated_lines(
10✔
121
        &self,
122
        annotations: Vec<Annotation<'a>>,
123
        fold: bool,
124
    ) -> (usize, Vec<AnnotatedLineInfo<'a>>) {
125
        let source_len = self.source.len();
16✔
126
        if let Some(bigger) = annotations.iter().find_map(|x| {
16✔
127
            // Allow highlighting one past the last character in the source.
128
            if source_len + 1 < x.span.end {
14✔
129
                Some(&x.span)
1✔
130
            } else {
131
                None
9✔
132
            }
133
        }) {
134
            panic!("Annotation range `{bigger:?}` is beyond the end of buffer `{source_len}`")
2✔
135
        }
136

137
        let mut annotated_line_infos = self
16✔
138
            .lines
×
139
            .iter()
140
            .map(|info| AnnotatedLineInfo {
17✔
141
                line: info.line,
7✔
142
                line_index: info.line_index,
9✔
143
                annotations: vec![],
7✔
144
            })
145
            .collect::<Vec<_>>();
146
        let mut multiline_annotations = vec![];
8✔
147

148
        for Annotation {
8✔
149
            span,
8✔
150
            label,
8✔
151
            kind,
8✔
152
            highlight_source,
8✔
153
        } in annotations
24✔
154
        {
155
            let (lo, mut hi) = self.span_to_locations(span.clone());
15✔
156

157
            // Watch out for "empty spans". If we get a span like 6..6, we
158
            // want to just display a `^` at 6, so convert that to
159
            // 6..7. This is degenerate input, but it's best to degrade
160
            // gracefully -- and the parser likes to supply a span like
161
            // that for EOF, in particular.
162

163
            if lo.display == hi.display && lo.line == hi.line {
15✔
164
                hi.display += 1;
4✔
165
            }
166

167
            if lo.line == hi.line {
7✔
168
                let line_ann = LineAnnotation {
169
                    start: lo,
170
                    end: hi,
171
                    kind,
172
                    label,
173
                    annotation_type: LineAnnotationType::Singleline,
174
                    highlight_source,
175
                };
176
                self.add_annotation_to_file(&mut annotated_line_infos, lo.line, line_ann);
7✔
177
            } else {
178
                multiline_annotations.push(MultilineAnnotation {
9✔
179
                    depth: 1,
×
180
                    start: lo,
×
181
                    end: hi,
5✔
182
                    kind,
×
183
                    label,
×
184
                    overlaps_exactly: false,
×
185
                    highlight_source,
×
186
                });
187
            }
188
        }
189

190
        let mut primary_spans = vec![];
3✔
191

192
        // Find overlapping multiline annotations, put them at different depths
193
        multiline_annotations.sort_by_key(|ml| (ml.start.line, usize::MAX - ml.end.line));
11✔
194
        for ann in multiline_annotations.clone() {
8✔
195
            if ann.kind.is_primary() {
10✔
196
                primary_spans.push((ann.start, ann.end));
6✔
197
            }
198
            for a in &mut multiline_annotations {
10✔
199
                // Move all other multiline annotations overlapping with this one
200
                // one level to the right.
201
                if !ann.same_span(a)
5✔
202
                    && num_overlap(ann.start.line, ann.end.line, a.start.line, a.end.line, true)
4✔
203
                {
204
                    a.increase_depth();
2✔
205
                } else if ann.same_span(a) && &ann != a {
16✔
206
                    a.overlaps_exactly = true;
1✔
207
                } else {
208
                    if primary_spans
19✔
209
                        .iter()
210
                        .any(|(s, e)| a.start == *s && a.end == *e)
13✔
211
                    {
212
                        a.kind = AnnotationKind::Primary;
4✔
213
                    }
214
                    break;
×
215
                }
216
            }
217
        }
218

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

292
        if fold {
3✔
293
            annotated_line_infos.retain(|l| !l.annotations.is_empty());
10✔
294
        }
295

296
        for l in annotated_line_infos.iter_mut() {
6✔
297
            l.annotations.sort_by(|a, b| a.start.cmp(&b.start));
13✔
298
        }
299

300
        (max_depth, annotated_line_infos)
4✔
301
    }
302

303
    fn add_annotation_to_file(
4✔
304
        &self,
305
        annotated_line_infos: &mut Vec<AnnotatedLineInfo<'a>>,
306
        line_index: usize,
307
        line_ann: LineAnnotation<'a>,
308
    ) {
309
        if let Some(line_info) = annotated_line_infos
3✔
310
            .iter_mut()
311
            .find(|line_info| line_info.line_index == line_index)
7✔
312
        {
313
            line_info.annotations.push(line_ann);
3✔
314
        } else {
315
            let info = self
×
316
                .lines
×
317
                .iter()
318
                .find(|l| l.line_index == line_index)
×
319
                .unwrap();
320
            annotated_line_infos.push(AnnotatedLineInfo {
×
321
                line: info.line,
×
322
                line_index,
×
323
                annotations: vec![line_ann],
×
324
            });
325
            annotated_line_infos.sort_by_key(|l| l.line_index);
×
326
        }
327
    }
328

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

374
        // Find the bounding span.
375
        let Some(lo) = patches.iter().map(|p| p.span.start).min() else {
6✔
376
            return Vec::new();
×
377
        };
378
        let Some(hi) = patches.iter().map(|p| p.span.end).max() else {
8✔
379
            return Vec::new();
×
380
        };
381

382
        let lines = self.span_to_lines(lo..hi);
2✔
383

384
        let mut highlights = vec![];
2✔
385
        // To build up the result, we do this for each span:
386
        // - push the line segment trailing the previous span
387
        //   (at the beginning a "phantom" span pointing at the start of the line)
388
        // - push lines between the previous and current span (if any)
389
        // - if the previous and current span are not on the same line
390
        //   push the line segment leading up to the current span
391
        // - splice in the span substitution
392
        //
393
        // Finally push the trailing line segment of the last span
394
        let (mut prev_hi, _) = self.span_to_locations(lo..hi);
4✔
395
        prev_hi.char = 0;
2✔
396
        let mut prev_line = lines.first().map(|line| line.line);
6✔
397
        let mut buf = String::new();
2✔
398

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

500
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
501
pub(crate) struct MultilineAnnotation<'a> {
502
    pub depth: usize,
503
    pub start: Loc,
504
    pub end: Loc,
505
    pub kind: AnnotationKind,
506
    pub label: Option<&'a str>,
507
    pub overlaps_exactly: bool,
508
    pub highlight_source: bool,
509
}
510

511
impl<'a> MultilineAnnotation<'a> {
512
    pub(crate) fn increase_depth(&mut self) {
2✔
513
        self.depth += 1;
2✔
514
    }
515

516
    /// Compare two `MultilineAnnotation`s considering only the `Span` they cover.
517
    pub(crate) fn same_span(&self, other: &MultilineAnnotation<'_>) -> bool {
5✔
518
        self.start == other.start && self.end == other.end
4✔
519
    }
520

521
    pub(crate) fn as_start(&self) -> LineAnnotation<'a> {
5✔
522
        LineAnnotation {
523
            start: self.start,
4✔
524
            end: Loc {
5✔
525
                line: self.start.line,
526
                char: self.start.char + 1,
527
                display: self.start.display + 1,
528
                byte: self.start.byte + 1,
529
            },
530
            kind: self.kind,
4✔
531
            label: None,
532
            annotation_type: LineAnnotationType::MultilineStart(self.depth),
5✔
533
            highlight_source: self.highlight_source,
5✔
534
        }
535
    }
536

537
    pub(crate) fn as_end(&self) -> LineAnnotation<'a> {
4✔
538
        LineAnnotation {
539
            start: Loc {
4✔
540
                line: self.end.line,
541
                char: self.end.char.saturating_sub(1),
542
                display: self.end.display.saturating_sub(1),
543
                byte: self.end.byte.saturating_sub(1),
544
            },
545
            end: self.end,
4✔
546
            kind: self.kind,
5✔
547
            label: self.label,
4✔
548
            annotation_type: LineAnnotationType::MultilineEnd(self.depth),
4✔
549
            highlight_source: self.highlight_source,
4✔
550
        }
551
    }
552

553
    pub(crate) fn as_line(&self) -> LineAnnotation<'a> {
3✔
554
        LineAnnotation {
555
            start: Loc::default(),
3✔
556
            end: Loc::default(),
3✔
557
            kind: self.kind,
3✔
558
            label: None,
559
            annotation_type: LineAnnotationType::MultilineLine(self.depth),
3✔
560
            highlight_source: self.highlight_source,
3✔
561
        }
562
    }
563
}
564

565
#[derive(Debug)]
566
pub(crate) struct LineInfo<'a> {
567
    pub(crate) line: &'a str,
568
    pub(crate) line_index: usize,
569
    pub(crate) start_byte: usize,
570
    pub(crate) end_byte: usize,
571
    end_line_size: usize,
572
}
573

574
#[derive(Debug)]
575
pub(crate) struct AnnotatedLineInfo<'a> {
576
    pub(crate) line: &'a str,
577
    pub(crate) line_index: usize,
578
    pub(crate) annotations: Vec<LineAnnotation<'a>>,
579
}
580

581
/// A source code location used for error reporting.
582
#[derive(Clone, Copy, Debug, Default, PartialOrd, Ord, PartialEq, Eq)]
583
pub(crate) struct Loc {
584
    /// The (1-based) line number.
585
    pub(crate) line: usize,
586
    /// The (0-based) column offset.
587
    pub(crate) char: usize,
588
    /// The (0-based) column offset when displayed.
589
    pub(crate) display: usize,
590
    /// The (0-based) byte offset.
591
    pub(crate) byte: usize,
592
}
593

594
struct CursorLines<'a>(&'a str);
595

596
impl CursorLines<'_> {
597
    fn new(src: &str) -> CursorLines<'_> {
8✔
598
        CursorLines(src)
599
    }
600
}
601

602
#[derive(Copy, Clone, Debug, PartialEq)]
603
enum EndLine {
604
    Eof,
605
    Lf,
606
    Crlf,
607
}
608

609
impl EndLine {
610
    /// The number of characters this line ending occupies in bytes.
611
    pub(crate) fn len(self) -> usize {
6✔
612
        match self {
8✔
613
            EndLine::Eof => 0,
614
            EndLine::Lf => 1,
615
            EndLine::Crlf => 2,
616
        }
617
    }
618
}
619

620
impl<'a> Iterator for CursorLines<'a> {
621
    type Item = (&'a str, EndLine);
622

623
    fn next(&mut self) -> Option<Self::Item> {
9✔
624
        if self.0.is_empty() {
7✔
625
            None
9✔
626
        } else {
627
            self.0
9✔
628
                .find('\n')
629
                .map(|x| {
6✔
630
                    let ret = if 0 < x {
12✔
631
                        if self.0.as_bytes()[x - 1] == b'\r' {
15✔
632
                            (&self.0[..x - 1], EndLine::Crlf)
4✔
633
                        } else {
634
                            (&self.0[..x], EndLine::Lf)
4✔
635
                        }
636
                    } else {
637
                        ("", EndLine::Lf)
4✔
638
                    };
639
                    self.0 = &self.0[x + 1..];
10✔
640
                    ret
8✔
641
                })
642
                .or_else(|| {
5✔
643
                    let ret = Some((self.0, EndLine::Eof));
5✔
644
                    self.0 = "";
5✔
645
                    ret
×
646
                })
647
        }
648
    }
649
}
650

651
/// Used to translate between `Span`s and byte positions within a single output line in highlighted
652
/// code of structured suggestions.
653
#[derive(Debug, Clone, Copy)]
654
pub(crate) struct SubstitutionHighlight {
655
    pub(crate) start: usize,
656
    pub(crate) end: usize,
657
}
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