annotate_snippets/renderer/
source_map.rs

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