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

rust-lang / annotate-snippets-rs / 14501550287

16 Apr 2025 08:05PM UTC coverage: 87.161% (+1.3%) from 85.84%
14501550287

push

github

web-flow
Merge pull request #195 from Muscraft/new-api

New api

1246 of 1415 new or added lines in 7 files covered. (88.06%)

2 existing lines in 1 file now uncovered.

1351 of 1550 relevant lines covered (87.16%)

4.15 hits per line

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

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

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

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

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

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

45
    pub(crate) fn span_to_locations(&self, span: Range<usize>) -> (Loc, Loc) {
4✔
46
        let start_info = self
10✔
NEW
47
            .lines
×
48
            .iter()
49
            .find(|info| span.start >= info.start_byte && span.start < info.end_byte)
10✔
50
            .unwrap_or(self.lines.last().unwrap());
5✔
51
        let (mut start_char_pos, start_display_pos) = start_info.line
10✔
52
            [0..(span.start - start_info.start_byte).min(start_info.line.len())]
5✔
53
            .chars()
54
            .fold((0, 0), |(char_pos, byte_pos), c| {
10✔
55
                let display = char_width(c);
5✔
56
                (char_pos + 1, byte_pos + display)
6✔
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 {
16✔
60
            start_char_pos += 1;
2✔
61
        }
62
        let start = Loc {
63
            line: start_info.line_index,
6✔
64
            char: start_char_pos,
65
            display: start_display_pos,
66
            byte: span.start,
6✔
67
        };
68

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

73
        let end_info = self
17✔
NEW
74
            .lines
×
75
            .iter()
76
            .find(|info| info.end_byte > span.end.saturating_sub(1))
16✔
77
            .unwrap_or(self.lines.last().unwrap());
4✔
78
        let (mut end_char_pos, end_display_pos) = end_info.line
9✔
79
            [0..(span.end - end_info.start_byte).min(end_info.line.len())]
9✔
80
            .chars()
81
            .fold((0, 0), |(char_pos, byte_pos), c| {
9✔
82
                let display = char_width(c);
3✔
83
                (char_pos + 1, byte_pos + display)
5✔
84
            });
85

86
        // correct the char pos if we are highlighting the end of a line
87
        if (span.end - end_info.start_byte).saturating_sub(end_info.line.len()) > 0 {
12✔
88
            end_char_pos += 1;
2✔
89
        }
90
        let mut end = Loc {
91
            line: end_info.line_index,
5✔
92
            char: end_char_pos,
93
            display: end_display_pos,
94
            byte: span.end,
6✔
95
        };
96
        if start.line != end.line && end.byte > end_info.end_byte - end_info.end_line_size {
12✔
97
            end.char += 1;
1✔
98
            end.display += 1;
2✔
99
        }
100

101
        (start, end)
5✔
102
    }
103

104
    pub(crate) fn span_to_snippet(&self, span: Range<usize>) -> Option<&str> {
1✔
105
        self.source.get(span)
1✔
106
    }
107

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

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

141
        let mut annotated_line_infos = self
10✔
NEW
142
            .lines
×
143
            .iter()
144
            .map(|info| AnnotatedLineInfo {
8✔
145
                line: info.line,
6✔
146
                line_index: info.line_index,
4✔
147
                annotations: vec![],
6✔
148
            })
149
            .collect::<Vec<_>>();
150
        let mut multiline_annotations = vec![];
6✔
151

152
        for Annotation {
4✔
153
            range,
5✔
154
            label,
5✔
155
            kind,
5✔
156
            highlight_source,
8✔
157
        } in annotations
15✔
158
        {
159
            let (lo, mut hi) = self.span_to_locations(range.clone());
9✔
160

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

167
            if lo.display == hi.display && lo.line == hi.line {
12✔
168
                hi.display += 1;
3✔
169
            }
170

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

194
        let mut primary_spans = vec![];
3✔
195

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

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

297
        if fold {
3✔
298
            annotated_line_infos.retain(|l| !l.annotations.is_empty());
9✔
299
        }
300

301
        annotated_line_infos
8✔
302
            .iter_mut()
303
            .for_each(|l| l.annotations.sort_by(|a, b| a.start.cmp(&b.start)));
14✔
304

305
        (max_depth, annotated_line_infos)
5✔
306
    }
307

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

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

379
        // Find the bounding span.
380
        let Some(lo) = patches.iter().map(|p| p.range.start).min() else {
3✔
NEW
381
            return Vec::new();
×
382
        };
383
        let Some(hi) = patches.iter().map(|p| p.range.end).max() else {
4✔
NEW
384
            return Vec::new();
×
385
        };
386

387
        let lines = self.span_to_lines(lo..hi);
1✔
388

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

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

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

516
impl<'a> MultilineAnnotation<'a> {
517
    pub(crate) fn increase_depth(&mut self) {
2✔
518
        self.depth += 1;
2✔
519
    }
520

521
    /// Compare two `MultilineAnnotation`s considering only the `Span` they cover.
522
    pub(crate) fn same_span(&self, other: &MultilineAnnotation<'_>) -> bool {
3✔
523
        self.start == other.start && self.end == other.end
3✔
524
    }
525

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

542
    pub(crate) fn as_end(&self) -> LineAnnotation<'a> {
3✔
543
        LineAnnotation {
544
            start: Loc {
3✔
545
                line: self.end.line,
546
                char: self.end.char.saturating_sub(1),
547
                display: self.end.display.saturating_sub(1),
548
                byte: self.end.byte.saturating_sub(1),
549
            },
550
            end: self.end,
3✔
551
            kind: self.kind,
3✔
552
            label: self.label,
3✔
553
            annotation_type: LineAnnotationType::MultilineEnd(self.depth),
3✔
554
            highlight_source: self.highlight_source,
3✔
555
        }
556
    }
557

558
    pub(crate) fn as_line(&self) -> LineAnnotation<'a> {
3✔
559
        LineAnnotation {
560
            start: Loc::default(),
3✔
561
            end: Loc::default(),
4✔
562
            kind: self.kind,
4✔
563
            label: None,
564
            annotation_type: LineAnnotationType::MultilineLine(self.depth),
4✔
565
            highlight_source: self.highlight_source,
4✔
566
        }
567
    }
568
}
569

