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

rust-lang / annotate-snippets-rs / 24260394800

10 Apr 2026 07:28PM UTC coverage: 90.893% (+0.2%) from 90.722%
24260394800

Pull #402

github

web-flow
Merge e3b405302 into 12ad7727c
Pull Request #402: Add a mode to the renderer to substitute *anything* outside of the ASCII space

58 of 61 new or added lines in 4 files covered. (95.08%)

1517 of 1669 relevant lines covered (90.89%)

5.0 hits per line

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

84.18
/src/renderer/source_map.rs
1
use alloc::borrow::Cow;
2
use alloc::string::String;
3
use alloc::{vec, vec::Vec};
4
use core::cmp::{max, min};
5
use core::ops::Range;
6

7
use crate::renderer::{LineAnnotation, LineAnnotationType, char_width, num_overlap};
8
use crate::{Annotation, AnnotationKind, Patch};
9

10
#[derive(Debug)]
11
pub(crate) struct SourceMap<'a> {
12
    lines: Vec<LineInfo<'a>>,
13
    pub(crate) source: &'a str,
14
    force_ascii: bool,
15
}
16

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

35
        let mut current_index = 0;
9✔
36

37
        let mut mapping = vec![];
10✔
38
        for (idx, (line, end_line)) in CursorLines::new(source).enumerate() {
27✔
39
            let line_length = line.len();
8✔
40
            let line_range = current_index..current_index + line_length;
5✔
41
            let end_line_size = end_line.len();
12✔
42

43
            mapping.push(LineInfo {
8✔
44
                line,
×
45
                line_index: line_start + idx,
8✔
46
                start_byte: line_range.start,
×
47
                end_byte: line_range.end + end_line_size,
8✔
48
                end_line_size,
×
49
            });
50

51
            current_index += line_length + end_line_size;
7✔
52
        }
53
        Self {
54
            lines: mapping,
55
            source,
56
            force_ascii,
57
        }
58
    }
59

60
    pub(crate) fn get_line(&self, idx: usize) -> Option<&'a str> {
4✔
61
        self.lines
4✔
62
            .iter()
63
            .find(|l| l.line_index == idx)
12✔
64
            .map(|info| info.line)
12✔
65
    }
66

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

91
        if span.start == span.end {
4✔
92
            return (start, start);
4✔
93
        }
94

95
        let end_info = self
4✔
96
            .lines
×
97
            .iter()
98
            .find(|info| span.end >= info.start_byte && span.end < info.end_byte)
16✔
99
            .unwrap_or(self.lines.last().unwrap());
4✔
100
        let (end_char_pos, end_display_pos) = end_info.line
10✔
101
            [0..(span.end - end_info.start_byte).min(end_info.line.len())]
10✔
102
            .chars()
103
            .fold((0, 0), |(char_pos, byte_pos), c| {
12✔
104
                let display = char_width(c, self.force_ascii);
4✔
105
                (char_pos + 1, byte_pos + display)
4✔
106
            });
107

108
        let mut end = Loc {
109
            line: end_info.line_index,
5✔
110
            char: end_char_pos,
111
            display: end_display_pos,
112
            byte: span.end,
6✔
113
        };
114
        if start.line != end.line && end.byte > end_info.end_byte - end_info.end_line_size {
12✔
115
            end.char += 1;
2✔
116
            end.display += 1;
4✔
117
        }
118

119
        (start, end)
4✔
120
    }
121

122
    pub(crate) fn span_to_snippet(&self, span: Range<usize>) -> Option<&str> {
4✔
123
        self.source.get(span)
4✔
124
    }
125

126
    pub(crate) fn span_to_lines(&self, span: Range<usize>) -> Vec<&LineInfo<'a>> {
3✔
127
        let mut lines = vec![];
4✔
128
        let start = span.start;
3✔
129
        let end = span.end;
4✔
130
        for line_info in &self.lines {
7✔
131
            if start >= line_info.end_byte {
3✔
132
                continue;
×
133
            }
134
            if end < line_info.start_byte {
4✔
135
                break;
×
136
            }
137
            lines.push(line_info);
3✔
138
        }
