annotate_snippets/
snippet.rs

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