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

rust-lang / annotate-snippets-rs / 20471627206

23 Dec 2025 09:08PM UTC coverage: 90.145% (+0.02%) from 90.127%
20471627206

Pull #357

github

web-flow
Merge 8c2589540 into d225a68fd
Pull Request #357: fix: Improve highlighting for patches containing Unicode characters

15 of 15 new or added lines in 2 files covered. (100.0%)

56 existing lines in 2 files now uncovered.

1491 of 1654 relevant lines covered (90.15%)

4.96 hits per line

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

84.75
/src/renderer/source_map.rs
1
use crate::renderer::{char_width, num_overlap, LineAnnotation, LineAnnotationType};
2
use crate::{Annotation, AnnotationKind, Patch};
3
use std::borrow::Cow;
4
use std::cmp::{max, min};
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 {
8✔
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() {
3✔
18
            return Self {
2✔
19
                lines: vec![LineInfo {
4✔
UNCOV
20
                    line: "",
×
UNCOV
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;
8✔
31

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

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

46
            current_index += line_length + end_line_size;
5✔
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)
12✔
58
            .map(|info| info.line)
12✔
59
    }
60

61
    pub(crate) fn span_to_locations(&self, span: Range<usize>) -> (Loc, Loc) {
6✔
62
        let start_info = self
6✔
UNCOV
63
            .lines
×
64
            .iter()
65
            .find(|info| span.start >= info.start_byte && span.start < info.end_byte)
16✔
66
            .unwrap_or(self.lines.last().unwrap());
8✔
67
        let (mut start_char_pos, start_display_pos) = start_info.line
10✔
68
            [0..(span.start - start_info.start_byte).min(start_info.line.len())]
8✔
69
            .chars()
70
            .fold((0, 0), |(char_pos, byte_pos), c| {
15✔
71
                let display = char_width(c);
5✔
72
                (char_pos + 1, byte_pos + display)
5✔
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,
4✔
80
            char: start_char_pos,
81
            display: start_display_pos,
82
            byte: span.start,
4✔
83
        };
84

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

89
        let end_info = self
3✔
UNCOV
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
9✔
95
            [0..(span.end - end_info.start_byte).min(end_info.line.len())]
10✔
96
            .chars()
97
            .fold((0, 0), |(char_pos, byte_pos), c| {
14✔
98
                let display = char_width(c);
5✔
99
                (char_pos + 1, byte_pos + display)
4✔
100
            });
101

102
        let mut end = Loc {
103
            line: end_info.line_index,
4✔
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 {
9✔
109
            end.char += 1;
2✔
110
            end.display += 1;
4✔
111
        }
112

113
        (start, end)
3✔
114
    }
115

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

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

134
        if lines.is_empty() && !self.lines.is_empty() {
11✔
135
            lines.push(self.lines.last().unwrap());
1✔
136
        }
137

138
        lines
5✔
139
    }
140

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

158
        let mut annotated_line_infos = self
6✔
UNCOV
159
            .lines
×
160
            .iter()
161
            .map(|info| AnnotatedLineInfo {
16✔
162
                line: info.line,
6✔
163
                line_index: info.line_index,
6✔
164
                annotations: vec![],
6✔
UNCOV
165
                keep: false,
×
166
            })
167
            .collect::<Vec<_>>();
168
        let mut multiline_annotations = vec![];
8✔
169

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

190
            if lo.display == hi.display && lo.line == hi.line {
10✔
191
                hi.display += 1;
3✔
192
            }
193

194
            if lo.line == hi.line {
4✔
195
                let line_ann = LineAnnotation {
196
                    start: lo,
197
                    end: hi,
198
                    kind,
199
                    label,
200
                    annotation_type: LineAnnotationType::Singleline,
201
                    highlight_source,
202
                };
203
                self.add_annotation_to_file(&mut annotated_line_infos, lo.line, line_ann);
8✔
204
            } else {
205
                multiline_annotations.push(MultilineAnnotation {
6✔
UNCOV
206
                    depth: 1,
×
UNCOV
207
                    start: lo,
×
208
                    end: hi,
4✔
UNCOV
209
                    kind,
×
210
                    label,
3✔
UNCOV
211
                    overlaps_exactly: false,
×
UNCOV
212
                    highlight_source,
×
213
                });
214
            }
215
        }
216

217
        let mut primary_spans = vec![];
4✔
218

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

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

319
        if fold {
4✔
320
            annotated_line_infos.retain(|l| !l.annotations.is_empty() || l.keep);
10✔
321
        }
322

323
        (max_depth, annotated_line_infos)
4✔
324
    }
325

326
    fn add_annotation_to_file(
3✔
327
        &self,
328
        annotated_line_infos: &mut Vec<AnnotatedLineInfo<'a>>,
329
        line_index: usize,
330
        line_ann: LineAnnotation<'a>,
331
    ) {
332
        if let Some(line_info) = annotated_line_infos
12✔
333
            .iter_mut()
334
            .find(|line_info| line_info.line_index == line_index)
12✔
335
        {
336
            line_info.annotations.push(line_ann);
7✔
337
        } else {
UNCOV
338
            let info = self
×
UNCOV
339
                .lines
×
340
                .iter()
UNCOV
341
                .find(|l| l.line_index == line_index)
×
342
                .unwrap();
UNCOV
343
            annotated_line_infos.push(AnnotatedLineInfo {
×
UNCOV
344
                line: info.line,
×
UNCOV
345
                line_index,
×
UNCOV
346
                annotations: vec![line_ann],
×
UNCOV
347
                keep: false,
×
348
            });
UNCOV
349
            annotated_line_infos.sort_by_key(|l| l.line_index);
×
350
        }
351
    }
352

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

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

418
        let source_len = self.source.len();
10✔
419
        if let Some(bigger) = patches.iter().find_map(|x| {
10✔
420
            // Allow patching one past the last character in the source.
421
            if source_len + 1 < x.span.end {
10✔
422
                Some(&x.span)
1✔
423
            } else {
424
                None
5✔
425
            }
426
        }) {
427
            panic!("Patch span `{bigger:?}` is beyond the end of buffer `{source_len}`")
2✔
428
        }
429

430
        // Assumption: all spans are in the same file, and all spans
431
        // are disjoint. Sort in ascending order.
432
        patches.sort_by_key(|p| p.span.start);
16✔
433

434
        // Find the bounding span.
435
        let (lo, hi) = if fold {
14✔
436
            let lo = patches.iter().map(|p| p.span.start).min()?;
19✔
437
            let hi = patches.iter().map(|p| p.span.end).max()?;
12✔
438
            (lo, hi)
4✔
439
        } else {
440
            (0, source_len)
1✔
441
        };
442

443
        let lines = self.span_to_lines(lo..hi);
4✔
444

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

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

568
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
569
pub(crate) struct MultilineAnnotation<'a> {
570
    pub depth: usize,
571
    pub start: Loc,
572
    pub end: Loc,
573
    pub kind: AnnotationKind,
574
    pub label: Option<Cow<'a, str>>,
575
    pub overlaps_exactly: bool,
576
    pub highlight_source: bool,
577
}
578

579
impl<'a> MultilineAnnotation<'a> {
580
    pub(crate) fn increase_depth(&mut self) {
2✔
581
        self.depth += 1;
2✔
582
    }
583

584
    /// Compare two `MultilineAnnotation`s considering only the `Span` they cover.
585
    pub(crate) fn same_span(&self, other: &MultilineAnnotation<'_>) -> bool {
4✔
586
        self.start == other.start && self.end == other.end
4✔
587
    }
588

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

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

621
    pub(crate) fn as_line(&self) -> LineAnnotation<'a> {
3✔
622
        LineAnnotation {
623
            start: Loc::default(),
3✔
624
            end: Loc::default(),
3✔
625
            kind: self.kind,
3✔
626
            label: None,
627
            annotation_type: LineAnnotationType::MultilineLine(self.depth),
3✔
628
            highlight_source: self.highlight_source,
3✔
629
        }
630
    }
631
}
632

