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
195/// A text [`Element`] in a [`Group`]
196///
197/// See [`Level::message`] to create this.
198#[derive(Clone, Debug)]
199pub struct Message<'a> {
200 pub(crate) level: Level<'a>,
201 pub(crate) text: Cow<'a, str>,
202}
203
204/// A source view [`Element`] in a [`Group`]
205///
206/// If you do not have [source][Snippet::source] available, see instead [`Origin`]
207///
208/// `Snippet`s come in the following styles (`T`):
209/// - With [`Annotation`]s, see [`Snippet::annotation`]
210/// - With [`Patch`]s, see [`Snippet::patch`]
211#[derive(Clone, Debug)]
212pub struct Snippet<'a, T> {
213 pub(crate) path: Option<Cow<'a, str>>,
214 pub(crate) line_start: usize,
215 pub(crate) source: Cow<'a, str>,
216 pub(crate) markers: Vec<T>,
217 pub(crate) fold: bool,
218}
219
220impl<'a, T: Clone> Snippet<'a, T> {
221 /// The source code to be rendered
222 ///
223 /// <div class="warning">
224 ///
225 /// Text passed to this function is considered "untrusted input", as such
226 /// all text is passed through a normalization function. Pre-styled text is
227 /// not allowed to be passed to this function.
228 ///
229 /// </div>
230 pub fn source(source: impl Into<Cow<'a, str>>) -> Self {
231 Self {
232 path: None,
233 line_start: 1,
234 source: source.into(),
235 markers: vec![],
236 fold: true,
237 }
238 }
239
240 /// When manually [`fold`][Self::fold]ing,
241 /// the [`source`][Self::source]s line offset from the original start
242 pub fn line_start(mut self, line_start: usize) -> Self {
243 self.line_start = line_start;
244 self
245 }
246
247 /// The location of the [`source`][Self::source] (e.g. a path)
248 ///
249 /// <div class="warning">
250 ///
251 /// Text passed to this function is considered "untrusted input", as such
252 /// all text is passed through a normalization function. Pre-styled text is
253 /// not allowed to be passed to this function.
254 ///
255 /// </div>
256 pub fn path(mut self, path: impl Into<OptionCow<'a>>) -> Self {
257 self.path = path.into().0;
258 self
259 }
260
261 /// Control whether lines without [`Annotation`]s are shown
262 ///
263 /// The default is `fold(true)`, collapsing uninteresting lines.
264 ///
265 /// See [`AnnotationKind::Visible`] to force specific spans to be shown.
266 pub fn fold(mut self, fold: bool) -> Self {
267 self.fold = fold;
268 self
269 }
270}
271
272impl<'a> Snippet<'a, Annotation<'a>> {
273 /// Highlight and describe a span of text within the [`source`][Self::source]
274 pub fn annotation(mut self, annotation: Annotation<'a>) -> Snippet<'a, Annotation<'a>> {
275 self.markers.push(annotation);
276 self
277 }
278
279 /// Highlight and describe spans of text within the [`source`][Self::source]
280 pub fn annotations(mut self, annotation: impl IntoIterator<Item = Annotation<'a>>) -> Self {
281 self.markers.extend(annotation);
282 self
283 }
284}
285
286impl<'a> Snippet<'a, Patch<'a>> {
287 /// Suggest to the user an edit to the [`source`][Self::source]
288 pub fn patch(mut self, patch: Patch<'a>) -> Snippet<'a, Patch<'a>> {
289 self.markers.push(patch);
290 self
291 }
292
293 /// Suggest to the user edits to the [`source`][Self::source]
294 pub fn patches(mut self, patches: impl IntoIterator<Item = Patch<'a>>) -> Self {
295 self.markers.extend(patches);
296 self
297 }
298}
299
300/// Highlight and describe a span of text within a [`Snippet`]
301///
302/// See [`AnnotationKind`] to create an annotation.
303///
304/// # Example
305///
306/// ```rust
307/// # #[allow(clippy::needless_doctest_main)]
308#[doc = include_str!("../examples/expected_type.rs")]
309/// ```
310///
311#[doc = include_str!("../examples/expected_type.svg")]
312#[derive(Clone, Debug)]
313pub struct Annotation<'a> {
314 pub(crate) span: Range<usize>,
315 pub(crate) label: Option<Cow<'a, str>>,
316 pub(crate) kind: AnnotationKind,
317 pub(crate) highlight_source: bool,
318}
319
320impl<'a> Annotation<'a> {
321 /// Describe the reason the span is highlighted
322 ///
323 /// This will be styled according to the [`AnnotationKind`]
324 ///
325 /// <div class="warning">
326 ///
327 /// Text passed to this function is considered "untrusted input", as such
328 /// all text is passed through a normalization function. Pre-styled text is
329 /// not allowed to be passed to this function.
330 ///
331 /// </div>
332 pub fn label(mut self, label: impl Into<OptionCow<'a>>) -> Self {
333 self.label = label.into().0;
334 self
335 }
336
337 /// Style the source according to the [`AnnotationKind`]
338 ///
339 /// This gives extra emphasis to this annotation
340 pub fn highlight_source(mut self, highlight_source: bool) -> Self {
341 self.highlight_source = highlight_source;
342 self
343 }
344}
345
346/// The type of [`Annotation`] being applied to a [`Snippet`]
347#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
348#[non_exhaustive]
349pub enum AnnotationKind {
350 /// For showing the source that the [Group's Title][Group::with_title] references
351 ///
352 /// For [`Title`]-less groups, see [`Group::with_level`]
353 Primary,
354 /// Additional context to better understand the [`Primary`][Self::Primary]
355 /// [`Annotation`]
356 ///
357 /// See also [`Renderer::context`].
358 ///
359 /// [`Renderer::context`]: crate::renderer::Renderer
360 Context,
361 /// Prevents the annotated text from getting [folded][Snippet::fold]
362 ///
363 /// By default, [`Snippet`]s will [fold][`Snippet::fold`] (remove) lines
364 /// that do not contain any annotations. [`Visible`][Self::Visible] makes
365 /// it possible to selectively prevent this behavior for specific text,
366 /// allowing context to be preserved without adding any annotation
367 /// characters.
368 ///
369 /// # Example
370 ///
371 /// ```rust
372 /// # #[allow(clippy::needless_doctest_main)]
373 #[doc = include_str!("../examples/struct_name_as_context.rs")]
374 /// ```
375 ///
376 #[doc = include_str!("../examples/struct_name_as_context.svg")]
377 ///
378 Visible,
379}
380
381impl AnnotationKind {
382 /// Annotate a byte span within [`Snippet`]
383 pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
384 Annotation {
385 span,
386 label: None,
387 kind: self,
388 highlight_source: false,
389 }
390 }
391
392 pub(crate) fn is_primary(&self) -> bool {
393 matches!(self, AnnotationKind::Primary)
394 }
395}
396
397/// Suggested edit to the [`Snippet`]
398///
399/// See [`Snippet::patch`]
400///
401/// # Example
402///
403/// ```rust
404/// # #[allow(clippy::needless_doctest_main)]
405#[doc = include_str!("../examples/multi_suggestion.rs")]
406/// ```
407///
408#[doc = include_str!("../examples/multi_suggestion.svg")]
409#[derive(Clone, Debug)]
410pub struct Patch<'a> {
411 pub(crate) span: Range<usize>,
412 pub(crate) replacement: Cow<'a, str>,
413}
414
415impl<'a> Patch<'a> {
416 /// Splice `replacement` into the [`Snippet`] at the specified byte span
417 ///
418 /// <div class="warning">
419 ///
420 /// Text passed to this function is considered "untrusted input", as such
421 /// all text is passed through a normalization function. Pre-styled text is
422 /// not allowed to be passed to this function.
423 ///
424 /// </div>
425 pub fn new(span: Range<usize>, replacement: impl Into<Cow<'a, str>>) -> Self {
426 Self {
427 span,
428 replacement: replacement.into(),
429 }
430 }
431
432 pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool {
433 !self.replacement.is_empty() && !self.replaces_meaningful_content(sm)
434 }
435
436 pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
437 self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
438 }
439
440 pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
441 !self.replacement.is_empty() && self.replaces_meaningful_content(sm)
442 }
443
444 /// Whether this is a replacement that overwrites source with a snippet
445 /// in a way that isn't a superset of the original string. For example,
446 /// replacing "abc" with "abcde" is not destructive, but replacing it
447 /// it with "abx" is, since the "c" character is lost.
448 pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool {
449 self.is_replacement(sm)
450 && !sm
451 .span_to_snippet(self.span.clone())
452 // This should use `is_some_and` when our MSRV is >= 1.70
453 .map_or(false, |s| {
454 as_substr(s.trim(), self.replacement.trim()).is_some()
455 })
456 }
457
458 fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
459 sm.span_to_snippet(self.span.clone())
460 .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
461 }
462
463 /// Try to turn a replacement into an addition when the span that is being
464 /// overwritten matches either the prefix or suffix of the replacement.
465 pub(crate) fn trim_trivial_replacements(&mut self, sm: &'a SourceMap<'a>) {
466 if self.replacement.is_empty() {
467 return;
468 }
469 let Some(snippet) = sm.span_to_snippet(self.span.clone()) else {
470 return;
471 };
472
473 if let Some((prefix, substr, suffix)) = as_substr(snippet, &self.replacement) {
474 self.span = self.span.start + prefix..self.span.end.saturating_sub(suffix);
475 self.replacement = Cow::Owned(substr.to_owned());
476 }
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/// Group::with_title(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}
580
581/// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect
582/// the case where a substring of the suggestion is "sandwiched" in the original, like
583/// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length
584/// of the suffix.
585fn as_substr<'a>(original: &'a str, suggestion: &'a str) -> Option<(usize, &'a str, usize)> {
586 let common_prefix = original
587 .chars()
588 .zip(suggestion.chars())
589 .take_while(|(c1, c2)| c1 == c2)
590 .map(|(c, _)| c.len_utf8())
591 .sum();
592 let original = &original[common_prefix..];
593 let suggestion = &suggestion[common_prefix..];
594 if let Some(stripped) = suggestion.strip_suffix(original) {
595 let common_suffix = original.len();
596 Some((common_prefix, stripped, common_suffix))
597 } else {
598 None
599 }
600}