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