633
#[derive(Debug)]
634
pub(crate) struct LineInfo<'a> {
635
    pub(crate) line: &'a str,
636
    pub(crate) line_index: usize,
637
    pub(crate) start_byte: usize,
638
    pub(crate) end_byte: usize,
639
    end_line_size: usize,
640
}
641

642
#[derive(Debug)]
643
pub(crate) struct AnnotatedLineInfo<'a> {
644
    pub(crate) line: &'a str,
645
    pub(crate) line_index: usize,
646
    pub(crate) annotations: Vec<LineAnnotation<'a>>,
647
    pub(crate) keep: bool,
648
}
649

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

663
struct CursorLines<'a>(&'a str);
664

665
impl CursorLines<'_> {
666
    fn new(src: &str) -> CursorLines<'_> {
3✔
667
        CursorLines(src)
668
    }
669
}
670

671
#[derive(Copy, Clone, Debug, PartialEq)]
672
enum EndLine {
673
    Eof,
674
    Lf,
675
    Crlf,
676
}
677

678
impl EndLine {
679
    /// The number of characters this line ending occupies in bytes.
680
    pub(crate) fn len(self) -> usize {
3✔
681
        match self {
6✔
682
            EndLine::Eof => 0,
683
            EndLine::Lf => 1,
684
            EndLine::Crlf => 2,
685
        }
686
    }
687
}
688