139

140
        if lines.is_empty() && !self.lines.is_empty() {
8✔
141
            lines.push(self.lines.last().unwrap());
1✔
142
        }
143

144
        lines
4✔
145
    }
146

147
    pub(crate) fn annotated_lines(
11✔
148
        &self,
149
        annotations: Vec<Annotation<'a>>,
150
        fold: bool,
151
    ) -> (usize, Vec<AnnotatedLineInfo<'a>>) {
152
        let source_len = self.source.len();
16✔
153
        if let Some(bigger) = annotations.iter().find_map(|x| {
12✔
154
            // Allow highlighting one past the last character in the source.
155
            if source_len + 1 < x.span.end {
7✔
156
                Some(&x.span)
1✔
157
            } else {
158
                None
7✔
159
            }
160
        }) {
161
            panic!("Annotation range `{bigger:?}` is beyond the end of buffer `{source_len}`")
2✔
162
        }
163

164
        let mut annotated_line_infos = self
6✔
165
            .lines
×
166
            .iter()
167
            .map(|info| AnnotatedLineInfo {
17✔
168
                line: info.line,
6✔
169
                line_index: info.line_index,
7✔
170
                annotations: vec![],
7✔
171
                keep: false,
×
172
            })
173
            .collect::<Vec<_>>();
174
        let mut multiline_annotations = vec![];
6✔
175

176
        for Annotation {
9✔
177
            span,
7✔
178
            label,
6✔
179
            kind,
6✔
180
            highlight_source,
6✔
181
        } in annotations
20✔
182
        {
183
            let (lo, mut hi) = self.span_to_locations(span.clone());
12✔
184
            if kind == AnnotationKind::Visible {
5✔
185
                for line_idx in lo.line..=hi.line {
4✔
186
                    self.keep_line(&mut annotated_line_infos, line_idx);
4✔
187
                }
188
                continue;
×
189
            }
190
            // Watch out for "empty spans". If we get a span like 6..6, we
191
            // want to just display a `^` at 6, so convert that to
192
            // 6..7. This is degenerate input, but it's best to degrade
193
            // gracefully -- and the parser likes to supply a span like
194
            // that for EOF, in particular.
195

196
            if lo.display == hi.display && lo.line == hi.line {
14✔
197
                hi.display += 1;
4✔
198
            }
199

200
            if lo.line == hi.line {
5✔
201
                let line_ann = LineAnnotation {
202
                    start: lo,
203
                    end: hi,
204
                    kind,
205
                    label,
206
                    annotation_type: LineAnnotationType::Singleline,
207
                    highlight_source,
208
                };
209
                self.add_annotation_to_file(&mut annotated_line_infos, lo.line, line_ann);
10✔
210
            } else {
211
                multiline_annotations.push(MultilineAnnotation {
6✔
212
                    depth: 1,
×
213
                    start: lo,
×
214
                    end: hi,
3✔
215
                    kind,
×
216
                    label,
3✔
217
                    overlaps_exactly: false,
×
218
                    highlight_source,
×
219
                });
220
            }
221
        }
222

223
        let mut primary_spans = vec![];
4✔
224

225
        // Find overlapping multiline annotations, put them at different depths
226
        multiline_annotations.sort_by_key(|ml| (ml.start.line, usize::MAX - ml.end.line));
12✔
227
        for (outer_i, ann) in multiline_annotations.clone().into_iter().enumerate() {
7✔
228
            if ann.kind.is_primary() {
7✔
229
                primary_spans.push((ann.start, ann.end));
4✔
230
            }
231
            for (inner_i, a) in &mut multiline_annotations.iter_mut().enumerate() {
8✔
232
                // Move all other multiline annotations overlapping with this one
233
                // one level to the right.
234
                if !ann.same_span(a)
10✔
235
                    && num_overlap(ann.start.line, ann.end.line, a.start.line, a.end.line, true)
4✔
236
                {
237
                    a.increase_depth();
2✔
238
                } else if ann.same_span(a) && outer_i != inner_i {
12✔
239
                    a.overlaps_exactly = true;
2✔
240
                } else {
241
                    if primary_spans
16✔
242
                        .iter()
243
                        .any(|(s, e)| a.start == *s && a.end == *e)
12✔
244
                    {
245
                        a.kind = AnnotationKind::Primary;
4✔
246
                    }
247
                    break;
×
248
                }
249
            }
250
        }
251

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

325
        if fold {
4✔
326
            annotated_line_infos.retain(|l| !l.annotations.is_empty() || l.keep);
14✔
327
        }
328

329
        (max_depth, annotated_line_infos)
4✔
330
    }
