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

rust-lang / annotate-snippets-rs / 16014034454

02 Jul 2025 01:37AM UTC coverage: 87.43%. Remained the same
16014034454

Pull #239

github

web-flow
Merge 3e787fa6f into 2db78b777
Pull Request #239: chore!: Make fold the default

1405 of 1607 relevant lines covered (87.43%)

4.88 hits per line

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

76.42
/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) primary_level: Level<'a>,
24
    pub(crate) elements: Vec<Element<'a>>,
25
}
26

27
impl<'a> Group<'a> {
28
    /// Create group with a title, deriving the primary [`Level`] for [`Annotation`]s from it
29
    pub fn with_title(title: Title<'a>) -> Self {
9✔
30
        let level = title.level.clone();
9✔
31
        Self::with_level(level).element(title)
9✔
32
    }
33

34
    /// Create a title-less group with a primary [`Level`] for [`Annotation`]s
35
    pub fn with_level(level: Level<'a>) -> Self {
9✔
36
        Self {
37
            primary_level: level,
38
            elements: vec![],
12✔
39
        }
40
    }
41

42
    pub fn element(mut self, section: impl Into<Element<'a>>) -> Self {
27✔
43
        self.elements.push(section.into());
60✔
44
        self
30✔
45
    }
46

47
    pub fn elements(mut self, sections: impl IntoIterator<Item = impl Into<Element<'a>>>) -> Self {
×
48
        self.elements.extend(sections.into_iter().map(Into::into));
×
49
        self
×
50
    }
51

52
    pub fn is_empty(&self) -> bool {
×
53
        self.elements.is_empty()
×
54
    }
55
}
56

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

68
impl<'a> From<Title<'a>> for Element<'a> {
69
    fn from(value: Title<'a>) -> Self {
12✔
70
        Element::Title(value)
12✔
71
    }
72
}
73

74
impl<'a> From<Snippet<'a, Annotation<'a>>> for Element<'a> {
75
    fn from(value: Snippet<'a, Annotation<'a>>) -> Self {
9✔
76
        Element::Cause(value)
11✔
77
    }
78
}
79

80
impl<'a> From<Snippet<'a, Patch<'a>>> for Element<'a> {
81
    fn from(value: Snippet<'a, Patch<'a>>) -> Self {
5✔
82
        Element::Suggestion(value)
5✔
83
    }
84
}
85

86
impl<'a> From<Origin<'a>> for Element<'a> {
87
    fn from(value: Origin<'a>) -> Self {
2✔
88
        Element::Origin(value)
2✔
89
    }
90
}
91

92
impl From<Padding> for Element<'_> {
93
    fn from(value: Padding) -> Self {
1✔
94
        Self::Padding(value)
1✔
95
    }
96
}
97

98
/// A whitespace [`Element`] in a [`Group`]
99
#[derive(Clone, Debug)]
100
pub struct Padding;
101

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

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

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

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

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

175
    /// When manually [`fold`][Self::fold]ing,
176
    /// the [`source`][Self::source]s line offset from the original start
177
    pub fn line_start(mut self, line_start: usize) -> Self {
13✔
178
        self.line_start = line_start;
13✔
179
        self
13✔
180
    }
181

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

196
    /// Hide lines without [`Annotation`]s
197
    pub fn fold(mut self, fold: bool) -> Self {
3✔
198
        self.fold = fold;
3✔
199
        self
3✔
200
    }
201
}
202

203
impl<'a> Snippet<'a, Annotation<'a>> {
204
    /// Highlight and describe a span of text within the [`source`][Self::source]
205
    pub fn annotation(mut self, annotation: Annotation<'a>) -> Snippet<'a, Annotation<'a>> {
7✔
206
        self.markers.push(annotation);
5✔
207
        self
9✔
208
    }
209

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

217
impl<'a> Snippet<'a, Patch<'a>> {
218
    /// Suggest to the user an edit to the [`source`][Self::source]
219
    pub fn patch(mut self, patch: Patch<'a>) -> Snippet<'a, Patch<'a>> {
4✔
220
        self.markers.push(patch);
4✔
221
        self
5✔
222
    }
223

224
    /// Suggest to the user edits to the [`source`][Self::source]