689
impl<'a> Iterator for CursorLines<'a> {
690
    type Item = (&'a str, EndLine);
691

692
    fn next(&mut self) -> Option<Self::Item> {
6✔
693
        if self.0.is_empty() {
6✔
694
            None
5✔
695
        } else {
696
            self.0
6✔
697
                .find('\n')
698
                .map(|x| {
10✔
699
                    let ret = if 0 < x {
11✔
700
                        if self.0.as_bytes()[x - 1] == b'\r' {
17✔
701
                            (&self.0[..x - 1], EndLine::Crlf)
4✔
702
                        } else {
703
                            (&self.0[..x], EndLine::Lf)
3✔
704
                        }
705
                    } else {
706
                        ("", EndLine::Lf)
4✔
707
                    };
708
                    self.0 = &self.0[x + 1..];
12✔
709
                    ret
5✔
710
                })
711
                .or_else(|| {
8✔
712
                    let ret = Some((self.0, EndLine::Eof));
3✔
713
                    self.0 = "";
4✔
UNCOV
714
                    ret
×
715
                })
716
        }
717
    }
718
}
719

720
pub(crate) type SplicedLines<'a> = (
721
    String,
722
    Vec<TrimmedPatch<'a>>,
723
    Vec<Vec<SubstitutionHighlight>>,
724
);
725

726
/// Used to translate between `Span`s and byte positions within a single output line in highlighted
727
/// code of structured suggestions.
728
#[derive(Debug, Clone, Copy)]
729
pub(crate) struct SubstitutionHighlight {
730
    pub(crate) start: usize,
731
    pub(crate) end: usize,
732
}
733

734
#[derive(Clone, Debug)]
735
pub(crate) struct TrimmedPatch<'a> {
736
    pub(crate) original_span: Range<usize>,
737
    pub(crate) span: Range<usize>,
738
    pub(crate) replacement: Cow<'a, str>,
739
}
740

741
impl<'a> TrimmedPatch<'a> {
742
    pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool {
2✔
743
        !self.replacement.is_empty() && !self.replaces_meaningful_content(sm)
2✔
744
    }
745

746
    pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
6✔
747
        self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
6✔
748
    }
749

750
    pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
5✔
751
        !self.replacement.is_empty() && self.replaces_meaningful_content(sm)
5✔
752
    }
753

754
    /// Whether this is a replacement that overwrites source with a snippet
755
    /// in a way that isn't a superset of the original string. For example,
756
    /// replacing "abc" with "abcde" is not destructive, but replacing it
757
    /// it with "abx" is, since the "c" character is lost.
758
    pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool {
5✔
759
        self.is_replacement(sm)
5✔
760
            && !sm
3✔
761
                .span_to_snippet(self.span.clone())
3✔
762
                // This should use `is_some_and` when our MSRV is >= 1.70
763
                .map_or(false, |s| {
6✔
764
                    as_substr(s.trim(), self.replacement.trim()).is_some()
3✔
765
                })
766
    }
767

768
    fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
6✔
769
        sm.span_to_snippet(self.span.clone())
5✔
770
            .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
17✔
771
    }
772
}
773

774
/// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect
775
/// the case where a substring of the suggestion is "sandwiched" in the original, like
776
/// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length
777
/// of the suffix.
778
pub(crate) fn as_substr<'a>(
4✔
779
    original: &'a str,
780
    suggestion: &'a str,
781
) -> Option<(usize, &'a str, usize)> {
782
    if let Some(stripped) = suggestion.strip_prefix(original) {
8✔
783
        Some((original.len(), stripped, 0))
4✔
784
    } else if let Some(stripped) = suggestion.strip_suffix(original) {
7✔
785
        Some((0, stripped, original.len()))
2✔
786
    } else {
UNCOV
787
        let common_prefix = original
×
788
            .chars()
789
            .zip(suggestion.chars())
3✔
790
            .take_while(|(c1, c2)| c1 == c2)
9✔
791
            .map(|(c, _)| c.len_utf8())
7✔
792
            .sum();
793
        let original = &original[common_prefix..];
3✔
794
        let suggestion = &suggestion[common_prefix..];
3✔
795
        if let Some(stripped) = suggestion.strip_suffix(original) {
7✔
796
            let common_suffix = original.len();
1✔
797
            Some((common_prefix, stripped, common_suffix))
1✔
798
        } else {
799
            None
3✔
800
        }
801
    }
802
}
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