331

332
    fn add_annotation_to_file(
4✔
333
        &self,
334
        annotated_line_infos: &mut Vec<AnnotatedLineInfo<'a>>,
335
        line_index: usize,
336
        line_ann: LineAnnotation<'a>,
337
    ) {
338
        if let Some(line_info) = annotated_line_infos
13✔
339
            .iter_mut()
340
            .find(|line_info| line_info.line_index == line_index)
16✔
341
        {
342
            line_info.annotations.push(line_ann);
9✔
343
        } else {
344
            let info = self
×
345
                .lines
×
346
                .iter()
347
                .find(|l| l.line_index == line_index)
×
348
                .unwrap();
349
            annotated_line_infos.push(AnnotatedLineInfo {
×
350
                line: info.line,
×
351
                line_index,
×
352
                annotations: vec![line_ann],
×
353
                keep: false,
×
354
            });
355
            annotated_line_infos.sort_by_key(|l| l.line_index);
×
356
        }
357
    }
358

359
    fn keep_line(&self, annotated_line_infos: &mut Vec<AnnotatedLineInfo<'a>>, line_index: usize) {
2✔
360
        if let Some(line_info) = annotated_line_infos
6✔
361
            .iter_mut()
362
            .find(|line_info| line_info.line_index == line_index)
6✔
363
        {
364
            line_info.keep = true;
2✔
365
        } else {
366
            let info = self
×
367
                .lines
×
368
                .iter()
369
                .find(|l| l.line_index == line_index)
×
370
                .unwrap();
371
            annotated_line_infos.push(AnnotatedLineInfo {
×
372
                line: info.line,
×
373
                line_index,
×
374
                annotations: vec![],
×
375
                keep: true,
×
376
            });
377
            annotated_line_infos.sort_by_key(|l| l.line_index);
×
378
        }
379
    }
380

381
    pub(crate) fn splice_lines<'b>(
5✔
382
        &'a self,
383
        mut patches: Vec<Patch<'b>>,
384
        fold: bool,
385
    ) -> Option<SplicedLines<'b>> {
386
        fn push_trailing(
4✔
387
            buf: &mut String,
388
            line_opt: Option<&str>,
389
            lo: &Loc,
390
            hi_opt: Option<&Loc>,
391
        ) -> usize {
392
            let mut line_count = 0;
4✔
393
            // Convert CharPos to Usize, as CharPose is character offset
394
            // Extract low index and high index
395
            let (lo, hi_opt) = (lo.char, hi_opt.map(|hi| hi.char));
12✔
396
            if let Some(line) = line_opt {
4✔
397
                if let Some(lo) = line.char_indices().map(|(i, _)| i).nth(lo) {
15✔
398
                    // Get high index while account for rare unicode and emoji with char_indices
399
                    let hi_opt = hi_opt.and_then(|hi| line.char_indices().map(|(i, _)| i).nth(hi));
18✔
400
                    match hi_opt {
3✔
401
                        // If high index exist, take string from low to high index
402
                        Some(hi) if hi > lo => {
7✔
403
                            // count how many '\n' exist
404
                            line_count = line[lo..hi].matches('\n').count();
4✔
405
                            buf.push_str(&line[lo..hi]);
3✔
406
                        }
407
                        Some(_) => (),
×
408
                        // If high index absence, take string from low index till end string.len
409
                        None => {
×
410
                            // count how many '\n' exist
411
                            line_count = line[lo..].matches('\n').count();
4✔
412
                            buf.push_str(&line[lo..]);
3✔
413
                        }
414
                    }
415
                }
416
                // If high index is None
417
                if hi_opt.is_none() {
4✔
418
                    buf.push('\n');
4✔
419
                }
420
            }
421
            line_count
3✔
422
        }
