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

rust-lang / annotate-snippets-rs / 16152807289

08 Jul 2025 07:41PM UTC coverage: 87.981% (-0.09%) from 88.067%
16152807289

Pull #254

github

web-flow
Merge a6be8a314 into 9ba3defe7
Pull Request #254: fix: Match rustc's annotation overlap

19 of 22 new or added lines in 2 files covered. (86.36%)

64 existing lines in 2 files now uncovered.

1486 of 1689 relevant lines covered (87.98%)

4.74 hits per line

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

84.84
/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::borrow::Cow;
4
use std::cmp::{max, min, Ordering};
5
use std::ops::Range;
6

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

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

30
        let mut current_index = 0;
10✔
31

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

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

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

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

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

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

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

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

113
        (start, end)
7✔
114
    }
115

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

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

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

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

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

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

179
            if lo.display == hi.display && lo.line == hi.line {
12✔
180
                hi.display += 1;
4✔
181
            }
182

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

206
        let mut primary_spans = vec![];
5✔
207

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

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

312
        if fold {
6✔
313
            annotated_line_infos.retain(|l| !l.annotations.is_empty());
21✔
314
        }
315

316
        (max_depth, annotated_line_infos)
5✔
317
    }
318

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

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

387
        let source_len = self.source.len();
6✔
388
        if let Some(bigger) = patches.iter().find_map(|x| {
6✔
389
            // Allow patching one past the last character in the source.
390
            if source_len + 1 < x.span.end {
6✔
391
                Some(&x.span)
1✔
392
            } else {
393
                None
3✔
394
            }
395
        }) {
396
            panic!("Patch span `{bigger:?}` is beyond the end of buffer `{source_len}`")
2✔
397
        }
398

399
        // Assumption: all spans are in the same file, and all spans
400
        // are disjoint. Sort in ascending order.
401
        patches.sort_by_key(|p| p.span.start);
10✔
402

403
        // Find the bounding span.
404
        let Some(lo) = patches.iter().map(|p| p.span.start).min() else {
11✔
UNCOV
405
            return Vec::new();
×
406
        };
407
        let Some(hi) = patches.iter().map(|p| p.span.end).max() else {
16✔
UNCOV
408
            return Vec::new();
×
409
        };
410

411
        let lines = self.span_to_lines(lo..hi);
3✔
412

413
        let mut highlights = vec![];
5✔
414
        // To build up the result, we do this for each span:
415
        // - push the line segment trailing the previous span
416
        //   (at the beginning a "phantom" span pointing at the start of the line)
417
        // - push lines between the previous and current span (if any)
418
        // - if the previous and current span are not on the same line
419
        //   push the line segment leading up to the current span
420
        // - splice in the span substitution
421
        //
422
        // Finally push the trailing line segment of the last span
423
        let (mut prev_hi, _) = self.span_to_locations(lo..hi);
9✔
424
        prev_hi.char = 0;
5✔
425
        let mut prev_line = lines.first().map(|line| line.line);
15✔
426
        let mut buf = String::new();
5✔
427

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

529
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
530
pub(crate) struct MultilineAnnotation<'a> {
531
    pub depth: usize,
532
    pub start: Loc,
533
    pub end: Loc,
534
    pub kind: AnnotationKind,
535
    pub label: Option<Cow<'a, str>>,
536
    pub overlaps_exactly: bool,
537
    pub highlight_source: bool,
538
}
539

540
impl<'a> MultilineAnnotation<'a> {
541
    pub(crate) fn increase_depth(&mut self) {
2✔
542
        self.depth += 1;
2✔
543
    }
544

545
    /// Compare two `MultilineAnnotation`s considering only the `Span` they cover.
546
    pub(crate) fn same_span(&self, other: &MultilineAnnotation<'_>) -> bool {
5✔
547
        self.start == other.start && self.end == other.end
5✔
548
    }
549

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

566
    pub(crate) fn as_end(&self) -> LineAnnotation<'a> {
4✔
567
        LineAnnotation {
568
            start: Loc {
4✔
569
                line: self.end.line,
570
                char: self.end.char.saturating_sub(1),
571
                display: self.end.display.saturating_sub(1),
572
                byte: self.end.byte.saturating_sub(1),
573
            },
574
            end: self.end,
3✔
575
            kind: self.kind,
4✔
576
            label: self.label.clone(),
4✔
577
            annotation_type: LineAnnotationType::MultilineEnd(self.depth),
4✔
578
            highlight_source: self.highlight_source,
4✔
579
        }
580
    }
581

582
    pub(crate) fn as_line(&self, line: usize) -> LineAnnotation<'a> {
3✔
583
        LineAnnotation {
584
            start: Loc {
3✔
585
                line,
586
                ..Default::default()
587
            },
588
            end: Loc {
3✔
589
                line,
590
                ..Default::default()
591
            },
592
            kind: self.kind,
3✔
593
            label: None,
594
            annotation_type: LineAnnotationType::MultilineLine(self.depth),
3✔
595
            highlight_source: self.highlight_source,
3✔
596
        }
597
    }
598
}
599

600
#[derive(Debug)]
601
pub(crate) struct LineInfo<'a> {
602
    pub(crate) line: &'a str,
603
    pub(crate) line_index: usize,
604
    pub(crate) start_byte: usize,
605
    pub(crate) end_byte: usize,
606
    end_line_size: usize,
607
}
608

609
#[derive(Debug)]
610
pub(crate) struct AnnotatedLineInfo<'a> {
611
    pub(crate) line: &'a str,
612
    pub(crate) line_index: usize,
613
    pub(crate) annotations: Vec<LineAnnotation<'a>>,
614
}
615

616
/// A source code location used for error reporting.
617
#[derive(Clone, Copy, Debug, Default, Eq)]
618
pub(crate) struct Loc {
619
    /// The (1-based) line number.
620
    pub(crate) line: usize,
621
    /// The (0-based) column offset.
622
    pub(crate) char: usize,
623
    /// The (0-based) column offset when displayed.
624
    pub(crate) display: usize,
625
    /// The (0-based) byte offset.
626
    pub(crate) byte: usize,
627
}
628

629
impl PartialEq for Loc {
630
    fn eq(&self, other: &Self) -> bool {
5✔
631
        self.line.eq(&other.line) && self.display.eq(&other.display) && self.char.eq(&other.char)
5✔
632
    }
633
}
634

635
impl Ord for Loc {
636
    fn cmp(&self, other: &Self) -> Ordering {
3✔
637
        match self.line.cmp(&other.line) {
3✔
638
            Ordering::Equal => match self.display.cmp(&other.display) {
3✔
639
                Ordering::Equal => self.char.cmp(&other.char),
2✔
640
                c => c,
3✔
641
            },
NEW
UNCOV
642
            c => c,
×
643
        }
644
    }
645
}
646

647
impl PartialOrd for Loc {
648
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
3✔
649
        Some(self.cmp(other))
3✔
650
    }
651
}
652

653
struct CursorLines<'a>(&'a str);
654

655
impl CursorLines<'_> {
656
    fn new(src: &str) -> CursorLines<'_> {
8✔
657
        CursorLines(src)
658
    }
659
}
660

661
#[derive(Copy, Clone, Debug, PartialEq)]
662
enum EndLine {
663
    Eof,
664
    Lf,
665
    Crlf,
666
}
667

668
impl EndLine {
669
    /// The number of characters this line ending occupies in bytes.
670
    pub(crate) fn len(self) -> usize {
7✔
671
        match self {
9✔
672
            EndLine::Eof => 0,
673
            EndLine::Lf => 1,
674
            EndLine::Crlf => 2,
675
        }
676
    }
677
}
678

679
impl<'a> Iterator for CursorLines<'a> {
680
    type Item = (&'a str, EndLine);
681

682
    fn next(&mut self) -> Option<Self::Item> {
8✔
683
        if self.0.is_empty() {
9✔
684
            None
5✔
685
        } else {
686
            self.0
6✔
687
                .find('\n')
688
                .map(|x| {
8✔
689
                    let ret = if 0 < x {
11✔
690
                        if self.0.as_bytes()[x - 1] == b'\r' {
27✔
691
                            (&self.0[..x - 1], EndLine::Crlf)
8✔
692
                        } else {
693
                            (&self.0[..x], EndLine::Lf)
6✔
694
                        }
695
                    } else {
696
                        ("", EndLine::Lf)
5✔
697
                    };
698
                    self.0 = &self.0[x + 1..];
18✔
699
                    ret
7✔
700
                })
701
                .or_else(|| {
4✔
702
                    let ret = Some((self.0, EndLine::Eof));
6✔
703
                    self.0 = "";
5✔
UNCOV
704
                    ret
×
705
                })
706
        }
707
    }
708
}
709

710
/// Used to translate between `Span`s and byte positions within a single output line in highlighted
711
/// code of structured suggestions.
712
#[derive(Debug, Clone, Copy)]
713
pub(crate) struct SubstitutionHighlight {
714
    pub(crate) start: usize,
715
    pub(crate) end: usize,
716
}
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