Skip to content

feat: make Level::span generic over RangeBounds #152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 24 additions & 15 deletions src/renderer/display_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use crate::snippet;
use std::cmp::{max, min, Reverse};
use std::collections::HashMap;
use std::fmt::Display;
use std::ops::Range;
use std::ops::{Bound, Range};
use std::{cmp, fmt};

use crate::renderer::styled_buffer::StyledBuffer;
Expand Down Expand Up @@ -1085,7 +1085,7 @@ fn format_snippet(
term_width: usize,
anonymized_line_numbers: bool,
) -> DisplaySet<'_> {
let main_range = snippet.annotations.first().map(|x| x.range.start);
let main_range = snippet.annotations.first().map(|x| x.inclusive_start());
let origin = snippet.origin;
let need_empty_header = origin.is_some() || is_first;
let mut body = format_body(
Expand Down Expand Up @@ -1175,7 +1175,7 @@ fn fold_prefix_suffix(mut snippet: snippet::Snippet<'_>) -> snippet::Snippet<'_>
let ann_start = snippet
.annotations
.iter()
.map(|ann| ann.range.start)
.map(|ann| ann.inclusive_start())
.min()
.unwrap_or(0);
if let Some(before_new_start) = snippet.source[0..ann_start].rfind('\n') {
Expand All @@ -1187,16 +1187,24 @@ fn fold_prefix_suffix(mut snippet: snippet::Snippet<'_>) -> snippet::Snippet<'_>
snippet.source = &snippet.source[new_start..];

for ann in &mut snippet.annotations {
let range_start = ann.range.start - new_start;
let range_end = ann.range.end - new_start;
ann.range = range_start..range_end;
let range_start = match ann.range.0 {
Bound::Unbounded => Bound::Unbounded,
Bound::Excluded(e) => Bound::Excluded(e - new_start),
Bound::Included(e) => Bound::Included(e - new_start),
};
let range_end = match ann.range.1 {
Bound::Unbounded => Bound::Unbounded,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I'm wondering if this needs to be Bound::Exclusive(ann.exclusive_end(foo) - new_start) instead.

Bound::Excluded(e) => Bound::Excluded(e - new_start),
Bound::Included(e) => Bound::Included(e - new_start),
};
ann.range = (range_start, range_end);
}
}

let ann_end = snippet
.annotations
.iter()
.map(|ann| ann.range.end)
.map(|ann| ann.exclusive_end(snippet.source.len()))
.max()
.unwrap_or(snippet.source.len());
if let Some(end_offset) = snippet.source[ann_end..].find('\n') {
Expand Down Expand Up @@ -1286,7 +1294,7 @@ fn format_body(
let source_len = snippet.source.len();
if let Some(bigger) = snippet.annotations.iter().find_map(|x| {
// Allow highlighting one past the last character in the source.
if source_len + 1 < x.range.end {
if source_len + 1 < x.exclusive_end(source_len) {
Some(&x.range)
} else {
None
Expand All @@ -1313,7 +1321,7 @@ fn format_body(
let mut annotations = snippet.annotations;
let ranges = annotations
.iter()
.map(|a| a.range.clone())
.map(|a| (a.inclusive_start(), a.exclusive_end(source_len)))
.collect::<Vec<_>>();
// We want to merge multiline annotations that have the same range into one
// multiline annotation to save space. This is done by making any duplicate
Expand Down Expand Up @@ -1351,19 +1359,20 @@ fn format_body(
.enumerate()
.skip(r_idx + 1)
.for_each(|(ann_idx, ann)| {
let ann_range = ann.make_range(source_len);
// Skip if the annotation's index matches the range index
if ann_idx != r_idx
// We only want to merge multiline annotations
&& snippet.source[ann.range.clone()].lines().count() > 1
&& snippet.source[ann_range].lines().count() > 1
// We only want to merge annotations that have the same range
&& ann.range.start == range.start
&& ann.range.end == range.end
&& ann.inclusive_start() == range.0
&& ann.exclusive_end(source_len) == range.1
{
ann.range.start = ann.range.end.saturating_sub(1);
ann.range.0 = Bound::Included(ann.exclusive_end(source_len).saturating_sub(1));
}
});
});
annotations.sort_by_key(|a| a.range.start);
annotations.sort_by_key(|a| a.inclusive_start());
let mut annotations = annotations.into_iter().enumerate().collect::<Vec<_>>();

for (idx, (line, end_line)) in CursorLines::new(snippet.source).enumerate() {
Expand Down Expand Up @@ -1411,7 +1420,7 @@ fn format_body(
_ => DisplayAnnotationType::from(annotation.level),
};
let label_right = annotation.label.map_or(0, |label| label.len() + 1);
match annotation.range {
match annotation.make_range(source_len) {
// This handles if the annotation is on the next line. We add
// the `end_line_size` to account for annotating the line end.
Range { start, .. } if start > line_end_index + end_line_size => true,
Expand Down
34 changes: 30 additions & 4 deletions src/snippet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//! .snippet(Snippet::source("Faa").line_start(129).origin("src/display.rs"));
//! ```

use std::ops::Range;
use std::ops::{Bound, Range, RangeBounds};

/// Primary structure provided for formatting
///
Expand Down Expand Up @@ -111,7 +111,7 @@ impl<'a> Snippet<'a> {
#[derive(Debug)]
pub struct Annotation<'a> {
/// The byte range of the annotation in the `source` string
pub(crate) range: Range<usize>,
pub(crate) range: (Bound<usize>, Bound<usize>),
pub(crate) label: Option<&'a str>,
pub(crate) level: Level,
}
Expand All @@ -121,6 +121,29 @@ impl<'a> Annotation<'a> {
self.label = Some(label);
self
}

pub(crate) fn inclusive_start(&self) -> usize {
match self.range.0 {
Bound::Included(i) => i,
Bound::Excluded(e) => e.checked_add(1).expect("start bound too large"),
Bound::Unbounded => 0,
}
}

pub(crate) fn exclusive_end(&self, len: usize) -> usize {
match self.range.1 {
Bound::Unbounded => len,
Bound::Included(i) => i.checked_add(1).expect("end bound too large"),
Bound::Excluded(e) => e,
}
}

pub(crate) fn make_range(&self, len: usize) -> Range<usize> {
let start = self.inclusive_start();
let end = self.exclusive_end(len);

start..end
}
}

/// Types of annotations.
Expand All @@ -147,9 +170,12 @@ impl Level {
}

/// Create a [`Annotation`] with the given span for a [`Snippet`]
pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
pub fn span<'a, T>(self, span: T) -> Annotation<'a>
where
T: RangeBounds<usize>,
{
Annotation {
range: span,
range: (span.start_bound().cloned(), span.end_bound().cloned()),
label: None,
level: self,
}
Expand Down
91 changes: 91 additions & 0 deletions tests/formatter.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,98 @@
use std::ops::Bound;

use annotate_snippets::{Level, Renderer, Snippet};

use snapbox::{assert_data_eq, str};

#[test]
fn test_i_29_unbounded_start() {
let snippets = Level::Error.title("oops").snippet(
Snippet::source("First line\r\nSecond oops line")
.origin("<current file>")
.annotation(Level::Error.span(..23).label("oops"))
.fold(true),
);
let expected = str![[r#"
error: oops
--> <current file>:1:1
|
1 | / First line
2 | | Second oops line
| |___________^ oops
|
"#]];

let renderer = Renderer::plain();
assert_data_eq!(renderer.render(snippets).to_string(), expected);
}

#[test]
fn test_i_29_unbounded_end() {
let snippets = Level::Error.title("oops").snippet(
Snippet::source("First line\r\nSecond oops line")
.origin("<current file>")
.annotation(Level::Error.span(19..).label("oops"))
.fold(true),
);
let expected = str![[r#"
error: oops
--> <current file>:2:8
|
2 | Second oops line
| ^^^^^^^^^ oops
|
"#]];

let renderer = Renderer::plain();
assert_data_eq!(renderer.render(snippets).to_string(), expected);
}

#[test]
fn test_i_29_included_end() {
let snippets = Level::Error.title("oops").snippet(
Snippet::source("First line\r\nSecond oops line")
.origin("<current file>")
.annotation(Level::Error.span(19..=22).label("oops"))
.fold(true),
);
let expected = str![[r#"
error: oops
--> <current file>:2:8
|
2 | Second oops line
| ^^^^ oops
|
"#]];

let renderer = Renderer::plain();
assert_data_eq!(renderer.render(snippets).to_string(), expected);
}

#[test]
fn test_i_29_excluded_start() {
let snippets = Level::Error.title("oops").snippet(
Snippet::source("First line\r\nSecond oops line")
.origin("<current file>")
.annotation(
Level::Error
.span((Bound::Excluded(18), Bound::Excluded(23)))
.label("oops"),
)
.fold(true),
);
let expected = str![[r#"
error: oops
--> <current file>:2:8
|
2 | Second oops line
| ^^^^ oops
|
"#]];

let renderer = Renderer::plain();
assert_data_eq!(renderer.render(snippets).to_string(), expected);
}

#[test]
fn test_i_29() {
let snippets = Level::Error.title("oops").snippet(
Expand Down
Loading