• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rust-lang / annotate-snippets-rs / 15987916984

01 Jul 2025 01:59AM UTC coverage: 87.259%. Remained the same
15987916984

Pull #236

github

web-flow
Merge 5debca162 into c195b8366
Pull Request #236: chore(deps): Update Rust crate anstream to v0.6.19

1404 of 1609 relevant lines covered (87.26%)

4.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.59
/src/snippet.rs
1
//! Structures used as an input for the library.
2

3
use crate::renderer::source_map::SourceMap;
4
use crate::Level;
5
use std::borrow::Cow;
6
use std::ops::Range;
7

8
pub(crate) const ERROR_TXT: &str = "error";
9
pub(crate) const HELP_TXT: &str = "help";
10
pub(crate) const INFO_TXT: &str = "info";
11
pub(crate) const NOTE_TXT: &str = "note";
12
pub(crate) const WARNING_TXT: &str = "warning";
13

14
#[derive(Clone, Debug, Default)]
15
pub(crate) struct Id<'a> {
16
    pub(crate) id: Option<Cow<'a, str>>,
17
    pub(crate) url: Option<Cow<'a, str>>,
18
}
19

20
/// An [`Element`] container
21
#[derive(Clone, Debug)]
22
pub struct Group<'a> {
23
    pub(crate) elements: Vec<Element<'a>>,
24
}
25

26
impl Default for Group<'_> {
27
    fn default() -> Self {
×
28
        Self::new()
×
29
    }
30
}
31

32
impl<'a> Group<'a> {
33
    pub fn new() -> Self {
12✔
34
        Self { elements: vec![] }
12✔
35
    }
36

37
    pub fn element(mut self, section: impl Into<Element<'a>>) -> Self {
22✔
38
        self.elements.push(section.into());
51✔
39
        self
27✔
40
    }
41

42
    pub fn elements(mut self, sections: impl IntoIterator<Item = impl Into<Element<'a>>>) -> Self {
×
43
        self.elements.extend(sections.into_iter().map(Into::into));
×
44
        self
×
45
    }
46

47
    pub fn is_empty(&self) -> bool {
×
48
        self.elements.is_empty()
×
49
    }
50
}
51

52
/// A section of content within a [`Group`]
53
#[derive(Clone, Debug)]
54
#[non_exhaustive]
55
pub enum Element<'a> {
56
    Title(Title<'a>),
57
    Cause(Snippet<'a, Annotation<'a>>),
58
    Suggestion(Snippet<'a, Patch<'a>>),
59
    Origin(Origin<'a>),
60
    Padding(Padding),
61
}
62

63
impl<'a> From<Title<'a>> for Element<'a> {
64
    fn from(value: Title<'a>) -> Self {
9✔
65
        Element::Title(value)
9✔
66
    }
67
}
68

69
impl<'a> From<Snippet<'a, Annotation<'a>>> for Element<'a> {
70
    fn from(value: Snippet<'a, Annotation<'a>>) -> Self {
8✔
71
        Element::Cause(value)
8✔
72
    }
73
}
74

75
impl<'a> From<Snippet<'a, Patch<'a>>> for Element<'a> {
76
    fn from(value: Snippet<'a, Patch<'a>>) -> Self {
4✔
77
        Element::Suggestion(value)
4✔
78
    }
79
}
80

81
impl<'a> From<Origin<'a>> for Element<'a> {
82
    fn from(value: Origin<'a>) -> Self {
2✔
83
        Element::Origin(value)
2✔
84
    }
85
}
86

87
impl From<Padding> for Element<'_> {
88
    fn from(value: Padding) -> Self {
1✔
89
        Self::Padding(value)
1✔
90
    }
91
}
92

93
/// A whitespace [`Element`] in a [`Group`]
94
#[derive(Clone, Debug)]
95
pub struct Padding;
96

97
/// A text [`Element`] in a [`Group`]
98
///
99
/// See [`Level::title`] to create this.
100
#[derive(Clone, Debug)]
101
pub struct Title<'a> {
102
    pub(crate) level: Level<'a>,
103
    pub(crate) id: Option<Id<'a>>,
104
    pub(crate) title: Cow<'a, str>,
105
    pub(crate) is_pre_styled: bool,
106
}
107

