Skip to main content

annotate_snippets/renderer/
mod.rs

1//! The [Renderer] and its settings
2//!
3//! # Example
4//!
5//! ```
6//! # use annotate_snippets::*;
7//! # use annotate_snippets::renderer::*;
8//! # use annotate_snippets::Level;
9//! let report = // ...
10//! # &[Group::with_title(
11//! #     Level::ERROR
12//! #         .primary_title("unresolved import `baz::zed`")
13//! #         .id("E0432")
14//! # )];
15//!
16//! let renderer = Renderer::styled().decor_style(DecorStyle::Unicode);
17//! let output = renderer.render(report);
18//! anstream::println!("{output}");
19//! ```
20
21pub(crate) mod render;
22pub(crate) mod source_map;
23pub(crate) mod stylesheet;
24
25mod margin;
26mod styled_buffer;
27
28use alloc::string::String;
29
30use crate::Report;
31
32pub(crate) use render::normalize_whitespace;
33pub(crate) use render::ElementStyle;
34pub(crate) use render::UnderlineParts;
35pub(crate) use render::{char_width, num_overlap, LineAnnotation, LineAnnotationType};
36pub(crate) use stylesheet::Stylesheet;
37
38pub use anstyle::*;
39
40/// See [`Renderer::term_width`]
41pub const DEFAULT_TERM_WIDTH: usize = 140;
42
43const USE_WINDOWS_COLORS: bool = cfg!(windows) && !cfg!(feature = "testing-colors");
44const BRIGHT_BLUE: Style = if USE_WINDOWS_COLORS {
45    AnsiColor::BrightCyan.on_default()
46} else {
47    AnsiColor::BrightBlue.on_default()
48};
49/// [`Renderer::error`] applied by [`Renderer::styled`]
50pub const DEFAULT_ERROR_STYLE: Style = AnsiColor::BrightRed.on_default().effects(Effects::BOLD);
51/// [`Renderer::warning`] applied by [`Renderer::styled`]
52pub const DEFAULT_WARNING_STYLE: Style = if USE_WINDOWS_COLORS {
53    AnsiColor::BrightYellow.on_default()
54} else {
55    AnsiColor::Yellow.on_default()
56}
57.effects(Effects::BOLD);
58/// [`Renderer::info`] applied by [`Renderer::styled`]
59pub const DEFAULT_INFO_STYLE: Style = BRIGHT_BLUE.effects(Effects::BOLD);
60/// [`Renderer::note`] applied by [`Renderer::styled`]
61pub const DEFAULT_NOTE_STYLE: Style = AnsiColor::BrightGreen.on_default().effects(Effects::BOLD);
62/// [`Renderer::help`] applied by [`Renderer::styled`]
63pub const DEFAULT_HELP_STYLE: Style = AnsiColor::BrightCyan.on_default().effects(Effects::BOLD);
64/// [`Renderer::line_num`] applied by [`Renderer::styled`]
65pub const DEFAULT_LINE_NUM_STYLE: Style = BRIGHT_BLUE.effects(Effects::BOLD);
66/// [`Renderer::emphasis`] applied by [`Renderer::styled`]
67pub const DEFAULT_EMPHASIS_STYLE: Style = if USE_WINDOWS_COLORS {
68    AnsiColor::BrightWhite.on_default()
69} else {
70    Style::new()
71}
72.effects(Effects::BOLD);
73/// [`Renderer::none`] applied by [`Renderer::styled`]
74pub const DEFAULT_NONE_STYLE: Style = Style::new();
75/// [`Renderer::context`] applied by [`Renderer::styled`]
76pub const DEFAULT_CONTEXT_STYLE: Style = BRIGHT_BLUE.effects(Effects::BOLD);
77/// [`Renderer::addition`] applied by [`Renderer::styled`]
78pub const DEFAULT_ADDITION_STYLE: Style = AnsiColor::BrightGreen.on_default();
79/// [`Renderer::removal`] applied by [`Renderer::styled`]
80pub const DEFAULT_REMOVAL_STYLE: Style = AnsiColor::BrightRed.on_default();
81
82/// The [Renderer] for a [`Report`]
83///
84/// The caller is expected to detect any relevant terminal features and configure the renderer,
85/// including
86/// - ANSI Escape code support (always outputted with [`Renderer::styled`])
87/// - Terminal width ([`Renderer::term_width`])
88/// - Unicode support ([`Renderer::decor_style`])
89///
90/// # Example
91///
92/// ```
93/// # use annotate_snippets::*;
94/// # use annotate_snippets::renderer::*;
95/// # use annotate_snippets::Level;
96/// let report = // ...
97/// # &[Group::with_title(
98/// #     Level::ERROR
99/// #         .primary_title("unresolved import `baz::zed`")
100/// #         .id("E0432")
101/// # )];
102///
103/// let renderer = Renderer::styled();
104/// let output = renderer.render(report);
105/// anstream::println!("{output}");
106/// ```
107#[derive(Clone, Debug)]
108pub struct Renderer {
109    anonymized_line_numbers: bool,
110    term_width: usize,
111    decor_style: DecorStyle,
112    stylesheet: Stylesheet,
113    short_message: bool,
114}
115
116impl Renderer {
117    /// No terminal styling
118    pub const fn plain() -> Self {
119        Self {
120            anonymized_line_numbers: false,
121            term_width: DEFAULT_TERM_WIDTH,
122            decor_style: DecorStyle::Ascii,
123            stylesheet: Stylesheet::plain(),
124            short_message: false,
125        }
126    }
127
128    /// Default terminal styling
129    ///
130    /// If ANSI escape codes are not supported, either
131    /// - Call [`Renderer::plain`] instead
132    /// - Strip them after the fact, like with [`anstream`](https://docs.rs/anstream/latest/anstream/)
133    ///
134    /// # Note
135    ///
136    /// When testing styled terminal output, see the [`testing-colors` feature](crate#features)
137    pub const fn styled() -> Self {
138        Self {
139            stylesheet: Stylesheet {
140                error: DEFAULT_ERROR_STYLE,
141                warning: DEFAULT_WARNING_STYLE,
142                info: DEFAULT_INFO_STYLE,
143                note: DEFAULT_NOTE_STYLE,
144                help: DEFAULT_HELP_STYLE,
145                line_num: DEFAULT_LINE_NUM_STYLE,
146                emphasis: DEFAULT_EMPHASIS_STYLE,
147                none: DEFAULT_NONE_STYLE,
148                context: DEFAULT_CONTEXT_STYLE,
149                addition: DEFAULT_ADDITION_STYLE,
150                removal: DEFAULT_REMOVAL_STYLE,
151            },
152            ..Self::plain()
153        }
154    }
155
156    /// Abbreviate the message
157    pub const fn short_message(mut self, short_message: bool) -> Self {
158        self.short_message = short_message;
159        self
160    }
161
162    /// Set the width to render within
163    ///
164    /// Affects the rendering of [`Snippet`][crate::Snippet]s
165    pub const fn term_width(mut self, term_width: usize) -> Self {
166        self.term_width = term_width;
167        self
168    }
169
170    /// Set the character set used for rendering decor
171    pub const fn decor_style(mut self, decor_style: DecorStyle) -> Self {
172        self.decor_style = decor_style;
173        self
174    }
175
176    /// Anonymize line numbers
177    ///
178    /// When enabled, line numbers are replaced with `LL` which is useful for tests.
179    ///
180    /// # Example
181    ///
182    /// ```text
183    ///   --> $DIR/whitespace-trimming.rs:4:193
184    ///    |
185    /// LL | ...                   let _: () = 42;
186    ///    |                                   ^^ expected (), found integer
187    ///    |
188    /// ```
189    pub const fn anonymized_line_numbers(mut self, anonymized_line_numbers: bool) -> Self {
190        self.anonymized_line_numbers = anonymized_line_numbers;
191        self
192    }
193}
194
195impl Renderer {
196    /// Render a diagnostic [`Report`]
197    pub fn render(&self, groups: Report<'_>) -> String {
198        render::render(self, groups)
199    }
200}
201
202/// Customize [`Renderer::styled`]
203impl Renderer {
204    /// Override the output style for [error][crate::Level::ERROR]
205    pub const fn error(mut self, style: Style) -> Self {
206        self.stylesheet.error = style;
207        self
208    }
209
210    /// Override the output style for [warnings][crate::Level::WARNING]
211    pub const fn warning(mut self, style: Style) -> Self {
212        self.stylesheet.warning = style;
213        self
214    }
215
216    /// Override the output style for [info][crate::Level::INFO]
217    pub const fn info(mut self, style: Style) -> Self {
218        self.stylesheet.info = style;
219        self
220    }
221
222    /// Override the output style for [notes][crate::Level::NOTE]
223    pub const fn note(mut self, style: Style) -> Self {
224        self.stylesheet.note = style;
225        self
226    }
227
228    /// Override the output style for [help][crate::Level::HELP]
229    pub const fn help(mut self, style: Style) -> Self {
230        self.stylesheet.help = style;
231        self
232    }
233
234    /// Override the output style for line numbers in the [`Snippet`][crate::Snippet] gutter
235    pub const fn line_num(mut self, style: Style) -> Self {
236        self.stylesheet.line_num = style;
237        self
238    }
239
240    /// Override the output style for emphasis for the
241    /// [`primary_title`][crate::Level::primary_title]
242    pub const fn emphasis(mut self, style: Style) -> Self {
243        self.stylesheet.emphasis = style;
244        self
245    }
246
247    /// Override the output style for [`AnnotationKind::Context`][crate::AnnotationKind::Context]
248    pub const fn context(mut self, style: Style) -> Self {
249        self.stylesheet.context = style;
250        self
251    }
252
253    /// Override the output style for [`Patch`][crate::Patch] additions
254    pub const fn addition(mut self, style: Style) -> Self {
255        self.stylesheet.addition = style;
256        self
257    }
258
259    /// Override the output style for [`Patch`][crate::Patch] removals
260    pub const fn removal(mut self, style: Style) -> Self {
261        self.stylesheet.removal = style;
262        self
263    }
264
265    /// Override the output style for all other text
266    pub const fn none(mut self, style: Style) -> Self {
267        self.stylesheet.none = style;
268        self
269    }
270}
271
272/// The character set for rendering for decor
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum DecorStyle {
275    Ascii,
276    Unicode,
277}
278
279impl DecorStyle {
280    fn col_separator(&self) -> char {
281        match self {
282            DecorStyle::Ascii => '|',
283            DecorStyle::Unicode => '│',
284        }
285    }
286
287    fn note_separator(&self, is_cont: bool) -> &str {
288        match self {
289            DecorStyle::Ascii => "= ",
290            DecorStyle::Unicode if is_cont => "├ ",
291            DecorStyle::Unicode => "╰ ",
292        }
293    }
294
295    fn multi_suggestion_separator(&self) -> &'static str {
296        match self {
297            DecorStyle::Ascii => "|",
298            DecorStyle::Unicode => "├╴",
299        }
300    }
301
302    fn file_start(&self, is_first: bool, alone: bool) -> &'static str {
303        match self {
304            DecorStyle::Ascii => "--> ",
305            DecorStyle::Unicode if is_first && alone => " ─▸ ",
306            DecorStyle::Unicode if is_first => " ╭▸ ",
307            DecorStyle::Unicode => " ├▸ ",
308        }
309    }
310
311    fn secondary_file_start(&self) -> &'static str {
312        match self {
313            DecorStyle::Ascii => "::: ",
314            DecorStyle::Unicode => " ⸬  ",
315        }
316    }
317
318    fn diff(&self) -> char {
319        match self {
320            DecorStyle::Ascii => '~',
321            DecorStyle::Unicode => '±',
322        }
323    }
324
325    fn margin(&self) -> &'static str {
326        match self {
327            DecorStyle::Ascii => "...",
328            DecorStyle::Unicode => "…",
329        }
330    }
331
332    fn underline(&self, is_primary: bool) -> UnderlineParts {
333        //               X0 Y0
334        // label_start > ┯━━━━ < underline
335        //               │ < vertical_text_line
336        //               text
337
338        //    multiline_start_down ⤷ X0 Y0
339        //            top_left > ┌───╿──┘ < top_right_flat
340        //           top_left > ┏│━━━┙ < top_right
341        // multiline_vertical > ┃│
342        //                      ┃│   X1 Y1
343        //                      ┃│   X2 Y2
344        //                      ┃└────╿──┘ < multiline_end_same_line
345        //        bottom_left > ┗━━━━━┥ < bottom_right_with_text
346        //   multiline_horizontal ^   `X` is a good letter
347
348        // multiline_whole_line > ┏ X0 Y0
349        //                        ┃   X1 Y1
350        //                        ┗━━━━┛ < multiline_end_same_line
351
352        // multiline_whole_line > ┏ X0 Y0
353        //                        ┃ X1 Y1
354        //                        ┃  ╿ < multiline_end_up
355        //                        ┗━━┛ < bottom_right
356
357        match (self, is_primary) {
358            (DecorStyle::Ascii, true) => UnderlineParts {
359                style: ElementStyle::UnderlinePrimary,
360                underline: '^',
361                label_start: '^',
362                vertical_text_line: '|',
363                multiline_vertical: '|',
364                multiline_horizontal: '_',
365                multiline_whole_line: '/',
366                multiline_start_down: '^',
367                bottom_right: '|',
368                top_left: ' ',
369                top_right_flat: '^',
370                bottom_left: '|',
371                multiline_end_up: '^',
372                multiline_end_same_line: '^',
373                multiline_bottom_right_with_text: '|',
374            },
375            (DecorStyle::Ascii, false) => UnderlineParts {
376                style: ElementStyle::UnderlineSecondary,
377                underline: '-',
378                label_start: '-',
379                vertical_text_line: '|',
380                multiline_vertical: '|',
381                multiline_horizontal: '_',
382                multiline_whole_line: '/',
383                multiline_start_down: '-',
384                bottom_right: '|',
385                top_left: ' ',
386                top_right_flat: '-',
387                bottom_left: '|',
388                multiline_end_up: '-',
389                multiline_end_same_line: '-',
390                multiline_bottom_right_with_text: '|',
391            },
392            (DecorStyle::Unicode, true) => UnderlineParts {
393                style: ElementStyle::UnderlinePrimary,
394                underline: '━',
395                label_start: '┯',
396                vertical_text_line: '│',
397                multiline_vertical: '┃',
398                multiline_horizontal: '━',
399                multiline_whole_line: '┏',
400                multiline_start_down: '╿',
401                bottom_right: '┙',
402                top_left: '┏',
403                top_right_flat: '┛',
404                bottom_left: '┗',
405                multiline_end_up: '╿',
406                multiline_end_same_line: '┛',
407                multiline_bottom_right_with_text: '┥',
408            },
409            (DecorStyle::Unicode, false) => UnderlineParts {
410                style: ElementStyle::UnderlineSecondary,
411                underline: '─',
412                label_start: '┬',
413                vertical_text_line: '│',
414                multiline_vertical: '│',
415                multiline_horizontal: '─',
416                multiline_whole_line: '┌',
417                multiline_start_down: '│',
418                bottom_right: '┘',
419                top_left: '┌',
420                top_right_flat: '┘',
421                bottom_left: '└',
422                multiline_end_up: '│',
423                multiline_end_same_line: '┘',
424                multiline_bottom_right_with_text: '┤',
425            },
426        }
427    }
428}