570
#[derive(Debug)]
571
pub(crate) struct LineInfo<'a> {
572
    pub(crate) line: &'a str,
573
    pub(crate) line_index: usize,
574
    pub(crate) start_byte: usize,
575
    pub(crate) end_byte: usize,
576
    end_line_size: usize,
577
}
578

579
#[derive(Debug)]
580
pub(crate) struct AnnotatedLineInfo<'a> {
581
    pub(crate) line: &'a str,
582
    pub(crate) line_index: usize,
583
    pub(crate) annotations: Vec<LineAnnotation<'a>>,
584
}
585

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

599
struct CursorLines<'a>(&'a str);
600

601
impl CursorLines<'_> {
602
    fn new(src: &str) -> CursorLines<'_> {
7✔
603
        CursorLines(src)
604
    }
605
}
606

607
#[derive(Copy, Clone, Debug, PartialEq)]
608
enum EndLine {
609
    Eof,
610
    Lf,
611
    Crlf,
612
}
613

614
impl EndLine {
615
    /// The number of characters this line ending occupies in bytes.
616
    pub(crate) fn len(self) -> usize {
7✔
617
        match self {
6✔
618
            EndLine::Eof => 0,
619
            EndLine::Lf => 1,
620
            EndLine::Crlf => 2,
621
        }
622
    }
623
}
624

625
impl<'a> Iterator for CursorLines<'a> {
626
    type Item = (&'a str, EndLine);
627

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

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