225
    pub fn patches(mut self, patches: impl IntoIterator<Item = Patch<'a>>) -> Self {
×
226
        self.markers.extend(patches);
×
227
        self
×
228
    }
229
}
230

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

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

259
    /// Style the source according to the [`AnnotationKind`]
260
    pub fn highlight_source(mut self, highlight_source: bool) -> Self {
×
261
        self.highlight_source = highlight_source;
×
262
        self
×
263
    }
264
}
265

266
/// The category of the [`Annotation`]
267
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
268
#[non_exhaustive]
269
pub enum AnnotationKind {
270
    /// Shows the source that the [Group's Title][Group::with_title] references
271
    ///
272
    /// For [`Title`]-less groups, see [`Group::with_level`]
273
    Primary,
274
    /// Additional context to explain the [`Primary`][Self::Primary]
275
    /// [`Annotation`]
276
    ///
277
    /// See also [`Renderer::context`].
278
    ///
279
    /// [`Renderer::context`]: crate::renderer::Renderer
280
    Context,
281
}
282

283
impl AnnotationKind {
284
    pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
8✔
285
        Annotation {
286
            span,
287
            label: None,
288
            kind: self,
289
            highlight_source: false,
290
        }
291
    }
292

293
    pub(crate) fn is_primary(&self) -> bool {
6✔
294
        matches!(self, AnnotationKind::Primary)
6✔
295
    }
296
}
297

298
/// Suggested edit to the [`Snippet`]
299
#[derive(Clone, Debug)]
300
pub struct Patch<'a> {
301
    pub(crate) span: Range<usize>,
302
    pub(crate) replacement: Cow<'a, str>,
303
}
304

305
impl<'a> Patch<'a> {
306
    /// Splice `replacement` into the [`Snippet`] at the `span`
307
    ///
308
    /// <div class="warning">
309
    ///
310
    /// Text passed to this function is considered "untrusted input", as such
311
    /// all text is passed through a normalization function. Pre-styled text is
312
    /// not allowed to be passed to this function.
313
    ///
314
    /// </div>
315
    pub fn new(span: Range<usize>, replacement: impl Into<Cow<'a, str>>) -> Self {
4✔
316
        Self {
317
            span,
318
            replacement: replacement.into(),
4✔
319
        }
320
    }
321

322
    pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool {
2✔
323
        !self.replacement.is_empty() && !self.replaces_meaningful_content(sm)
2✔
324
    }
325

326
    pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool {
4✔
327
        self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm)
4✔
328
    }
329

330
    pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool {
3✔
331
        !self.replacement.is_empty() && self.replaces_meaningful_content(sm)
3✔
332
    }
333

334
    /// Whether this is a replacement that overwrites source with a snippet
335
    /// in a way that isn't a superset of the original string. For example,
336
    /// replacing "abc" with "abcde" is not destructive, but replacing it
337
    /// it with "abx" is, since the "c" character is lost.
338
    pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool {
3✔
339
        self.is_replacement(sm)
3✔
340
            && !sm
2✔
341
                .span_to_snippet(self.span.clone())
2✔
342
                // This should use `is_some_and` when our MSRV is >= 1.70
343
                .map_or(false, |s| {
2✔
344
                    as_substr(s.trim(), self.replacement.trim()).is_some()
2✔
345
                })
346
    }
347

348
    fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool {
4✔
349
        sm.span_to_snippet(self.span.clone())
8✔
350
            .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty())
12✔
351
    }
352

353
    /// Try to turn a replacement into an addition when the span that is being
354
    /// overwritten matches either the prefix or suffix of the replacement.
355
    pub(crate) fn trim_trivial_replacements(&mut self, sm: &'a SourceMap<'a>) {
3✔
356
        if self.replacement.is_empty() {
3✔
357
            return;
×
358
        }
359
        let Some(snippet) = sm.span_to_snippet(self.span.clone()) else {
4✔
360
            return;
×
361
        };
362

363
        if let Some((prefix, substr, suffix)) = as_substr(snippet, &self.replacement) {
4✔
364
            self.span = self.span.start + prefix..self.span.end.saturating_sub(suffix);
2✔
365
            self.replacement = Cow::Owned(substr.to_owned());
4✔
366
        }
367
    }