423

424
        let source_len = self.source.len();
8✔
425
        if let Some(bigger) = patches.iter().find_map(|x| {
8✔
426
            // Allow patching one past the last character in the source.
427
            if source_len + 1 < x.span.end {
8✔
428
                Some(&x.span)
1✔
429
            } else {
430
                None
4✔
431
            }
432
        }) {
433
            panic!("Patch span `{bigger:?}` is beyond the end of buffer `{source_len}`")
2✔
434
        }
435

436
        // Assumption: all spans are in the same file, and all spans
437
        // are disjoint. Sort in ascending order.
438
        patches.sort_by_key(|p| p.span.start);
14✔
439

440
        // Find the bounding span.
441
        let (lo, hi) = if fold {
13✔
442
            let lo = patches.iter().map(|p| p.span.start).min()?;
16✔
443
            let hi = patches.iter().map(|p| p.span.end).max()?;
12✔
444
            (lo, hi)
4✔
445
        } else {
446
            (0, source_len)
2✔
447
        };
448

449
        let lines = self.span_to_lines(lo..hi);
4✔
450

451
        let mut highlights = vec![];
3✔
452
        // To build up the result, we do this for each span:
453
        // - push the line segment trailing the previous span
454
        //   (at the beginning a "phantom" span pointing at the start of the line)
455
        // - push lines between the previous and current span (if any)
456
        // - if the previous and current span are not on the same line
457
        //   push the line segment leading up to the current span
458
        // - splice in the span substitution
459
        //
460
        // Finally push the trailing line segment of the last span
461
        let (mut prev_hi, _) = self.span_to_locations(lo..hi);
7✔
462
        prev_hi.char = 0;
3✔
463
        let mut prev_line = lines.first().map(|line| line.line);
10✔
464
        let mut buf = String::new();
4✔
465

466
        let trimmed_patches = patches
3✔
467
            .into_iter()
468
            // If this is a replacement of, e.g. `"a"` into `"ab"`, adjust the
469
            // suggestion and snippet to look as if we just suggested to add
470
            // `"b"`, which is typically much easier for the user to understand.
471
            .map(|part| part.trim_trivial_replacements(self.source))
11✔
472
            .collect::<Vec<_>>();
473
        let mut line_highlight = vec![];
4✔
474
        // We need to keep track of the difference between the existing code and the added
475
        // or deleted code in order to point at the correct column *after* substitution.
476
        let mut acc = 0;
