Skip to content

Commit 6516261

Browse files
committed
feat: make Level::span generic over RangeBounds
1 parent 9b26728 commit 6516261

File tree

3 files changed

+144
-19
lines changed

3 files changed

+144
-19
lines changed

src/renderer/display_list.rs

+23-15
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use crate::snippet;
3535
use std::cmp::{max, min, Reverse};
3636
use std::collections::HashMap;
3737
use std::fmt::Display;
38-
use std::ops::Range;
38+
use std::ops::{Bound, Range};
3939
use std::{cmp, fmt};
4040

4141
use crate::renderer::styled_buffer::StyledBuffer;
@@ -1085,7 +1085,7 @@ fn format_snippet(
10851085
term_width: usize,
10861086
anonymized_line_numbers: bool,
10871087
) -> DisplaySet<'_> {
1088-
let main_range = snippet.annotations.first().map(|x| x.range.start);
1088+
let main_range = snippet.annotations.first().map(|x| x.inclusive_start());
10891089
let origin = snippet.origin;
10901090
let need_empty_header = origin.is_some() || is_first;
10911091
let mut body = format_body(
@@ -1175,7 +1175,7 @@ fn fold_prefix_suffix(mut snippet: snippet::Snippet<'_>) -> snippet::Snippet<'_>
11751175
let ann_start = snippet
11761176
.annotations
11771177
.iter()
1178-
.map(|ann| ann.range.start)
1178+
.map(|ann| ann.inclusive_start())
11791179
.min()
11801180
.unwrap_or(0);
11811181
if let Some(before_new_start) = snippet.source[0..ann_start].rfind('\n') {
@@ -1187,16 +1187,24 @@ fn fold_prefix_suffix(mut snippet: snippet::Snippet<'_>) -> snippet::Snippet<'_>
11871187
snippet.source = &snippet.source[new_start..];
11881188

11891189
for ann in &mut snippet.annotations {
1190-
let range_start = ann.range.start - new_start;
1191-
let range_end = ann.range.end - new_start;
1192-
ann.range = range_start..range_end;
1190+
let range_start = match ann.range.0 {
1191+
Bound::Unbounded => Bound::Unbounded,
1192+
Bound::Excluded(e) => Bound::Excluded(e - new_start),
1193+
Bound::Included(e) => Bound::Included(e - new_start),
1194+
};
1195+
let range_end = match ann.range.1 {
1196+
Bound::Unbounded => Bound::Unbounded,
1197+
Bound::Excluded(e) => Bound::Excluded(e - new_start),
1198+
Bound::Included(e) => Bound::Included(e - new_start),
1199+
};
1200+
ann.range = (range_start, range_end);
11931201
}
11941202
}
11951203

11961204
let ann_end = snippet
11971205
.annotations
11981206
.iter()
1199-
.map(|ann| ann.range.end)
1207+
.map(|ann| ann.exclusive_end(snippet.source.len()))
12001208
.max()
12011209
.unwrap_or(snippet.source.len());
12021210
if let Some(end_offset) = snippet.source[ann_end..].find('\n') {
@@ -1286,7 +1294,7 @@ fn format_body(
12861294
let source_len = snippet.source.len();
12871295
if let Some(bigger) = snippet.annotations.iter().find_map(|x| {
12881296
// Allow highlighting one past the last character in the source.
1289-
if source_len + 1 < x.range.end {
1297+
if source_len + 1 < x.exclusive_end(source_len) {
12901298
Some(&x.range)
12911299
} else {
12921300
None
@@ -1313,7 +1321,7 @@ fn format_body(
13131321
let mut annotations = snippet.annotations;
13141322
let ranges = annotations
13151323
.iter()
1316-
.map(|a| a.range.clone())
1324+
.map(|a| (a.inclusive_start(), a.exclusive_end(source_len)))
13171325
.collect::<Vec<_>>();
13181326
// We want to merge multiline annotations that have the same range into one
13191327
// multiline annotation to save space. This is done by making any duplicate
@@ -1354,16 +1362,16 @@ fn format_body(
13541362
// Skip if the annotation's index matches the range index
13551363
if ann_idx != r_idx
13561364
// We only want to merge multiline annotations
1357-
&& snippet.source[ann.range.clone()].lines().count() > 1
1365+
&& snippet.source[ann.range].lines().count() > 1
13581366
// We only want to merge annotations that have the same range
1359-
&& ann.range.start == range.start
1360-
&& ann.range.end == range.end
1367+
&& ann.inclusive_start() == range.0
1368+
&& ann.exclusive_end(source_len) == range.1
13611369
{
1362-
ann.range.start = ann.range.end.saturating_sub(1);
1370+
ann.range.0 = Bound::Included(ann.exclusive_end(source_len).saturating_sub(1));
13631371
}
13641372
});
13651373
});
1366-
annotations.sort_by_key(|a| a.range.start);
1374+
annotations.sort_by_key(|a| a.inclusive_start());
13671375
let mut annotations = annotations.into_iter().enumerate().collect::<Vec<_>>();
13681376

13691377
for (idx, (line, end_line)) in CursorLines::new(snippet.source).enumerate() {
@@ -1411,7 +1419,7 @@ fn format_body(
14111419
_ => DisplayAnnotationType::from(annotation.level),
14121420
};
14131421
let label_right = annotation.label.map_or(0, |label| label.len() + 1);
1414-
match annotation.range {
1422+
match annotation.make_range(source_len) {
14151423
// This handles if the annotation is on the next line. We add
14161424
// the `end_line_size` to account for annotating the line end.
14171425
Range { start, .. } if start > line_end_index + end_line_size => true,

src/snippet.rs

+30-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//! .snippet(Snippet::source("Faa").line_start(129).origin("src/display.rs"));
1111
//! ```
1212
13-
use std::ops::Range;
13+
use std::ops::{Bound, Range, RangeBounds};
1414

1515
/// Primary structure provided for formatting
1616
///
@@ -111,7 +111,7 @@ impl<'a> Snippet<'a> {
111111
#[derive(Debug)]
112112
pub struct Annotation<'a> {
113113
/// The byte range of the annotation in the `source` string
114-
pub(crate) range: Range<usize>,
114+
pub(crate) range: (Bound<usize>, Bound<usize>),
115115
pub(crate) label: Option<&'a str>,
116116
pub(crate) level: Level,
117117
}
@@ -121,6 +121,29 @@ impl<'a> Annotation<'a> {
121121
self.label = Some(label);
122122
self
123123
}
124+
125+
pub(crate) fn inclusive_start(&self) -> usize {
126+
match self.range.0 {
127+
Bound::Included(i) => i,
128+
Bound::Excluded(e) => e.checked_add(1).expect("start bound too large"),
129+
Bound::Unbounded => 0,
130+
}
131+
}
132+
133+
pub(crate) fn exclusive_end(&self, len: usize) -> usize {
134+
match self.range.1 {
135+
Bound::Unbounded => len,
136+
Bound::Included(i) => i.checked_add(1).expect("end bound too large"),
137+
Bound::Excluded(e) => e,
138+
}
139+
}
140+
141+
pub(crate) fn make_range(&self, len: usize) -> Range<usize> {
142+
let start = self.inclusive_start();
143+
let end = self.exclusive_end(len);
144+
145+
start..end
146+
}
124147
}
125148

126149
/// Types of annotations.
@@ -147,9 +170,12 @@ impl Level {
147170
}
148171

149172
/// Create a [`Annotation`] with the given span for a [`Snippet`]
150-
pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
173+
pub fn span<'a, T>(self, span: T) -> Annotation<'a>
174+
where
175+
T: RangeBounds<usize>,
176+
{
151177
Annotation {
152-
range: span,
178+
range: (span.start_bound().cloned(), span.end_bound().cloned()),
153179
label: None,
154180
level: self,
155181
}

tests/formatter.rs

+91
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,98 @@
1+
use std::ops::Bound;
2+
13
use annotate_snippets::{Level, Renderer, Snippet};
24

35
use snapbox::{assert_data_eq, str};
46

7+
#[test]
8+
fn test_i_29_unbounded_start() {
9+
let snippets = Level::Error.title("oops").snippet(
10+
Snippet::source("First line\r\nSecond oops line")
11+
.origin("<current file>")
12+
.annotation(Level::Error.span(..23).label("oops"))
13+
.fold(true),
14+
);
15+
let expected = str![[r#"
16+
error: oops
17+
--> <current file>:1:1
18+
|
19+
1 | / First line
20+
2 | | Second oops line
21+
| |___________^ oops
22+
|
23+
"#]];
24+
25+
let renderer = Renderer::plain();
26+
assert_data_eq!(renderer.render(snippets).to_string(), expected);
27+
}
28+
29+
#[test]
30+
fn test_i_29_unbounded_end() {
31+
let snippets = Level::Error.title("oops").snippet(
32+
Snippet::source("First line\r\nSecond oops line")
33+
.origin("<current file>")
34+
.annotation(Level::Error.span(19..).label("oops"))
35+
.fold(true),
36+
);
37+
let expected = str![[r#"
38+
error: oops
39+
--> <current file>:2:8
40+
|
41+
2 | Second oops line
42+
| ^^^^^^^^^ oops
43+
|
44+
"#]];
45+
46+
let renderer = Renderer::plain();
47+
assert_data_eq!(renderer.render(snippets).to_string(), expected);
48+
}
49+
50+
#[test]
51+
fn test_i_29_included_end() {
52+
let snippets = Level::Error.title("oops").snippet(
53+
Snippet::source("First line\r\nSecond oops line")
54+
.origin("<current file>")
55+
.annotation(Level::Error.span(19..=22).label("oops"))
56+
.fold(true),
57+
);
58+
let expected = str![[r#"
59+
error: oops
60+
--> <current file>:2:8
61+
|
62+
2 | Second oops line
63+
| ^^^^ oops
64+
|
65+
"#]];
66+
67+
let renderer = Renderer::plain();
68+
assert_data_eq!(renderer.render(snippets).to_string(), expected);
69+
}
70+
71+
#[test]
72+
fn test_i_29_excluded_start() {
73+
let snippets = Level::Error.title("oops").snippet(
74+
Snippet::source("First line\r\nSecond oops line")
75+
.origin("<current file>")
76+
.annotation(
77+
Level::Error
78+
.span((Bound::Excluded(18), Bound::Excluded(23)))
79+
.label("oops"),
80+
)
81+
.fold(true),
82+
);
83+
let expected = str![[r#"
84+
error: oops
85+
--> <current file>:2:8
86+
|
87+
2 | Second oops line
88+
| ^^^^ oops
89+
|
90+
"#]];
91+
92+
let renderer = Renderer::plain();
93+
assert_data_eq!(renderer.render(snippets).to_string(), expected);
94+
}
95+
596
#[test]
697
fn test_i_29() {
798
let snippets = Level::Error.title("oops").snippet(

0 commit comments

Comments
 (0)