Skip to main content

annotate_snippets/renderer/
preprocess.rs

1use alloc::borrow::Cow;
2use alloc::vec::Vec;
3use core::cmp::max;
4
5use super::source_map::{AnnotatedLineInfo, SourceMap, SplicedLines, TrimmedPatch};
6use crate::{Annotation, Element, Group, Message, Origin, Padding, Patch, Snippet};
7
8pub(crate) struct Preprocessed<'a> {
9    pub(crate) max_line_num: Option<usize>,
10    pub(crate) report_primary_path: Option<&'a Cow<'a, str>>,
11    pub(crate) groups: Vec<PreprocessedGroup<'a>>,
12}
13
14impl Preprocessed<'_> {
15    pub(crate) fn preprocess<'a>(groups: &'a [Group<'a>]) -> Preprocessed<'a> {
16        preprocess(groups)
17    }
18}
19
20pub(crate) struct PreprocessedGroup<'a> {
21    pub(crate) group: &'a Group<'a>,
22    pub(crate) elements: Vec<PreprocessedElement<'a>>,
23    pub(crate) primary_path: Option<&'a Cow<'a, str>>,
24    pub(crate) max_depth: usize,
25}
26
27pub(crate) enum PreprocessedElement<'a> {
28    Message(&'a Message<'a>),
29    Cause(
30        (
31            &'a Snippet<'a, Annotation<'a>>,
32            SourceMap<'a>,
33            Vec<AnnotatedLineInfo<'a>>,
34        ),
35    ),
36    Suggestion(
37        (
38            &'a Snippet<'a, Patch<'a>>,
39            SourceMap<'a>,
40            SplicedLines<'a>,
41            DisplaySuggestion,
42        ),
43    ),
44    Origin(&'a Origin<'a>),
45    Padding(Padding),
46}
47
48fn preprocess<'a>(groups: &'a [Group<'a>]) -> Preprocessed<'a> {
49    let mut max_line_num = None;
50    let mut report_primary_path = None;
51    let mut out = Vec::with_capacity(groups.len());
52    for group in groups {
53        let mut elements = Vec::with_capacity(group.elements.len());
54        let mut primary_path = None;
55        let mut max_depth = 0;
56        for element in &group.elements {
57            match element {
58                Element::Message(message) => {
59                    elements.push(PreprocessedElement::Message(message));
60                }
61                Element::Cause(cause) => {
62                    let sm = SourceMap::new(&cause.source, cause.line_start);
63                    let (depth, annotated_lines) =
64                        sm.annotated_lines(cause.markers.clone(), cause.fold);
65
66                    if cause.fold {
67                        let end = cause
68                            .markers
69                            .iter()
70                            .map(|a| a.span.end)
71                            .max()
72                            .unwrap_or(cause.source.len())
73                            .min(cause.source.len());
74
75                        if cause.line_numbering {
76                            max_line_num = Some(max(
77                                cause.line_start + newline_count(&cause.source[..end]),
78                                max_line_num.unwrap_or(0),
79                            ));
80                        }
81                    } else {
82                        if cause.line_numbering {
83                            max_line_num = Some(max(
84                                cause.line_start + newline_count(&cause.source),
85                                max_line_num.unwrap_or(0),
86                            ));
87                        }
88                    }
89
90                    if primary_path.is_none() {
91                        primary_path = Some(cause.path.as_ref());
92                    }
93                    max_depth = max(depth, max_depth);
94                    elements.push(PreprocessedElement::Cause((cause, sm, annotated_lines)));
95                }
96                Element::Suggestion(suggestion) => {
97                    let sm = SourceMap::new(&suggestion.source, suggestion.line_start);
98                    if let Some(spliced_lines) =
99                        sm.splice_lines(suggestion.markers.clone(), suggestion.fold)
100                    {
101                        let display_suggestion = DisplaySuggestion::new(
102                            &spliced_lines.complete,
103                            &spliced_lines.patches,
104                            &sm,
105                        );
106
107                        if suggestion.line_numbering {
108                            if suggestion.fold {
109                                if let Some(first) = spliced_lines.patches.first() {
110                                    let (l_start, _) =
111                                        sm.span_to_locations(first.original_span.clone());
112                                    let nc = newline_count(&spliced_lines.complete);
113                                    let sugg_max_line_num = match display_suggestion {
114                                        DisplaySuggestion::Underline => l_start.line,
115                                        DisplaySuggestion::Diff => {
116                                            let file_lines = sm.span_to_lines(first.span.clone());
117                                            file_lines
118                                                .last()
119                                                .map_or(l_start.line + nc, |line| line.line_index)
120                                        }
121                                        DisplaySuggestion::None => l_start.line + nc,
122                                        DisplaySuggestion::Add => l_start.line + nc,
123                                    };
124                                    max_line_num =
125                                        Some(max(sugg_max_line_num, max_line_num.unwrap_or(0)));
126                                }
127                            } else {
128                                max_line_num = Some(max(
129                                    suggestion.line_start + newline_count(&spliced_lines.complete),
130                                    max_line_num.unwrap_or(0),
131                                ));
132                            }
133                        }
134
135                        elements.push(PreprocessedElement::Suggestion((
136                            suggestion,
137                            sm,
138                            spliced_lines,
139                            display_suggestion,
140                        )));
141                    }
142                }
143                Element::Origin(origin) => {
144                    if primary_path.is_none() {
145                        primary_path = Some(Some(&origin.path));
146                    }
147                    elements.push(PreprocessedElement::Origin(origin));
148                }
149                Element::Padding(padding) => {
150                    elements.push(PreprocessedElement::Padding(padding.clone()));
151                }
152            }
153        }
154        let group = PreprocessedGroup {
155            group,
156            elements,
157            primary_path: primary_path.unwrap_or_default(),
158            max_depth,
159        };
160        if report_primary_path.is_none() && group.primary_path.is_some() {
161            report_primary_path = group.primary_path;
162        }
163        out.push(group);
164    }
165
166    Preprocessed {
167        max_line_num,
168        report_primary_path,
169        groups: out,
170    }
171}
172
173fn newline_count(body: &str) -> usize {
174    #[cfg(feature = "simd")]
175    {
176        // Trailing newlines do not count towards the number of lines
177        // (this is based into `str::lines`)
178        let trailing_newline = body.ends_with('\n');
179        memchr::memchr_iter(b'\n', body.as_bytes()).count() - usize::from(trailing_newline)
180    }
181    #[cfg(not(feature = "simd"))]
182    {
183        body.lines().count().saturating_sub(1)
184    }
185}
186
187#[derive(Clone, Copy, Debug)]
188pub(crate) enum DisplaySuggestion {
189    Underline,
190    Diff,
191    None,
192    Add,
193}
194
195impl DisplaySuggestion {
196    fn new(complete: &str, patches: &[TrimmedPatch<'_>], sm: &SourceMap<'_>) -> Self {
197        let has_deletion = patches
198            .iter()
199            .any(|p| p.is_deletion(sm) || p.is_destructive_replacement(sm));
200        let is_multiline = complete.lines().count() > 1;
201        if has_deletion && !is_multiline {
202            Self::Diff
203        } else if patches.len() == 1
204            && patches.first().is_some_and(|p| {
205                p.replacement.ends_with('\n') && p.replacement.trim() == complete.trim()
206            })
207        {
208            // We are adding a line(s) of code before code that was already there.
209            Self::Add
210        } else if (patches.len() != 1 || patches[0].replacement.trim() != complete.trim())
211            && !is_multiline
212        {
213            Self::Underline
214        } else {
215            Self::None
216        }
217    }
218}
219
220#[cfg(test)]
221mod test {
222    use super::newline_count;
223
224    #[test]
225    fn ensure_newline_count_correct() {
226        let source = r#"
227                cargo-features = ["path-bases"]
228
229                [package]
230                name = "foo"
231                version = "0.5.0"
232                authors = ["wycats@example.com"]
233
234                [dependencies]
235                bar = { base = '^^not-valid^^', path = 'bar' }
236            "#;
237        assert_eq!(newline_count(source), 10);
238
239        assert_eq!(newline_count(""), 0);
240
241        assert_eq!(newline_count("one"), 0);
242
243        assert_eq!(newline_count("one\n"), 0);
244
245        assert_eq!(newline_count("one\ntwo"), 1);
246
247        assert_eq!(newline_count("one\ntwo\n"), 1);
248
249        assert_eq!(newline_count("one\n\n"), 1);
250
251        assert_eq!(newline_count("one\r\ntwo\r\n"), 1);
252    }
253}