4✔
477
        for part in &trimmed_patches {
8✔
478
            let (cur_lo, cur_hi) = self.span_to_locations(part.span.clone());
8✔
479
            if prev_hi.line == cur_lo.line {
4✔
480
                let mut count = push_trailing(&mut buf, prev_line, &prev_hi, Some(&cur_lo));
8✔
481
                while count > 0 {
3✔
482
                    highlights.push(core::mem::take(&mut line_highlight));
×
483
                    acc = 0;
×
484
                    count -= 1;
×
485
                }
486
            } else {
487
                acc = 0;
1✔
488
                highlights.push(core::mem::take(&mut line_highlight));
2✔
489
                let mut count = push_trailing(&mut buf, prev_line, &prev_hi, None);
1✔
490
                while count > 0 {
1✔
491
                    highlights.push(core::mem::take(&mut line_highlight));
×
492
                    count -= 1;
×
493
                }
494
                // push lines between the previous and current span (if any)
495
                for idx in prev_hi.line + 1..(cur_lo.line) {
2✔
496
                    if let Some(line) = self.get_line(idx) {
2✔
497
                        buf.push_str(line.as_ref());
1✔
498
                        buf.push('\n');
1✔
499
                        highlights.push(core::mem::take(&mut line_highlight));
1✔
500
                    }
501
                }
502
                if let Some(cur_line) = self.get_line(cur_lo.line) {
1✔
503
                    let end = match cur_line.char_indices().nth(cur_lo.char) {
2✔
504
                        Some((i, _)) => i,
1✔
505
                        None => cur_line.len(),
1✔
506
                    };
507
                    buf.push_str(&cur_line[..end]);
1✔
508
                }
509
            }
510
            // Add a whole line highlight per line in the snippet.
511
            let len: isize = part
8✔
512
                .replacement
×
513
                .split('\n')
514
                .next()
515
                .unwrap_or(&part.replacement)
3✔
516
                .chars()
517
                .map(|c| match c {
10✔
518
                    '\t' => 4,
×
519
                    _ => 1,
3✔
520
                })
521
                .sum();
522
            line_highlight.push(SubstitutionHighlight {
4✔
523
                start: (cur_lo.char as isize + acc) as usize,
4✔
524
                end: (cur_lo.char as isize + acc + len) as usize,
7✔
525
            });
526
            buf.push_str(&part.replacement);
3✔
527
            // Account for the difference between the width of the current code and the
528
            // snippet being suggested, so that the *later* suggestions are correctly
529
            // aligned on the screen. Note that cur_hi and cur_lo can be on different
530
            // lines, so cur_hi.col can be smaller than cur_lo.col
531
            acc += len - (cur_hi.char as isize - cur_lo.char as isize);
4✔
532
            prev_hi = cur_hi;
3✔
533
            prev_line = self.get_line(prev_hi.line);
7✔
534
            for line in part.replacement.split('\n').skip(1) {
4✔
535
                acc = 0;
2✔
536
                highlights.push(core::mem::take(&mut line_highlight));
2✔
537
                let end: usize = line
2✔
538
                    .chars()
539
                    .map(|c| match c {
6✔
540
                        '\t' => 4,
×
541
                        _ => 1,
2✔
542
                    })
543
                    .sum();
544
                line_highlight.push(SubstitutionHighlight { start: 0, end });
2✔
545
            }
546
        }
547
        highlights.push(core::mem::take(&mut line_highlight));
3✔
548
        if fold {
4✔
549
            // if the replacement already ends with a newline, don't print the next line
550
            if !buf.ends_with('\n') {
7✔
551
                push_trailing(&mut buf, prev_line, &prev_hi, None);
3✔
552
            }
553
        } else {
554
            // Add the trailing part of the source after the last patch
555
            if let Some(snippet) = self.span_to_snippet(prev_hi.byte..source_len) {
4✔
556
                buf.push_str(snippet);
2✔
557
                for _ in snippet.matches('\n') {
2✔
558
                    highlights.push(core::mem::take(&mut line_highlight));
2✔
559
                }
560
            }
561
        }
562
        // remove trailing newlines
563
        while buf.ends_with('\n') {
7✔
564
            buf.pop();
7✔
565
        }
566
        if highlights.iter().all(|parts| parts.is_empty()) {
18✔
567
            None
×
568
        } else {
569
            Some((buf, trimmed_patches, highlights))
3✔
570
        }
571
    }
572
}
573

574
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
575
pub(crate) struct MultilineAnnotation<'a> {
576
    pub depth: usize,
577
    pub start: Loc,
578
    pub end: Loc,
579
    pub kind: AnnotationKind,
580
    pub label: Option<Cow<'a, str>>,
581
    pub overlaps_exactly: bool,
582
    pub highlight_source: bool,
583
}
584

