1use std::any::Any;
10use std::collections::HashMap;
11use std::sync::Arc;
12
13use frontend::commands::block_commands;
14
15use crate::flow::FragmentContent;
16use crate::inner::TextDocumentInner;
17use crate::{CharVerticalAlignment, Color, TextFormat, UnderlineStyle};
18
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
29pub struct HighlightFormat {
30 pub foreground_color: Option<Color>,
31 pub background_color: Option<Color>,
32 pub underline_color: Option<Color>,
33 pub font_family: Option<String>,
34 pub font_point_size: Option<u32>,
35 pub font_weight: Option<u32>,
36 pub font_bold: Option<bool>,
37 pub font_italic: Option<bool>,
38 pub font_underline: Option<bool>,
39 pub font_overline: Option<bool>,
40 pub font_strikeout: Option<bool>,
41 pub letter_spacing: Option<i32>,
42 pub word_spacing: Option<i32>,
43 pub underline_style: Option<UnderlineStyle>,
44 pub vertical_alignment: Option<CharVerticalAlignment>,
45 pub tooltip: Option<String>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct HighlightSpan {
53 pub start: usize,
54 pub length: usize,
55 pub format: HighlightFormat,
56}
57
58pub struct HighlightContext {
62 spans: Vec<HighlightSpan>,
63 previous_state: i64,
64 current_state: i64,
65 block_id: usize,
66 user_data: Option<Box<dyn Any + Send + Sync>>,
67}
68
69impl HighlightContext {
70 pub fn new(
72 block_id: usize,
73 previous_state: i64,
74 user_data: Option<Box<dyn Any + Send + Sync>>,
75 ) -> Self {
76 Self {
77 spans: Vec::new(),
78 previous_state,
79 current_state: -1,
80 block_id,
81 user_data,
82 }
83 }
84
85 pub fn set_format(&mut self, start: usize, length: usize, format: HighlightFormat) {
89 if length == 0 {
90 return;
91 }
92 self.spans.push(HighlightSpan {
93 start,
94 length,
95 format,
96 });
97 }
98
99 pub fn previous_block_state(&self) -> i64 {
101 self.previous_state
102 }
103
104 pub fn set_current_block_state(&mut self, state: i64) {
109 self.current_state = state;
110 }
111
112 pub fn current_block_state(&self) -> i64 {
114 self.current_state
115 }
116
117 pub fn block_id(&self) -> usize {
119 self.block_id
120 }
121
122 pub fn set_user_data(&mut self, data: Box<dyn Any + Send + Sync>) {
124 self.user_data = Some(data);
125 }
126
127 pub fn user_data(&self) -> Option<&(dyn Any + Send + Sync)> {
129 self.user_data.as_deref()
130 }
131
132 pub fn user_data_mut(&mut self) -> Option<&mut (dyn Any + Send + Sync)> {
134 self.user_data.as_deref_mut()
135 }
136
137 pub fn into_parts(self) -> (Vec<HighlightSpan>, i64, Option<Box<dyn Any + Send + Sync>>) {
140 (self.spans, self.current_state, self.user_data)
141 }
142}
143
144pub trait SyntaxHighlighter: Send + Sync {
155 fn highlight_block(&self, text: &str, ctx: &mut HighlightContext);
157}
158
159pub(crate) struct BlockHighlightData {
165 pub spans: Vec<HighlightSpan>,
166 pub state: i64,
167 pub user_data: Option<Box<dyn Any + Send + Sync>>,
168}
169
170pub(crate) struct HighlightData {
172 pub highlighter: Arc<dyn SyntaxHighlighter>,
173 pub blocks: HashMap<usize, BlockHighlightData>,
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub(crate) enum HighlighterKind {
182 None,
184 PaintOnly,
189 Metric,
192}
193
194pub(crate) fn spans_touch_metrics(spans: &[HighlightSpan]) -> bool {
200 spans.iter().any(|s| {
201 let f = &s.format;
202 f.font_family.is_some()
203 || f.font_point_size.is_some()
204 || f.font_weight.is_some()
205 || f.font_bold.is_some()
206 || f.font_italic.is_some()
207 || f.letter_spacing.is_some()
208 || f.word_spacing.is_some()
209 || f.vertical_alignment.is_some()
210 })
211}
212
213fn apply_highlight(base: &TextFormat, hl: &HighlightFormat) -> TextFormat {
219 TextFormat {
220 font_family: hl.font_family.clone().or_else(|| base.font_family.clone()),
221 font_point_size: hl.font_point_size.or(base.font_point_size),
222 font_weight: hl.font_weight.or(base.font_weight),
223 font_bold: hl.font_bold.or(base.font_bold),
224 font_italic: hl.font_italic.or(base.font_italic),
225 font_underline: hl.font_underline.or(base.font_underline),
226 font_overline: hl.font_overline.or(base.font_overline),
227 font_strikeout: hl.font_strikeout.or(base.font_strikeout),
228 letter_spacing: hl.letter_spacing.or(base.letter_spacing),
229 word_spacing: hl.word_spacing.or(base.word_spacing),
230 underline_style: hl
231 .underline_style
232 .clone()
233 .or_else(|| base.underline_style.clone()),
234 vertical_alignment: hl
235 .vertical_alignment
236 .clone()
237 .or_else(|| base.vertical_alignment.clone()),
238 tooltip: hl.tooltip.clone().or_else(|| base.tooltip.clone()),
239 foreground_color: hl.foreground_color.or(base.foreground_color),
240 background_color: hl.background_color.or(base.background_color),
241 underline_color: hl.underline_color.or(base.underline_color),
242 anchor_href: base.anchor_href.clone(),
244 anchor_names: base.anchor_names.clone(),
245 is_anchor: base.is_anchor,
246 }
247}
248
249fn merge_overlapping_highlights(spans: &[&HighlightSpan]) -> HighlightFormat {
252 let mut merged = HighlightFormat::default();
253 for span in spans {
254 let f = &span.format;
255 if f.foreground_color.is_some() {
256 merged.foreground_color = f.foreground_color;
257 }
258 if f.background_color.is_some() {
259 merged.background_color = f.background_color;
260 }
261 if f.underline_color.is_some() {
262 merged.underline_color = f.underline_color;
263 }
264 if f.font_family.is_some() {
265 merged.font_family = f.font_family.clone();
266 }
267 if f.font_point_size.is_some() {
268 merged.font_point_size = f.font_point_size;
269 }
270 if f.font_weight.is_some() {
271 merged.font_weight = f.font_weight;
272 }
273 if f.font_bold.is_some() {
274 merged.font_bold = f.font_bold;
275 }
276 if f.font_italic.is_some() {
277 merged.font_italic = f.font_italic;
278 }
279 if f.font_underline.is_some() {
280 merged.font_underline = f.font_underline;
281 }
282 if f.font_overline.is_some() {
283 merged.font_overline = f.font_overline;
284 }
285 if f.font_strikeout.is_some() {
286 merged.font_strikeout = f.font_strikeout;
287 }
288 if f.letter_spacing.is_some() {
289 merged.letter_spacing = f.letter_spacing;
290 }
291 if f.word_spacing.is_some() {
292 merged.word_spacing = f.word_spacing;
293 }
294 if f.underline_style.is_some() {
295 merged.underline_style = f.underline_style.clone();
296 }
297 if f.vertical_alignment.is_some() {
298 merged.vertical_alignment = f.vertical_alignment.clone();
299 }
300 if f.tooltip.is_some() {
301 merged.tooltip = f.tooltip.clone();
302 }
303 }
304 merged
305}
306
307pub(crate) fn extract_paint_spans(
318 spans: &[HighlightSpan],
319 block_len: usize,
320) -> Vec<crate::flow::PaintHighlightSpan> {
321 if spans.is_empty() || block_len == 0 {
322 return Vec::new();
323 }
324
325 let mut boundaries = vec![0usize, block_len];
327 for s in spans {
328 let end = s.start.saturating_add(s.length);
329 if s.start > 0 && s.start < block_len {
330 boundaries.push(s.start);
331 }
332 if end > 0 && end < block_len {
333 boundaries.push(end);
334 }
335 }
336 boundaries.sort_unstable();
337 boundaries.dedup();
338
339 let mut result = Vec::new();
340 for w in boundaries.windows(2) {
341 let (sub_start, sub_end) = (w[0], w[1]);
342 if sub_end <= sub_start {
343 continue;
344 }
345 let active: Vec<&HighlightSpan> = spans
346 .iter()
347 .filter(|s| s.start < sub_end && s.start + s.length > sub_start)
348 .collect();
349 if active.is_empty() {
350 continue;
351 }
352 let merged = merge_overlapping_highlights(&active);
353 if merged.foreground_color.is_none()
354 && merged.background_color.is_none()
355 && merged.underline_color.is_none()
356 && merged.underline_style.is_none()
357 && merged.font_underline.is_none()
358 && merged.font_overline.is_none()
359 && merged.font_strikeout.is_none()
360 {
361 continue;
362 }
363 result.push(crate::flow::PaintHighlightSpan {
364 start: sub_start,
365 length: sub_end - sub_start,
366 foreground_color: merged.foreground_color,
367 background_color: merged.background_color,
368 underline_color: merged.underline_color,
369 underline_style: merged.underline_style,
370 font_underline: merged.font_underline,
371 font_overline: merged.font_overline,
372 font_strikeout: merged.font_strikeout,
373 });
374 }
375 result
376}
377
378fn compute_word_starts_local(text: &str) -> Vec<u8> {
389 use unicode_segmentation::UnicodeSegmentation;
390 let mut result = Vec::new();
391 let mut byte_to_char: Vec<(usize, usize)> = Vec::new();
392 for (ci, (bi, _)) in text.char_indices().enumerate() {
393 byte_to_char.push((bi, ci));
394 }
395 for (byte_off, _word) in text.unicode_word_indices() {
396 let char_idx = byte_to_char
397 .iter()
398 .find(|(bi, _)| *bi == byte_off)
399 .map(|(_, ci)| *ci)
400 .unwrap_or(0);
401 if let Ok(idx) = u8::try_from(char_idx) {
402 result.push(idx);
403 } else {
404 break;
405 }
406 }
407 result
408}
409
410pub(crate) fn merge_highlight_spans(
411 fragments: Vec<FragmentContent>,
412 spans: &[HighlightSpan],
413) -> Vec<FragmentContent> {
414 if spans.is_empty() {
415 return fragments;
416 }
417
418 let mut result = Vec::with_capacity(fragments.len());
419
420 for frag in fragments {
421 match frag {
422 FragmentContent::Text {
423 ref text,
424 ref format,
425 offset,
426 length,
427 element_id,
428 word_starts: _,
429 } => {
430 let frag_end = offset + length;
431
432 let mut boundaries = Vec::new();
434 boundaries.push(offset);
435 boundaries.push(frag_end);
436
437 for span in spans {
438 let span_end = span.start + span.length;
439 if span.start < frag_end && span_end > offset {
441 if span.start > offset && span.start < frag_end {
442 boundaries.push(span.start);
443 }
444 if span_end > offset && span_end < frag_end {
445 boundaries.push(span_end);
446 }
447 }
448 }
449
450 boundaries.sort_unstable();
451 boundaries.dedup();
452
453 let chars: Vec<char> = text.chars().collect();
455 for window in boundaries.windows(2) {
456 let sub_start = window[0];
457 let sub_end = window[1];
458 let sub_len = sub_end - sub_start;
459 if sub_len == 0 {
460 continue;
461 }
462
463 let active: Vec<&HighlightSpan> = spans
465 .iter()
466 .filter(|s| {
467 let s_end = s.start + s.length;
468 s.start < sub_end && s_end > sub_start
469 })
470 .collect();
471
472 let char_start = sub_start - offset;
473 let char_end = char_start + sub_len;
474 let sub_text: String = chars[char_start..char_end].iter().collect();
475
476 let sub_format = if active.is_empty() {
477 format.clone()
478 } else {
479 let merged_hl = merge_overlapping_highlights(&active);
480 apply_highlight(format, &merged_hl)
481 };
482
483 let sub_word_starts = compute_word_starts_local(&sub_text);
484 result.push(FragmentContent::Text {
485 text: sub_text,
486 format: sub_format,
487 offset: sub_start,
488 length: sub_len,
489 element_id,
502 word_starts: sub_word_starts,
503 });
504 }
505 }
506 FragmentContent::Image {
507 ref name,
508 width,
509 height,
510 quality,
511 ref format,
512 offset,
513 element_id,
514 } => {
515 let active: Vec<&HighlightSpan> = spans
517 .iter()
518 .filter(|s| {
519 let s_end = s.start + s.length;
520 s.start < offset + 1 && s_end > offset
521 })
522 .collect();
523
524 let img_format = if active.is_empty() {
525 format.clone()
526 } else {
527 let merged_hl = merge_overlapping_highlights(&active);
528 apply_highlight(format, &merged_hl)
529 };
530
531 result.push(FragmentContent::Image {
532 name: name.clone(),
533 width,
534 height,
535 quality,
536 format: img_format,
537 offset,
538 element_id,
539 });
540 }
541 }
542 }
543
544 result
545}
546
547fn ordered_block_ids(inner: &TextDocumentInner) -> Vec<(u64, String)> {
553 let mut blocks = block_commands::get_all_block(&inner.ctx).unwrap_or_default();
554 let store = inner.ctx.db_context.get_store();
555 crate::inner::refresh_block_positions(&mut blocks, store);
556 blocks.sort_by_key(|b| b.document_position);
557 blocks
558 .into_iter()
559 .map(|b| {
560 let entity: common::entities::Block = b.clone().into();
561 let text = common::database::rope_helpers::block_content_via_store(&entity, store);
562 (b.id, text)
563 })
564 .collect()
565}
566
567impl TextDocumentInner {
568 pub(crate) fn rehighlight_all(&mut self) {
570 let hl = match self.highlight {
571 Some(ref mut hl) => hl,
572 None => return,
573 };
574
575 let highlighter = Arc::clone(&hl.highlighter);
576 hl.blocks.clear();
577
578 let blocks = ordered_block_ids(self);
579 let mut previous_state: i64 = -1;
580
581 for (block_id, text) in &blocks {
582 let bid = *block_id as usize;
583 let mut ctx = HighlightContext::new(bid, previous_state, None);
584 highlighter.highlight_block(text, &mut ctx);
585 let (spans, state, user_data) = ctx.into_parts();
586
587 previous_state = state;
588
589 let hl = self.highlight.as_mut().unwrap();
591 hl.blocks.insert(
592 bid,
593 BlockHighlightData {
594 spans,
595 state,
596 user_data,
597 },
598 );
599 }
600
601 self.recompute_highlight_kind();
602 }
603
604 pub(crate) fn recompute_highlight_kind(&mut self) {
608 self.highlight_kind = match &self.highlight {
609 None => HighlighterKind::None,
610 Some(hl) => {
611 if hl.blocks.values().any(|bd| spans_touch_metrics(&bd.spans)) {
612 HighlighterKind::Metric
613 } else {
614 HighlighterKind::PaintOnly
615 }
616 }
617 };
618 }
619
620 pub(crate) fn rehighlight_from_block(&mut self, start_block_id: usize) {
623 let hl = match self.highlight {
624 Some(ref hl) => hl,
625 None => return,
626 };
627
628 let highlighter = Arc::clone(&hl.highlighter);
629 let blocks = ordered_block_ids(self);
630
631 let start_idx = match blocks
633 .iter()
634 .position(|(id, _)| *id as usize == start_block_id)
635 {
636 Some(idx) => idx,
637 None => return,
638 };
639
640 for i in start_idx..blocks.len() {
641 let (block_id, ref text) = blocks[i];
642 let bid = block_id as usize;
643
644 let hl = self.highlight.as_ref().unwrap();
645
646 let previous_state = if i == 0 {
648 -1
649 } else {
650 let prev_bid = blocks[i - 1].0 as usize;
651 hl.blocks.get(&prev_bid).map_or(-1, |d| d.state)
652 };
653
654 let user_data = self
656 .highlight
657 .as_mut()
658 .unwrap()
659 .blocks
660 .get_mut(&bid)
661 .and_then(|d| d.user_data.take());
662
663 let old_state = self
664 .highlight
665 .as_ref()
666 .unwrap()
667 .blocks
668 .get(&bid)
669 .map_or(-1, |d| d.state);
670
671 let mut ctx = HighlightContext::new(bid, previous_state, user_data);
672 highlighter.highlight_block(text, &mut ctx);
673 let (spans, state, user_data) = ctx.into_parts();
674
675 let hl = self.highlight.as_mut().unwrap();
676 hl.blocks.insert(
677 bid,
678 BlockHighlightData {
679 spans,
680 state,
681 user_data,
682 },
683 );
684
685 if i > start_idx && state == old_state {
688 break;
689 }
690 }
691
692 self.recompute_highlight_kind();
693 }
694
695 pub(crate) fn rehighlight_affected(&mut self, position: usize) {
698 if self.highlight.is_none() {
699 return;
700 }
701
702 let blocks = ordered_block_ids(self);
703
704 let store = self.ctx.db_context.get_store();
705 let target_bid = blocks
707 .iter()
708 .rev()
709 .find_map(|(id, _)| {
710 let mut dto = block_commands::get_block(&self.ctx, id).ok().flatten()?;
711 crate::inner::refresh_block_position(&mut dto, store);
712 let bp = dto.document_position as usize;
713 if position >= bp {
714 Some(*id as usize)
715 } else {
716 None
717 }
718 })
719 .unwrap_or_else(|| blocks.first().map_or(0, |(id, _)| *id as usize));
720
721 if blocks.is_empty() {
722 return;
723 }
724
725 self.rehighlight_from_block(target_bid);
726 }
727}