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