585
impl<'a> MultilineAnnotation<'a> {
586
    pub(crate) fn increase_depth(&mut self) {
2✔
587
        self.depth += 1;
2✔
588
    }
589

590
    /// Compare two `MultilineAnnotation`s considering only the `Span` they cover.
591
    pub(crate) fn same_span(&self, other: &MultilineAnnotation<'_>) -> bool {
4✔
592
        self.start == other.start && self.end == other.end
4✔
593
    }
594

595
    pub(crate) fn as_start(&self) -> LineAnnotation<'a> {
4✔
596
        LineAnnotation {
597
            start: self.start,
4✔
598
            end: Loc {
4✔
599
                line: self.start.line,
600
                char: self.start.char + 1,
601
                display: self.start.display + 1,
602
                byte: self.start.byte + 1,
603
            },
604
            kind: self.kind,
4✔
605
            label: None,
606
            annotation_type: LineAnnotationType::MultilineStart(self.depth),
4✔
607
            highlight_source: self.highlight_source,
4✔
608
        }
609
    }
610

611
    pub(crate) fn as_end(&self) -> LineAnnotation<'a> {
4✔
612
        LineAnnotation {
613
            start: Loc {
4✔
614
                line: self.end.line,
615
                char: self.end.char.saturating_sub(1),
616
                display: self.end.display.saturating_sub(1),
617
                byte: self.end.byte.saturating_sub(1),
618
            },
619
            end: self.end,
4✔
620
            kind: self.kind,
4✔
621
            label: self.label.clone(),
4✔
622
            annotation_type: LineAnnotationType::MultilineEnd(self.depth),
4✔
623
            highlight_source: self.highlight_source,
4✔
624
        }
625
    }
626

627
    pub(crate) fn as_line(&self) -> LineAnnotation<'a> {
3✔
628
        LineAnnotation {
629
            start: Loc::default(),
3✔
630
            end: Loc::default(),
3✔
631
            kind: self.kind,
3✔
632
            label: None,
633
            annotation_type: LineAnnotationType::MultilineLine(self.depth),
3✔
634
            highlight_source: self.highlight_source,
3✔
635
        }
636
    }
637
}
638

639
#[derive(Debug)]
640
pub(crate) struct LineInfo<'a> {
641
    pub(crate) line: &'a str,
642
    pub(crate) line_index: usize,
643
    pub(crate) start_byte: usize,
644
    pub(crate) end_byte: usize,
645
    end_line_size: usize,
646
}
647

648
#[derive(Debug)]
649
pub(crate) struct AnnotatedLineInfo<'a> {
650
    pub(crate) line: &'a str,
651
    pub(crate) line_index: usize,
652
    pub(crate) annotations: Vec<LineAnnotation<'a>>,
653
    pub(crate) keep: bool,
654
}
655

656
/// A source code location used for error reporting.
657
#[derive(Clone, Copy, Debug, Default, PartialOrd, Ord, PartialEq, Eq)]
658
pub(crate) struct Loc {
659
    /// The (1-based) line number.
660
    pub(crate) line: usize,
661
    /// The (0-based) column offset.
662
    pub(crate) char: usize,
663
    /// The (0-based) column offset when displayed.
664
    pub(crate) display: usize,
665
    /// The (0-based) byte offset.
666
    pub(crate) byte: usize,
667
}
668

669
struct CursorLines<'a>(&'a str);
670

671
impl CursorLines<'_> {
672
    fn new(src: &str) -> CursorLines<'_> {
10✔
673
        CursorLines(src)
674
    }
675
}
676

677
#[derive(Copy, Clone, Debug, PartialEq)]
678
enum EndLine {
679
    Eof,
680
    Lf,
681
    Crlf,
682
}
683

684
impl EndLine {
685
    /// The number of characters this line ending occupies in bytes.
686
    pub(crate) fn len(self) -> usize {
5✔
687
        match self {
8✔
688
            EndLine::Eof => 0,
689
            EndLine::Lf => 1,
690
            EndLine::Crlf => 2,
691
        }
692
    }
693
}
694

