annotate_snippets/renderer/
source_map.rs

1use crate::renderer::{char_width, num_overlap, LineAnnotation, LineAnnotationType};
2use crate::{Annotation, AnnotationKind, Patch};
3use std::borrow::Cow;
4use std::cmp::{max, min};
5use std::ops::Range;
6
7#[derive(Debug)]
8pub(crate) struct SourceMap<'a> {
9    lines: Vec<LineInfo<'a>>,
10    pub(crate) source: &'a str,
11}
12
13impl<'a> SourceMap<'a> {
14    pub(crate) fn new(source: &'a str, line_start: usize) -> Self {
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() {
18            return Self {
19                lines: vec![LineInfo {
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;
31
32        let mut mapping = vec![];
33        for (idx, (line, end_line)) in CursorLines::new(source).enumerate() {
34            let line_length = line.len();
35            let line_range = current_index..current_index + line_length;
36            let end_line_size = end_line.len();
37
38            mapping.push(LineInfo {
39                line,
40                line_index: line_start + idx,
41                start_byte: line_range.start,
42                end_byte: line_range.end + end_line_size,
43                end_line_size,
44            });
45
46            current_index += line_length + end_line_size;
47        }
48        Self {
49            lines: mapping,
50            source,
51        }
52    }
53
54    pub(crate) fn get_line(&self, idx: usize) -> Option<&'a str> {
55        self.lines
56            .iter()
57            .find(|l| l.line_index == idx)
58            .map(|info| info.line)
59    }
60
61    pub(crate) fn span_to_locations(&self, span: Range<usize>) -> (Loc, Loc) {
62        let start_info = self
63            .lines
64            .iter()
65            .find(|info| span.start >= info.start_byte && span.start < info.end_byte)
66            .unwrap_or(self.lines.last().unwrap());
67        let (mut start_char_pos, start_display_pos) = start_info.line
68            [0..(span.start - start_info.start_byte).min(start_info.line.len())]
69            .chars()
70            .fold((0, 0), |(char_pos, byte_pos), c| {
71                let display = char_width(c);
72                (char_pos + 1, byte_pos + display)
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 {
76            start_char_pos += 1;
77        }
78        let start = Loc {
79            line: start_info.line_index,
80            char: start_char_pos,
81            display: start_display_pos,
82            byte: span.start,
83        };
84
85        if span.start == span.end {
86            return (start, start);
87        }
88
89        let end_info = self
90            .lines
91            .iter()
92            .find(|info| span.end >= info.start_byte && span.end < info.end_byte)
93            .unwrap_or(self.lines.last().unwrap());
94        let (end_char_pos, end_display_pos) = end_info.line
95            [0..(span.end - end_info.start_byte).min(end_info.line.len())]
96            .chars()
97            .fold((0, 0), |(char_pos, byte_pos), c| {
98                let display = char_width(c);
99                (char_pos + 1, byte_pos + display)
100            });
101
102        let mut end = Loc {
103            line: end_info.line_index,
104            char: end_char_pos,
105            display: end_display_pos,
106            byte: span.end,
107        };
108        if start.line != end.line && end.byte > end_info.end_byte - end_info.end_line_size {
109            end.char += 1;
110            end.display += 1;
111        }
112
113        (start, end)
114    }
115
116    pub(crate) fn span_to_snippet(&self, span: Range<usize>) -> Option<&str> {
117        self.source.get(span)
118    }
119
120    pub(crate) fn span_to_lines(&self, span: Range<usize>) -> Vec<&LineInfo<'a>> {
121        let mut lines = vec![];
122        let start = span.start;
123        let end = span.end;
124        for line_info in &self.lines {
125            if start >= line_info.end_byte {
126                continue;
127            }
128            if end < line_info.start_byte {
129                break;
130            }
131            lines.push(line_info);
132        }
133
134        if lines.is_empty() && !self.lines.is_empty() {
135            lines.push(self.lines.last().unwrap());
136        }
137
138        lines
139    }
140
141    pub(crate) fn annotated_lines(
142        &self,
143        annotations: Vec<Annotation<'a>>,
144        fold: bool,
145    ) -> (usize, Vec<AnnotatedLineInfo<'a>>) {
146        let source_len = self.source.len();
147        if let Some(bigger) = annotations.iter().find_map(|x| {
148            // Allow highlighting one past the last character in the source.
149            if source_len + 1 < x.span.end {
150                Some(&x.span)
151            } else {
152                None
153            }
154        }) {
155            panic!("Annotation range `{bigger:?}` is beyond the end of buffer `{source_len}`")
156        }
157
158        let mut annotated_line_infos = self
159            .lines
160            .iter()
161            .map(|info| AnnotatedLineInfo {
162                line: info.line,
163                line_index: info.line_index,
164                annotations: vec![],
165                keep: false,
166            })
167            .collect::<Vec<_>>();
168        let mut multiline_annotations = vec![];
169
170        for Annotation {
171            span,
172            label,
173            kind,
174            highlight_source,
175        } in annotations
176        {
177            let (lo, mut hi) = self.span_to_locations(span.clone());
178            if kind == AnnotationKind::Visible {
179                for line_idx in lo.line..=hi.line {
180                    self.keep_line(&mut annotated_line_infos, line_idx);
181                }
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 {
191                hi.display += 1;
192            }
193
194            if lo.line == hi.line {
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);
204            } else {
205                multiline_annotations.push(MultilineAnnotation {
206                    depth: 1,
207                    start: lo,
208                    end: hi,
209                    kind,
210                    label,
211                    overlaps_exactly: false,
212                    highlight_source,
213                });
214            }
215        }
216
217        let mut primary_spans = vec![];
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));
221        for (outer_i, ann) in multiline_annotations.clone().into_iter().enumerate() {
222            if ann.kind.is_primary() {
223                primary_spans.push((ann.start, ann.end));
224            }
225            for (inner_i, a) in &mut multiline_annotations.iter_mut().enumerate() {
226                // Move all other multiline annotations overlapping with this one
227                // one level to the right.
228                if !ann.same_span(a)
229                    && num_overlap(ann.start.line, ann.end.line, a.start.line, a.end.line, true)
230                {
231                    a.increase_depth();
232                } else if ann.same_span(a) && outer_i != inner_i {
233                    a.overlaps_exactly = true;
234                } else {
235                    if primary_spans
236                        .iter()
237                        .any(|(s, e)| a.start == *s && a.end == *e)
238                    {
239                        a.kind = AnnotationKind::Primary;
240                    }
241                    break;
242                }
243            }
244        }
245
246        let mut max_depth = 0; // max overlapping multiline spans
247        for ann in &multiline_annotations {
248            max_depth = max(max_depth, ann.depth);
249        }
250        // Change order of multispan depth to minimize the number of overlaps in the ASCII art.
251        for a in &mut multiline_annotations {
252            a.depth = max_depth - a.depth + 1;
253        }
254        for ann in multiline_annotations {
255            let mut end_ann = ann.as_end();
256            if ann.overlaps_exactly {
257                end_ann.annotation_type = LineAnnotationType::Singleline;
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(
282                    &mut annotated_line_infos,
283                    ann.start.line,
284                    ann.as_start(),
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);
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| {
295                    let s = s.trim();
296                    // Consider comments as empty, but don't consider docstrings to be empty.
297                    !(s.starts_with("//") && !(s.starts_with("///") || s.starts_with("//!")))
298                        // Consider lines with nothing but whitespace, a single delimiter as empty.
299                        && !["", "{", "}", "(", ")", "[", "]"].contains(&s)
300                };
301                let until = (ann.start.line..middle)
302                    .rev()
303                    .filter_map(|line| self.get_line(line).map(|s| (line + 1, s)))
304                    .find(|(_, s)| filter(s))
305                    .map_or(ann.start.line, |(line, _)| line);
306                for line in ann.start.line + 1..until {
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());
309                }
310                let line_end = ann.end.line - 1;
311                let end_is_empty = self.get_line(line_end).map_or(false, |s| !filter(s));
312                if middle < line_end && !end_is_empty {
313                    self.add_annotation_to_file(&mut annotated_line_infos, line_end, ann.as_line());
314                }
315            }
316            self.add_annotation_to_file(&mut annotated_line_infos, end_ann.end.line, end_ann);
317        }
318
319        if fold {
320            annotated_line_infos.retain(|l| !l.annotations.is_empty() || l.keep);
321        }
322
323        (max_depth, annotated_line_infos)
324    }
325
326    fn add_annotation_to_file(
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
333            .iter_mut()
334            .find(|line_info| line_info.line_index == line_index)
335        {
336            line_info.annotations.push(line_ann);
337        } else {
338            let info = self
339                .lines
340                .iter()
341                .find(|l| l.line_index == line_index)
342                .unwrap();
343            annotated_line_infos.push(AnnotatedLineInfo {
344                line: info.line,
345                line_index,
346                annotations: vec![line_ann],
347                keep: false,
348            });
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) {
354        if let Some(line_info) = annotated_line_infos
355            .iter_mut()
356            .find(|line_info| line_info.line_index == line_index)
357        {
358            line_info.keep = true;
359        } else {
360            let info = self
361                .lines
362                .iter()
363                .find(|l| l.line_index == line_index)
364                .unwrap();
365            annotated_line_infos.push(AnnotatedLineInfo {
366                line: info.line,
367                line_index,
368                annotations: vec![],
369                keep: true,
370            });
371            annotated_line_infos.sort_by_key(|l| l.line_index);
372        }
373    }
374
375    pub(crate) fn splice_lines<'b>(
376        &'a self,
377        mut patches: Vec<Patch<'b>>,
378        fold: bool,
379    ) -> Option<SplicedLines<'b>> {
380        fn push_trailing(
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;
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));
390            if let Some(line) = line_opt {
391                if let Some(lo) = line.char_indices().map(|(i, _)| i).nth(lo) {
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));
394                    match hi_opt {
395                        // If high index exist, take string from low to high index
396                        Some(hi) if hi > lo => {
397                            // count how many '\n' exist
398                            line_count = line[lo..hi].matches('\n').count();
399                            buf.push_str(&line[lo..hi]);
400                        }
401                        Some(_) => (),
402                        // If high index absence, take string from low index till end string.len
403                        None => {
404                            // count how many '\n' exist
405                            line_count = line[lo..].matches('\n').count();
406                            buf.push_str(&line[lo..]);
407                        }
408                    }
409                }
410                // If high index is None
411                if hi_opt.is_none() {
412                    buf.push('\n');
413                }
414            }
415            line_count
416        }
417
418        let source_len = self.source.len();
419        if let Some(bigger) = patches.iter().find_map(|x| {
420            // Allow patching one past the last character in the source.
421            if source_len + 1 < x.span.end {
422                Some(&x.span)
423            } else {
424                None
425            }
426        }) {
427            panic!("Patch span `{bigger:?}` is beyond the end of buffer `{source_len}`")
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);
433
434        // Find the bounding span.
435        let (lo, hi) = if fold {
436            let lo = patches.iter().map(|p| p.span.start).min()?;
437            let hi = patches.iter().map(|p| p.span.end).max()?;
438            (lo, hi)
439        } else {
440            (0, source_len)
441        };
442
443        let lines = self.span_to_lines(lo..hi);
444
445        let mut highlights = vec![];
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);
456        prev_hi.char = 0;
457        let mut prev_line = lines.first().map(|line| line.line);
458        let mut buf = String::new();
459
460        let trimmed_patches = patches
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))
466            .collect::<Vec<_>>();
467        let mut line_highlight = vec![];
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;
471        for part in &trimmed_patches {
472            let (cur_lo, cur_hi) = self.span_to_locations(part.span.clone());
473            if prev_hi.line == cur_lo.line {
474                let mut count = push_trailing(&mut buf, prev_line, &prev_hi, Some(&cur_lo));
475                while count > 0 {
476                    highlights.push(std::mem::take(&mut line_highlight));
477                    acc = 0;
478                    count -= 1;
479                }
480            } else {
481                acc = 0;
482                highlights.push(std::mem::take(&mut line_highlight));
483                let mut count = push_trailing(&mut buf, prev_line, &prev_hi, None);
484                while count > 0 {
485                    highlights.push(std::mem::take(&mut line_highlight));
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) {
490                    if let Some(line) = self.get_line(idx) {
491                        buf.push_str(line.as_ref());
492                        buf.push('\n');
493                        highlights.push(std::mem::take(&mut line_highlight));
494                    }
495                }
496                if let Some(cur_line) = self.get_line(cur_lo.line) {
497                    let end = match cur_line.char_indices().nth(cur_lo.char) {
498                        Some((i, _)) => i,
499                        None => cur_line.len(),
500                    };
501                    buf.push_str(&cur_line[..end]);
502                }
503            }
504            // Add a whole line highlight per line in the snippet.
505            let len: isize = part
506                .replacement
507                .split('\n')
508                .next()
509                .unwrap_or(&part.replacement)
510                .chars()
511                .map(|c| match c {
512                    '\t' => 4,
513                    _ => 1,
514                })
515                .sum();
516            line_highlight.push(SubstitutionHighlight {
517                start: (cur_lo.char as isize + acc) as usize,
518                end: (cur_lo.char as isize + acc + len) as usize,
519            });
520            buf.push_str(&part.replacement);
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);
526            prev_hi = cur_hi;
527            prev_line = self.get_line(prev_hi.line);
528            for line in part.replacement.split('\n').skip(1) {
529                acc = 0;
530                highlights.push(std::mem::take(&mut line_highlight));
531                let end: usize = line
532                    .chars()
533                    .map(|c| match c {
534                        '\t' => 4,
535                        _ => 1,
536                    })
537                    .sum();
538                line_highlight.push(SubstitutionHighlight { start: 0, end });
539            }
540        }
541        highlights.push(std::mem::take(&mut line_highlight));
542        if fold {
543            // if the replacement already ends with a newline, don't print the next line
544            if !buf.ends_with('\n') {
545                push_trailing(&mut buf, prev_line, &prev_hi, None);
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) {
550                buf.push_str(snippet);
551                for _ in snippet.matches('\n') {
552                    highlights.push(std::mem::take(&mut line_highlight));
553                }
554            }
555        }
556        // remove trailing newlines
557        while buf.ends_with('\n') {
558            buf.pop();
559        }
560        if highlights.iter().all(|parts| parts.is_empty()) {
561            None
562        } else {
563            Some((buf, trimmed_patches, highlights))
564        }
565    }
566}
567
568#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
569pub(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
579impl<'a> MultilineAnnotation<'a> {
580    pub(crate) fn increase_depth(&mut self) {
581        self.depth += 1;
582    }
583
584    /// Compare two `MultilineAnnotation`s considering only the `Span` they cover.
585    pub(crate) fn same_span(&self, other: &MultilineAnnotation<'_>) -> bool {
586        self.start == other.start && self.end == other.end
587    }
588
589    pub(crate) fn as_start(&self) -> LineAnnotation<'a> {
590        LineAnnotation {
591            start: self.start,
592            end: Loc {
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,
599            label: None,
600            annotation_type: LineAnnotationType::MultilineStart(self.depth),
601            highlight_source: self.highlight_source,
602        }
603    }
604
605    pub(crate) fn as_end(&self) -> LineAnnotation<'a> {
606        LineAnnotation {
607            start: Loc {
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,
614            kind: self.kind,
615            label: self.label.clone(),
616            annotation_type: LineAnnotationType::MultilineEnd(self.depth),
617            highlight_source: self.highlight_source,
618        }
619    }
620
621    pub(crate) fn as_line(&self) -> LineAnnotation<'a> {
622        LineAnnotation {
623            start: Loc::default(),
624            end: Loc::default(),
625            kind: self.kind,
626            label: None,
627            annotation_type: LineAnnotationType::MultilineLine(self.depth),
628            highlight_source: self.highlight_source,
629        }
630    }
631}
632
633#[derive(Debug)]
634pub(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)]
643pub(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)]
652pub(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
663struct CursorLines<'a>(&'a str);
664
665impl CursorLines<'_> {
666    fn new(src: &str) -> CursorLines<'_> {
667        CursorLines(src)
668    }
669}
670
671#[derive(Copy, Clone, Debug, PartialEq)]
672enum EndLine {
673    Eof,
674    Lf,
675    Crlf,
676}
677
678impl EndLine {
679    /// The number of characters this line ending occupies in bytes.
680    pub(crate) fn len(self) -> usize {
681        match self {
682            EndLine::Eof => 0,
683            EndLine::Lf => 1,
684            EndLine::Crlf => 2,
685        }
686    }
687}
688
689impl<'a> Iterator for CursorLines<'a> {
690    type Item = (&'a str, EndLine);
691
692    fn next(&mut self) -> Option<Self::Item> {
693        if self.0.is_empty() {
694            None
695        } else {
696            self.0
697                .find('\n')
698                .map(|x| {
699                    let ret = if 0 < x {
700                        if self.0.as_bytes()[x - 1] == b'\r' {
701                            (&self.0[..x - 1], EndLine::Crlf)
702                        } else {
703                            (&self.0[..x], EndLine::Lf)
704                        }
705                    } else {
706                        ("", EndLine::Lf)
707                    };
708                    self.0 = &self.0[x + 1..];
709                    ret
710                })
711                .or_else(|| {
712                    let ret = Some((self.0, EndLine::Eof));
713                    self.0 = "";
714                    ret
715                })
716        }
717    }
718}
719
720pub(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)]
729pub(crate) struct SubstitutionHighlight {
730    pub(crate) start: usize,
731    pub(crate) end: usize,
732}
733
734#[derive(Clone, Debug)]
735pub(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
741impl<'a> TrimmedPatch<'a> {
742    pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool {
743        !self.replacement.is_empty() && !self.replaces_meaningful_content(sm)
744    }
745
746    pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
747        self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
748    }
749
750    pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
751        !self.replacement.is_empty() && self.replaces_meaningful_content(sm)
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 {
759        self.is_replacement(sm)
760            && !sm
761                .span_to_snippet(self.span.clone())
762                // This should use `is_some_and` when our MSRV is >= 1.70
763                .map_or(false, |s| {
764                    as_substr(s.trim(), self.replacement.trim()).is_some()
765                })
766    }
767
768    fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
769        sm.span_to_snippet(self.span.clone())
770            .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
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.
778pub(crate) fn as_substr<'a>(
779    original: &'a str,
780    suggestion: &'a str,
781) -> Option<(usize, &'a str, usize)> {
782    let common_prefix = original
783        .chars()
784        .zip(suggestion.chars())
785        .take_while(|(c1, c2)| c1 == c2)
786        .map(|(c, _)| c.len_utf8())
787        .sum();
788    let original = &original[common_prefix..];
789    let suggestion = &suggestion[common_prefix..];
790    if let Some(stripped) = suggestion.strip_suffix(original) {
791        let common_suffix = original.len();
792        Some((common_prefix, stripped, common_suffix))
793    } else {
794        None
795    }
796}