Skip to main content

annotate_snippets/renderer/
no_graphics.rs

1use alloc::borrow::{Cow, ToOwned};
2use alloc::string::{String, ToString};
3use alloc::vec::Vec;
4use core::cmp::Reverse;
5use core::fmt::{self, Write};
6
7use super::graphics::{Hyperlink, MessageOrTitle, TitleStyle, str_width};
8use super::preprocess::{Preprocessed, PreprocessedElement, PreprocessedGroup};
9use super::{ElementStyle, Stylesheet, normalize_whitespace};
10use crate::renderer::graphics::ANONYMIZED_LINE_NUM;
11use crate::{AnnotationKind, Id, Renderer, Report};
12
13/// Render all errors from a `Report` as text only.
14///
15/// We will render output as follows:
16///
17/// ```text
18/// error EXXXX: main message
19///  at $DIR/file.ext:LL:CC: span label
20///   on line LL, column KK: span label
21/// note: note message
22///  at $DIR/file.ext:LL:CC
23/// help: suggestion message
24///   on line LL, add `suggestion`
25/// ```
26pub(crate) fn render_no_graphics(
27    renderer: &Renderer,
28    groups: Report<'_>,
29) -> Result<String, fmt::Error> {
30    let mut output = String::new();
31    let Preprocessed {
32        max_line_num: _,
33        report_primary_path,
34        groups,
35    } = Preprocessed::preprocess(groups);
36
37    for PreprocessedGroup {
38        group,
39        elements,
40        primary_path,
41        max_depth: _,
42    } in groups
43    {
44        if let Some(title) = &group.title {
45            if !output.is_empty() {
46                writeln!(output)?;
47            }
48            let title_style = if title.allows_styling {
49                TitleStyle::Secondary
50            } else {
51                TitleStyle::Primary
52            };
53            render_title(
54                title,
55                &mut output,
56                title_style,
57                &renderer.stylesheet,
58                renderer.hyperlink,
59            )?;
60        }
61
62        let mut last_suggestion_path = None;
63        let mut suggestion_count = 0;
64
65        let mut message_iter = elements.into_iter().peekable();
66        while let Some(section) = message_iter.next() {
67            match section {
68                PreprocessedElement::Message(message) => {
69                    last_suggestion_path = None;
70                    if !output.is_empty() {
71                        writeln!(output)?;
72                    }
73                    render_title(
74                        message,
75                        &mut output,
76                        TitleStyle::Message,
77                        &renderer.stylesheet,
78                        renderer.hyperlink,
79                    )?;
80                }
81                PreprocessedElement::Cause((snippet, sm, _)) => {
82                    last_suggestion_path = None;
83
84                    let mut annotations = snippet
85                        .markers
86                        .iter()
87                        .filter(|ann| !matches!(ann.kind, AnnotationKind::Visible))
88                        .collect::<Vec<_>>();
89                    annotations.sort_by_key(|a| (Reverse(a.kind.is_primary()), a.span.start));
90                    if annotations.is_empty() {
91                        if !output.is_empty() {
92                            writeln!(output)?;
93                        }
94                        // We have a diagnostic with no span labels, but we should show *some*
95                        // position. We use the snippet start to provide that minimal context.
96                        write!(output, " ")?;
97                        render_path(
98                            &mut output,
99                            snippet.path.as_deref(),
100                            Some(snippet.line_start),
101                            None,
102                            renderer.anonymized_origin_line_numbers,
103                        )?;
104                    } else {
105                        for (i, annotation) in annotations.iter().enumerate() {
106                            let label = annotation.label.as_ref().filter(|s| !s.is_empty());
107                            if i > 0 && label.is_none() {
108                                continue;
109                            }
110                            let (lo, hi) =
111                                sm.span_to_locations(annotation.span.start..annotation.span.end);
112
113                            if !output.is_empty() {
114                                writeln!(output)?;
115                            }
116                            // Indent subsequent labels under the first label's file path.
117                            // Without a path, keep all labels at the same indentation.
118                            let padding = if i > 0 && snippet.path.is_some() {
119                                "  "
120                            } else {
121                                " "
122                            };
123                            write!(output, "{padding}")?;
124
125                            if i == 0
126                                && let Some(path) = &snippet.path
127                            {
128                                // `at $DIR/file.txt:LL:CC: label`
129                                //  ^^^^^^^^^^^^^^^^^^^^^^
130                                render_path(
131                                    &mut output,
132                                    Some(path),
133                                    Some(lo.line),
134                                    Some(lo.char + 1),
135                                    renderer.anonymized_origin_line_numbers,
136                                )?;
137                                if lo.line != hi.line {
138                                    // This is a multiline highlight, so we mention both the start and the
139                                    // end. `LL:CC to MM:DD`
140                                    //            ^^^^^^^^^
141                                    write!(output, " to {}:{}", hi.line, hi.char + 1)?;
142                                }
143                            } else {
144                                let col = if let Some(line) = sm.get_line(lo.line)
145                                    && line.chars().take(lo.char).all(|c| c.is_whitespace())
146                                {
147                                    // Everything before the span is whitespace, mentioning the column
148                                    // doesn't add information.
149                                    None
150                                } else {
151                                    Some(lo.char + 1)
152                                };
153                                render_path(
154                                    &mut output,
155                                    None,
156                                    Some(lo.line),
157                                    col,
158                                    renderer.anonymized_origin_line_numbers,
159                                )?;
160                                if lo.line != hi.line {
161                                    // This is a multiline highlight, so we mention both the start and the
162                                    // end. `line LL, column CC to line MM, column DD`
163                                    //                         ^^^^^^^^^^^^^^^^^^^^^^
164                                    write!(output, " to line {}", hi.line)?;
165                                    if let Some(line) = sm.get_line(hi.line)
166                                        && let Some(post) = line.get(hi.char..)
167                                        && post.chars().all(|c| c.is_whitespace())
168                                    {
169                                        // Everything after the span is whitespace, mentioning the
170                                        // column doesn't add information.
171                                    } else {
172                                        write!(output, ", column {}", hi.char + 1)?;
173                                    }
174                                }
175                            }
176
177                            if let Some(label) = label {
178                                // If the span has a label, we render it to the right of the position
179                                // information.
180                                // `at $DIR/file.txt:LL:CC: this is the label`
181                                //                        ^^^^^^^^^^^^^^^^^^^
182                                write!(output, ": {label}")?;
183                            }
184                        }
185                    }
186                }
187                PreprocessedElement::Suggestion((
188                    suggestion,
189                    sm,
190                    spliced_lines,
191                    _display_suggestion,
192                )) => {
193                    if spliced_lines.patches.is_empty() {
194                        // We have a suggestion with no patches, we don't render anything.
195                        continue;
196                    }
197
198                    let matches_previous_suggestion =
199                        last_suggestion_path == Some(suggestion.path.as_ref());
200
201                    suggestion_count = if matches_previous_suggestion {
202                        suggestion_count + 1
203                    } else {
204                        1
205                    };
206
207                    let matches_next_suggestion = matches!(
208                        message_iter.peek(),
209                        Some(PreprocessedElement::Suggestion((next, ..)))
210                            if next.path.as_ref() == suggestion.path.as_ref()
211                    );
212
213                    // We only include the file path when it is different from
214                    // the previous suggestion's path, or the primary path
215                    //
216                    // `at $DIR/file.txt:LL:CC: label`
217                    //  ^^^^^^^^^^^^^^^^^
218                    let previous_path =
219                        last_suggestion_path.unwrap_or(primary_path.or(report_primary_path));
220                    let path = if suggestion.path.as_ref() != previous_path
221                        && let Some(path) = suggestion.path.as_ref()
222                    {
223                        Some(path.as_ref())
224                    } else {
225                        None
226                    };
227
228                    let padding = if matches_previous_suggestion || matches_next_suggestion {
229                        if !output.is_empty() {
230                            writeln!(output)?;
231                        }
232                        write!(output, " option {suggestion_count}")?;
233                        "  "
234                    } else {
235                        " "
236                    };
237
238                    for patch in spliced_lines.patches.iter() {
239                        let (lo, hi) = sm.span_to_locations(patch.span.start..patch.span.end);
240
241                        let has_replacement = !patch.replacement.trim().is_empty();
242                        let is_addition = patch.is_addition(&sm);
243                        if !output.is_empty() {
244                            writeln!(output)?;
245                        }
246                        write!(output, "{padding}")?;
247
248                        if has_replacement && !is_addition {
249                            render_path(
250                                &mut output,
251                                path,
252                                Some(lo.line),
253                                Some(lo.char + 1),
254                                renderer.anonymized_origin_line_numbers,
255                            )?;
256                            if lo.line != hi.line {
257                                let line = if renderer.anonymized_origin_line_numbers {
258                                    ANONYMIZED_LINE_NUM.to_owned()
259                                } else {
260                                    hi.line.to_string()
261                                };
262                                write!(output, " to line {line}, column {}", hi.char + 1)?;
263                            } else {
264                                write!(output, " to column {}", hi.char + 1)?;
265                            }
266                        } else {
267                            let col = if lo.line == hi.line
268                                && let Some(line) = sm.get_line(lo.line)
269                                && let Some(pre) = line.get(..lo.byte)
270                                && pre.chars().all(|c| c.is_whitespace())
271                                && let Some(post) = line.get(hi.byte..)
272                                && (patch.replacement.lines().count() > 1
273                                    || post.chars().all(|c| c.is_whitespace()))
274                            {
275                                // We are changing the whole text in the line, no need to mention the
276                                // column.
277                                None
278                            } else {
279                                Some(lo.char + 1)
280                            };
281                            render_path(
282                                &mut output,
283                                path,
284                                Some(lo.line),
285                                col,
286                                renderer.anonymized_origin_line_numbers,
287                            )?;
288                        }
289
290                        if has_replacement {
291                            let action = if is_addition { "add" } else { "replace with" };
292                            write!(output, " {action}: ")?;
293
294                            let st = ElementStyle::Addition
295                                .color_spec(&crate::Level::NOTE, &renderer.stylesheet);
296
297                            write!(
298                                output,
299                                "{st}{}{st:#}",
300                                normalize_whitespace(patch.replacement.trim_end_matches('\n'))
301                            )?;
302                        }
303                    }
304                    last_suggestion_path = Some(suggestion.path.as_ref());
305                }
306                PreprocessedElement::Origin(origin) => {
307                    last_suggestion_path = None;
308                    if !output.is_empty() {
309                        writeln!(output)?;
310                    }
311                    write!(output, " ")?;
312                    render_path(
313                        &mut output,
314                        Some(&origin.path),
315                        origin.line,
316                        origin.char_column,
317                        renderer.anonymized_origin_line_numbers,
318                    )?;
319                }
320                PreprocessedElement::Padding(_) => {
321                    last_suggestion_path = None;
322                }
323            }
324        }
325    }
326
327    Ok(output)
328}
329
330/// Render the main message line for a diagnostic
331///
332/// This will print out:
333///
334/// ```text
335/// LEVEL CODE: message
336/// ```
337/// like the following:
338/// ```text
339/// error E0308: mismatched types
340/// ```
341fn render_title(
342    title: &dyn MessageOrTitle,
343    buffer: &mut String,
344    title_style: TitleStyle,
345    stylesheet: &Stylesheet,
346    hyperlink: bool,
347) -> Result<(), fmt::Error> {
348    let (label_style, title_element_style) = match title_style {
349        TitleStyle::Primary => (
350            ElementStyle::Level(title.level().level),
351            ElementStyle::MainHeaderMsg,
352        ),
353        TitleStyle::Secondary => (
354            ElementStyle::Level(title.level().level),
355            ElementStyle::HeaderMsg,
356        ),
357        TitleStyle::Message => (ElementStyle::MainHeaderMsg, ElementStyle::NoStyle),
358    };
359    let label_style = label_style.color_spec(title.level(), stylesheet);
360    let title_element_style = title_element_style.color_spec(title.level(), stylesheet);
361
362    let mut label_width = 0;
363    let level_is_visible = title.level().name != Some(None);
364    if level_is_visible || title.id().is_some() {
365        if title_style == TitleStyle::Message {
366            write!(buffer, " ")?;
367            label_width += 1;
368        }
369
370        if level_is_visible {
371            // error EXXXX: message
372            // ^^^^^
373            write!(buffer, "{}{}{0:#}", label_style, title.level().as_str(),)?;
374            label_width += str_width(title.level().as_str());
375        }
376
377        if let Some(Id { id: Some(id), url }) = &title.id() {
378            let url = url
379                .as_deref()
380                .filter(|_| hyperlink)
381                .map(Hyperlink::with_url)
382                .unwrap_or_default();
383
384            if level_is_visible {
385                // error EXXXX: message
386                //      ^
387                write!(buffer, " ")?;
388                label_width += 1;
389            }
390            // error EXXXX: message
391            //       ^^^^^
392            write!(buffer, "{label_style}{url}{id}{url:#}{label_style:#}")?;
393            label_width += str_width(id);
394        }
395        // error EXXXX: message
396        //            ^
397        write!(buffer, "{title_element_style}: {title_element_style:#}")?;
398        label_width += 2;
399    }
400    let padding = " ".repeat(label_width);
401
402    // error EXXXX: message
403    //              ^^^^^^^
404    let (title_str, st) = if title.allows_styling() {
405        (
406            Cow::Borrowed(title.text()),
407            ElementStyle::NoStyle.color_spec(title.level(), stylesheet),
408        )
409    } else {
410        (normalize_whitespace(title.text()), title_element_style)
411    };
412    for (i, text) in title_str.split('\n').enumerate() {
413        if i != 0 {
414            write!(buffer, "\n{padding}")?;
415        }
416        write!(buffer, "{st}{text}{st:#}")?;
417    }
418    Ok(())
419}
420
421/// Print out a file position optimized for the data available.
422///
423/// When the `path` is available, we print `at PATH:LL:CC`, which is detected by terminals and
424/// editors as a path to a specific place in a file. Otherwise, we print`on line LL, column CC`,
425/// for the elements that are available.
426fn render_path(
427    output: &mut String,
428    path: Option<&str>,
429    line: Option<usize>,
430    col: Option<usize>,
431    anonymized_origin_line_numbers: bool,
432) -> Result<(), fmt::Error> {
433    let line = line.map(|l| {
434        if anonymized_origin_line_numbers {
435            ANONYMIZED_LINE_NUM.to_owned()
436        } else {
437            l.to_string()
438        }
439    });
440
441    match (path, line, col) {
442        (Some(path), Some(line), Some(col)) => {
443            // `at $DIR/file.txt:LL:CC`
444            write!(output, "at {path}:{line}:{col}")?;
445        }
446        (Some(path), Some(line), None) => {
447            // `at $DIR/file.txt:LL`
448            write!(output, "at {path}:{line}")?;
449        }
450        (Some(path), None, None) => {
451            // `at $DIR/file.txt`
452            write!(output, "at {path}")?;
453        }
454        (None, Some(line), Some(col)) => {
455            // We are not printing the path, so instead we show
456            // ` on line LL, column CC`.
457            write!(output, "on line {line}, column {col}")?;
458        }
459        (None, Some(line), None) => {
460            // ` on line LL`
461            write!(output, "on line {line}")?;
462        }
463        (None, None, Some(col)) => {
464            // ` on column CC`
465            write!(output, "on column {col}")?;
466        }
467        (Some(path), None, Some(col)) => {
468            // This condition should never have been called.
469            write!(output, "at {path}, column {col}")?;
470        }
471        (None, None, None) => {}
472    }
473    Ok(())
474}