use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use frontend::commands::block_commands;
use crate::flow::FragmentContent;
use crate::inner::TextDocumentInner;
use crate::{CharVerticalAlignment, Color, TextFormat, UnderlineStyle};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HighlightFormat {
pub foreground_color: Option<Color>,
pub background_color: Option<Color>,
pub underline_color: Option<Color>,
pub font_family: Option<String>,
pub font_point_size: Option<u32>,
pub font_weight: Option<u32>,
pub font_bold: Option<bool>,
pub font_italic: Option<bool>,
pub font_underline: Option<bool>,
pub font_overline: Option<bool>,
pub font_strikeout: Option<bool>,
pub letter_spacing: Option<i32>,
pub word_spacing: Option<i32>,
pub underline_style: Option<UnderlineStyle>,
pub vertical_alignment: Option<CharVerticalAlignment>,
pub tooltip: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HighlightSpan {
pub start: usize,
pub length: usize,
pub format: HighlightFormat,
}
pub struct HighlightContext {
spans: Vec<HighlightSpan>,
previous_state: i64,
current_state: i64,
block_id: usize,
user_data: Option<Box<dyn Any + Send + Sync>>,
}
impl HighlightContext {
pub fn new(
block_id: usize,
previous_state: i64,
user_data: Option<Box<dyn Any + Send + Sync>>,
) -> Self {
Self {
spans: Vec::new(),
previous_state,
current_state: -1,
block_id,
user_data,
}
}
pub fn set_format(&mut self, start: usize, length: usize, format: HighlightFormat) {
if length == 0 {
return;
}
self.spans.push(HighlightSpan {
start,
length,
format,
});
}
pub fn previous_block_state(&self) -> i64 {
self.previous_state
}
pub fn set_current_block_state(&mut self, state: i64) {
self.current_state = state;
}
pub fn current_block_state(&self) -> i64 {
self.current_state
}
pub fn block_id(&self) -> usize {
self.block_id
}
pub fn set_user_data(&mut self, data: Box<dyn Any + Send + Sync>) {
self.user_data = Some(data);
}
pub fn user_data(&self) -> Option<&(dyn Any + Send + Sync)> {
self.user_data.as_deref()
}
pub fn user_data_mut(&mut self) -> Option<&mut (dyn Any + Send + Sync)> {
self.user_data.as_deref_mut()
}
pub fn into_parts(self) -> (Vec<HighlightSpan>, i64, Option<Box<dyn Any + Send + Sync>>) {
(self.spans, self.current_state, self.user_data)
}
}
pub trait SyntaxHighlighter: Send + Sync {
fn highlight_block(&self, text: &str, ctx: &mut HighlightContext);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SessionId(pub u64);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RangeHighlight {
pub start: usize,
pub length: usize,
pub format: HighlightFormat,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HighlightMask {
included: Option<Vec<SessionId>>,
}
impl HighlightMask {
pub(crate) const ALL: HighlightMask = HighlightMask { included: None };
pub fn all() -> Self {
Self { included: None }
}
pub fn none() -> Self {
Self {
included: Some(Vec::new()),
}
}
pub fn only(ids: impl IntoIterator<Item = SessionId>) -> Self {
Self {
included: Some(ids.into_iter().collect()),
}
}
pub fn with(mut self, id: SessionId) -> Self {
if let Some(ids) = &mut self.included
&& !ids.contains(&id)
{
ids.push(id);
}
self
}
pub(crate) fn admits(&self, id: SessionId) -> bool {
match &self.included {
None => true,
Some(ids) => ids.contains(&id),
}
}
pub(crate) fn is_empty(&self) -> bool {
matches!(&self.included, Some(ids) if ids.is_empty())
}
}
#[derive(Clone, Copy)]
pub(crate) struct SnapshotHighlights<'a> {
pub kind: HighlighterKind,
pub mask: &'a HighlightMask,
pub suppress_paint: bool,
}
pub(crate) struct BlockHighlightData {
pub spans: Vec<HighlightSpan>,
pub state: i64,
pub user_data: Option<Box<dyn Any + Send + Sync>>,
}
pub(crate) struct SyntaxSession {
pub highlighter: Arc<dyn SyntaxHighlighter>,
pub blocks: HashMap<usize, BlockHighlightData>,
}
pub(crate) struct RangeSession {
pub ranges: Vec<RangeHighlight>,
block_index: HashMap<usize, Vec<u32>>,
kind: HighlighterKind,
}
pub(crate) enum SessionBody {
Syntax(SyntaxSession),
Range(RangeSession),
}
pub(crate) struct Session {
pub id: SessionId,
pub body: SessionBody,
}
#[derive(Default)]
pub(crate) struct HighlightRegistry {
pub sessions: Vec<Session>,
next_id: u64,
shim: Option<SessionId>,
}
impl HighlightRegistry {
fn alloc_id(&mut self) -> SessionId {
let id = SessionId(self.next_id);
self.next_id += 1;
id
}
pub(crate) fn add_syntax(&mut self, highlighter: Arc<dyn SyntaxHighlighter>) -> SessionId {
let id = self.alloc_id();
self.sessions.push(Session {
id,
body: SessionBody::Syntax(SyntaxSession {
highlighter,
blocks: HashMap::new(),
}),
});
id
}
pub(crate) fn add_range(&mut self) -> SessionId {
let id = self.alloc_id();
self.sessions.push(Session {
id,
body: SessionBody::Range(RangeSession {
ranges: Vec::new(),
block_index: HashMap::new(),
kind: HighlighterKind::None,
}),
});
id
}
pub(crate) fn set_ranges(
&mut self,
id: SessionId,
ranges: Vec<RangeHighlight>,
block_positions: &[(u64, usize)],
) -> bool {
for s in &mut self.sessions {
if s.id == id {
if let SessionBody::Range(r) = &mut s.body {
r.kind = compute_range_kind(&ranges);
r.block_index = build_block_index(&ranges, block_positions);
r.ranges = ranges;
return true;
}
return false;
}
}
false
}
pub(crate) fn remove(&mut self, id: SessionId) -> bool {
let before = self.sessions.len();
self.sessions.retain(|s| s.id != id);
self.sessions.len() != before
}
pub(crate) fn set_shim(&mut self, highlighter: Option<Arc<dyn SyntaxHighlighter>>) {
if let Some(id) = self.shim.take() {
self.remove(id);
}
if let Some(hl) = highlighter {
self.shim = Some(self.add_syntax(hl));
}
}
pub(crate) fn is_empty(&self) -> bool {
self.sessions.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum HighlighterKind {
None,
PaintOnly,
Metric,
}
pub(crate) fn format_touches_metrics(f: &HighlightFormat) -> bool {
f.font_family.is_some()
|| f.font_point_size.is_some()
|| f.font_weight.is_some()
|| f.font_bold.is_some()
|| f.font_italic.is_some()
|| f.letter_spacing.is_some()
|| f.word_spacing.is_some()
|| f.vertical_alignment.is_some()
}
pub(crate) fn spans_touch_metrics(spans: &[HighlightSpan]) -> bool {
spans.iter().any(|s| format_touches_metrics(&s.format))
}
impl HighlighterKind {
fn rank(self) -> u8 {
match self {
HighlighterKind::None => 0,
HighlighterKind::PaintOnly => 1,
HighlighterKind::Metric => 2,
}
}
fn join(self, other: HighlighterKind) -> HighlighterKind {
if other.rank() > self.rank() {
other
} else {
self
}
}
}
fn syntax_session_kind(s: &SyntaxSession) -> HighlighterKind {
let mut any = false;
for bd in s.blocks.values() {
if spans_touch_metrics(&bd.spans) {
return HighlighterKind::Metric;
}
any |= !bd.spans.is_empty();
}
if any {
HighlighterKind::PaintOnly
} else {
HighlighterKind::None
}
}
fn compute_range_kind(ranges: &[RangeHighlight]) -> HighlighterKind {
let mut any = false;
for r in ranges {
if format_touches_metrics(&r.format) {
return HighlighterKind::Metric;
}
any |= r.length > 0;
}
if any {
HighlighterKind::PaintOnly
} else {
HighlighterKind::None
}
}
fn build_block_index(
ranges: &[RangeHighlight],
block_positions: &[(u64, usize)],
) -> HashMap<usize, Vec<u32>> {
let mut index: HashMap<usize, Vec<u32>> = HashMap::new();
if block_positions.is_empty() {
return index;
}
for (ri, r) in ranges.iter().enumerate() {
let r_end = r.start.saturating_add(r.length); let mut bi = match block_positions.binary_search_by_key(&r.start, |&(_, p)| p) {
Ok(i) => i,
Err(i) => i.saturating_sub(1),
};
while bi < block_positions.len() {
let b_start = block_positions[bi].1;
if b_start >= r_end {
break; }
let b_end = block_positions
.get(bi + 1)
.map(|&(_, p)| p)
.unwrap_or(usize::MAX);
if r.start < b_end && b_start < r_end {
index
.entry(block_positions[bi].0 as usize)
.or_default()
.push(ri as u32);
}
bi += 1;
}
}
index
}
impl HighlightRegistry {
pub(crate) fn effective_kind(&self, mask: &HighlightMask) -> HighlighterKind {
if mask.is_empty() {
return HighlighterKind::None;
}
let mut kind = HighlighterKind::None;
for s in &self.sessions {
if !mask.admits(s.id) {
continue;
}
let k = match &s.body {
SessionBody::Syntax(syn) => syntax_session_kind(syn),
SessionBody::Range(r) => r.kind, };
kind = kind.join(k);
if kind == HighlighterKind::Metric {
break;
}
}
kind
}
}
fn apply_highlight(base: &TextFormat, hl: &HighlightFormat) -> TextFormat {
TextFormat {
font_family: hl.font_family.clone().or_else(|| base.font_family.clone()),
font_point_size: hl.font_point_size.or(base.font_point_size),
font_weight: hl.font_weight.or(base.font_weight),
font_bold: hl.font_bold.or(base.font_bold),
font_italic: hl.font_italic.or(base.font_italic),
font_underline: hl.font_underline.or(base.font_underline),
font_overline: hl.font_overline.or(base.font_overline),
font_strikeout: hl.font_strikeout.or(base.font_strikeout),
letter_spacing: hl.letter_spacing.or(base.letter_spacing),
word_spacing: hl.word_spacing.or(base.word_spacing),
underline_style: hl
.underline_style
.clone()
.or_else(|| base.underline_style.clone()),
vertical_alignment: hl
.vertical_alignment
.clone()
.or_else(|| base.vertical_alignment.clone()),
tooltip: hl.tooltip.clone().or_else(|| base.tooltip.clone()),
foreground_color: hl.foreground_color.or(base.foreground_color),
background_color: hl.background_color.or(base.background_color),
underline_color: hl.underline_color.or(base.underline_color),
anchor_href: base.anchor_href.clone(),
anchor_names: base.anchor_names.clone(),
is_anchor: base.is_anchor,
}
}
fn merge_overlapping_highlights(spans: &[&HighlightSpan]) -> HighlightFormat {
let mut merged = HighlightFormat::default();
for span in spans {
let f = &span.format;
if f.foreground_color.is_some() {
merged.foreground_color = f.foreground_color;
}
if f.background_color.is_some() {
merged.background_color = f.background_color;
}
if f.underline_color.is_some() {
merged.underline_color = f.underline_color;
}
if f.font_family.is_some() {
merged.font_family = f.font_family.clone();
}
if f.font_point_size.is_some() {
merged.font_point_size = f.font_point_size;
}
if f.font_weight.is_some() {
merged.font_weight = f.font_weight;
}
if f.font_bold.is_some() {
merged.font_bold = f.font_bold;
}
if f.font_italic.is_some() {
merged.font_italic = f.font_italic;
}
if f.font_underline.is_some() {
merged.font_underline = f.font_underline;
}
if f.font_overline.is_some() {
merged.font_overline = f.font_overline;
}
if f.font_strikeout.is_some() {
merged.font_strikeout = f.font_strikeout;
}
if f.letter_spacing.is_some() {
merged.letter_spacing = f.letter_spacing;
}
if f.word_spacing.is_some() {
merged.word_spacing = f.word_spacing;
}
if f.underline_style.is_some() {
merged.underline_style = f.underline_style.clone();
}
if f.vertical_alignment.is_some() {
merged.vertical_alignment = f.vertical_alignment.clone();
}
if f.tooltip.is_some() {
merged.tooltip = f.tooltip.clone();
}
}
merged
}
pub(crate) fn extract_paint_spans(
spans: &[HighlightSpan],
block_len: usize,
) -> Vec<crate::flow::PaintHighlightSpan> {
if spans.is_empty() || block_len == 0 {
return Vec::new();
}
let mut boundaries = vec![0usize, block_len];
for s in spans {
let end = s.start.saturating_add(s.length);
if s.start > 0 && s.start < block_len {
boundaries.push(s.start);
}
if end > 0 && end < block_len {
boundaries.push(end);
}
}
boundaries.sort_unstable();
boundaries.dedup();
let n = spans.len();
let mut by_start: Vec<usize> = (0..n).collect();
by_start.sort_by_key(|&i| spans[i].start);
let mut by_end: Vec<usize> = (0..n).collect();
by_end.sort_by_key(|&i| spans[i].start.saturating_add(spans[i].length));
let mut active: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
let mut ps = 0usize; let mut pe = 0usize; let mut scratch: Vec<&HighlightSpan> = Vec::new();
let mut result = Vec::new();
for w in boundaries.windows(2) {
let (sub_start, sub_end) = (w[0], w[1]);
if sub_end <= sub_start {
continue;
}
while ps < n && spans[by_start[ps]].start <= sub_start {
active.insert(by_start[ps]);
ps += 1;
}
while pe < n
&& spans[by_end[pe]]
.start
.saturating_add(spans[by_end[pe]].length)
<= sub_start
{
active.remove(&by_end[pe]);
pe += 1;
}
if active.is_empty() {
continue;
}
scratch.clear();
scratch.extend(active.iter().map(|&i| &spans[i]));
let merged = merge_overlapping_highlights(&scratch);
if merged.foreground_color.is_none()
&& merged.background_color.is_none()
&& merged.underline_color.is_none()
&& merged.underline_style.is_none()
&& merged.font_underline.is_none()
&& merged.font_overline.is_none()
&& merged.font_strikeout.is_none()
{
continue;
}
result.push(crate::flow::PaintHighlightSpan {
start: sub_start,
length: sub_end - sub_start,
foreground_color: merged.foreground_color,
background_color: merged.background_color,
underline_color: merged.underline_color,
underline_style: merged.underline_style,
font_underline: merged.font_underline,
font_overline: merged.font_overline,
font_strikeout: merged.font_strikeout,
});
}
result
}
fn compute_word_starts_local(text: &str) -> Vec<u8> {
use unicode_segmentation::UnicodeSegmentation;
let mut result = Vec::new();
let mut byte_to_char: Vec<(usize, usize)> = Vec::new();
for (ci, (bi, _)) in text.char_indices().enumerate() {
byte_to_char.push((bi, ci));
}
for (byte_off, _word) in text.unicode_word_indices() {
let char_idx = byte_to_char
.iter()
.find(|(bi, _)| *bi == byte_off)
.map(|(_, ci)| *ci)
.unwrap_or(0);
if let Ok(idx) = u8::try_from(char_idx) {
result.push(idx);
} else {
break;
}
}
result
}
pub(crate) fn merge_highlight_spans(
fragments: Vec<FragmentContent>,
spans: &[HighlightSpan],
) -> Vec<FragmentContent> {
if spans.is_empty() {
return fragments;
}
let mut result = Vec::with_capacity(fragments.len());
for frag in fragments {
match frag {
FragmentContent::Text {
ref text,
ref format,
offset,
length,
element_id,
word_starts: _,
} => {
let frag_end = offset + length;
let mut boundaries = Vec::new();
boundaries.push(offset);
boundaries.push(frag_end);
for span in spans {
let span_end = span.start + span.length;
if span.start < frag_end && span_end > offset {
if span.start > offset && span.start < frag_end {
boundaries.push(span.start);
}
if span_end > offset && span_end < frag_end {
boundaries.push(span_end);
}
}
}
boundaries.sort_unstable();
boundaries.dedup();
let chars: Vec<char> = text.chars().collect();
for window in boundaries.windows(2) {
let sub_start = window[0];
let sub_end = window[1];
let sub_len = sub_end - sub_start;
if sub_len == 0 {
continue;
}
let active: Vec<&HighlightSpan> = spans
.iter()
.filter(|s| {
let s_end = s.start + s.length;
s.start < sub_end && s_end > sub_start
})
.collect();
let char_start = sub_start - offset;
let char_end = char_start + sub_len;
let sub_text: String = chars[char_start..char_end].iter().collect();
let sub_format = if active.is_empty() {
format.clone()
} else {
let merged_hl = merge_overlapping_highlights(&active);
apply_highlight(format, &merged_hl)
};
let sub_word_starts = compute_word_starts_local(&sub_text);
result.push(FragmentContent::Text {
text: sub_text,
format: sub_format,
offset: sub_start,
length: sub_len,
element_id,
word_starts: sub_word_starts,
});
}
}
FragmentContent::Image {
ref name,
width,
height,
quality,
ref format,
offset,
element_id,
} => {
let active: Vec<&HighlightSpan> = spans
.iter()
.filter(|s| {
let s_end = s.start + s.length;
s.start < offset + 1 && s_end > offset
})
.collect();
let img_format = if active.is_empty() {
format.clone()
} else {
let merged_hl = merge_overlapping_highlights(&active);
apply_highlight(format, &merged_hl)
};
result.push(FragmentContent::Image {
name: name.clone(),
width,
height,
quality,
format: img_format,
offset,
element_id,
});
}
}
}
result
}
fn ordered_block_ids(inner: &TextDocumentInner) -> Vec<(u64, String)> {
let mut blocks = block_commands::get_all_block(&inner.ctx).unwrap_or_default();
let store = inner.ctx.db_context.get_store();
crate::inner::refresh_block_positions(&mut blocks, store);
blocks.sort_by_key(|b| b.document_position);
blocks
.into_iter()
.map(|b| {
let entity: common::entities::Block = b.clone().into();
let text = common::database::rope_helpers::block_content_via_store(&entity, store);
(b.id, text)
})
.collect()
}
pub(crate) fn ordered_block_positions(inner: &TextDocumentInner) -> Vec<(u64, usize)> {
let mut blocks = block_commands::get_all_block(&inner.ctx).unwrap_or_default();
let store = inner.ctx.db_context.get_store();
crate::inner::refresh_block_positions(&mut blocks, store);
blocks.sort_by_key(|b| b.document_position);
blocks
.into_iter()
.map(|b| (b.id, b.document_position.max(0) as usize))
.collect()
}
fn block_geometry(inner: &TextDocumentInner, block_id: usize) -> (usize, usize) {
let store = inner.ctx.db_context.get_store();
let Some(mut dto) = block_commands::get_block(&inner.ctx, &(block_id as u64))
.ok()
.flatten()
else {
return (0, 0);
};
crate::inner::refresh_block_position(&mut dto, store);
let abs_start = dto.document_position.max(0) as usize;
let entity: common::entities::Block = dto.into();
let len = common::database::rope_helpers::block_char_length(&entity, store).max(0) as usize;
(abs_start, len)
}
pub(crate) fn merged_spans_for_block(
inner: &TextDocumentInner,
block_id: usize,
mask: &HighlightMask,
) -> Vec<HighlightSpan> {
if mask.is_empty() || inner.highlights.is_empty() {
return Vec::new();
}
let mut geom: Option<(usize, usize)> = None;
let mut out: Vec<HighlightSpan> = Vec::new();
for s in &inner.highlights.sessions {
if !mask.admits(s.id) {
continue;
}
match &s.body {
SessionBody::Syntax(syn) => {
if let Some(bd) = syn.blocks.get(&block_id) {
out.extend(bd.spans.iter().cloned());
}
}
SessionBody::Range(r) => {
let Some(indices) = r.block_index.get(&block_id) else {
continue;
};
let (abs_start, len) = *geom.get_or_insert_with(|| block_geometry(inner, block_id));
let block_end = abs_start + len;
for &ri in indices {
let rng = &r.ranges[ri as usize];
let lo = rng.start.max(abs_start);
let hi = rng.start.saturating_add(rng.length).min(block_end);
if lo < hi {
out.push(HighlightSpan {
start: lo - abs_start,
length: hi - lo,
format: rng.format.clone(),
});
}
}
}
}
}
out
}
impl TextDocumentInner {
pub(crate) fn rehighlight_all(&mut self) {
if self.highlights.is_empty() {
self.recompute_highlight_kind();
return;
}
let blocks = ordered_block_ids(self);
for si in 0..self.highlights.sessions.len() {
let SessionBody::Syntax(syn) = &self.highlights.sessions[si].body else {
continue;
};
let highlighter = Arc::clone(&syn.highlighter);
let mut fresh: HashMap<usize, BlockHighlightData> = HashMap::new();
let mut previous_state: i64 = -1;
for (block_id, text) in &blocks {
let bid = *block_id as usize;
let mut ctx = HighlightContext::new(bid, previous_state, None);
highlighter.highlight_block(text, &mut ctx);
let (spans, state, user_data) = ctx.into_parts();
previous_state = state;
fresh.insert(
bid,
BlockHighlightData {
spans,
state,
user_data,
},
);
}
if let SessionBody::Syntax(syn) = &mut self.highlights.sessions[si].body {
syn.blocks = fresh;
}
}
self.recompute_highlight_kind();
}
pub(crate) fn recompute_highlight_kind(&mut self) {
self.highlight_kind = self.highlights.effective_kind(&HighlightMask::all());
}
fn syntax_block_state(&self, session_idx: usize, block_id: usize) -> i64 {
match &self.highlights.sessions[session_idx].body {
SessionBody::Syntax(s) => s.blocks.get(&block_id).map_or(-1, |d| d.state),
_ => -1,
}
}
pub(crate) fn rehighlight_from_block(&mut self, start_block_id: usize) {
if self.highlights.is_empty() {
return;
}
let blocks = ordered_block_ids(self);
let Some(start_idx) = blocks
.iter()
.position(|(id, _)| *id as usize == start_block_id)
else {
return;
};
for si in 0..self.highlights.sessions.len() {
if matches!(self.highlights.sessions[si].body, SessionBody::Syntax(_)) {
self.rehighlight_session_from(si, start_idx, &blocks);
}
}
self.recompute_highlight_kind();
}
fn rehighlight_session_from(
&mut self,
session_idx: usize,
start_idx: usize,
blocks: &[(u64, String)],
) {
let highlighter = match &self.highlights.sessions[session_idx].body {
SessionBody::Syntax(s) => Arc::clone(&s.highlighter),
_ => return,
};
for i in start_idx..blocks.len() {
let (block_id, ref text) = blocks[i];
let bid = block_id as usize;
let previous_state = if i == 0 {
-1
} else {
self.syntax_block_state(session_idx, blocks[i - 1].0 as usize)
};
let user_data = match &mut self.highlights.sessions[session_idx].body {
SessionBody::Syntax(s) => s.blocks.get_mut(&bid).and_then(|d| d.user_data.take()),
_ => None,
};
let old_state = self.syntax_block_state(session_idx, bid);
let mut ctx = HighlightContext::new(bid, previous_state, user_data);
highlighter.highlight_block(text, &mut ctx);
let (spans, state, user_data) = ctx.into_parts();
if let SessionBody::Syntax(s) = &mut self.highlights.sessions[session_idx].body {
s.blocks.insert(
bid,
BlockHighlightData {
spans,
state,
user_data,
},
);
}
if i > start_idx && state == old_state {
break;
}
}
}
pub(crate) fn rehighlight_affected(&mut self, position: usize) {
if self.highlights.is_empty() {
return;
}
let positions = ordered_block_positions(self);
if positions.is_empty() {
return;
}
let target_bid = positions
.iter()
.rev()
.find(|(_, bp)| position >= *bp)
.map(|(id, _)| *id as usize)
.unwrap_or_else(|| positions[0].0 as usize);
self.rehighlight_from_block(target_bid);
}
}
#[cfg(test)]
mod paint_span_tests {
use super::*;
fn extract_paint_spans_reference(
spans: &[HighlightSpan],
block_len: usize,
) -> Vec<crate::flow::PaintHighlightSpan> {
if spans.is_empty() || block_len == 0 {
return Vec::new();
}
let mut boundaries = vec![0usize, block_len];
for s in spans {
let end = s.start.saturating_add(s.length);
if s.start > 0 && s.start < block_len {
boundaries.push(s.start);
}
if end > 0 && end < block_len {
boundaries.push(end);
}
}
boundaries.sort_unstable();
boundaries.dedup();
let mut result = Vec::new();
for w in boundaries.windows(2) {
let (sub_start, sub_end) = (w[0], w[1]);
if sub_end <= sub_start {
continue;
}
let active: Vec<&HighlightSpan> = spans
.iter()
.filter(|s| s.start < sub_end && s.start + s.length > sub_start)
.collect();
if active.is_empty() {
continue;
}
let merged = merge_overlapping_highlights(&active);
if merged.foreground_color.is_none()
&& merged.background_color.is_none()
&& merged.underline_color.is_none()
&& merged.underline_style.is_none()
&& merged.font_underline.is_none()
&& merged.font_overline.is_none()
&& merged.font_strikeout.is_none()
{
continue;
}
result.push(crate::flow::PaintHighlightSpan {
start: sub_start,
length: sub_end - sub_start,
foreground_color: merged.foreground_color,
background_color: merged.background_color,
underline_color: merged.underline_color,
underline_style: merged.underline_style,
font_underline: merged.font_underline,
font_overline: merged.font_overline,
font_strikeout: merged.font_strikeout,
});
}
result
}
fn span(start: usize, length: usize, k: usize) -> HighlightSpan {
let c = |v: usize| crate::Color {
red: v as u8,
green: (v >> 8) as u8,
blue: 7,
alpha: 255,
};
let format = match k % 4 {
0 => HighlightFormat {
background_color: Some(c(k)),
..Default::default()
},
1 => HighlightFormat {
foreground_color: Some(c(k)),
..Default::default()
},
2 => HighlightFormat {
underline_color: Some(c(k)),
..Default::default()
},
_ => HighlightFormat {
font_bold: Some(true),
..Default::default()
},
};
HighlightSpan {
start,
length,
format,
}
}
#[test]
fn sweep_matches_reference_on_edge_cases() {
let cases: Vec<(Vec<HighlightSpan>, usize)> = vec![
(vec![], 10),
(vec![span(0, 4, 0)], 0), (vec![span(0, 5, 0)], 10), (vec![span(0, 3, 0), span(5, 3, 1)], 10), (vec![span(2, 5, 0), span(4, 5, 1)], 12), (vec![span(0, 10, 0), span(3, 2, 1)], 10), (vec![span(0, 3, 0), span(3, 3, 1)], 10), (vec![span(4, 0, 0)], 10), (vec![span(0, 4, 3)], 10), (vec![span(8, 5, 0)], 10), (vec![span(12, 3, 0)], 10), (vec![span(0, 4, 0), span(0, 4, 1), span(0, 4, 2)], 10), ];
for (i, (spans, len)) in cases.iter().enumerate() {
assert_eq!(
extract_paint_spans(spans, *len),
extract_paint_spans_reference(spans, *len),
"edge case {i}: spans={spans:?} block_len={len}"
);
}
}
#[test]
fn sweep_matches_reference_randomized() {
let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
let mut next = |bound: usize| -> usize {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((state >> 33) as usize) % bound.max(1)
};
for trial in 0..2000 {
let block_len = 1 + next(40);
let m = next(14); let mut spans = Vec::with_capacity(m);
for k in 0..m {
let start = next(block_len + 4); let length = next(block_len + 2); spans.push(span(start, length, trial + k));
}
assert_eq!(
extract_paint_spans(&spans, block_len),
extract_paint_spans_reference(&spans, block_len),
"trial {trial}: spans={spans:?} block_len={block_len}"
);
}
}
}
#[cfg(test)]
mod index_tests {
use super::*;
fn paint(start: usize, length: usize) -> RangeHighlight {
RangeHighlight {
start,
length,
format: HighlightFormat {
background_color: Some(crate::Color {
red: 255,
green: 0,
blue: 0,
alpha: 255,
}),
..Default::default()
},
}
}
fn metric(start: usize, length: usize) -> RangeHighlight {
RangeHighlight {
start,
length,
format: HighlightFormat {
font_bold: Some(true),
..Default::default()
},
}
}
#[test]
fn kind_is_none_when_nothing_paints() {
assert_eq!(compute_range_kind(&[]), HighlighterKind::None);
assert_eq!(compute_range_kind(&[paint(3, 0)]), HighlighterKind::None);
}
#[test]
fn kind_is_paint_only_for_a_background_range() {
assert_eq!(
compute_range_kind(&[paint(0, 4)]),
HighlighterKind::PaintOnly
);
}
#[test]
fn kind_is_metric_when_any_range_touches_metrics() {
assert_eq!(
compute_range_kind(&[paint(0, 2), metric(4, 3), paint(8, 1)]),
HighlighterKind::Metric
);
}
#[test]
fn set_ranges_caches_the_kind_read_back_by_effective_kind() {
let mut reg = HighlightRegistry::default();
let id = reg.add_range();
let positions = [(0u64, 0usize)];
assert_eq!(
reg.effective_kind(&HighlightMask::all()),
HighlighterKind::None
);
reg.set_ranges(id, vec![paint(0, 5)], &positions);
assert_eq!(
reg.effective_kind(&HighlightMask::all()),
HighlighterKind::PaintOnly
);
reg.set_ranges(id, vec![metric(0, 5)], &positions);
assert_eq!(
reg.effective_kind(&HighlightMask::all()),
HighlighterKind::Metric,
"the cached kind updates on every push"
);
}
fn three_blocks() -> Vec<(u64, usize)> {
vec![(100, 0), (101, 10), (102, 20)]
}
fn bucket(index: &std::collections::HashMap<usize, Vec<u32>>, block: usize) -> Vec<u32> {
index.get(&block).cloned().unwrap_or_default()
}
#[test]
fn a_range_lands_only_in_its_own_block() {
let idx = build_block_index(&[paint(12, 3)], &three_blocks());
assert_eq!(
bucket(&idx, 101),
vec![0],
"12..15 is inside block 101 [10,20)"
);
assert!(bucket(&idx, 100).is_empty());
assert!(bucket(&idx, 102).is_empty());
}
#[test]
fn a_straddling_range_lands_in_every_block_it_touches() {
let idx = build_block_index(&[paint(8, 14)], &three_blocks());
assert_eq!(bucket(&idx, 100), vec![0]);
assert_eq!(bucket(&idx, 101), vec![0]);
assert_eq!(bucket(&idx, 102), vec![0]);
}
#[test]
fn a_zero_length_range_buckets_into_its_block_but_paints_nothing() {
let idx = build_block_index(&[paint(12, 0)], &three_blocks());
assert_eq!(bucket(&idx, 101), vec![0]);
assert!(bucket(&idx, 100).is_empty());
assert!(bucket(&idx, 102).is_empty());
}
#[test]
fn an_out_of_range_start_is_bucketed_into_the_last_block_only_if_it_overlaps() {
let idx = build_block_index(&[paint(9999, 3)], &three_blocks());
assert_eq!(
bucket(&idx, 102),
vec![0],
"the unbounded last block is the only candidate"
);
assert!(bucket(&idx, 100).is_empty());
assert!(bucket(&idx, 101).is_empty());
}
#[test]
fn empty_positions_yields_an_empty_index() {
assert!(build_block_index(&[paint(0, 5)], &[]).is_empty());
}
}