108
impl<'a> Title<'a> {
109
    /// <div class="warning">
110
    ///
111
    /// This is only relevant if the title is the first element of a group.
112
    ///
113
    /// </div>
114
    /// <div class="warning">
115
    ///
116
    /// Text passed to this function is considered "untrusted input", as such
117
    /// all text is passed through a normalization function. Pre-styled text is
118
    /// not allowed to be passed to this function.
119
    ///
120
    /// </div>
121
    pub fn id(mut self, id: impl Into<Cow<'a, str>>) -> Self {
4✔
122
        self.id.get_or_insert(Id::default()).id = Some(id.into());
10✔
123
        self
4✔
124
    }
125

126
    /// <div class="warning">
127
    ///
128
    /// This is only relevant if the title is the first element of a group and
129
    /// `id` present
130
    ///
131
    /// </div>
132
    pub fn id_url(mut self, url: impl Into<Cow<'a, str>>) -> Self {
×
133
        self.id.get_or_insert(Id::default()).url = Some(url.into());
×
134
        self
×
135
    }
136
}
137

138
/// A source view [`Element`] in a [`Group`]
139
///
140
/// If you do not have [source][Snippet::source] available, see instead [`Origin`]
141
#[derive(Clone, Debug)]
142
pub struct Snippet<'a, T> {
143
    pub(crate) path: Option<Cow<'a, str>>,
144
    pub(crate) line_start: usize,
145
    pub(crate) source: Cow<'a, str>,
146
    pub(crate) markers: Vec<T>,
147
    pub(crate) fold: bool,
148
}
149

150
impl<'a, T: Clone> Snippet<'a, T> {
151
    /// The source code to be rendered
152
    ///
153
    /// <div class="warning">
154
    ///
155
    /// Text passed to this function is considered "untrusted input", as such
156
    /// all text is passed through a normalization function. Pre-styled text is
157
    /// not allowed to be passed to this function.
158
    ///
159
    /// </div>
160
    pub fn source(source: impl Into<Cow<'a, str>>) -> Self {
17✔
161
        Self {
162
            path: None,
163
            line_start: 1,
164
            source: source.into(),
17✔
165
            markers: vec![],
17✔
166
            fold: false,
167
        }
168
    }
169

170
    /// When manually [`fold`][Self::fold]ing,
171
    /// the [`source`][Self::source]s line offset from the original start
172
    pub fn line_start(mut self, line_start: usize) -> Self {
13✔
173
        self.line_start = line_start;
13✔
174
        self
12✔
175
    }
176

177
    /// The location of the [`source`][Self::source] (e.g. a path)
178
    ///
179
    /// <div class="warning">
180
    ///
181
    /// Text passed to this function is considered "untrusted input", as such
182
    /// all text is passed through a normalization function. Pre-styled text is
183
    /// not allowed to be passed to this function.
184
    ///
185
    /// </div>
186
    pub fn path(mut self, path: impl Into<OptionCow<'a>>) -> Self {
14✔
187
        self.path = path.into().0;
29✔
188
        self
15✔
189
    }
190

191
    /// Hide lines without [`Annotation`]s
192
    pub fn fold(mut self, fold: bool) -> Self {
8✔
193
        self.fold = fold;
7✔
194
        self
8✔
195
    }
196
}
197

198
impl<'a> Snippet<'a, Annotation<'a>> {
199
    /// Highlight and describe a span of text within the [`source`][Self::source]
200
    pub fn annotation(mut self, annotation: Annotation<'a>) -> Snippet<'a, Annotation<'a>> {
9✔
201
        self.markers.push(annotation);
7✔
202
        self
9✔
203
    }
204

205
    /// Highlight and describe spans of text within the [`source`][Self::source]
206
    pub fn annotations(mut self, annotation: impl IntoIterator<Item = Annotation<'a>>) -> Self {
×
207
        self.markers.extend(annotation);
×
208
        self
×
209
    }
210
}
211

212
impl<'a> Snippet<'a, Patch<'a>> {
213
    /// Suggest to the user an edit to the [`source`][Self::source]
214
    pub fn patch(mut self, patch: Patch<'a>) -> Snippet<'a, Patch<'a>> {
4✔
215
        self.markers.push(patch);
4✔
216
        self
4✔
217
    }
218

219
    /// Suggest to the user edits to the [`source`][Self::source]
220
    pub fn patches(mut self, patches: impl IntoIterator<Item = Patch<'a>>) -> Self {
×
221
        self.markers.extend(patches);
×
222
        self
×
223
    }
224
}
225

226
/// Highlighted and describe a span of text within a [`Snippet`]
227
///
228
/// See [`AnnotationKind`] to create an annotation.
229
#[derive(Clone, Debug)]
230
pub struct Annotation<'a> {
231
    pub(crate) span: Range<usize>,
