use crate::annotations::{compose_annotations_innermost, Annotation};
use crate::diff_words::DiffWordRange;
use crate::events::HighlightEvent;
use crate::highlights::HIGHLIGHT_NAMES;
use std::borrow::Cow;
use std::ops::{Range, RangeInclusive};
use std::sync::LazyLock;
pub const RAINBOW_BRACKET_SCOPES: [&str; 6] = [
"punctuation.bracket.rainbow.1",
"punctuation.bracket.rainbow.2",
"punctuation.bracket.rainbow.3",
"punctuation.bracket.rainbow.4",
"punctuation.bracket.rainbow.5",
"punctuation.bracket.rainbow.6",
];
#[doc(hidden)]
pub static RAINBOW_SCOPE_INDICES: LazyLock<[usize; RAINBOW_BRACKET_SCOPES.len()]> =
LazyLock::new(|| {
let fallback = HIGHLIGHT_NAMES
.iter()
.position(|candidate| *candidate == "punctuation.bracket")
.unwrap_or(0);
std::array::from_fn(|index| {
HIGHLIGHT_NAMES
.iter()
.position(|candidate| *candidate == RAINBOW_BRACKET_SCOPES[index])
.unwrap_or(fallback)
})
});
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Decoration {
Line {
number: usize,
highlighted: bool,
},
RainbowBracket {
depth: usize,
},
DiffWord {
kind: DiffWordKind,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum DiffWordKind {
Removed,
Added,
}
impl DiffWordKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Removed => "removed",
Self::Added => "added",
}
}
#[doc(hidden)]
#[must_use]
pub const fn scope(self) -> &'static str {
match self {
Self::Removed => "diff.minus.word",
Self::Added => "diff.plus.word",
}
}
#[doc(hidden)]
#[must_use]
pub fn scope_index(self) -> usize {
static INDICES: LazyLock<[usize; 2]> = LazyLock::new(|| {
[DiffWordKind::Removed, DiffWordKind::Added].map(|kind| {
HIGHLIGHT_NAMES
.iter()
.position(|candidate| *candidate == kind.scope())
.expect("highlights.toml names both diff word scopes")
})
});
INDICES[self as usize]
}
}
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RainbowRange {
pub start: usize,
pub end: usize,
pub depth: usize,
}
#[doc(hidden)]
pub fn rainbow_scope_index(depth: usize) -> usize {
RAINBOW_SCOPE_INDICES[depth % RAINBOW_SCOPE_INDICES.len()]
}
#[doc(hidden)]
pub fn rainbow_scope(depth: usize) -> &'static str {
RAINBOW_BRACKET_SCOPES[depth % RAINBOW_BRACKET_SCOPES.len()]
}
#[doc(hidden)]
pub fn compose_rainbow_decorations(
source: &str,
events: &[HighlightEvent<'_, ()>],
ranges: &[RainbowRange],
) -> Vec<HighlightEvent<'static, ()>> {
compose_innermost_decorations(
source,
events,
ranges.iter().map(|range| {
(
range.start..range.end,
Decoration::RainbowBracket { depth: range.depth },
)
}),
)
}
#[doc(hidden)]
pub fn compose_diff_word_decorations(
source: &str,
events: &[HighlightEvent<'_, ()>],
ranges: &[DiffWordRange],
) -> Vec<HighlightEvent<'static, ()>> {
compose_innermost_decorations(
source,
events,
ranges.iter().map(|range| {
(
range.start..range.end,
Decoration::DiffWord { kind: range.kind },
)
}),
)
}
fn compose_innermost_decorations(
source: &str,
events: &[HighlightEvent<'_, ()>],
ranges: impl Iterator<Item = (Range<usize>, Decoration)>,
) -> Vec<HighlightEvent<'static, ()>> {
let annotations = ranges
.map(|(range, decoration)| {
Annotation::new(range, decoration)
.expect("decoration ranges are ordered parser byte ranges")
})
.collect::<Vec<_>>();
let composed = compose_annotations_innermost(source, events, &annotations)
.expect("decoration ranges come from this parsed source");
composed
.into_iter()
.map(|event| match event {
HighlightEvent::Start {
scope_index,
language,
} => HighlightEvent::Start {
scope_index,
language,
},
HighlightEvent::Source { start, end } => HighlightEvent::Source { start, end },
HighlightEvent::End => HighlightEvent::End,
HighlightEvent::AnnotationStart { data, .. } => {
HighlightEvent::DecorationStart { decoration: *data }
}
HighlightEvent::AnnotationEnd | HighlightEvent::DecorationEnd => {
HighlightEvent::DecorationEnd
}
HighlightEvent::DecorationStart { decoration } => {
HighlightEvent::DecorationStart { decoration }
}
})
.collect()
}
pub(crate) fn last_line_number<T>(events: &[HighlightEvent<'_, T>]) -> usize {
events
.iter()
.rev()
.find_map(|event| match event {
HighlightEvent::DecorationStart {
decoration: Decoration::Line { number, .. },
} => Some(*number),
_ => None,
})
.unwrap_or(1)
}
pub(crate) fn gutter_width(last: usize) -> usize {
last.max(1).ilog10() as usize + 1
}
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SteppedLineRange {
first: usize,
last: usize,
step: usize,
}
impl SteppedLineRange {
#[doc(hidden)]
pub fn new(start: usize, end: usize, step: isize) -> Option<Self> {
let stride = step.unsigned_abs();
if stride == 0 {
return None;
}
let (first, last) = if step > 0 {
if start > end {
return None;
}
let span = end - start;
(start, start + span / stride * stride)
} else {
if start < end {
return None;
}
let span = start - end;
(start - span / stride * stride, start)
};
Some(Self {
first,
last,
step: stride,
})
}
#[doc(hidden)]
pub fn contains(&self, line_number: usize) -> bool {
(self.first..=self.last).contains(&line_number)
&& (line_number - self.first).is_multiple_of(self.step)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct LineSelection {
spans: Vec<(usize, usize)>,
stepped: Vec<SteppedLineRange>,
}
impl LineSelection {
pub(crate) fn new(ranges: &[RangeInclusive<usize>], stepped: &[SteppedLineRange]) -> Self {
let mut spans: Vec<(usize, usize)> = ranges
.iter()
.map(|range| (*range.start(), *range.end()))
.chain(
stepped
.iter()
.filter(|range| range.step == 1)
.map(|range| (range.first, range.last)),
)
.filter_map(|(start, end)| (start.max(1) <= end).then_some((start.max(1), end)))
.collect();
spans.sort_unstable();
spans.dedup_by(|(next_start, next_end), (_, end)| {
let mergeable = *next_start <= end.saturating_add(1);
if mergeable {
*end = (*end).max(*next_end);
}
mergeable
});
Self {
spans,
stepped: stepped
.iter()
.filter(|range| range.step > 1)
.cloned()
.collect(),
}
}
pub(crate) fn is_empty(&self) -> bool {
self.spans.is_empty() && self.stepped.is_empty()
}
fn cursor(&self) -> LineCursor<'_> {
LineCursor {
selection: self,
index: 0,
}
}
}
struct LineCursor<'a> {
selection: &'a LineSelection,
index: usize,
}
impl LineCursor<'_> {
fn contains(&mut self, line: usize) -> bool {
let spans = &self.selection.spans;
while self.index < spans.len() && spans[self.index].1 < line {
self.index += 1;
}
spans
.get(self.index)
.is_some_and(|(start, end)| *start <= line && line <= *end)
|| self
.selection
.stepped
.iter()
.any(|range| range.contains(line))
}
}
struct OpenLayer<'e, 'a, T>(&'e HighlightEvent<'a, T>);
impl<'a, T> OpenLayer<'_, 'a, T> {
fn closes(&self, event: &HighlightEvent<'a, T>) -> bool {
matches!(
(self.0, event),
(HighlightEvent::Start { .. }, HighlightEvent::End)
| (
HighlightEvent::AnnotationStart { .. },
HighlightEvent::AnnotationEnd
)
| (
HighlightEvent::DecorationStart { .. },
HighlightEvent::DecorationEnd
)
)
}
fn close_event(&self) -> HighlightEvent<'a, T> {
match self.0 {
HighlightEvent::AnnotationStart { .. } => HighlightEvent::AnnotationEnd,
HighlightEvent::DecorationStart { .. } => HighlightEvent::DecorationEnd,
_ => HighlightEvent::End,
}
}
}
pub(crate) fn compose_line_decorations<'a, T>(
source: &str,
events: &[HighlightEvent<'a, T>],
selection: &LineSelection,
) -> Vec<HighlightEvent<'a, T>> {
let mut output = Vec::with_capacity(events.len() + 2);
compose_line_decorations_into(source, events, selection, &mut |event| {
output.push(event.into_owned());
});
output
}
pub(crate) fn compose_line_decorations_into<'e, 'a, T>(
source: &str,
events: &'e [HighlightEvent<'a, T>],
selection: &LineSelection,
emit: &mut dyn FnMut(Cow<'e, HighlightEvent<'a, T>>),
) {
let output = &mut Emit(emit);
let mut layers: Vec<OpenLayer<'e, 'a, T>> = Vec::new();
let mut cursor = selection.cursor();
let mut line = 1usize;
let mut pending = false;
output.owned(line_start(line, &mut cursor));
for event in events {
if pending
&& matches!(
event,
HighlightEvent::Start { .. }
| HighlightEvent::AnnotationStart { .. }
| HighlightEvent::Source { .. }
| HighlightEvent::DecorationStart {
decoration: Decoration::RainbowBracket { .. } | Decoration::DiffWord { .. }
}
)
{
reopen_layers(output, &layers);
pending = false;
}
match event {
HighlightEvent::Start { .. }
| HighlightEvent::AnnotationStart { .. }
| HighlightEvent::DecorationStart {
decoration: Decoration::RainbowBracket { .. } | Decoration::DiffWord { .. },
} => {
output.lent(event);
layers.push(OpenLayer(event));
}
HighlightEvent::Source { start, end } => {
pending = split_source(
output,
source,
*start,
*end,
&layers,
&mut line,
&mut cursor,
);
}
HighlightEvent::DecorationStart {
decoration: Decoration::Line { .. },
} => {}
HighlightEvent::End | HighlightEvent::AnnotationEnd | HighlightEvent::DecorationEnd => {
close_layer(output, &mut layers, event, pending);
}
}
}
if !pending {
close_layers(output, &layers);
}
output.owned(HighlightEvent::DecorationEnd);
}
struct Emit<'s, 'e, 'a, T>(&'s mut dyn FnMut(Cow<'e, HighlightEvent<'a, T>>));
impl<'e, 'a, T> Emit<'_, 'e, 'a, T> {
fn owned(&mut self, event: HighlightEvent<'a, T>) {
(self.0)(Cow::Owned(event));
}
fn lent(&mut self, event: &'e HighlightEvent<'a, T>) {
(self.0)(Cow::Borrowed(event));
}
}
fn line_start<'a, T>(line: usize, cursor: &mut LineCursor<'_>) -> HighlightEvent<'a, T> {
HighlightEvent::DecorationStart {
decoration: Decoration::Line {
number: line,
highlighted: cursor.contains(line),
},
}
}
fn close_layers<'e, 'a, T>(output: &mut Emit<'_, 'e, 'a, T>, layers: &[OpenLayer<'e, 'a, T>]) {
for layer in layers.iter().rev() {
output.owned(layer.close_event());
}
}
fn close_layer<'e, 'a, T>(
output: &mut Emit<'_, 'e, 'a, T>,
layers: &mut Vec<OpenLayer<'e, 'a, T>>,
event: &'e HighlightEvent<'a, T>,
pending: bool,
) {
if layers.last().is_some_and(|layer| layer.closes(event)) {
layers.pop();
if !pending {
output.lent(event);
}
}
}
fn reopen_layers<'e, 'a, T>(output: &mut Emit<'_, 'e, 'a, T>, layers: &[OpenLayer<'e, 'a, T>]) {
for layer in layers {
output.lent(layer.0);
}
}
fn split_source<'e, 'a, T>(
output: &mut Emit<'_, 'e, 'a, T>,
source: &str,
start: usize,
end: usize,
layers: &[OpenLayer<'e, 'a, T>],
line: &mut usize,
cursor: &mut LineCursor<'_>,
) -> bool {
let mut cursor_offset = start.min(source.len());
let end = end.min(source.len()).max(cursor_offset);
while cursor_offset < end {
let Some(newline) = source.as_bytes()[cursor_offset..end]
.iter()
.position(|byte| *byte == b'\n')
else {
break;
};
let line_end = cursor_offset + newline + 1;
output.owned(HighlightEvent::Source {
start: cursor_offset,
end: line_end,
});
close_layers(output, layers);
output.owned(HighlightEvent::DecorationEnd);
*line += 1;
output.owned(line_start(*line, cursor));
cursor_offset = line_end;
if cursor_offset == end {
return true;
}
reopen_layers(output, layers);
}
if cursor_offset < end {
output.owned(HighlightEvent::Source {
start: cursor_offset,
end,
});
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::annotations::{compose_annotations, Annotation};
fn selection(ranges: impl IntoIterator<Item = RangeInclusive<usize>>) -> LineSelection {
LineSelection::new(&ranges.into_iter().collect::<Vec<_>>(), &[])
}
fn lines<T>(events: &[HighlightEvent<'_, T>]) -> Vec<(usize, bool)> {
events
.iter()
.filter_map(|event| match event {
HighlightEvent::DecorationStart {
decoration:
Decoration::Line {
number,
highlighted,
},
} => Some((*number, *highlighted)),
_ => None,
})
.collect()
}
fn rendered<T>(source: &str, events: &[HighlightEvent<'_, T>]) -> String {
let mut out = String::new();
for event in events {
if let HighlightEvent::Source { start, end } = event {
out.push_str(&source[*start..*end]);
}
}
out
}
#[test]
fn an_empty_document_is_one_line() {
let events: [HighlightEvent<'_>; 0] = [];
let composed = compose_line_decorations("", &events, &LineSelection::default());
assert_eq!(lines(&composed), [(1, false)]);
assert_eq!(composed.len(), 2, "a start and an end: {composed:?}");
}
#[test]
fn a_trailing_newline_opens_one_more_line() {
let source = "a\n";
let events: [HighlightEvent<'_, ()>; 1] = [HighlightEvent::Source { start: 0, end: 2 }];
let composed = compose_line_decorations(source, &events, &LineSelection::default());
assert_eq!(lines(&composed), [(1, false), (2, false)]);
assert_eq!(rendered(source, &composed), source);
}
#[test]
fn source_events_still_reproduce_the_source() {
let source = "one\ntwo\nthree";
let events: [HighlightEvent<'_, ()>; 1] = [HighlightEvent::Source {
start: 0,
end: source.len(),
}];
let composed =
compose_line_decorations(source, &events, &selection(std::iter::once(2..=2)));
assert_eq!(lines(&composed), [(1, false), (2, true), (3, false)]);
assert_eq!(rendered(source, &composed), source);
}
#[test]
fn a_scope_crossing_a_newline_is_closed_and_reopened() {
let source = "a\nb";
let events: [HighlightEvent<'_, ()>; 3] = [
HighlightEvent::Start {
scope_index: 1,
language: "rust".to_string(),
},
HighlightEvent::Source { start: 0, end: 3 },
HighlightEvent::End,
];
let composed = compose_line_decorations(source, &events, &LineSelection::default());
assert_eq!(
composed,
[
HighlightEvent::DecorationStart {
decoration: Decoration::Line {
number: 1,
highlighted: false
}
},
HighlightEvent::Start {
scope_index: 1,
language: "rust".to_string(),
},
HighlightEvent::Source { start: 0, end: 2 },
HighlightEvent::End,
HighlightEvent::DecorationEnd,
HighlightEvent::DecorationStart {
decoration: Decoration::Line {
number: 2,
highlighted: false
}
},
HighlightEvent::Start {
scope_index: 1,
language: "rust".to_string(),
},
HighlightEvent::Source { start: 2, end: 3 },
HighlightEvent::End,
HighlightEvent::DecorationEnd,
]
);
}
#[test]
fn an_annotation_crossing_a_newline_is_closed_and_reopened() {
let source = "ab\ncd";
let syntax = [HighlightEvent::Source { start: 0, end: 5 }];
let annotations = [Annotation::new(1..4, "span").unwrap()];
let events = compose_annotations(source, &syntax, &annotations).unwrap();
let composed = compose_line_decorations(source, &events, &LineSelection::default());
let starts = composed
.iter()
.filter(|event| matches!(event, HighlightEvent::AnnotationStart { .. }))
.count();
let ends = composed
.iter()
.filter(|event| matches!(event, HighlightEvent::AnnotationEnd))
.count();
assert_eq!((starts, ends), (2, 2));
assert_eq!(rendered(source, &composed), source);
}
#[test]
fn an_unbalanced_stream_still_closes_before_the_last_line_ends() {
let events: [HighlightEvent<'_, ()>; 1] = [HighlightEvent::Start {
scope_index: 1,
language: "rust".to_string(),
}];
let composed = compose_line_decorations("", &events, &LineSelection::default());
assert_eq!(
composed.last(),
Some(&HighlightEvent::DecorationEnd),
"{composed:?}"
);
assert!(
matches!(composed[composed.len() - 2], HighlightEvent::End),
"{composed:?}"
);
}
#[test]
fn composing_twice_gives_the_same_stream() {
let source = "a\nb\n";
let events: [HighlightEvent<'_, ()>; 1] = [HighlightEvent::Source { start: 0, end: 4 }];
let once = compose_line_decorations(source, &events, &selection(std::iter::once(1..=1)));
let twice = compose_line_decorations(source, &once, &selection(std::iter::once(1..=1)));
assert_eq!(once, twice);
}
#[test]
fn overlapping_ranges_merge_and_line_zero_is_dropped() {
let selection = selection([0..=2, 2..=4, 8..=9]);
assert_eq!(selection.spans, [(1, 4), (8, 9)]);
}
#[test]
fn adjacent_ranges_merge() {
assert_eq!(selection([1..=2, 3..=4]).spans, [(1, 4)]);
}
#[test]
#[allow(clippy::reversed_empty_ranges)]
fn a_reversed_range_selects_nothing() {
assert!(selection(std::iter::once(5..=3)).is_empty());
}
#[test]
fn a_selection_costs_nothing_for_a_range_larger_than_the_document() {
let huge = selection(std::iter::once(1..=usize::MAX));
let mut cursor = huge.cursor();
assert!(cursor.contains(1));
assert!(cursor.contains(1_000_000_000));
}
#[test]
fn a_stride_of_one_is_a_span_either_way_round() {
let ascending = SteppedLineRange::new(1, 1_000_000_000, 1).unwrap();
let descending = SteppedLineRange::new(1_000_000_000, 1, -1).unwrap();
let selection = LineSelection::new(&[], &[ascending, descending]);
assert_eq!(selection.spans, [(1, 1_000_000_000)]);
assert_eq!(selection.stepped, Vec::new());
}
#[test]
fn stepped_ranges_stay_compact() {
let ascending = SteppedLineRange::new(1, 1_000_000_000, 2).unwrap();
let descending = SteppedLineRange::new(1_000_000_000, 1, -3).unwrap();
let selection = LineSelection::new(&[], &[ascending, descending]);
let mut cursor = selection.cursor();
assert_eq!(
(1..=6)
.map(|line| cursor.contains(line))
.collect::<Vec<_>>(),
[true, false, true, true, true, false],
);
}
#[test]
fn a_descending_stepped_range_keeps_the_lines_the_step_lands_on() {
let unaligned = SteppedLineRange::new(10, 2, -3).unwrap();
assert!(unaligned.contains(4));
assert!(!unaligned.contains(2));
}
#[test]
fn many_ranges_merge_before_any_line_is_tested() {
let ranges: Vec<_> = (1..=20_000).step_by(2).map(|line| line..=line).collect();
let selection = selection(ranges);
let mut cursor = selection.cursor();
assert_eq!(selection.spans.len(), 10_000);
assert_eq!(
(1..=20_000).filter(|line| cursor.contains(*line)).count(),
10_000
);
}
}
#[cfg(test)]
#[path = "decorations_parity.rs"]
mod parity;