695
impl<'a> Iterator for CursorLines<'a> {
696
    type Item = (&'a str, EndLine);
697

698
    fn next(&mut self) -> Option<Self::Item> {
8✔
699
        if self.0.is_empty() {
7✔
700
            None
6✔
701
        } else {
702
            self.0
8✔
703
                .find('\n')
704
                .map(|x| {
13✔
705
                    let ret = if 0 < x {
11✔
706
                        if self.0.as_bytes()[x - 1] == b'\r' {
20✔
707
                            (&self.0[..x - 1], EndLine::Crlf)
3✔
708
                        } else {
709
                            (&self.0[..x], EndLine::Lf)
3✔
710
                        }
711
                    } else {
712
                        ("", EndLine::Lf)
4✔
713
                    };
714
                    self.0 = &self.0[x + 1..];
11✔
715
                    ret
6✔
716
                })
717
                .or_else(|| {
9✔
718
                    let ret = Some((self.0, EndLine::Eof));
4✔
719
                    self.0 = "";
4✔
720
                    ret
×
721
                })
722
        }
723
    }
724
}
725

726
pub(crate) type SplicedLines<'a> = (
727
    String,
728
    Vec<TrimmedPatch<'a>>,
729
    Vec<Vec<SubstitutionHighlight>>,
730
);
731

732
/// Used to translate between `Span`s and byte positions within a single output line in highlighted
733
/// code of structured suggestions.
734
#[derive(Debug, Clone, Copy)]
735
pub(crate) struct SubstitutionHighlight {
736
    pub(crate) start: usize,
737
    pub(crate) end: usize,
738
}
739

740
#[derive(Clone, Debug)]
741
pub(crate) struct TrimmedPatch<'a> {
742
    pub(crate) original_span: Range<usize>,
743
    pub(crate) span: Range<usize>,
744
    pub(crate) replacement: Cow<'a, str>,
745
}
746

747
impl<'a> TrimmedPatch<'a> {
748
    pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool {
2✔
749
        !self.replacement.is_empty() && !self.replaces_meaningful_content(sm)
2✔
750
    }
751

752
    pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
4✔
753
        self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
3✔
754
    }
755

756
    pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
3✔
757
        !self.replacement.is_empty() && self.replaces_meaningful_content(sm)
3✔
758
    }
759

760
    /// Whether this is a replacement that overwrites source with a snippet
761
    /// in a way that isn't a superset of the original string. For example,
762
    /// replacing "abc" with "abcde" is not destructive, but replacing it
763
    /// it with "abx" is, since the "c" character is lost.
764
    pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool {
3✔
765
        self.is_replacement(sm)
3✔
766
            && sm
×
767
                .span_to_snippet(self.span.clone())
3✔
768
                .is_none_or(|s| as_substr(s.trim(), self.replacement.trim()).is_none())
9✔
769
    }
770

771
    fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
4✔
772
        sm.span_to_snippet(self.span.clone())
4✔
773
            .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
11✔
774
    }
775
}
776

777
/// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect
778
/// the case where a substring of the suggestion is "sandwiched" in the original, like
779
/// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length
780
/// of the suffix.
781
pub(crate) fn as_substr<'a>(
3✔
782
    original: &'a str,
783
    suggestion: &'a str,
784
) -> Option<(usize, &'a str, usize)> {
785
    if let Some(stripped) = suggestion.strip_prefix(original) {
6✔
786
        Some((original.len(), stripped, 0))
3✔
787
    } else if let Some(stripped) = suggestion.strip_suffix(original) {
7✔
788
        Some((0, stripped, original.len()))
2✔
789
    } else {
790
        let common_prefix = original
×
791
            .chars()
792
            .zip(suggestion.chars())
3✔
793
            .take_while(|(c1, c2)| c1 == c2)
9✔
794
            .map(|(c, _)| c.len_utf8())
7✔
795
            .sum();
796
        let original = &original[common_prefix..];
3✔
797
        let suggestion = &suggestion[common_prefix..];
3✔
798
        if let Some(stripped) = suggestion.strip_suffix(original) {
7✔
799
            let common_suffix = original.len();
1✔
800
            Some((common_prefix, stripped, common_suffix))
1✔
801
        } else {
802
            None
3✔
803
        }
804
    }
805
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc