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        let (bounding_lo, bounding_hi) = self.span_to_locations(lo..hi);
433
434        let mut highlights = vec![];
435        // To build up the result, we do this for each span:
436        // - push the line segment trailing the previous span
437        //   (at the beginning a "phantom" span pointing at the start of the line)
438        // - push lines between the previous and current span (if any)
439        // - if the previous and current span are not on the same line
440        //   push the line segment leading up to the current span
441        // - splice in the span substitution
442        //
443        // Finally push the trailing line segment of the last span
444        let mut prev_hi = bounding_lo;
445        prev_hi.char = 0;
446        let mut prev_line = lines.first().map(|line| line.line);
447        let mut buf = String::new();
448
449        let trimmed_patches = patches
450            .into_iter()
451            // If this is a replacement of, e.g. `"a"` into `"ab"`, adjust the
452            // suggestion and snippet to look as if we just suggested to add
453            // `"b"`, which is typically much easier for the user to understand.
454            .map(|part| part.trim_trivial_replacements(self.source))
455            .collect::<Vec<_>>();
456        let mut line_highlight = vec![];
457        // We need to keep track of the difference between the existing code and the added
458        // or deleted code in order to point at the correct column *after* substitution.
459        let mut acc = 0;
460        for part in &trimmed_patches {
461            let (cur_lo, cur_hi) = self.span_to_locations(part.span.clone());
462            if prev_hi.line == cur_lo.line {
463                push_trailing(&mut buf, prev_line, &prev_hi, Some(&cur_lo));
464            } else {
465                acc = 0;
466                highlights.push(core::mem::take(&mut line_highlight));
467                push_trailing(&mut buf, prev_line, &prev_hi, None);
468                // push lines between the previous and current span (if any)
469                for idx in prev_hi.line + 1..(cur_lo.line) {
470                    if let Some(line) = self.get_line(idx) {
471                        buf.push_str(line.as_ref());
472                        buf.push('\n');
473                        highlights.push(core::mem::take(&mut line_highlight));
474                    }
475                }
476                if let Some(cur_line) = self.get_line(cur_lo.line) {
477                    let end = match cur_line.char_indices().nth(cur_lo.char) {
478                        Some((i, _)) => i,
479                        None => cur_line.len(),
480                    };
481                    buf.push_str(&cur_line[..end]);
482                }
483            }
484            // Add a whole line highlight per line in the snippet.
485            let len: isize = part
486                .replacement
487                .split('\n')
488                .next()
489                .unwrap_or(&part.replacement)
490                .chars()
491                .map(|c| match c {
492                    '\t' => 4,
493                    _ => 1,
494                })
495                .sum();
496            line_highlight.push(SubstitutionHighlight {
497                start: (cur_lo.char.cast_signed() + acc).cast_unsigned(),
498                end: (cur_lo.char.cast_signed() + acc + len).cast_unsigned(),
499            });
500            buf.push_str(&part.replacement);
501            // Account for the difference between the width of the current code and the
502            // snippet being suggested, so that the *later* suggestions are correctly
503            // aligned on the screen. Note that cur_hi and cur_lo can be on different
504            // lines, so cur_hi.col can be smaller than cur_lo.col
505            acc += len - (cur_hi.char.cast_signed() - cur_lo.char.cast_signed());
506            prev_hi = cur_hi;
507            prev_line = self.get_line(prev_hi.line);
508            for line in part.replacement.split('\n').skip(1) {
509                acc = 0;
510                highlights.push(core::mem::take(&mut line_highlight));
511                let end: usize = line
512                    .chars()
513                    .map(|c| match c {
514                        '\t' => 4,
515                        _ => 1,
516                    })
517                    .sum();
518                line_highlight.push(SubstitutionHighlight { start: 0, end });
519            }
520        }
521        highlights.push(core::mem::take(&mut line_highlight));
522        if fold {
523            // if the replacement already ends with a newline, don't print the next line
524            if !buf.ends_with('\n') {
525                push_trailing(&mut buf, prev_line, &prev_hi, None);
526            }
527        } else {
528            // Add the trailing part of the source after the last patch
529            if let Some(snippet) = self.span_to_snippet(prev_hi.byte..source_len) {
530                buf.push_str(snippet);
531                for _ in snippet.matches('\n') {
532                    highlights.push(core::mem::take(&mut line_highlight));
533                }
534            }
535        }
536        // remove trailing newlines
537        while buf.ends_with('\n') {
538            buf.pop();
539        }
540
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(SplicedLines {
561                complete: buf,
562                patches: trimmed_patches,
563                highlights,
564                replaced_highlights,
565            })
566        }
567    }
568}
569
570#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
571pub(crate) struct MultilineAnnotation<'a> {
572    pub depth: usize,
573    pub start: Loc,
574    pub end: Loc,
575    pub kind: AnnotationKind,
576    pub label: Option<Cow<'a, str>>,
577    pub overlaps_exactly: bool,
578    pub highlight_source: bool,
579}
580
581impl<'a> MultilineAnnotation<'a> {
582    pub(crate) fn increase_depth(&mut self) {
583        self.depth += 1;
584    }
585
586    /// Compare two `MultilineAnnotation`s considering only the `Span` they cover.
587    pub(crate) fn same_span(&self, other: &MultilineAnnotation<'_>) -> bool {
588        self.start == other.start && self.end == other.end
589    }
590
591    pub(crate) fn as_start(&self) -> LineAnnotation<'a> {
592        LineAnnotation {
593            start: self.start,
594            end: Loc {
595                line: self.start.line,
596                char: self.start.char + 1,
597                display: self.start.display + 1,
598                byte: self.start.byte + 1,
599            },
600            kind: self.kind,
601            label: None,
602            annotation_type: LineAnnotationType::MultilineStart(self.depth),
603            highlight_source: self.highlight_source,
604        }
605    }
606
607    pub(crate) fn as_end(&self) -> LineAnnotation<'a> {
608        LineAnnotation {
609            start: Loc {
610                line: self.end.line,
611                char: self.end.char.saturating_sub(1),
612                display: self.end.display.saturating_sub(1),
613                byte: self.end.byte.saturating_sub(1),
614            },
615            end: self.end,
616            kind: self.kind,
617            label: self.label.clone(),
618            annotation_type: LineAnnotationType::MultilineEnd(self.depth),
619            highlight_source: self.highlight_source,
620        }
621    }
622
623    pub(crate) fn as_line(&self) -> LineAnnotation<'a> {
624        LineAnnotation {
625            start: Loc::default(),
626            end: Loc::default(),
627            kind: self.kind,
628            label: None,
629            annotation_type: LineAnnotationType::MultilineLine(self.depth),
630            highlight_source: self.highlight_source,
631        }
632    }
633}
634
635#[derive(Debug)]
636pub(crate) struct LineInfo<'a> {
637    pub(crate) line: &'a str,
638    pub(crate) line_index: usize,
639    pub(crate) start_byte: usize,
640    pub(crate) end_byte: usize,
641    end_line_size: usize,
642}
643
644#[derive(Debug)]
645pub(crate) struct AnnotatedLineInfo<'a> {
646    pub(crate) line: &'a str,
647    pub(crate) line_index: usize,
648    pub(crate) annotations: Vec<LineAnnotation<'a>>,
649    pub(crate) keep: bool,
650}
651
652/// A source code location used for error reporting.
653#[derive(Clone, Copy, Debug, Default, PartialOrd, Ord, PartialEq, Eq)]
654pub(crate) struct Loc {
655    /// The (1-based) line number.
656    pub(crate) line: usize,
657    /// The (0-based) column offset.
658    pub(crate) char: usize,
659    /// The (0-based) column offset when displayed.
660    pub(crate) display: usize,
661    /// The (0-based) byte offset.
662    pub(crate) byte: usize,
663}
664
665struct CursorLines<'a>(&'a str);
666
667impl CursorLines<'_> {
668    fn new(src: &str) -> CursorLines<'_> {
669        CursorLines(src)
670    }
671}
672
673#[derive(Copy, Clone, Debug, PartialEq)]
674enum EndLine {
675    Eof,
676    Lf,
677    Crlf,
678}
679
680impl EndLine {
681    /// The number of characters this line ending occupies in bytes.
682    pub(crate) fn len(self) -> usize {
683        match self {
684            Self::Eof => 0,
685            Self::Lf => 1,
686            Self::Crlf => 2,
687        }
688    }
689}
690
691impl<'a> Iterator for CursorLines<'a> {
692    type Item = (&'a str, EndLine);
693
694    fn next(&mut self) -> Option<Self::Item> {
695        if self.0.is_empty() {
696            None
697        } else {
698            self.0
699                .find('\n')
700                .map(|x| {
701                    let ret = if 0 < x {
702                        if self.0.as_bytes()[x - 1] == b'\r' {
703                            (&self.0[..x - 1], EndLine::Crlf)
704                        } else {
705                            (&self.0[..x], EndLine::Lf)
706                        }
707                    } else {
708                        ("", EndLine::Lf)
709                    };
710                    self.0 = &self.0[x + 1..];
711                    ret
712                })
713                .or_else(|| {
714                    let ret = Some((self.0, EndLine::Eof));
715                    self.0 = "";
716                    ret
717                })
718        }
719    }
720}
721
722pub(crate) struct SplicedLines<'a> {
723    pub(crate) complete: String,
724    pub(crate) patches: Vec<TrimmedPatch<'a>>,
725    // Char spans to highlight per line of the post-substitution output.
726    pub(crate) highlights: Vec<Vec<SubstitutionHighlight>>,
727    // Char spans of the replaced (original) code, per original line in the
728    // bounding range covered by the splice.
729    pub(crate) replaced_highlights: Vec<Vec<SubstitutionHighlight>>,
730}
731
732/// Used to translate between `Span`s and byte positions within a single output line in highlighted
733/// code of structured suggestions.
734#[derive(Debug, Clone, Copy)]
735pub(crate) struct SubstitutionHighlight {
736    pub(crate) start: usize,
737    pub(crate) end: usize,
738}
739
740#[derive(Clone, Debug)]
741pub(crate) struct TrimmedPatch<'a> {
742    pub(crate) original_span: Range<usize>,
743    pub(crate) span: Range<usize>,
744    pub(crate) replacement: Cow<'a, str>,
745}
746
747impl<'a> TrimmedPatch<'a> {
748    pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool {
749        !self.replacement.is_empty() && !self.replaces_meaningful_content(sm)
750    }
751
752    pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
753        self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
754    }
755
756    pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
757        !self.replacement.is_empty() && self.replaces_meaningful_content(sm)
758    }
759
760    /// Whether this is a replacement that overwrites source with a snippet
761    /// in a way that isn't a superset of the original string. For example,
762    /// replacing "abc" with "abcde" is not destructive, but replacing it
763    /// it with "abx" is, since the "c" character is lost.
764    pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool {
765        self.is_replacement(sm)
766            && sm
767                .span_to_snippet(self.span.clone())
768                .is_none_or(|s| as_substr(s.trim(), self.replacement.trim()).is_none())
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}