Skip to main content

annotate_snippets/
snippet.rs

1//! Structures used as an input for the library.
2
3use alloc::borrow::{Cow, ToOwned};
4use alloc::string::String;
5use alloc::{vec, vec::Vec};
6use core::ops::Range;
7
8use crate::Level;
9use crate::renderer::source_map::{TrimmedPatch, as_substr};
10
11pub(crate) const ERROR_TXT: &str = "error";
12pub(crate) const HELP_TXT: &str = "help";
13pub(crate) const INFO_TXT: &str = "info";
14pub(crate) const NOTE_TXT: &str = "note";
15pub(crate) const WARNING_TXT: &str = "warning";
16
17/// A [diagnostic message][Title] and any associated [context][Element] to help users
18/// understand it
19///
20/// The first [`Group`] is the ["primary" group][Level::primary_title], ie it contains the diagnostic
21/// message.
22///
23/// All subsequent [`Group`]s are for distinct pieces of [context][Level::secondary_title].
24/// The primary group will be visually distinguished to help tell them apart.
25pub type Report<'a> = &'a [Group<'a>];
26
27#[derive(Clone, Debug, Default)]
28pub(crate) struct Id<'a> {
29    pub(crate) id: Option<Cow<'a, str>>,
30    pub(crate) url: Option<Cow<'a, str>>,
31}
32
33/// A [`Title`] with supporting [context][Element] within a [`Report`]
34///
35/// [Decor][crate::renderer::DecorStyle] is used to visually connect [`Element`]s of a `Group`.
36///
37/// Generally, you will create separate group's for:
38/// - New [`Snippet`]s, especially if they need their own [`AnnotationKind::Primary`]
39/// - Each logically distinct set of [suggestions][Patch`]
40///
41/// # Example
42///
43/// ```rust
44/// # #[allow(clippy::needless_doctest_main)]
45#[doc = include_str!("../examples/highlight_message.rs")]
46/// ```
47#[doc = include_str!("../examples/highlight_message.svg")]
48#[derive(Clone, Debug)]
49pub struct Group<'a> {
50    pub(crate) primary_level: Level<'a>,
51    pub(crate) title: Option<Title<'a>>,
52    pub(crate) elements: Vec<Element<'a>>,
53}
54
55impl<'a> Group<'a> {
56    /// Create group with a [`Title`], deriving [`AnnotationKind::Primary`] from its [`Level`]
57    pub fn with_title(title: Title<'a>) -> Self {
58        let level = title.level.clone();
59        let mut x = Self::with_level(level);
60        x.title = Some(title);
61        x
62    }
63
64    /// Create a title-less group with a primary [`Level`] for [`AnnotationKind::Primary`]
65    ///
66    /// # Example
67    ///
68    /// ```rust
69    /// # #[allow(clippy::needless_doctest_main)]
70    #[doc = include_str!("../examples/elide_header.rs")]
71    /// ```
72    #[doc = include_str!("../examples/elide_header.svg")]
73    pub fn with_level(level: Level<'a>) -> Self {
74        Self {
75            primary_level: level,
76            title: None,
77            elements: vec![],
78        }
79    }
80
81    /// Append an [`Element`] that adds context to the [`Title`]
82    pub fn element(mut self, section: impl Into<Element<'a>>) -> Self {
83        self.elements.push(section.into());
84        self
85    }
86
87    /// Append [`Element`]s that adds context to the [`Title`]
88    pub fn elements(mut self, sections: impl IntoIterator<Item = impl Into<Element<'a>>>) -> Self {
89        self.elements.extend(sections.into_iter().map(Into::into));
90        self
91    }
92
93    pub fn is_empty(&self) -> bool {
94        self.elements.is_empty() && self.title.is_none()
95    }
96}
97
98/// A section of content within a [`Group`]
99#[derive(Clone, Debug)]
100#[non_exhaustive]
101pub enum Element<'a> {
102    Message(Message<'a>),
103    Cause(Snippet<'a, Annotation<'a>>),
104    Suggestion(Snippet<'a, Patch<'a>>),
105    Origin(Origin<'a>),
106    Padding(Padding),
107}
108
109impl<'a> From<Message<'a>> for Element<'a> {
110    fn from(value: Message<'a>) -> Self {
111        Element::Message(value)
112    }
113}
114
115impl<'a> From<Snippet<'a, Annotation<'a>>> for Element<'a> {
116    fn from(value: Snippet<'a, Annotation<'a>>) -> Self {
117        Element::Cause(value)
118    }
119}
120
121impl<'a> From<Snippet<'a, Patch<'a>>> for Element<'a> {
122    fn from(value: Snippet<'a, Patch<'a>>) -> Self {
123        Element::Suggestion(value)
124    }
125}
126
127impl<'a> From<Origin<'a>> for Element<'a> {
128    fn from(value: Origin<'a>) -> Self {
129        Element::Origin(value)
130    }
131}
132
133impl From<Padding> for Element<'_> {
134    fn from(value: Padding) -> Self {
135        Self::Padding(value)
136    }
137}
138
139/// A whitespace [`Element`] in a [`Group`]
140#[derive(Clone, Debug)]
141pub struct Padding;
142
143/// A title that introduces a [`Group`], describing the main point
144///
145/// To create a `Title`, see [`Level::primary_title`] or [`Level::secondary_title`].
146///
147/// # Example
148///
149/// ```rust
150/// # use annotate_snippets::*;
151/// let report = &[
152///     Group::with_title(
153///         Level::ERROR.primary_title("mismatched types").id("E0308")
154///     ),
155///     Group::with_title(
156///         Level::HELP.secondary_title("function defined here")
157///     ),
158/// ];
159/// ```
160#[derive(Clone, Debug)]
161pub struct Title<'a> {
162    pub(crate) level: Level<'a>,
163    pub(crate) id: Option<Id<'a>>,
164    pub(crate) text: Cow<'a, str>,
165    pub(crate) allows_styling: bool,
166}
167
168impl<'a> Title<'a> {
169    /// The category for this [`Report`]
170    ///
171    /// Useful for looking searching for more information to resolve the diagnostic.
172    ///
173    /// <div class="warning">
174    ///
175    /// Text passed to this function is considered "untrusted input", as such
176    /// all text is passed through a normalization function. Styled text is
177    /// not allowed to be passed to this function.
178    ///
179    /// </div>
180    pub fn id(mut self, id: impl Into<Cow<'a, str>>) -> Self {
181        self.id.get_or_insert(Id::default()).id = Some(id.into());
182        self
183    }
184
185    /// Provide a URL for [`Title::id`] for more information on this diagnostic
186    ///
187    /// <div class="warning">
188    ///
189    /// This is only relevant if `id` is present
190    ///
191    /// </div>
192    pub fn id_url(mut self, url: impl Into<Cow<'a, str>>) -> Self {
193        self.id.get_or_insert(Id::default()).url = Some(url.into());
194        self
195    }
196
197    /// Append an [`Element`] that adds context to the [`Title`]
198    pub fn element(self, section: impl Into<Element<'a>>) -> Group<'a> {
199        Group::with_title(self).element(section)
200    }
201
202    /// Append [`Element`]s that adds context to the [`Title`]
203    pub fn elements(self, sections: impl IntoIterator<Item = impl Into<Element<'a>>>) -> Group<'a> {
204        Group::with_title(self).elements(sections)
205    }
206}
207
208/// A text [`Element`] in a [`Group`]
209///
210/// See [`Level::message`] to create this.
211#[derive(Clone, Debug)]
212pub struct Message<'a> {
213    pub(crate) level: Level<'a>,
214    pub(crate) text: Cow<'a, str>,
215}
216
217/// A source view [`Element`] in a [`Group`]
218///
219/// If you do not have [source][Snippet::source] available, see instead [`Origin`]
220///
221/// `Snippet`s come in the following styles (`T`):
222/// - With [`Annotation`]s, see [`Snippet::annotation`]
223/// - With [`Patch`]s, see [`Snippet::patch`]
224#[derive(Clone, Debug)]
225pub struct Snippet<'a, T> {
226    pub(crate) path: Option<Cow<'a, str>>,
227    pub(crate) line_start: usize,
228    pub(crate) source: Cow<'a, str>,
229    pub(crate) markers: Vec<T>,
230    pub(crate) line_numbering: bool,
231    pub(crate) fold: bool,
232}
233
234impl<'a, T: Clone> Snippet<'a, T> {
235    /// The source code to be rendered
236    ///
237    /// <div class="warning">
238    ///
239    /// Text passed to this function is considered "untrusted input", as such
240    /// all text is passed through a normalization function. Pre-styled text is
241    /// not allowed to be passed to this function.
242    ///
243    /// </div>
244    pub fn source(source: impl Into<Cow<'a, str>>) -> Self {
245        Self {
246            path: None,
247            line_start: 1,
248            source: source.into(),
249            markers: vec![],
250            line_numbering: true,
251            fold: true,
252        }
253    }
254
255    /// Whether to show the line number for each line (default: `true`)
256    ///
257    /// The default is `line_numbering(true)`, showing line numbers
258    pub fn line_numbering(mut self, yes: bool) -> Self {
259        self.line_numbering = yes;
260        self
261    }
262
263    /// When manually [`fold`][Self::fold]ing,
264    /// the [`source`][Self::source]s line offset from the original start
265    pub fn line_start(mut self, line_start: usize) -> Self {
266        self.line_start = line_start;
267        self
268    }
269
270    /// The location of the [`source`][Self::source] (e.g. a path)
271    ///
272    /// <div class="warning">
273    ///
274    /// Text passed to this function is considered "untrusted input", as such
275    /// all text is passed through a normalization function. Pre-styled text is
276    /// not allowed to be passed to this function.
277    ///
278    /// </div>
279    pub fn path(mut self, path: impl Into<OptionCow<'a>>) -> Self {
280        self.path = path.into().0;
281        self
282    }
283
284    /// Control whether lines without [`Annotation`]s are shown
285    ///
286    /// The default is `fold(true)`, collapsing uninteresting lines.
287    ///
288    /// See [`AnnotationKind::Visible`] to force specific spans to be shown.
289    pub fn fold(mut self, fold: bool) -> Self {
290        self.fold = fold;
291        self
292    }
293}
294
295impl<'a> Snippet<'a, Annotation<'a>> {
296    /// Highlight and describe a span of text within the [`source`][Self::source]
297    pub fn annotation(mut self, annotation: Annotation<'a>) -> Self {
298        self.markers.push(annotation);
299        self
300    }
301
302    /// Highlight and describe spans of text within the [`source`][Self::source]
303    pub fn annotations(mut self, annotation: impl IntoIterator<Item = Annotation<'a>>) -> Self {
304        self.markers.extend(annotation);
305        self
306    }
307}
308
309impl<'a> Snippet<'a, Patch<'a>> {
310    /// Suggest to the user an edit to the [`source`][Self::source]
311    pub fn patch(mut self, patch: Patch<'a>) -> Self {
312        self.markers.push(patch);
313        self
314    }
315
316    /// Suggest to the user edits to the [`source`][Self::source]
317    pub fn patches(mut self, patches: impl IntoIterator<Item = Patch<'a>>) -> Self {
318        self.markers.extend(patches);
319        self
320    }
321}
322
323/// Highlight and describe a span of text within a [`Snippet`]
324///
325/// See [`AnnotationKind`] to create an annotation.
326///
327/// # Example
328///
329/// ```rust
330/// # #[allow(clippy::needless_doctest_main)]
331#[doc = include_str!("../examples/expected_type.rs")]
332/// ```
333///
334#[doc = include_str!("../examples/expected_type.svg")]
335#[derive(Clone, Debug)]
336pub struct Annotation<'a> {
337    pub(crate) span: Range<usize>,
338    pub(crate) label: Option<Cow<'a, str>>,
339    pub(crate) kind: AnnotationKind,
340    pub(crate) highlight_source: bool,
341}
342
343impl<'a> Annotation<'a> {
344    /// Describe the reason the span is highlighted
345    ///
346    /// This will be styled according to the [`AnnotationKind`]
347    ///
348    /// <div class="warning">
349    ///
350    /// Text passed to this function is considered "untrusted input", as such
351    /// all text is passed through a normalization function. Pre-styled text is
352    /// not allowed to be passed to this function.
353    ///
354    /// </div>
355    pub fn label(mut self, label: impl Into<OptionCow<'a>>) -> Self {
356        self.label = label.into().0;
357        self
358    }
359
360    /// Style the source according to the [`AnnotationKind`]
361    ///
362    /// This gives extra emphasis to this annotation
363    pub fn highlight_source(mut self, highlight_source: bool) -> Self {
364        self.highlight_source = highlight_source;
365        self
366    }
367}
368
369/// The type of [`Annotation`] being applied to a [`Snippet`]
370#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
371#[non_exhaustive]
372pub enum AnnotationKind {
373    /// For showing the source that the [Group's Title][Group::with_title] references
374    ///
375    /// For [`Title`]-less groups, see [`Group::with_level`]
376    Primary,
377    /// Additional context to better understand the [`Primary`][Self::Primary]
378    /// [`Annotation`]
379    ///
380    /// See also [`Renderer::context`].
381    ///
382    /// [`Renderer::context`]: crate::renderer::Renderer
383    Context,
384    /// Prevents the annotated text from getting [folded][Snippet::fold]
385    ///
386    /// By default, [`Snippet`]s will [fold][`Snippet::fold`] (remove) lines
387    /// that do not contain any annotations. [`Visible`][Self::Visible] makes
388    /// it possible to selectively prevent this behavior for specific text,
389    /// allowing context to be preserved without adding any annotation
390    /// characters.
391    ///
392    /// # Example
393    ///
394    /// ```rust
395    /// # #[allow(clippy::needless_doctest_main)]
396    #[doc = include_str!("../examples/struct_name_as_context.rs")]
397    /// ```
398    ///
399    #[doc = include_str!("../examples/struct_name_as_context.svg")]
400    ///
401    Visible,
402}
403
404impl AnnotationKind {
405    /// Annotate a byte span within [`Snippet`]
406    pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
407        Annotation {
408            span,
409            label: None,
410            kind: self,
411            highlight_source: false,
412        }
413    }
414
415    pub(crate) fn is_primary(&self) -> bool {
416        matches!(self, Self::Primary)
417    }
418}
419
420/// Suggested edit to the [`Snippet`]
421///
422/// See [`Snippet::patch`]
423///
424/// # Example
425///
426/// ```rust
427/// # #[allow(clippy::needless_doctest_main)]
428#[doc = include_str!("../examples/multi_suggestion.rs")]
429/// ```
430///
431#[doc = include_str!("../examples/multi_suggestion.svg")]
432#[derive(Clone, Debug)]
433pub struct Patch<'a> {
434    pub(crate) span: Range<usize>,
435    pub(crate) replacement: Cow<'a, str>,
436}
437
438impl<'a> Patch<'a> {
439    /// Splice `replacement` into the [`Snippet`] at the specified byte span
440    ///
441    /// <div class="warning">
442    ///
443    /// Text passed to this function is considered "untrusted input", as such
444    /// all text is passed through a normalization function. Pre-styled text is
445    /// not allowed to be passed to this function.
446    ///
447    /// </div>
448    pub fn new(span: Range<usize>, replacement: impl Into<Cow<'a, str>>) -> Self {
449        Self {
450            span,
451            replacement: replacement.into(),
452        }
453    }
454
455    /// Try to turn a replacement into an addition when the span that is being
456    /// overwritten matches either the prefix or suffix of the replacement.
457    pub(crate) fn trim_trivial_replacements(self, source: &str) -> TrimmedPatch<'a> {
458        let mut trimmed = TrimmedPatch {
459            original_span: self.span.clone(),
460            span: self.span,
461            replacement: self.replacement,
462        };
463
464        if trimmed.replacement.is_empty() {
465            return trimmed;
466        }
467        let Some(snippet) = source.get(trimmed.original_span.clone()) else {
468            return trimmed;
469        };
470
471        if let Some((prefix, substr, suffix)) = as_substr(snippet, &trimmed.replacement) {
472            trimmed.span = trimmed.original_span.start + prefix
473                ..trimmed.original_span.end.saturating_sub(suffix);
474            trimmed.replacement = Cow::Owned(substr.to_owned());
475        }
476        trimmed
477    }
478}
479
480/// A source location [`Element`] in a [`Group`]
481///
482/// If you have source available, see instead [`Snippet`]
483///
484/// # Example
485///
486/// ```rust
487/// # use annotate_snippets::{Group, Snippet, AnnotationKind, Level, Origin};
488/// let report = &[
489///     Level::ERROR.primary_title("mismatched types").id("E0308")
490///         .element(
491///             Origin::path("$DIR/mismatched-types.rs")
492///         )
493/// ];
494/// ```
495#[derive(Clone, Debug)]
496pub struct Origin<'a> {
497    pub(crate) path: Cow<'a, str>,
498    pub(crate) line: Option<usize>,
499    pub(crate) char_column: Option<usize>,
500}
501
502impl<'a> Origin<'a> {
503    /// <div class="warning">
504    ///
505    /// Text passed to this function is considered "untrusted input", as such
506    /// all text is passed through a normalization function. Pre-styled text is
507    /// not allowed to be passed to this function.
508    ///
509    /// </div>
510    pub fn path(path: impl Into<Cow<'a, str>>) -> Self {
511        Self {
512            path: path.into(),
513            line: None,
514            char_column: None,
515        }
516    }
517
518    /// Set the default line number to display
519    pub fn line(mut self, line: usize) -> Self {
520        self.line = Some(line);
521        self
522    }
523
524    /// Set the default column to display
525    ///
526    /// <div class="warning">
527    ///
528    /// `char_column` is only be respected if [`Origin::line`] is also set.
529    ///
530    /// </div>
531    pub fn char_column(mut self, char_column: usize) -> Self {
532        self.char_column = Some(char_column);
533        self
534    }
535}
536
537impl<'a> From<Cow<'a, str>> for Origin<'a> {
538    fn from(origin: Cow<'a, str>) -> Self {
539        Self::path(origin)
540    }
541}
542
543#[derive(Debug)]
544pub struct OptionCow<'a>(pub(crate) Option<Cow<'a, str>>);
545
546impl<'a, T: Into<Cow<'a, str>>> From<Option<T>> for OptionCow<'a> {
547    fn from(value: Option<T>) -> Self {
548        Self(value.map(Into::into))
549    }
550}
551
552impl<'a> From<&'a Cow<'a, str>> for OptionCow<'a> {
553    fn from(value: &'a Cow<'a, str>) -> Self {
554        Self(Some(Cow::Borrowed(value)))
555    }
556}
557
558impl<'a> From<Cow<'a, str>> for OptionCow<'a> {
559    fn from(value: Cow<'a, str>) -> Self {
560        Self(Some(value))
561    }
562}
563
564impl<'a> From<&'a str> for OptionCow<'a> {
565    fn from(value: &'a str) -> Self {
566        Self(Some(Cow::Borrowed(value)))
567    }
568}
569impl<'a> From<String> for OptionCow<'a> {
570    fn from(value: String) -> Self {
571        Self(Some(Cow::Owned(value)))
572    }
573}
574
575impl<'a> From<&'a String> for OptionCow<'a> {
576    fn from(value: &'a String) -> Self {
577        Self(Some(Cow::Borrowed(value.as_str())))
578    }
579}