use std::ops::Range;
pub use crate::decorations::Decoration;
#[non_exhaustive]
#[derive(Debug, PartialEq, Eq)]
pub enum HighlightEvent<'a, T = ()> {
Start {
scope_index: usize,
language: String,
},
Source { start: usize, end: usize },
End,
AnnotationStart {
range: Range<usize>,
data: &'a T,
},
AnnotationEnd,
DecorationStart {
decoration: Decoration,
},
DecorationEnd,
}
impl<T> Clone for HighlightEvent<'_, T> {
fn clone(&self) -> Self {
match self {
Self::Start {
scope_index,
language,
} => Self::Start {
scope_index: *scope_index,
language: language.clone(),
},
Self::Source { start, end } => Self::Source {
start: *start,
end: *end,
},
Self::End => Self::End,
Self::AnnotationStart { range, data } => Self::AnnotationStart {
range: range.clone(),
data: *data,
},
Self::AnnotationEnd => Self::AnnotationEnd,
Self::DecorationStart { decoration } => Self::DecorationStart {
decoration: *decoration,
},
Self::DecorationEnd => Self::DecorationEnd,
}
}
}
impl<T> HighlightEvent<'_, T> {
pub fn scope(&self) -> Option<&'static str> {
match self {
Self::Start { scope_index, .. } => crate::highlights::HIGHLIGHT_NAMES
.get(*scope_index)
.copied(),
_ => None,
}
}
pub fn language(&self) -> Option<&str> {
match self {
Self::Start { language, .. } => Some(language),
_ => None,
}
}
}
pub struct Coalescing<'a, T = ()> {
events: Vec<HighlightEvent<'a, T>>,
open: Vec<usize>,
reopenable: Option<usize>,
}
impl<T> Default for Coalescing<'_, T> {
fn default() -> Self {
Self::new()
}
}
impl<'a, T> Coalescing<'a, T> {
pub const fn new() -> Self {
Self {
events: Vec::new(),
open: Vec::new(),
reopenable: None,
}
}
pub fn start(&mut self, scope_index: usize, language: String) {
if let Some(index) = self.reopenable.take() {
if matches!(
&self.events[index],
HighlightEvent::Start {
scope_index: open_scope,
language: open_language,
} if *open_scope == scope_index && *open_language == language
) {
self.events.pop();
self.open.push(index);
return;
}
}
self.open.push(self.events.len());
self.events.push(HighlightEvent::Start {
scope_index,
language,
});
}
pub fn end(&mut self) {
self.reopenable = self.open.pop();
self.events.push(HighlightEvent::End);
}
pub fn source(&mut self, start: usize, end: usize) {
self.reopenable = None;
if let Some(HighlightEvent::Source {
end: previous_end, ..
}) = self.events.last_mut()
{
if *previous_end == start {
*previous_end = end;
return;
}
}
self.events.push(HighlightEvent::Source { start, end });
}
pub fn finish(self) -> Vec<HighlightEvent<'a, T>> {
self.events
}
}
#[cfg(test)]
mod coalescing_tests {
use super::*;
fn scopes_and_text(events: &[HighlightEvent<'_>]) -> Vec<String> {
events
.iter()
.map(|event| match event {
HighlightEvent::Start { scope_index, .. } => format!("<{scope_index}"),
HighlightEvent::End => ">".to_string(),
HighlightEvent::Source { start, end } => format!("{start}..{end}"),
_ => "?".to_string(),
})
.collect()
}
#[test]
fn a_scope_reopened_with_nothing_between_stays_open() {
let mut stream = Coalescing::<()>::new();
stream.start(1, "json".to_string());
stream.source(0, 1);
stream.end();
stream.start(1, "json".to_string());
stream.source(1, 2);
stream.end();
assert_eq!(scopes_and_text(&stream.finish()), ["<1", "0..2", ">"]);
}
#[test]
fn a_different_scope_or_language_opens_its_own_run() {
let mut stream = Coalescing::<()>::new();
stream.start(1, "json".to_string());
stream.source(0, 1);
stream.end();
stream.start(2, "json".to_string());
stream.source(1, 2);
stream.end();
stream.start(2, "html".to_string());
stream.source(2, 3);
stream.end();
assert_eq!(
scopes_and_text(&stream.finish()),
["<1", "0..1", ">", "<2", "1..2", ">", "<2", "2..3", ">"]
);
}
#[test]
fn text_between_two_runs_of_one_scope_keeps_them_apart() {
let mut stream = Coalescing::<()>::new();
stream.start(1, "json".to_string());
stream.source(0, 1);
stream.end();
stream.source(1, 2);
stream.start(1, "json".to_string());
stream.source(2, 3);
stream.end();
assert_eq!(
scopes_and_text(&stream.finish()),
["<1", "0..1", ">", "1..2", "<1", "2..3", ">"]
);
}
#[test]
fn a_source_range_that_does_not_continue_the_last_one_is_its_own_event() {
let mut stream = Coalescing::<()>::new();
stream.source(0, 1);
stream.source(2, 3);
assert_eq!(scopes_and_text(&stream.finish()), ["0..1", "2..3"]);
}
#[test]
fn an_inner_scope_reopens_without_disturbing_the_one_around_it() {
let mut stream = Coalescing::<()>::new();
stream.start(1, "json".to_string());
stream.start(2, "json".to_string());
stream.source(0, 1);
stream.end();
stream.start(2, "json".to_string());
stream.source(1, 2);
stream.end();
stream.end();
assert_eq!(
scopes_and_text(&stream.finish()),
["<1", "<2", "0..2", ">", ">"]
);
}
#[test]
fn closing_an_outer_scope_after_an_inner_one_does_not_reopen_the_inner() {
let mut stream = Coalescing::<()>::new();
stream.start(1, "json".to_string());
stream.start(2, "json".to_string());
stream.source(0, 1);
stream.end();
stream.end();
stream.start(2, "json".to_string());
stream.source(1, 2);
stream.end();
assert_eq!(
scopes_and_text(&stream.finish()),
["<1", "<2", "0..1", ">", ">", "<2", "1..2", ">"]
);
}
}