368
}
369

370
/// The referenced location (e.g. a path)
371
///
372
/// If you have source available, see instead [`Snippet`]
373
#[derive(Clone, Debug)]
374
pub struct Origin<'a> {
375
    pub(crate) path: Cow<'a, str>,
376
    pub(crate) line: Option<usize>,
377
    pub(crate) char_column: Option<usize>,
378
    pub(crate) primary: bool,
379
}
380

381
impl<'a> Origin<'a> {
382
    /// <div class="warning">
383
    ///
384
    /// Text passed to this function is considered "untrusted input", as such
385
    /// all text is passed through a normalization function. Pre-styled text is
386
    /// not allowed to be passed to this function.
387
    ///
388
    /// </div>
389
    pub fn new(path: impl Into<Cow<'a, str>>) -> Self {
6✔
390
        Self {
391
            path: path.into(),
6✔
392
            line: None,
393
            char_column: None,
394
            primary: false,
395
        }
396
    }
397

398
    /// Set the default line number to display
399
    pub fn line(mut self, line: usize) -> Self {
2✔
400
        self.line = Some(line);
2✔
401
        self
2✔
402
    }
403

404
    /// Set the default column to display
405
    ///
406
    /// <div class="warning">
407
    ///
408
    /// `char_column` is only be respected if [`Origin::line`] is also set.
409
    ///
410
    /// </div>
411
    pub fn char_column(mut self, char_column: usize) -> Self {
2✔
412
        self.char_column = Some(char_column);
2✔
413
        self
2✔
414
    }
415

416
    /// Mark this as the source that the [Group's Title][Group::with_title] references
417
    pub fn primary(mut self, primary: bool) -> Self {
1✔
418
        self.primary = primary;
1✔
419
        self
1✔
420
    }
421
}
422

423
impl<'a> From<Cow<'a, str>> for Origin<'a> {
424
    fn from(origin: Cow<'a, str>) -> Self {
×
425
        Self::new(origin)
×
426
    }
427
}
428

429
#[derive(Debug)]
430
pub struct OptionCow<'a>(pub(crate) Option<Cow<'a, str>>);
431

432
impl<'a, T: Into<Cow<'a, str>>> From<Option<T>> for OptionCow<'a> {
433
    fn from(value: Option<T>) -> Self {
1✔
434
        Self(value.map(Into::into))
1✔
435
    }
436
}
437

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

444
impl<'a> From<Cow<'a, str>> for OptionCow<'a> {
445
    fn from(value: Cow<'a, str>) -> Self {
×
446
        Self(Some(value))
×
447
    }
448
}
449

450
impl<'a> From<&'a str> for OptionCow<'a> {
451
    fn from(value: &'a str) -> Self {
10✔
452
        Self(Some(Cow::Borrowed(value)))
12✔
453
    }
454
}
455
impl<'a> From<String> for OptionCow<'a> {
456
    fn from(value: String) -> Self {
×
457
        Self(Some(Cow::Owned(value)))
×
458
    }
459
}
460

461
impl<'a> From<&'a String> for OptionCow<'a> {
462
    fn from(value: &'a String) -> Self {
×
463
        Self(Some(Cow::Borrowed(value.as_str())))
×
464
    }
465
}
466

467
/// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect
468
/// the case where a substring of the suggestion is "sandwiched" in the original, like
469
/// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length
470
/// of the suffix.
471
fn as_substr<'a>(original: &'a str, suggestion: &'a str) -> Option<(usize, &'a str, usize)> {
2✔
472
    let common_prefix = original
4✔
473
        .chars()
474
        .zip(suggestion.chars())
2✔
475
        .take_while(|(c1, c2)| c1 == c2)
5✔
476
        .map(|(c, _)| c.len_utf8())
4✔
477
        .sum();
478
    let original = &original[common_prefix..];
2✔
479
    let suggestion = &suggestion[common_prefix..];
2✔
480
    if let Some(stripped) = suggestion.strip_suffix(original) {
4✔
481
        let common_suffix = original.len();
2✔
482
        Some((common_prefix, stripped, common_suffix))
2✔
483
    } else {
484
        None
2✔
485
    }
486
}
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