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::ElementStyle;
33pub(crate) use render::UnderlineParts;
34pub(crate) use render::normalize_whitespace;
35pub(crate) use render::{LineAnnotation, LineAnnotationType, char_width, num_overlap};
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    hyperlink: bool,
114    short_message: bool,
115    cut_indicator: Option<&'static str>,
116}
117
118impl Renderer {
119    /// No terminal styling
120    pub const fn plain() -> Self {
121        Self {
122            anonymized_line_numbers: false,
123            term_width: DEFAULT_TERM_WIDTH,
124            decor_style: DecorStyle::Ascii,
125            stylesheet: Stylesheet::plain(),
126            hyperlink: false,
127            short_message: false,
128            cut_indicator: None,
129        }
130    }
131
132    /// Default terminal styling
133    ///
134    /// If ANSI escape codes are not supported, either
135    /// - Call [`Renderer::plain`] instead
136    /// - Strip them after the fact, like with [`anstream`](https://docs.rs/anstream/latest/anstream/)
137    ///
138    /// # Note
139    ///
140    /// When testing styled terminal output, see the [`testing-colors` feature](crate#features)
141    pub const fn styled() -> Self {
142        Self {
143            stylesheet: Stylesheet {
144                error: DEFAULT_ERROR_STYLE,
145                warning: DEFAULT_WARNING_STYLE,
146                info: DEFAULT_INFO_STYLE,
147                note: DEFAULT_NOTE_STYLE,
148                help: DEFAULT_HELP_STYLE,
149                line_num: DEFAULT_LINE_NUM_STYLE,
150                emphasis: DEFAULT_EMPHASIS_STYLE,
151                none: DEFAULT_NONE_STYLE,
152                context: DEFAULT_CONTEXT_STYLE,
153                addition: DEFAULT_ADDITION_STYLE,
154                removal: DEFAULT_REMOVAL_STYLE,
155            },
156            hyperlink: true,
157            ..Self::plain()
158        }
159    }
160
161    /// Abbreviate the message
162    pub const fn short_message(mut self, short_message: bool) -> Self {
163        self.short_message = short_message;
164        self
165    }
166
167    /// Set the width to render within
168    ///
169    /// Affects the rendering of [`Snippet`][crate::Snippet]s
170    pub const fn term_width(mut self, term_width: usize) -> Self {
171        self.term_width = term_width;
172        self
173    }
174
175    /// Set the character set used for rendering decor
176    pub const fn decor_style(mut self, decor_style: DecorStyle) -> Self {
177        self.decor_style = decor_style;
178        self
179    }
180
181    /// Anonymize line numbers
182    ///
183    /// When enabled, line numbers are replaced with `LL` which is useful for tests.
184    ///
185    /// # Example
186    ///
187    /// ```text
188    ///   --> $DIR/whitespace-trimming.rs:4:193
189    ///    |
190    /// LL | ...                   let _: () = 42;
191    ///    |                                   ^^ expected (), found integer
192    ///    |
193    /// ```
194    pub const fn anonymized_line_numbers(mut self, anonymized_line_numbers: bool) -> Self {
195        self.anonymized_line_numbers = anonymized_line_numbers;
196        self
197    }
198}
199
200impl Renderer {
201    /// Render a diagnostic [`Report`]
202    pub fn render(&self, groups: Report<'_>) -> String {
203        render::render(self, groups)
204    }
205}
206
207/// Customize [`Renderer::styled`]
208impl Renderer {
209    /// Override the output style for [error][crate::Level::ERROR]
210    pub const fn error(mut self, style: Style) -> Self {
211        self.stylesheet.error = style;
212        self
213    }
214
215    /// Override the output style for [warnings][crate::Level::WARNING]
216    pub const fn warning(mut self, style: Style) -> Self {
217        self.stylesheet.warning = style;
218        self
219    }
220
221    /// Override the output style for [info][crate::Level::INFO]
222    pub const fn info(mut self, style: Style) -> Self {
223        self.stylesheet.info = style;
224        self
225    }
226
227    /// Override the output style for [notes][crate::Level::NOTE]
228    pub const fn note(mut self, style: Style) -> Self {
229        self.stylesheet.note = style;
230        self
231    }
232
233    /// Override the output style for [help][crate::Level::HELP]
234    pub const fn help(mut self, style: Style) -> Self {
235        self.stylesheet.help = style;
236        self
237    }
238
239    /// Override the output style for line numbers in the [`Snippet`][crate::Snippet] gutter
240    pub const fn line_num(mut self, style: Style) -> Self {
241        self.stylesheet.line_num = style;
242        self
243    }
244
245    /// Override the output style for emphasis for the
246    /// [`primary_title`][crate::Level::primary_title]
247    pub const fn emphasis(mut self, style: Style) -> Self {
248        self.stylesheet.emphasis = style;
249        self
250    }
251
252    /// Override the output style for [`AnnotationKind::Context`][crate::AnnotationKind::Context]
253    pub const fn context(mut self, style: Style) -> Self {
254        self.stylesheet.context = style;
255        self
256    }
257
258    /// Override the output style for [`Patch`][crate::Patch] additions
259    pub const fn addition(mut self, style: Style) -> Self {
260        self.stylesheet.addition = style;
261        self
262    }
263
264    /// Override the output style for [`Patch`][crate::Patch] removals
265    pub const fn removal(mut self, style: Style) -> Self {
266        self.stylesheet.removal = style;
267        self
268    }
269
270    /// Override the output style for all other text
271    pub const fn none(mut self, style: Style) -> Self {
272        self.stylesheet.none = style;
273        self
274    }
275
276    pub const fn hyperlink(mut self, hyperlink: bool) -> Self {
277        self.hyperlink = hyperlink;
278        self
279    }
280
281    /// Set the string used for when a long line is cut.
282    ///
283    /// The default for [`DecorStyle::Ascii`] is `...` (three `U+002E` characters).
284    pub const fn cut_indicator(mut self, cut: &'static str) -> Self {
285        self.cut_indicator = Some(cut);
286        self
287    }
288}
289
290/// The character set for rendering for decor
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub enum DecorStyle {
293    Ascii,
294    Unicode,
295}
296
297impl DecorStyle {
298    fn col_separator(&self) -> char {
299        match self {
300            DecorStyle::Ascii => '|',
301            DecorStyle::Unicode => '│',
302        }
303    }
304
305    fn note_separator(&self, is_cont: bool) -> &str {
306        match self {
307            DecorStyle::Ascii => "= ",
308            DecorStyle::Unicode if is_cont => "├ ",
309            DecorStyle::Unicode => "╰ ",
310        }
311    }
312
313    fn multi_suggestion_separator(&self) -> &'static str {
314        match self {
315            DecorStyle::Ascii => "|",
316            DecorStyle::Unicode => "├╴",
317        }
318    }
319
320    fn file_start(&self, is_first: bool, alone: bool) -> &'static str {
321        match self {
322            DecorStyle::Ascii => "--> ",
323            DecorStyle::Unicode if is_first && alone => " ─▸ ",
324            DecorStyle::Unicode if is_first => " ╭▸ ",
325            DecorStyle::Unicode => " ├▸ ",
326        }
327    }
328
329    fn secondary_file_start(&self) -> &'static str {
330        match self {
331            DecorStyle::Ascii => "::: ",
332            DecorStyle::Unicode => " ⸬  ",
333        }
334    }
335
336    fn diff(&self) -> char {
337        match self {
338            DecorStyle::Ascii => '~',
339            DecorStyle::Unicode => '±',
340        }
341    }
342
343    fn margin(&self) -> &'static str {
344        match self {
345            DecorStyle::Ascii => "...",
346            DecorStyle::Unicode => "…",
347        }
348    }
349
350    fn underline(&self, is_primary: bool) -> UnderlineParts {
351        //               X0 Y0
352        // label_start > ┯━━━━ < underline
353        //               │ < vertical_text_line
354        //               text
355
356        //    multiline_start_down ⤷ X0 Y0
357        //            top_left > ┌───╿──┘ < top_right_flat
358        //           top_left > ┏│━━━┙ < top_right
359        // multiline_vertical > ┃│
360        //                      ┃│   X1 Y1
361        //                      ┃│   X2 Y2
362        //                      ┃└────╿──┘ < multiline_end_same_line
363        //        bottom_left > ┗━━━━━┥ < bottom_right_with_text
364        //   multiline_horizontal ^   `X` is a good letter
365
366        // multiline_whole_line > ┏ X0 Y0
367        //                        ┃   X1 Y1
368        //                        ┗━━━━┛ < multiline_end_same_line
369
370        // multiline_whole_line > ┏ X0 Y0
371        //                        ┃ X1 Y1
372        //                        ┃  ╿ < multiline_end_up
373        //                        ┗━━┛ < bottom_right
374
375        match (self, is_primary) {
376            (DecorStyle::Ascii, true) => UnderlineParts {
377                style: ElementStyle::UnderlinePrimary,
378                underline: '^',
379                label_start: '^',
380                vertical_text_line: '|',
381                multiline_vertical: '|',
382                multiline_horizontal: '_',
383                multiline_whole_line: '/',
384                multiline_start_down: '^',
385                bottom_right: '|',
386                top_left: ' ',
387                top_right_flat: '^',
388                bottom_left: '|',
389                multiline_end_up: '^',
390                multiline_end_same_line: '^',
391                multiline_bottom_right_with_text: '|',
392            },
393            (DecorStyle::Ascii, false) => UnderlineParts {
394                style: ElementStyle::UnderlineSecondary,
395                underline: '-',
396                label_start: '-',
397                vertical_text_line: '|',
398                multiline_vertical: '|',
399                multiline_horizontal: '_',
400                multiline_whole_line: '/',
401                multiline_start_down: '-',
402                bottom_right: '|',
403                top_left: ' ',
404                top_right_flat: '-',
405                bottom_left: '|',
406                multiline_end_up: '-',
407                multiline_end_same_line: '-',
408                multiline_bottom_right_with_text: '|',
409            },
410            (DecorStyle::Unicode, true) => UnderlineParts {
411                style: ElementStyle::UnderlinePrimary,
412                underline: '━',
413                label_start: '┯',
414                vertical_text_line: '│',
415                multiline_vertical: '┃',
416                multiline_horizontal: '━',
417                multiline_whole_line: '┏',
418                multiline_start_down: '╿',
419                bottom_right: '┙',
420                top_left: '┏',
421                top_right_flat: '┛',
422                bottom_left: '┗',
423                multiline_end_up: '╿',
424                multiline_end_same_line: '┛',
425                multiline_bottom_right_with_text: '┥',
426            },
427            (DecorStyle::Unicode, false) => UnderlineParts {
428                style: ElementStyle::UnderlineSecondary,
429                underline: '─',
430                label_start: '┬',
431                vertical_text_line: '│',
432                multiline_vertical: '│',
433                multiline_horizontal: '─',
434                multiline_whole_line: '┌',
435                multiline_start_down: '│',
436                bottom_right: '┘',
437                top_left: '┌',
438                top_right_flat: '┘',
439                bottom_left: '└',
440                multiline_end_up: '│',
441                multiline_end_same_line: '┘',
442                multiline_bottom_right_with_text: '┤',
443            },
444        }
445    }
446}