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);
}
pub(crate) struct BlockHighlightData {
pub spans: Vec<HighlightSpan>,
pub state: i64,
pub user_data: Option<Box<dyn Any + Send + Sync>>,
}
pub(crate) struct HighlightData {
pub highlighter: Arc<dyn SyntaxHighlighter>,
pub blocks: HashMap<usize, BlockHighlightData>,
}
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
}
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();
blocks.sort_by_key(|b| b.document_position);
blocks.into_iter().map(|b| (b.id, b.plain_text)).collect()
}
impl TextDocumentInner {
pub(crate) fn rehighlight_all(&mut self) {
let hl = match self.highlight {
Some(ref mut hl) => hl,
None => return,
};
let highlighter = Arc::clone(&hl.highlighter);
hl.blocks.clear();
let blocks = ordered_block_ids(self);
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;
let hl = self.highlight.as_mut().unwrap();
hl.blocks.insert(
bid,
BlockHighlightData {
spans,
state,
user_data,
},
);
}
}
pub(crate) fn rehighlight_from_block(&mut self, start_block_id: usize) {
let hl = match self.highlight {
Some(ref hl) => hl,
None => return,
};
let highlighter = Arc::clone(&hl.highlighter);
let blocks = ordered_block_ids(self);
let start_idx = match blocks
.iter()
.position(|(id, _)| *id as usize == start_block_id)
{
Some(idx) => idx,
None => return,
};
for i in start_idx..blocks.len() {
let (block_id, ref text) = blocks[i];
let bid = block_id as usize;
let hl = self.highlight.as_ref().unwrap();
let previous_state = if i == 0 {
-1
} else {
let prev_bid = blocks[i - 1].0 as usize;
hl.blocks.get(&prev_bid).map_or(-1, |d| d.state)
};
let user_data = self
.highlight
.as_mut()
.unwrap()
.blocks
.get_mut(&bid)
.and_then(|d| d.user_data.take());
let old_state = self
.highlight
.as_ref()
.unwrap()
.blocks
.get(&bid)
.map_or(-1, |d| d.state);
let mut ctx = HighlightContext::new(bid, previous_state, user_data);
highlighter.highlight_block(text, &mut ctx);
let (spans, state, user_data) = ctx.into_parts();
let hl = self.highlight.as_mut().unwrap();
hl.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.highlight.is_none() {
return;
}
let blocks = ordered_block_ids(self);
let target_bid = blocks
.iter()
.rev()
.find_map(|(id, _)| {
let dto = block_commands::get_block(&self.ctx, id).ok().flatten()?;
let bp = dto.document_position as usize;
if position >= bp {
Some(*id as usize)
} else {
None
}
})
.unwrap_or_else(|| blocks.first().map_or(0, |(id, _)| *id as usize));
if blocks.is_empty() {
return;
}
self.rehighlight_from_block(target_bid);
}
}