Skip to main content

annotate_snippets/renderer/
source_map.rs

1use alloc::borrow::Cow;
2use alloc::string::String;
3use alloc::{vec, vec::Vec};
4use core::cmp::{max, min};
5use core::ops::Range;
6
7use crate::renderer::{char_width, num_overlap, LineAnnotation, LineAnnotationType};
8use crate::{Annotation, AnnotationKind, Patch};
9
10#[derive(Debug)]
11pub(crate) struct SourceMap<'a> {
12    lines: Vec<LineInfo<'a>>,
13    pub(crate) source: &'a str,
14}
15
16impl<'a> SourceMap<'a> {
17    pub(crate) fn new(source: &'a str, line_start: usize) -> Self {
18        // Empty sources do have a "line", but it is empty, so we need to add
19        // a line with an empty string to the source map.
20        if source.is_empty() {
21            return Self {
22                lines: vec![LineInfo {
23                    line: "",
24                    line_index: line_start,
25                    start_byte: 0,
26                    end_byte: 0,
27                    end_line_size: 0,
28                }],
29                source,
30            };
31        }
32
33        let mut current_index = 0;
34
35        let mut mapping = vec![];
36        for (idx, (line, end_line)) in CursorLines::new(source).enumerate() {
37            let line_length = line.len();
38            let line_range = current_index..current_index + line_length;
39            let end_line_size = end_line.len();
40
41            mapping.push(LineInfo {
42                line,
43                line_index: line_start + idx,
44                start_byte: line_range.start,
45                end_byte: line_range.end + end_line_size,
46                end_line_size,
47            });
48
49            current_index += line_length + end_line_size;
50        }
51        Self {
52            lines: mapping,
53            source,
54        }
55    }
56
57    pub(crate) fn get_line(&self, idx: usize) -> Option<&'a str> {
58        self.lines
59            .iter()
60            .find(|l| l.line_index == idx)
61            .map(|info| info.line)
62    }
63
64    pub(crate) fn span_to_locations(&self, span: Range<usize>) -> (Loc, Loc) {
65        let start_info = self
66            .lines
67            .iter()
68            .find(|info| span.start >= info.start_byte && span.start < info.end_byte)
69            .unwrap_or(self.lines.last().unwrap());
70        let (mut start_char_pos, start_display_pos) = start_info.line
71            [0..(span.start - start_info.start_byte).min(start_info.line.len())]
72            .chars()
73            .fold((0, 0), |(char_pos, byte_pos), c| {
74                let display = char_width(c);
75                (char_pos + 1, byte_pos + display)
76            });
77        // correct the char pos if we are highlighting the end of a line
78        if (span.start - start_info.start_byte).saturating_sub(start_info.line.len()) > 0 {
79            start_char_pos += 1;
80        }
81        let start = Loc {
82            line: start_info.line_index,
83            char: start_char_pos,
84            display: start_display_pos,
85            byte: span.start,
86        };
87
88        if span.start == span.end {
89            return (start, start);
90        }
91
92        let end_info = self
93            .lines
94            .iter()
95            .find(|info| span.end >= info.start_byte && span.end < info.end_byte)
96            .unwrap_or(self.lines.last().unwrap());
97        let (end_char_pos, end_display_pos) = end_info.line
98            [0..(span.end - end_info.start_byte).min(end_info.line.len())]
99            .chars()
100            .fold((0, 0), |(char_pos, byte_pos), c| {
101                let display = char_width(c);
102                (char_pos + 1, byte_pos + display)
103            });
104
105        let mut end = Loc {
106            line: end_info.line_index,
107            char: end_char_pos,
108            display: end_display_pos,
109            byte: span.end,
110        };
111        if start.line != end.line && end.byte > end_info.end_byte - end_info.end_line_size {
112            end.char += 1;
113            end.display += 1;
114        }
115
116        (start, end)
117    }
118
119    pub(crate) fn span_to_snippet(&self, span: Range<usize>) -> Option<&str> {
120        self.source.get(span)
121    }
122
123    pub(crate) fn span_to_lines(&self, span: Range<usize>) -> Vec<&LineInfo<'a>> {
124        let mut lines = vec![];
125        let start = span.start;
126        let end = span.end;
127        for line_info in &self.lines {
128            if start >= line_info.end_byte {
129                continue;
130            }
131            if end < line_info.start_byte {
132                break;
133            }
134            lines.push(line_info);
135        }
136
137        if lines.is_empty() && !self.lines.is_empty() {
138            lines.push(self.lines.last().unwrap());
139        }
140
141        lines
142    }
143
144    pub(crate) fn annotated_lines(
145        &self,
146        annotations: Vec<Annotation<'a>>,
147        fold: bool,
148    ) -> (usize, Vec<AnnotatedLineInfo<'a>>) {
149        let source_len = self.source.len();
150        if let Some(bigger) = annotations.iter().find_map(|x| {
151            // Allow highlighting one past the last character in the source.
152            if source_len + 1 < x.span.end {
153                Some(&x.span)
154            } else {
155                None
156            }
157        }) {
158            panic!("Annotation range `{bigger:?}` is beyond the end of buffer `{source_len}`")
159        }
160
161        let mut annotated_line_infos = self
162            .lines
163            .iter()
164            .map(|info| AnnotatedLineInfo {
165                line: info.line,
166                line_index: info.line_index,
167                annotations: vec![],
168                keep: false,
169            })
170            .collect::<Vec<_>>();
171        let mut multiline_annotations = vec![];
172
173        for Annotation {
174            span,
175            label,
176            kind,
177            highlight_source,
178        } in annotations
179        {
180            let (lo, mut hi) = self.span_to_locations(span.clone());
181            if kind == AnnotationKind::Visible {
182                for line_idx in lo.line..=hi.line {
183                    self.keep_line(&mut annotated_line_infos, line_idx);
184                }
185                continue;
186            }
187            // Watch out for "empty spans". If we get a span like 6..6, we
188            // want to just display a `^` at 6, so convert that to
189            // 6..7. This is degenerate input, but it's best to degrade
190            // gracefully -- and the parser likes to supply a span like
191            // that for EOF, in particular.
192
193            if lo.display == hi.display && lo.line == hi.line {
194                hi.display += 1;
195            }
196
197            if lo.line == hi.line {
198                let line_ann = LineAnnotation {
199                    start: lo,
200                    end: hi,
201                    kind,
202                    label,
203                    annotation_type: LineAnnotationType::Singleline,
204                    highlight_source,
205                };
206                self.add_annotation_to_file(&mut annotated_line_infos, lo.line, line_ann);
207            } else {
208                multiline_annotations.push(MultilineAnnotation {
209                    depth: 1,
210                    start: lo,
211                    end: hi,
212                    kind,
213                    label,
214                    overlaps_exactly: false,
215                    highlight_source,
216                });
217            }
218        }
219
220        let mut primary_spans = vec![];
221
222        // Find overlapping multiline annotations, put them at different depths
223        multiline_annotations.sort_by_key(|ml| (ml.start.line, usize::MAX - ml.end.line));
224        for (outer_i, ann) in multiline_annotations.clone().into_iter().enumerate() {
225            if ann.kind.is_primary() {
226                primary_spans.push((ann.start, ann.end));
227            }
228            for (inner_i, a) in &mut multiline_annotations.iter_mut().enumerate() {
229                // Move all other multiline annotations overlapping with this one
230                // one level to the right.
231                if !ann.same_span(a)
232                    && num_overlap(ann.start.line, ann.end.line, a.start.line, a.end.line, true)
233                {
234                    a.increase_depth();
235                } else if ann.same_span(a) && outer_i != inner_i {
236                    a.overlaps_exactly = true;
237                } else {
238                    if primary_spans
239                        .iter()
240                        .any(|(s, e)| a.start == *s && a.end == *e)
241                    {
242                        a.kind = AnnotationKind::Primary;
243                    }
244                    break;
245                }
246            }
247        }
248
249        let mut max_depth = 0; // max overlapping multiline spans
250        for ann in &multiline_annotations {
251            max_depth = max(max_depth, ann.depth);
252        }
253        // Change order of multispan depth to minimize the number of overlaps in the ASCII art.
254        for a in &mut multiline_annotations {
255            a.depth = max_depth - a.depth + 1;
256        }
257        for ann in multiline_annotations {
258            let mut end_ann = ann.as_end();
259            if ann.overlaps_exactly {
260                end_ann.annotation_type = LineAnnotationType::Singleline;
261            } else {
262                // avoid output like
263                //
264                //  |        foo(
265                //  |   _____^
266                //  |  |_____|
267                //  | ||         bar,
268                //  | ||     );
269                //  | ||      ^
270                //  | ||______|
271                //  |  |______foo
272                //  |         baz
273                //
274                // and instead get
275                //
276                //  |       foo(
277                //  |  _____^
278                //  | |         bar,
279                //  | |     );
280                //  | |      ^
281                //  | |      |
282                //  | |______foo
283                //  |        baz
284                self.add_annotation_to_file(
285                    &mut annotated_line_infos,
286                    ann.start.line,
287                    ann.as_start(),
288                );
289                // 4 is the minimum vertical length of a multiline span when presented: two lines
290                // of code and two lines of underline. This is not true for the special case where
291                // the beginning doesn't have an underline, but the current logic seems to be
292                // working correctly.
293                let middle = min(ann.start.line + 4, ann.end.line);
294                // We'll show up to 4 lines past the beginning of the multispan start.
295                // We will *not* include the tail of lines that are only whitespace, a comment or
296                // a bare delimiter.
297                let filter = |s: &str| {
298                    let s = s.trim();
299                    // Consider comments as empty, but don't consider docstrings to be empty.
300                    !(s.starts_with("//") && !(s.starts_with("///") || s.starts_with("//!")))
301                        // Consider lines with nothing but whitespace, a single delimiter as empty.
302                        && !["", "{", "}", "(", ")", "[", "]"].contains(&s)
303                };
304                let until = (ann.start.line..middle)
305                    .rev()
306                    .filter_map(|line| self.get_line(line).map(|s| (line + 1, s)))
307                    .find(|(_, s)| filter(s))
308                    .map_or(ann.start.line, |(line, _)| line);
309                for line in ann.start.line + 1..until {
310                    // Every `|` that joins the beginning of the span (`___^`) to the end (`|__^`).
311                    self.add_annotation_to_file(&mut annotated_line_infos, line, ann.as_line());
312                }
313                let line_end = ann.end.line - 1;
314                let end_is_empty = self.get_line(line_end).map_or(false, |s| !filter(s));
315                if middle < line_end && !end_is_empty {
316                    self.add_annotation_to_file(&mut annotated_line_infos, line_end, ann.as_line());
317                }
318            }
319            self.add_annotation_to_file(&mut annotated_line_infos, end_ann.end.line, end_ann);
320        }
321
322        if fold {
323            annotated_line_infos.retain(|l| !l.annotations.is_empty() || l.keep);
324        }
325
326        (max_depth, annotated_line_infos)
327    }
328
329    fn add_annotation_to_file(
330        &self,
331        annotated_line_infos: &mut Vec<AnnotatedLineInfo<'a>>,
332        line_index: usize,
333        line_ann: LineAnnotation<'a>,
334    ) {
335        if let Some(line_info) = annotated_line_infos
336            .iter_mut()
337            .find(|line_info| line_info.line_index == line_index)
338        {
339            line_info.annotations.push(line_ann);
340        } else {
341            let info = self
342                .lines
343                .iter()
344                .find(|l| l.line_index == line_index)
345                .unwrap();
346            annotated_line_infos.push(AnnotatedLineInfo {
347                line: info.line,
348                line_index,
349                annotations: vec![line_ann],
350                keep: false,
351            });
352            annotated_line_infos.sort_by_key(|l| l.line_index);
353        }
354    }
355
356    fn keep_line(&self, annotated_line_infos: &mut Vec<AnnotatedLineInfo<'a>>, line_index: usize) {
357        if let Some(line_info) = annotated_line_infos
358            .iter_mut()
359            .find(|line_info| line_info.line_index == line_index)
360        {
361            line_info.keep = true;
362        } else {
363            let info = self
364                .lines
365                .iter()
366                .find(|l| l.line_index == line_index)
367                .unwrap();
368            annotated_line_infos.push(AnnotatedLineInfo {
369                line: info.line,
370                line_index,
371                annotations: vec![],
372                keep: true,
373            });
374            annotated_line_infos.sort_by_key(|l| l.line_index);
375        }
376    }
377
378    pub(crate) fn splice_lines<'b>(
379        &'a self,
380        mut patches: Vec<Patch<'b>>,
381        fold: bool,
382    ) -> Option<SplicedLines<'b>> {
383        fn push_trailing(
384            buf: &mut String,
385            line_opt: Option<&str>,
386            lo: &Loc,
387            hi_opt: Option<&Loc>,
388        ) -> usize {
389            let mut line_count = 0;
390            // Convert CharPos to Usize, as CharPose is character offset
391            // Extract low index and high index
392            let (lo, hi_opt) = (lo.char, hi_opt.map(|hi| hi.char));
393            if let Some(line) = line_opt {
394                if let Some(lo) = line.char_indices().map(|(i, _)| i).nth(lo) {
395                    // Get high index while account for rare unicode and emoji with char_indices
396                    let hi_opt = hi_opt.and_then(|hi| line.char_indices().map(|(i, _)| i).nth(hi));
397                    match hi_opt {
398                        // If high index exist, take string from low to high index
399                        Some(hi) if hi > lo => {
400                            // count how many '\n' exist
401                            line_count = line[lo..hi].matches('\n').count();
402                            buf.push_str(&line[lo..hi]);
403                        }
404                        Some(_) => (),
405                        // If high index absence, take string from low index till end string.len
406                        None => {
407                            // count how many '\n' exist
408                            line_count = line[lo..].matches('\n').count();
409                            buf.push_str(&line[lo..]);
410                        }
411                    }
412                }
413                // If high index is None
414                if hi_opt.is_none() {
415                    buf.push('\n');
416                }
417            }
418            line_count
419        }
420
421        let source_len = self.source.len();
422        if let Some(bigger) = patches.iter().find_map(|x| {
423            // Allow patching one past the last character in the source.
424            if source_len + 1 < x.span.end {
425                Some(&x.span)
426            } else {
427                None
428            }
429        }) {
430            panic!("Patch span `{bigger:?}` is beyond the end of buffer `{source_len}`")
431        }
432
433        // Assumption: all spans are in the same file, and all spans
434        // are disjoint. Sort in ascending order.
435        patches.sort_by_key(|p| p.span.start);
436
437        // Find the bounding span.
438        let (lo, hi) = if fold {
439            let lo = patches.iter().map(|p| p.span.start).min()?;
440            let hi = patches.iter().map(|p| p.span.end).max()?;
441            (lo, hi)
442        } else {
443            (0, source_len)
444        };
445
446        let lines = self.span_to_lines(lo..hi);
447
448        let mut highlights = vec![];
449        // To build up the result, we do this for each span:
450        // - push the line segment trailing the previous span
451        //   (at the beginning a "phantom" span pointing at the start of the line)
452        // - push lines between the previous and current span (if any)
453        // - if the previous and current span are not on the same line
454        //   push the line segment leading up to the current span
455        // - splice in the span substitution
456        //
457        // Finally push the trailing line segment of the last span
458        let (mut prev_hi, _) = self.span_to_locations(lo..hi);
459        prev_hi.char = 0;
460        let mut prev_line = lines.first().map(|line| line.line);
461        let mut buf = String::new();
462
463        let trimmed_patches = patches
464            .into_iter()
465            // If this is a replacement of, e.g. `"a"` into `"ab"`, adjust the
466            // suggestion and snippet to look as if we just suggested to add
467            // `"b"`, which is typically much easier for the user to understand.
468            .map(|part| part.trim_trivial_replacements(self.source))
469            .collect::<Vec<_>>();
470        let mut line_highlight = vec![];
471        // We need to keep track of the difference between the existing code and the added
472        // or deleted code in order to point at the correct column *after* substitution.
473        let mut acc = 0;
474        for part in &trimmed_patches {
475            let (cur_lo, cur_hi) = self.span_to_locations(part.span.clone());
476            if prev_hi.line == cur_lo.line {
477                let mut count = push_trailing(&mut buf, prev_line, &prev_hi, Some(&cur_lo));
478                while count > 0 {
479                    highlights.push(core::mem::take(&mut line_highlight));
480                    acc = 0;
481                    count -= 1;
482                }
483            } else {
484                acc = 0;
485                highlights.push(core::mem::take(&mut line_highlight));
486                let mut count = push_trailing(&mut buf, prev_line, &prev_hi, None);
487                while count > 0 {
488                    highlights.push(core::mem::take(&mut line_highlight));
489                    count -= 1;
490                }
491                // push lines between the previous and current span (if any)
492                for idx in prev_hi.line + 1..(cur_lo.line) {
493                    if let Some(line) = self.get_line(idx) {
494                        buf.push_str(line.as_ref());
495                        buf.push('\n');
496                        highlights.push(core::mem::take(&mut line_highlight));
497                    }
498                }
499                if let Some(cur_line) = self.get_line(cur_lo.line) {
500                    let end = match cur_line.char_indices().nth(cur_lo.char) {
501                        Some((i, _)) => i,
502                        None => cur_line.len(),
503                    };
504                    buf.push_str(&cur_line[..end]);
505                }
506            }
507            // Add a whole line highlight per line in the snippet.
508            let len: isize = part
509                .replacement
510                .split('\n')
511                .next()
512                .unwrap_or(&part.replacement)
513                .chars()
514                .map(|c| match c {
515                    '\t' => 4,
516                    _ => 1,
517                })
518                .sum();
519            line_highlight.push(SubstitutionHighlight {
520                start: (cur_lo.char as isize + acc) as usize,
521                end: (cur_lo.char as isize + acc + len) as usize,
522            });
523            buf.push_str(&part.replacement);
524            // Account for the difference between the width of the current code and the
525            // snippet being suggested, so that the *later* suggestions are correctly
526            // aligned on the screen. Note that cur_hi and cur_lo can be on different
527            // lines, so cur_hi.col can be smaller than cur_lo.col
528            acc += len - (cur_hi.char as isize - cur_lo.char as isize);
529            prev_hi = cur_hi;
530            prev_line = self.get_line(prev_hi.line);
531            for line in part.replacement.split('\n').skip(1) {
532                acc = 0;
533                highlights.push(core::mem::take(&mut line_highlight));
534                let end: usize = line
535                    .chars()
536                    .map(|c| match c {
537                        '\t' => 4,
538                        _ => 1,
539                    })
540                    .sum();
541                line_highlight.push(SubstitutionHighlight { start: 0, end });
542            }
543        }
544        highlights.push(core::mem::take(&mut line_highlight));
545        if fold {
546            // if the replacement already ends with a newline, don't print the next line
547            if !buf.ends_with('\n') {
548                push_trailing(&mut buf, prev_line, &prev_hi, None);
549            }
550        } else {
551            // Add the trailing part of the source after the last patch
552            if let Some(snippet) = self.span_to_snippet(prev_hi.byte..source_len) {
553                buf.push_str(snippet);
554                for _ in snippet.matches('\n') {
555                    highlights.push(core::mem::take(&mut line_highlight));
556                }
557            }
558        }
559        // remove trailing newlines
560        while buf.ends_with('\n') {
561            buf.pop();
562        }
563        if highlights.iter().all(|parts| parts.is_empty()) {
564            None
565        } else {
566            Some((buf, trimmed_patches, highlights))
567        }
568    }
569}
570
571#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
572pub(crate) struct MultilineAnnotation<'a> {
573    pub depth: usize,
574    pub start: Loc,
575    pub end: Loc,
576    pub kind: AnnotationKind,
577    pub label: Option<Cow<'a, str>>,
578    pub overlaps_exactly: bool,
579    pub highlight_source: bool,
580}
581
582impl<'a> MultilineAnnotation<'a> {
583    pub(crate) fn increase_depth(&mut self) {
584        self.depth += 1;
585    }
586
587    /// Compare two `MultilineAnnotation`s considering only the `Span` they cover.
588    pub(crate) fn same_span(&self, other: &MultilineAnnotation<'_>) -> bool {
589        self.start == other.start && self.end == other.end
590    }
591
592    pub(crate) fn as_start(&self) -> LineAnnotation<'a> {
593        LineAnnotation {
594            start: self.start,
595            end: Loc {
596                line: self.start.line,
597                char: self.start.char + 1,
598                display: self.start.display + 1,
599                byte: self.start.byte + 1,
600            },
601            kind: self.kind,
602            label: None,
603            annotation_type: LineAnnotationType::MultilineStart(self.depth),
604            highlight_source: self.highlight_source,
605        }
606    }
607
608    pub(crate) fn as_end(&self) -> LineAnnotation<'a> {
609        LineAnnotation {
610            start: Loc {
611                line: self.end.line,
612                char: self.end.char.saturating_sub(1),
613                display: self.end.display.saturating_sub(1),
614                byte: self.end.byte.saturating_sub(1),
615            },
616            end: self.end,
617            kind: self.kind,
618            label: self.label.clone(),
619            annotation_type: LineAnnotationType::MultilineEnd(self.depth),
620            highlight_source: self.highlight_source,
621        }
622    }
623
624    pub(crate) fn as_line(&self) -> LineAnnotation<'a> {
625        LineAnnotation {
626            start: Loc::default(),
627            end: Loc::default(),
628            kind: self.kind,
629            label: None,
630            annotation_type: LineAnnotationType::MultilineLine(self.depth),
631            highlight_source: self.highlight_source,
632        }
633    }
634}
635
636#[derive(Debug)]
637pub(crate) struct LineInfo<'a> {
638    pub(crate) line: &'a str,
639    pub(crate) line_index: usize,
640    pub(crate) start_byte: usize,
641    pub(crate) end_byte: usize,
642    end_line_size: usize,
643}
644
645#[derive(Debug)]
646pub(crate) struct AnnotatedLineInfo<'a> {
647    pub(crate) line: &'a str,
648    pub(crate) line_index: usize,
649    pub(crate) annotations: Vec<LineAnnotation<'a>>,
650    pub(crate) keep: bool,
651}
652
653/// A source code location used for error reporting.
654#[derive(Clone, Copy, Debug, Default, PartialOrd, Ord, PartialEq, Eq)]
655pub(crate) struct Loc {
656    /// The (1-based) line number.
657    pub(crate) line: usize,
658    /// The (0-based) column offset.
659    pub(crate) char: usize,
660    /// The (0-based) column offset when displayed.
661    pub(crate) display: usize,
662    /// The (0-based) byte offset.
663    pub(crate) byte: usize,
664}
665
666struct CursorLines<'a>(&'a str);
667
668impl CursorLines<'_> {
669    fn new(src: &str) -> CursorLines<'_> {
670        CursorLines(src)
671    }
672}
673
674#[derive(Copy, Clone, Debug, PartialEq)]
675enum EndLine {
676    Eof,
677    Lf,
678    Crlf,
679}
680
681impl EndLine {
682    /// The number of characters this line ending occupies in bytes.
683    pub(crate) fn len(self) -> usize {
684        match self {
685            EndLine::Eof => 0,
686            EndLine::Lf => 1,
687            EndLine::Crlf => 2,
688        }
689    }
690}
691
692impl<'a> Iterator for CursorLines<'a> {
693    type Item = (&'a str, EndLine);
694
695    fn next(&mut self) -> Option<Self::Item> {
696        if self.0.is_empty() {
697            None
698        } else {
699            self.0
700                .find('\n')
701                .map(|x| {
702                    let ret = if 0 < x {
703                        if self.0.as_bytes()[x - 1] == b'\r' {
704                            (&self.0[..x - 1], EndLine::Crlf)
705                        } else {
706                            (&self.0[..x], EndLine::Lf)
707                        }
708                    } else {
709                        ("", EndLine::Lf)
710                    };
711                    self.0 = &self.0[x + 1..];
712                    ret
713                })
714                .or_else(|| {
715                    let ret = Some((self.0, EndLine::Eof));
716                    self.0 = "";
717                    ret
718                })
719        }
720    }
721}
722
723pub(crate) type SplicedLines<'a> = (
724    String,
725    Vec<TrimmedPatch<'a>>,
726    Vec<Vec<SubstitutionHighlight>>,
727);
728
729/// Used to translate between `Span`s and byte positions within a single output line in highlighted
730/// code of structured suggestions.
731#[derive(Debug, Clone, Copy)]
732pub(crate) struct SubstitutionHighlight {
733    pub(crate) start: usize,
734    pub(crate) end: usize,
735}
736
737#[derive(Clone, Debug)]
738pub(crate) struct TrimmedPatch<'a> {
739    pub(crate) original_span: Range<usize>,
740    pub(crate) span: Range<usize>,
741    pub(crate) replacement: Cow<'a, str>,
742}
743
744impl<'a> TrimmedPatch<'a> {
745    pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool {
746        !self.replacement.is_empty() && !self.replaces_meaningful_content(sm)
747    }
748
749    pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
750        self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
751    }
752
753    pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
754        !self.replacement.is_empty() && self.replaces_meaningful_content(sm)
755    }
756
757    /// Whether this is a replacement that overwrites source with a snippet
758    /// in a way that isn't a superset of the original string. For example,
759    /// replacing "abc" with "abcde" is not destructive, but replacing it
760    /// it with "abx" is, since the "c" character is lost.
761    pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool {
762        self.is_replacement(sm)
763            && !sm
764                .span_to_snippet(self.span.clone())
765                // This should use `is_some_and` when our MSRV is >= 1.70
766                .map_or(false, |s| {
767                    as_substr(s.trim(), self.replacement.trim()).is_some()
768                })
769    }
770
771    fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
772        sm.span_to_snippet(self.span.clone())
773            .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
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.
781pub(crate) fn as_substr<'a>(
782    original: &'a str,
783    suggestion: &'a str,
784) -> Option<(usize, &'a str, usize)> {
785    if let Some(stripped) = suggestion.strip_prefix(original) {
786        Some((original.len(), stripped, 0))
787    } else if let Some(stripped) = suggestion.strip_suffix(original) {
788        Some((0, stripped, original.len()))
789    } else {
790        let common_prefix = original
791            .chars()
792            .zip(suggestion.chars())
793            .take_while(|(c1, c2)| c1 == c2)
794            .map(|(c, _)| c.len_utf8())
795            .sum();
796        let original = &original[common_prefix..];
797        let suggestion = &suggestion[common_prefix..];
798        if let Some(stripped) = suggestion.strip_suffix(original) {
799            let common_suffix = original.len();
800            Some((common_prefix, stripped, common_suffix))
801        } else {
802            None
803        }
804    }
805}