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::renderer::source_map::{as_substr, TrimmedPatch};
9use crate::Level;
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) fold: bool,
231}
232
233impl<'a, T: Clone> Snippet<'a, T> {
234 /// The source code to be rendered
235 ///
236 /// <div class="warning">
237 ///
238 /// Text passed to this function is considered "untrusted input", as such
239 /// all text is passed through a normalization function. Pre-styled text is
240 /// not allowed to be passed to this function.
241 ///
242 /// </div>
243 pub fn source(source: impl Into<Cow<'a, str>>) -> Self {
244 Self {
245 path: None,
246 line_start: 1,
247 source: source.into(),
248 markers: vec![],
249 fold: true,
250 }
251 }
252
253 /// When manually [`fold`][Self::fold]ing,
254 /// the [`source`][Self::source]s line offset from the original start
255 pub fn line_start(mut self, line_start: usize) -> Self {
256 self.line_start = line_start;
257 self
258 }
259
260 /// The location of the [`source`][Self::source] (e.g. a path)
261 ///
262 /// <div class="warning">
263 ///
264 /// Text passed to this function is considered "untrusted input", as such
265 /// all text is passed through a normalization function. Pre-styled text is
266 /// not allowed to be passed to this function.
267 ///
268 /// </div>
269 pub fn path(mut self, path: impl Into<OptionCow<'a>>) -> Self {
270 self.path = path.into().0;
271 self
272 }
273
274 /// Control whether lines without [`Annotation`]s are shown
275 ///
276 /// The default is `fold(true)`, collapsing uninteresting lines.
277 ///
278 /// See [`AnnotationKind::Visible`] to force specific spans to be shown.
279 pub fn fold(mut self, fold: bool) -> Self {
280 self.fold = fold;
281 self
282 }
283}
284
285impl<'a> Snippet<'a, Annotation<'a>> {
286 /// Highlight and describe a span of text within the [`source`][Self::source]
287 pub fn annotation(mut self, annotation: Annotation<'a>) -> Snippet<'a, Annotation<'a>> {
288 self.markers.push(annotation);
289 self
290 }
291
292 /// Highlight and describe spans of text within the [`source`][Self::source]
293 pub fn annotations(mut self, annotation: impl IntoIterator<Item = Annotation<'a>>) -> Self {
294 self.markers.extend(annotation);
295 self
296 }
297}
298
299impl<'a> Snippet<'a, Patch<'a>> {
300 /// Suggest to the user an edit to the [`source`][Self::source]
301 pub fn patch(mut self, patch: Patch<'a>) -> Snippet<'a, Patch<'a>> {
302 self.markers.push(patch);
303 self
304 }
305
306 /// Suggest to the user edits to the [`source`][Self::source]
307 pub fn patches(mut self, patches: impl IntoIterator<Item = Patch<'a>>) -> Self {
308 self.markers.extend(patches);
309 self
310 }
311}
312
313/// Highlight and describe a span of text within a [`Snippet`]
314///
315/// See [`AnnotationKind`] to create an annotation.
316///
317/// # Example
318///
319/// ```rust
320/// # #[allow(clippy::needless_doctest_main)]
321#[doc = include_str!("../examples/expected_type.rs")]
322/// ```
323///
324#[doc = include_str!("../examples/expected_type.svg")]
325#[derive(Clone, Debug)]
326pub struct Annotation<'a> {
327 pub(crate) span: Range<usize>,
328 pub(crate) label: Option<Cow<'a, str>>,
329 pub(crate) kind: AnnotationKind,
330 pub(crate) highlight_source: bool,
331}
332
333impl<'a> Annotation<'a> {
334 /// Describe the reason the span is highlighted
335 ///
336 /// This will be styled according to the [`AnnotationKind`]
337 ///
338 /// <div class="warning">
339 ///
340 /// Text passed to this function is considered "untrusted input", as such
341 /// all text is passed through a normalization function. Pre-styled text is
342 /// not allowed to be passed to this function.
343 ///
344 /// </div>
345 pub fn label(mut self, label: impl Into<OptionCow<'a>>) -> Self {
346 self.label = label.into().0;
347 self
348 }
349
350 /// Style the source according to the [`AnnotationKind`]
351 ///
352 /// This gives extra emphasis to this annotation
353 pub fn highlight_source(mut self, highlight_source: bool) -> Self {
354 self.highlight_source = highlight_source;
355 self
356 }
357}
358
359/// The type of [`Annotation`] being applied to a [`Snippet`]
360#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
361#[non_exhaustive]
362pub enum AnnotationKind {
363 /// For showing the source that the [Group's Title][Group::with_title] references
364 ///
365 /// For [`Title`]-less groups, see [`Group::with_level`]
366 Primary,
367 /// Additional context to better understand the [`Primary`][Self::Primary]
368 /// [`Annotation`]
369 ///
370 /// See also [`Renderer::context`].
371 ///
372 /// [`Renderer::context`]: crate::renderer::Renderer
373 Context,
374 /// Prevents the annotated text from getting [folded][Snippet::fold]
375 ///
376 /// By default, [`Snippet`]s will [fold][`Snippet::fold`] (remove) lines
377 /// that do not contain any annotations. [`Visible`][Self::Visible] makes
378 /// it possible to selectively prevent this behavior for specific text,
379 /// allowing context to be preserved without adding any annotation
380 /// characters.
381 ///
382 /// # Example
383 ///
384 /// ```rust
385 /// # #[allow(clippy::needless_doctest_main)]
386 #[doc = include_str!("../examples/struct_name_as_context.rs")]
387 /// ```
388 ///
389 #[doc = include_str!("../examples/struct_name_as_context.svg")]
390 ///
391 Visible,
392}
393
394impl AnnotationKind {
395 /// Annotate a byte span within [`Snippet`]
396 pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
397 Annotation {
398 span,
399 label: None,
400 kind: self,
401 highlight_source: false,
402 }
403 }
404
405 pub(crate) fn is_primary(&self) -> bool {
406 matches!(self, AnnotationKind::Primary)
407 }
408}
409
410/// Suggested edit to the [`Snippet`]
411///
412/// See [`Snippet::patch`]
413///
414/// # Example
415///
416/// ```rust
417/// # #[allow(clippy::needless_doctest_main)]
418#[doc = include_str!("../examples/multi_suggestion.rs")]
419/// ```
420///
421#[doc = include_str!("../examples/multi_suggestion.svg")]
422#[derive(Clone, Debug)]
423pub struct Patch<'a> {
424 pub(crate) span: Range<usize>,
425 pub(crate) replacement: Cow<'a, str>,
426}
427
428impl<'a> Patch<'a> {
429 /// Splice `replacement` into the [`Snippet`] at the specified byte span
430 ///
431 /// <div class="warning">
432 ///
433 /// Text passed to this function is considered "untrusted input", as such
434 /// all text is passed through a normalization function. Pre-styled text is
435 /// not allowed to be passed to this function.
436 ///
437 /// </div>
438 pub fn new(span: Range<usize>, replacement: impl Into<Cow<'a, str>>) -> Self {
439 Self {
440 span,
441 replacement: replacement.into(),
442 }
443 }
444
445 /// Try to turn a replacement into an addition when the span that is being
446 /// overwritten matches either the prefix or suffix of the replacement.
447 pub(crate) fn trim_trivial_replacements(self, source: &str) -> TrimmedPatch<'a> {
448 let mut trimmed = TrimmedPatch {
449 original_span: self.span.clone(),
450 span: self.span,
451 replacement: self.replacement,
452 };
453
454 if trimmed.replacement.is_empty() {
455 return trimmed;
456 }
457 let Some(snippet) = source.get(trimmed.original_span.clone()) else {
458 return trimmed;
459 };
460
461 if let Some((prefix, substr, suffix)) = as_substr(snippet, &trimmed.replacement) {
462 trimmed.span = trimmed.original_span.start + prefix
463 ..trimmed.original_span.end.saturating_sub(suffix);
464 trimmed.replacement = Cow::Owned(substr.to_owned());
465 }
466 trimmed
467 }
468}
469
470/// A source location [`Element`] in a [`Group`]
471///
472/// If you have source available, see instead [`Snippet`]
473///
474/// # Example
475///
476/// ```rust
477/// # use annotate_snippets::{Group, Snippet, AnnotationKind, Level, Origin};
478/// let report = &[
479/// Level::ERROR.primary_title("mismatched types").id("E0308")
480/// .element(
481/// Origin::path("$DIR/mismatched-types.rs")
482/// )
483/// ];
484/// ```
485#[derive(Clone, Debug)]
486pub struct Origin<'a> {
487 pub(crate) path: Cow<'a, str>,
488 pub(crate) line: Option<usize>,
489 pub(crate) char_column: Option<usize>,
490}
491
492impl<'a> Origin<'a> {
493 /// <div class="warning">
494 ///
495 /// Text passed to this function is considered "untrusted input", as such
496 /// all text is passed through a normalization function. Pre-styled text is
497 /// not allowed to be passed to this function.
498 ///
499 /// </div>
500 pub fn path(path: impl Into<Cow<'a, str>>) -> Self {
501 Self {
502 path: path.into(),
503 line: None,
504 char_column: None,
505 }
506 }
507
508 /// Set the default line number to display
509 pub fn line(mut self, line: usize) -> Self {
510 self.line = Some(line);
511 self
512 }
513
514 /// Set the default column to display
515 ///
516 /// <div class="warning">
517 ///
518 /// `char_column` is only be respected if [`Origin::line`] is also set.
519 ///
520 /// </div>
521 pub fn char_column(mut self, char_column: usize) -> Self {
522 self.char_column = Some(char_column);
523 self
524 }
525}
526
527impl<'a> From<Cow<'a, str>> for Origin<'a> {
528 fn from(origin: Cow<'a, str>) -> Self {
529 Self::path(origin)
530 }
531}
532
533#[derive(Debug)]
534pub struct OptionCow<'a>(pub(crate) Option<Cow<'a, str>>);
535
536impl<'a, T: Into<Cow<'a, str>>> From<Option<T>> for OptionCow<'a> {
537 fn from(value: Option<T>) -> Self {
538 Self(value.map(Into::into))
539 }
540}
541
542impl<'a> From<&'a Cow<'a, str>> for OptionCow<'a> {
543 fn from(value: &'a Cow<'a, str>) -> Self {
544 Self(Some(Cow::Borrowed(value)))
545 }
546}
547
548impl<'a> From<Cow<'a, str>> for OptionCow<'a> {
549 fn from(value: Cow<'a, str>) -> Self {
550 Self(Some(value))
551 }
552}
553
554impl<'a> From<&'a str> for OptionCow<'a> {
555 fn from(value: &'a str) -> Self {
556 Self(Some(Cow::Borrowed(value)))
557 }
558}
559impl<'a> From<String> for OptionCow<'a> {
560 fn from(value: String) -> Self {
561 Self(Some(Cow::Owned(value)))
562 }
563}
564
565impl<'a> From<&'a String> for OptionCow<'a> {
566 fn from(value: &'a String) -> Self {
567 Self(Some(Cow::Borrowed(value.as_str())))
568 }
569}