232
    pub(crate) label: Option<Cow<'a, str>>,
233
    pub(crate) kind: AnnotationKind,
234
    pub(crate) highlight_source: bool,
235
}
236

237
impl<'a> Annotation<'a> {
238
    /// Describe the reason the span is highlighted
239
    ///
240
    /// This will be styled according to the [`AnnotationKind`]
241
    ///
242
    /// <div class="warning">
243
    ///
244
    /// Text passed to this function is considered "untrusted input", as such
245
    /// all text is passed through a normalization function. Pre-styled text is
246
    /// not allowed to be passed to this function.
247
    ///
248
    /// </div>
249
    pub fn label(mut self, label: impl Into<OptionCow<'a>>) -> Self {
6✔
250
        self.label = label.into().0;
11✔
251
        self
5✔
252
    }
253

254
    /// Style the source according to the [`AnnotationKind`]
255
    pub fn highlight_source(mut self, highlight_source: bool) -> Self {
×
256
        self.highlight_source = highlight_source;
×
257
        self
×
258
    }
259
}
260

261
/// The category of the [`Annotation`]
262
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
263
#[non_exhaustive]
264
pub enum AnnotationKind {
265
    /// Color to the [`Level`] the first [`Title`] in [`Group`]. If no [`Title`]
266
    /// is present, it will default to `error`.
267
    Primary,
268
    /// "secondary"; fixed color
269
    Context,
270
}
271

272
impl AnnotationKind {
273
    pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
10✔
274
        Annotation {
275
            span,
276
            label: None,
277
            kind: self,
278
            highlight_source: false,
279
        }
280
    }
281

282
    pub(crate) fn is_primary(&self) -> bool {
4✔
283
        matches!(self, AnnotationKind::Primary)
6✔
284
    }
285
}
286

287
/// Suggested edit to the [`Snippet`]
288
#[derive(Clone, Debug)]
289
pub struct Patch<'a> {
290
    pub(crate) span: Range<usize>,
291
    pub(crate) replacement: Cow<'a, str>,
292
}
293

294
impl<'a> Patch<'a> {
295
    /// Splice `replacement` into the [`Snippet`] at the `span`
296
    ///
297
    /// <div class="warning">
298
    ///
299
    /// Text passed to this function is considered "untrusted input", as such
300
    /// all text is passed through a normalization function. Pre-styled text is
301
    /// not allowed to be passed to this function.
302
    ///
303
    /// </div>
304
    pub fn new(span: Range<usize>, replacement: impl Into<Cow<'a, str>>) -> Self {
4✔
305
        Self {
306
            span,
307
            replacement: replacement.into(),
4✔
308
        }
309
    }
310

311
    pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool {
2✔
312
        !self.replacement.is_empty() && !self.replaces_meaningful_content(sm)
3✔
313
    }
314

315
    pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
4✔
316
        self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
4✔
317
    }
318

319
    pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
3✔
320
        !self.replacement.is_empty() && self.replaces_meaningful_content(sm)
3✔
321
    }
322

323
    /// Whether this is a replacement that overwrites source with a snippet
324
    /// in a way that isn't a superset of the original string. For example,
325
    /// replacing "abc" with "abcde" is not destructive, but replacing it
326
    /// it with "abx" is, since the "c" character is lost.
327
    pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool {
3✔
328
        self.is_replacement(sm)
3✔
329
            && !sm
2✔
330
                .span_to_snippet(self.span.clone())
2✔
331
                // This should use `is_some_and` when our MSRV is >= 1.70
332
                .map_or(false, |s| {
2✔
333
                    as_substr(s.trim(), self.replacement.trim()).is_some()
2✔
334
                })
335
    }
336

337
    fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
4✔
338
        sm.span_to_snippet(self.span.clone())
8✔
339
            .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
12✔
340
    }
341

342
    /// Try to turn a replacement into an addition when the span that is being
343
    /// overwritten matches either the prefix or suffix of the replacement.
344
    pub(crate) fn trim_trivial_replacements(&mut self, sm: &'a SourceMap<'a>) {
3✔
345
        if self.replacement.is_empty() {
3✔
346
            return;
×
347
        }
348
        let Some(snippet) = sm.span_to_snippet(self.span.clone()) else {
4✔
349
            return;
×
350
        };
351

352
        if let Some((prefix, substr, suffix)) = as_substr(snippet, &self.replacement) {
6✔
353
            self.span = self.span.start + prefix..self.span.end.saturating_sub(suffix);
4✔
354
            self.replacement = Cow::Owned(substr.to_owned());
8✔
355
        }
356
    }
