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::{LineAnnotation, LineAnnotationType, char_width, num_overlap};
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).is_some_and(|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(buf: &mut String, line_opt: Option<&str>, lo: &Loc, hi_opt: Option<&Loc>) {
384            // Convert CharPos to Usize, as CharPose is character offset
385            // Extract low index and high index
386            let (lo, hi_opt) = (lo.char, hi_opt.map(|hi| hi.char));
387            if let Some(line) = line_opt {
388                if let Some(lo) = line.char_indices().map(|(i, _)| i).nth(lo) {
389                    // Get high index while account for rare unicode and emoji with char_indices
390                    let hi_opt = hi_opt.and_then(|hi| line.char_indices().map(|(i, _)| i).nth(hi));
391                    match hi_opt {
392                        // If high index exist, take string from low to high index
393                        Some(hi) if hi > lo => buf.push_str(&line[lo..hi]),
394                        Some(_) => (),
395                        // If high index absence, take string from low index till end string.len
396                        None => buf.push_str(&line[lo..]),
397                    }
398                }
399                // If high index is None
400                if hi_opt.is_none() {
401                    buf.push('\n');
402                }
403            }
404        }
405
406        let source_len = self.source.len();
407        if let Some(bigger) = patches.iter().find_map(|x| {
408            // Allow patching one past the last character in the source.
409            if source_len + 1 < x.span.end {
410                Some(&x.span)
411            } else {
412                None
413            }
414        }) {
415            panic!("Patch span `{bigger:?}` is beyond the end of buffer `{source_len}`")
416        }
417
418        // Assumption: all spans are in the same file, and all spans
419        // are disjoint. Sort in ascending order.
420        patches.sort_by_key(|p| p.span.start);
421
422        // Find the bounding span.
423        let (lo, hi) = if fold {
424            let lo = patches.iter().map(|p| p.span.start).min()?;
425            let hi = patches.iter().map(|p| p.span.end).max()?;
426            (lo, hi)
427        } else {
428            (0, source_len)
429        };
430
431        let lines = self.span_to_lines(lo..hi);
432
433        let mut highlights = vec![];
434        // To build up the result, we do this for each span:
435        // - push the line segment trailing the previous span
436        //   (at the beginning a "phantom" span pointing at the start of the line)
437        // - push lines between the previous and current span (if any)
438        // - if the previous and current span are not on the same line
439        //   push the line segment leading up to the current span
440        // - splice in the span substitution
441        //
442        // Finally push the trailing line segment of the last span
443        let (mut prev_hi, _) = self.span_to_locations(lo..hi);
444        prev_hi.char = 0;
445        let mut prev_line = lines.first().map(|line| line.line);
446        let mut buf = String::new();
447
448        let trimmed_patches = patches
449            .into_iter()
450            // If this is a replacement of, e.g. `"a"` into `"ab"`, adjust the
451            // suggestion and snippet to look as if we just suggested to add
452            // `"b"`, which is typically much easier for the user to understand.
453            .map(|part| part.trim_trivial_replacements(self.source))
454            .collect::<Vec<_>>();
455        let mut line_highlight = vec![];
456        // We need to keep track of the difference between the existing code and the added
457        // or deleted code in order to point at the correct column *after* substitution.
458        let mut acc = 0;
459        for part in &trimmed_patches {
460            let (cur_lo, cur_hi) = self.span_to_locations(part.span.clone());
461            if prev_hi.line == cur_lo.line {
462                push_trailing(&mut buf, prev_line, &prev_hi, Some(&cur_lo));
463            } else {
464                acc = 0;
465                highlights.push(core::mem::take(&mut line_highlight));
466                push_trailing(&mut buf, prev_line, &prev_hi, None);
467                // push lines between the previous and current span (if any)
468                for idx in prev_hi.line + 1..(cur_lo.line) {
469                    if let Some(line) = self.get_line(idx) {
470                        buf.push_str(line.as_ref());
471                        buf.push('\n');
472                        highlights.push(core::mem::take(&mut line_highlight));
473                    }
474                }
475                if let Some(cur_line) = self.get_line(cur_lo.line) {
476                    let end = match cur_line.char_indices().nth(cur_lo.char) {
477                        Some((i, _)) => i,
478                        None => cur_line.len(),
479                    };
480                    buf.push_str(&cur_line[..end]);
481                }
482            }
483            // Add a whole line highlight per line in the snippet.
484            let len: isize = part
485                .replacement
486                .split('\n')
487                .next()
488                .unwrap_or(&part.replacement)
489                .chars()
490                .map(|c| match c {
491                    '\t' => 4,
492                    _ => 1,
493                })
494                .sum();
495            line_highlight.push(SubstitutionHighlight {
496                start: (cur_lo.char as isize + acc) as usize,
497                end: (cur_lo.char as isize + acc + len) as usize,
498            });
499            buf.push_str(&part.replacement);
500            // Account for the difference between the width of the current code and the
501            // snippet being suggested, so that the *later* suggestions are correctly
502            // aligned on the screen. Note that cur_hi and cur_lo can be on different
503            // lines, so cur_hi.col can be smaller than cur_lo.col
504            acc += len - (cur_hi.char as isize - cur_lo.char as isize);
505            prev_hi = cur_hi;
506            prev_line = self.get_line(prev_hi.line);
507            for line in part.replacement.split('\n').skip(1) {
508                acc = 0;
509                highlights.push(core::mem::take(&mut line_highlight));
510                let end: usize = line
511                    .chars()
512                    .map(|c| match c {
513                        '\t' => 4,
514                        _ => 1,
515                    })
516                    .sum();
517                line_highlight.push(SubstitutionHighlight { start: 0, end });
518            }
519        }
520        highlights.push(core::mem::take(&mut line_highlight));
521        if fold {
522            // if the replacement already ends with a newline, don't print the next line
523            if !buf.ends_with('\n') {
524                push_trailing(&mut buf, prev_line, &prev_hi, None);
525            }
526        } else {
527            // Add the trailing part of the source after the last patch
528            if let Some(snippet) = self.span_to_snippet(prev_hi.byte..source_len) {
529                buf.push_str(snippet);
530                for _ in snippet.matches('\n') {
531                    highlights.push(core::mem::take(&mut line_highlight));
532                }
533            }
534        }
535        // remove trailing newlines
536        while buf.ends_with('\n') {
537            buf.pop();
538        }
539
540        let (bounding_lo, bounding_hi) = self.span_to_locations(lo..hi);
541        let line_count = bounding_hi.line.saturating_sub(bounding_lo.line) + 1;
542        let mut replaced_highlights: Vec<Vec<SubstitutionHighlight>> = vec![Vec::new(); line_count];
543        for part in &trimmed_patches {
544            let (cur_lo, cur_hi) = self.span_to_locations(part.span.clone());
545            for line in cur_lo.line..=cur_hi.line {
546                let start = if line == cur_lo.line { cur_lo.char } else { 0 };
547                let end = if line == cur_hi.line {
548                    cur_hi.char
549                } else {
550                    self.get_line(line).unwrap_or_default().chars().count()
551                };
552                replaced_highlights[line - bounding_lo.line]
553                    .push(SubstitutionHighlight { start, end });
554            }
555        }
556
557        if highlights.iter().all(|parts| parts.is_empty()) {
558            None
559        } else {
560            Some((buf, trimmed_patches, highlights, replaced_highlights))
561        }
562    }
563}
564
565#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
566pub(crate) struct MultilineAnnotation<'a> {
567    pub depth: usize,
568    pub start: Loc,
569    pub end: Loc,
570    pub kind: AnnotationKind,
571    pub label: Option<Cow<'a, str>>,
572    pub overlaps_exactly: bool,
573    pub highlight_source: bool,
574}
575
576impl<'a> MultilineAnnotation<'a> {
577    pub(crate) fn increase_depth(&mut self) {
578        self.depth += 1;
579    }
580
581    /// Compare two `MultilineAnnotation`s considering only the `Span` they cover.
582    pub(crate) fn same_span(&self, other: &MultilineAnnotation<'_>) -> bool {
583        self.start == other.start && self.end == other.end
584    }
585
586    pub(crate) fn as_start(&self) -> LineAnnotation<'a> {
587        LineAnnotation {
588            start: self.start,
589            end: Loc {
590                line: self.start.line,
591                char: self.start.char + 1,
592                display: self.start.display + 1,
593                byte: self.start.byte + 1,
594            },
595            kind: self.kind,
596            label: None,
597            annotation_type: LineAnnotationType::MultilineStart(self.depth),
598            highlight_source: self.highlight_source,
599        }
600    }
601
602    pub(crate) fn as_end(&self) -> LineAnnotation<'a> {
603        LineAnnotation {
604            start: Loc {
605                line: self.end.line,
606                char: self.end.char.saturating_sub(1),
607                display: self.end.display.saturating_sub(1),
608                byte: self.end.byte.saturating_sub(1),
609            },
610            end: self.end,
611            kind: self.kind,
612            label: self.label.clone(),
613            annotation_type: LineAnnotationType::MultilineEnd(self.depth),
614            highlight_source: self.highlight_source,
615        }
616    }
617
618    pub(crate) fn as_line(&self) -> LineAnnotation<'a> {
619        LineAnnotation {
620            start: Loc::default(),
621            end: Loc::default(),
622            kind: self.kind,
623            label: None,
624            annotation_type: LineAnnotationType::MultilineLine(self.depth),
625            highlight_source: self.highlight_source,
626        }
627    }
628}
629
630#[derive(Debug)]
631pub(crate) struct LineInfo<'a> {
632    pub(crate) line: &'a str,
633    pub(crate) line_index: usize,
634    pub(crate) start_byte: usize,
635    pub(crate) end_byte: usize,
636    end_line_size: usize,
637}
638
639#[derive(Debug)]
640pub(crate) struct AnnotatedLineInfo<'a> {
641    pub(crate) line: &'a str,
642    pub(crate) line_index: usize,
643    pub(crate) annotations: Vec<LineAnnotation<'a>>,
644    pub(crate) keep: bool,
645}
646
647/// A source code location used for error reporting.
648#[derive(Clone, Copy, Debug, Default, PartialOrd, Ord, PartialEq, Eq)]
649pub(crate) struct Loc {
650    /// The (1-based) line number.
651    pub(crate) line: usize,
652    /// The (0-based) column offset.
653    pub(crate) char: usize,
654    /// The (0-based) column offset when displayed.
655    pub(crate) display: usize,
656    /// The (0-based) byte offset.
657    pub(crate) byte: usize,
658}
659
660struct CursorLines<'a>(&'a str);
661
662impl CursorLines<'_> {
663    fn new(src: &str) -> CursorLines<'_> {
664        CursorLines(src)
665    }
666}
667
668#[derive(Copy, Clone, Debug, PartialEq)]
669enum EndLine {
670    Eof,
671    Lf,
672    Crlf,
673}
674
675impl EndLine {
676    /// The number of characters this line ending occupies in bytes.
677    pub(crate) fn len(self) -> usize {
678        match self {
679            EndLine::Eof => 0,
680            EndLine::Lf => 1,
681            EndLine::Crlf => 2,
682        }
683    }
684}
685
686impl<'a> Iterator for CursorLines<'a> {
687    type Item = (&'a str, EndLine);
688
689    fn next(&mut self) -> Option<Self::Item> {
690        if self.0.is_empty() {
691            None
692        } else {
693            self.0
694                .find('\n')
695                .map(|x| {
696                    let ret = if 0 < x {
697                        if self.0.as_bytes()[x - 1] == b'\r' {
698                            (&self.0[..x - 1], EndLine::Crlf)
699                        } else {
700                            (&self.0[..x], EndLine::Lf)
701                        }
702                    } else {
703                        ("", EndLine::Lf)
704                    };
705                    self.0 = &self.0[x + 1..];
706                    ret
707                })
708                .or_else(|| {
709                    let ret = Some((self.0, EndLine::Eof));
710                    self.0 = "";
711                    ret
712                })
713        }
714    }
715}
716
717pub(crate) type SplicedLines<'a> = (
718    String,
719    Vec<TrimmedPatch<'a>>,
720    // Char spans to highlight per line of the post-substitution output.
721    Vec<Vec<SubstitutionHighlight>>,
722    // Char spans of the replaced (original) code, per original line in the
723    // bounding range covered by the splice.
724    Vec<Vec<SubstitutionHighlight>>,
725);
726
727/// Used to translate between `Span`s and byte positions within a single output line in highlighted
728/// code of structured suggestions.
729#[derive(Debug, Clone, Copy)]
730pub(crate) struct SubstitutionHighlight {
731    pub(crate) start: usize,
732    pub(crate) end: usize,
733}
734
735#[derive(Clone, Debug)]
736pub(crate) struct TrimmedPatch<'a> {
737    pub(crate) original_span: Range<usize>,
738    pub(crate) span: Range<usize>,
739    pub(crate) replacement: Cow<'a, str>,
740}
741
742impl<'a> TrimmedPatch<'a> {
743    pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool {
744        !self.replacement.is_empty() && !self.replaces_meaningful_content(sm)
745    }
746
747    pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
748        self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
749    }
750
751    pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
752        !self.replacement.is_empty() && self.replaces_meaningful_content(sm)
753    }
754
755    /// Whether this is a replacement that overwrites source with a snippet
756    /// in a way that isn't a superset of the original string. For example,
757    /// replacing "abc" with "abcde" is not destructive, but replacing it
758    /// it with "abx" is, since the "c" character is lost.
759    pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool {
760        self.is_replacement(sm)
761            && sm
762                .span_to_snippet(self.span.clone())
763                .is_none_or(|s| as_substr(s.trim(), self.replacement.trim()).is_none())
764    }
765
766    fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
767        sm.span_to_snippet(self.span.clone())
768            .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
769    }
770}
771
772/// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect
773/// the case where a substring of the suggestion is "sandwiched" in the original, like
774/// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length
775/// of the suffix.
776pub(crate) fn as_substr<'a>(
777    original: &'a str,
778    suggestion: &'a str,
779) -> Option<(usize, &'a str, usize)> {
780    if let Some(stripped) = suggestion.strip_prefix(original) {
781        Some((original.len(), stripped, 0))
782    } else if let Some(stripped) = suggestion.strip_suffix(original) {
783        Some((0, stripped, original.len()))
784    } else {
785        let common_prefix = original
786            .chars()
787            .zip(suggestion.chars())
788            .take_while(|(c1, c2)| c1 == c2)
789            .map(|(c, _)| c.len_utf8())
790            .sum();
791        let original = &original[common_prefix..];
792        let suggestion = &suggestion[common_prefix..];
793        if let Some(stripped) = suggestion.strip_suffix(original) {
794            let common_suffix = original.len();
795            Some((common_prefix, stripped, common_suffix))
796        } else {
797            None
798        }
799    }
800}