357
}
358

359
/// The referenced location (e.g. a path)
360
///
361
/// If you have source available, see instead [`Snippet`]
362
#[derive(Clone, Debug)]
363
pub struct Origin<'a> {
364
    pub(crate) path: Cow<'a, str>,
365
    pub(crate) line: Option<usize>,
366
    pub(crate) char_column: Option<usize>,
367
    pub(crate) primary: bool,
368
}
369

370
impl<'a> Origin<'a> {
371
    /// <div class="warning">
372
    ///
373
    /// Text passed to this function is considered "untrusted input", as such
374
    /// all text is passed through a normalization function. Pre-styled text is
375
    /// not allowed to be passed to this function.
376
    ///
377
    /// </div>
378
    pub fn new(path: impl Into<Cow<'a, str>>) -> Self {
5✔
379
        Self {
380
            path: path.into(),
5✔
381
            line: None,
382
            char_column: None,
383
            primary: false,
384
        }
385
    }
386

387
    /// Set the default line number to display
388
    ///
389
    /// Otherwise this will be inferred from the primary [`Annotation`]
390
    pub fn line(mut self, line: usize) -> Self {
2✔
391
        self.line = Some(line);
2✔
392
        self
2✔
393
    }
394

395
    /// Set the default column to display
396
    ///
397
    /// Otherwise this will be inferred from the primary [`Annotation`]
398
    ///
399
    /// <div class="warning">
400
    ///
401
    /// `char_column` is only be respected if [`Origin::line`] is also set.
402
    ///
403
    /// </div>
404
    pub fn char_column(mut self, char_column: usize) -> Self {
2✔
405
        self.char_column = Some(char_column);
2✔
406
        self
2✔
407
    }
408

409
    pub fn primary(mut self, primary: bool) -> Self {
1✔
410
        self.primary = primary;
1✔
411
        self
1✔
412
    }
413
}
414

415
impl<'a> From<Cow<'a, str>> for Origin<'a> {
416
    fn from(origin: Cow<'a, str>) -> Self {
×
417
        Self::new(origin)
×
418
    }
419
}
420

421
#[derive(Debug)]
422
pub struct OptionCow<'a>(pub(crate) Option<Cow<'a, str>>);
423

424
impl<'a, T: Into<Cow<'a, str>>> From<Option<T>> for OptionCow<'a> {
425
    fn from(value: Option<T>) -> Self {
1✔
426
        Self(value.map(Into::into))
1✔
427
    }
428
}
429

430
impl<'a> From<&'a Cow<'a, str>> for OptionCow<'a> {
431
    fn from(value: &'a Cow<'a, str>) -> Self {
×
432
        Self(Some(Cow::Borrowed(value)))
×
433
    }
434
}
435

436
impl<'a> From<Cow<'a, str>> for OptionCow<'a> {
437
    fn from(value: Cow<'a, str>) -> Self {
×
438
        Self(Some(value))
×
439
    }
440
}
441

442
impl<'a> From<&'a str> for OptionCow<'a> {
443
    fn from(value: &'a str) -> Self {
10✔
444
        Self(Some(Cow::Borrowed(value)))
11✔
445
    }
446
}
447
impl<'a> From<String> for OptionCow<'a> {
448
    fn from(value: String) -> Self {
×
449
        Self(Some(Cow::Owned(value)))
×
450
    }
451
}
452

453
impl<'a> From<&'a String> for OptionCow<'a> {
454
    fn from(value: &'a String) -> Self {
×
455
        Self(Some(Cow::Borrowed(value.as_str())))
×
456
    }
457
}
458

459
/// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect
460
/// the case where a substring of the suggestion is "sandwiched" in the original, like
461
/// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length
462
/// of the suffix.
463
fn as_substr<'a>(original: &'a str, suggestion: &'a str) -> Option<(usize, &'a str, usize)> {
2✔
464
    let common_prefix = original
4✔
465
        .chars()
466
        .zip(suggestion.chars())
2✔
467
        .take_while(|(c1, c2)| c1 == c2)
5✔
468
        .map(|(c, _)| c.len_utf8())
4✔
469
        .sum();
470
    let original = &original[common_prefix..];
2✔
471
    let suggestion = &suggestion[common_prefix..];
2✔
472
    if let Some(stripped) = suggestion.strip_suffix(original) {
6✔
473
        let common_suffix = original.len();
2✔
474
        Some((common_prefix, stripped, common_suffix))
2✔
475
    } else {
476
        None
2✔
477
    }
478
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc