1use std::ops::Range;
2
3use crate::EditorBuffer;
4use crate::folding::FoldRange;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub struct Rgba {
9 pub r: u8,
11 pub g: u8,
13 pub b: u8,
15 pub a: u8,
17}
18
19impl Rgba {
20 pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
22 Self { r, g, b, a }
23 }
24
25 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
27 Self { r, g, b, a: 255 }
28 }
29
30 pub const fn hex(hex: u32) -> Self {
32 let r = ((hex >> 16) & 0xFF) as u8;
33 let g = ((hex >> 8) & 0xFF) as u8;
34 let b = (hex & 0xFF) as u8;
35 Self { r, g, b, a: 255 }
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum UnderlineDecoration {
42 Solid,
44 Wavy,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub enum HighlightTag {
56 Keyword,
58 Function,
60 Type,
62 String,
64 Number,
66 Comment,
68 Operator,
70 Punctuation,
72 Heading(u8),
75 Bold,
77 Italic,
79 Highlight,
81 Code,
83 Link,
85 Blockquote,
87 Callout(CalloutKind),
90 HorizontalRule,
92 TaskUnchecked,
94 TaskChecked,
96 Dimmed,
98 Hidden,
100 Custom(&'static str),
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub enum CalloutKind {
111 Note,
113 Tip,
115 Warning,
117 Caution,
119 Important,
121 Other,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
127pub struct TextStyle {
128 pub color: Option<Rgba>,
130 pub background: Option<Rgba>,
132 pub bold: bool,
134 pub italic: bool,
136 pub underline: Option<UnderlineDecoration>,
138 pub strikethrough: bool,
140}
141
142impl TextStyle {
143 pub const fn new() -> Self {
145 Self {
146 color: None,
147 background: None,
148 bold: false,
149 italic: false,
150 underline: None,
151 strikethrough: false,
152 }
153 }
154
155 pub const fn color(mut self, color: Rgba) -> Self {
157 self.color = Some(color);
158 self
159 }
160
161 pub const fn background(mut self, background: Rgba) -> Self {
163 self.background = Some(background);
164 self
165 }
166
167 pub const fn bold(mut self) -> Self {
169 self.bold = true;
170 self
171 }
172
173 pub const fn italic(mut self) -> Self {
175 self.italic = true;
176 self
177 }
178
179 pub const fn underline(mut self, underline: UnderlineDecoration) -> Self {
181 self.underline = Some(underline);
182 self
183 }
184
185 pub const fn strikethrough(mut self) -> Self {
187 self.strikethrough = true;
188 self
189 }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
194pub enum StyleValue {
195 Tag(HighlightTag),
197 Direct(TextStyle),
199}
200
201impl From<HighlightTag> for StyleValue {
202 fn from(tag: HighlightTag) -> Self {
203 Self::Tag(tag)
204 }
205}
206
207impl From<TextStyle> for StyleValue {
208 fn from(style: TextStyle) -> Self {
209 Self::Direct(style)
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
215pub struct StyleSpan {
216 pub range: Range<usize>,
218 pub style: StyleValue,
220}
221
222impl StyleSpan {
223 pub fn new(range: Range<usize>, style: impl Into<StyleValue>) -> Self {
225 Self {
226 range,
227 style: style.into(),
228 }
229 }
230
231 pub fn tag(range: Range<usize>, tag: HighlightTag) -> Self {
233 Self {
234 range,
235 style: StyleValue::Tag(tag),
236 }
237 }
238
239 pub fn direct(range: Range<usize>, style: TextStyle) -> Self {
241 Self {
242 range,
243 style: StyleValue::Direct(style),
244 }
245 }
246}
247
248pub trait SyntaxHighlighter: Send + Sync + 'static {
250 fn highlight_line(&self, buffer: &EditorBuffer, row: usize, line_text: &str) -> Vec<StyleSpan>;
254
255 fn extract_links(
260 &self,
261 _buffer: &EditorBuffer,
262 _row: usize,
263 _line_text: &str,
264 ) -> Vec<(Range<usize>, String)> {
265 Vec::new()
266 }
267
268 fn expand_line(
274 &self,
275 _buffer: &EditorBuffer,
276 _row: usize,
277 _concealed: &ConcealedLine,
278 ) -> Vec<DisplayPad> {
279 Vec::new()
280 }
281
282 fn should_wrap_line(&self, _buffer: &EditorBuffer, _row: usize) -> bool {
287 true
288 }
289
290 fn foldable_ranges(&self, _buffer: &EditorBuffer) -> Vec<FoldRange> {
295 Vec::new()
296 }
297}
298
299#[derive(Debug, Clone, PartialEq, Eq)]
301pub struct StyledSegment<'a> {
302 pub range: Range<usize>,
304 pub style: Option<&'a StyleValue>,
306 pub is_selected: bool,
308}
309
310pub fn split_line_intervals<'a>(
312 line_len: usize,
313 spans: &'a [StyleSpan],
314 selection_range: Option<(usize, usize)>,
315) -> Vec<StyledSegment<'a>> {
316 if line_len == 0 {
317 return Vec::new();
318 }
319
320 let mut boundaries = Vec::with_capacity(spans.len() * 2 + 4);
321 boundaries.push(0);
322 boundaries.push(line_len);
323
324 if let Some((s_start, s_end)) = selection_range {
325 boundaries.push(s_start.min(line_len));
326 boundaries.push(s_end.min(line_len));
327 }
328
329 for span in spans {
330 boundaries.push(span.range.start.min(line_len));
331 boundaries.push(span.range.end.min(line_len));
332 }
333
334 boundaries.sort_unstable();
335 boundaries.dedup();
336
337 let mut segments = Vec::with_capacity(boundaries.len());
338
339 for window in boundaries.windows(2) {
340 let start = window[0];
341 let end = window[1];
342 if start >= end {
343 continue;
344 }
345
346 let is_selected = if let Some((s_start, s_end)) = selection_range {
347 start >= s_start && end <= s_end
348 } else {
349 false
350 };
351
352 let style = spans
353 .iter()
354 .rev()
355 .find(|s| s.range.start <= start && end <= s.range.end)
356 .map(|s| &s.style);
357
358 segments.push(StyledSegment {
359 range: start..end,
360 style,
361 is_selected,
362 });
363 }
364
365 segments
366}
367
368pub fn display_width(s: &str) -> usize {
375 use unicode_width::UnicodeWidthStr;
376 s.width()
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq)]
389pub struct DisplayPad {
390 pub display_at: usize,
393 pub fill: char,
395 pub len: usize,
397}
398
399#[derive(Debug, Clone)]
402pub struct ConcealedLine {
403 pub display_text: String,
405 pub spans: Vec<StyleSpan>,
407 byte_map: Vec<usize>,
409}
410
411impl ConcealedLine {
412 pub fn build(line_text: &str, spans: &[StyleSpan]) -> Self {
414 let has_hidden = spans
415 .iter()
416 .any(|s| matches!(s.style, StyleValue::Tag(HighlightTag::Hidden)));
417
418 if !has_hidden {
419 let byte_map = (0..=line_text.len()).collect();
420 return Self {
421 display_text: line_text.to_string(),
422 spans: spans.to_vec(),
423 byte_map,
424 };
425 }
426
427 let mut display_text = String::with_capacity(line_text.len());
428 let mut byte_map = Vec::with_capacity(line_text.len() + 1);
429
430 for (byte_idx, ch) in line_text.char_indices() {
431 let is_hidden = spans.iter().any(|s| {
432 matches!(s.style, StyleValue::Tag(HighlightTag::Hidden))
433 && s.range.contains(&byte_idx)
434 });
435
436 if !is_hidden {
437 let ch_len = ch.len_utf8();
438 for b in 0..ch_len {
439 byte_map.push(byte_idx + b);
440 }
441 display_text.push(ch);
442 }
443 }
444 byte_map.push(line_text.len());
445
446 let mut new_spans = Vec::new();
447 for span in spans {
448 if matches!(span.style, StyleValue::Tag(HighlightTag::Hidden)) {
449 continue;
450 }
451
452 let new_start = byte_map
453 .iter()
454 .position(|&src_idx| src_idx >= span.range.start)
455 .unwrap_or(display_text.len());
456 let new_end = byte_map
457 .iter()
458 .position(|&src_idx| src_idx >= span.range.end)
459 .unwrap_or(display_text.len());
460
461 if new_start < new_end {
462 new_spans.push(StyleSpan {
463 range: new_start..new_end,
464 style: span.style.clone(),
465 });
466 }
467 }
468
469 Self {
470 display_text,
471 spans: new_spans,
472 byte_map,
473 }
474 }
475
476 pub fn expanded(&self, pads: &[DisplayPad]) -> Self {
484 if pads.is_empty() {
485 return self.clone();
486 }
487 let mut sorted: Vec<DisplayPad> = pads.to_vec();
488 sorted.sort_by_key(|p| p.display_at);
489
490 let total_pad: usize = sorted.iter().map(|p| p.len * p.fill.len_utf8()).sum();
491 let mut display_text = String::with_capacity(self.display_text.len() + total_pad);
492 let mut byte_map = Vec::with_capacity(self.byte_map.len() + total_pad);
493
494 let mut consumed = 0;
496
497 for pad in &sorted {
498 if pad.len == 0 {
499 continue;
500 }
501 let mut at = pad.display_at.min(self.display_text.len());
502 while at < self.display_text.len() && !self.display_text.is_char_boundary(at) {
503 at += 1;
504 }
505 if at < consumed {
506 continue;
507 }
508 display_text.push_str(&self.display_text[consumed..at]);
509 byte_map.extend_from_slice(&self.byte_map[consumed..at]);
510 let anchor = self.byte_map[at];
511 let fill: String = std::iter::repeat_n(pad.fill, pad.len).collect();
512 display_text.push_str(&fill);
513 byte_map.extend(std::iter::repeat_n(anchor, fill.len()));
514 consumed = at;
515 }
516 display_text.push_str(&self.display_text[consumed..]);
517 byte_map.extend_from_slice(&self.byte_map[consumed..]);
518
519 let spans = self
522 .spans
523 .iter()
524 .map(|span| {
525 let shift = |b: usize| {
526 let mut out = b;
527 for pad in &sorted {
528 if pad.display_at <= b {
529 out += pad.len * pad.fill.len_utf8();
530 } else {
531 break;
532 }
533 }
534 out
535 };
536 StyleSpan {
537 range: shift(span.range.start)..shift(span.range.end),
538 style: span.style.clone(),
539 }
540 })
541 .collect();
542
543 Self {
544 display_text,
545 spans,
546 byte_map,
547 }
548 }
549
550 pub fn display_to_source(&self, display_col: usize) -> usize {
552 if display_col >= self.byte_map.len() {
553 *self.byte_map.last().unwrap_or(&0)
554 } else {
555 self.byte_map[display_col]
556 }
557 }
558
559 pub fn source_to_display(&self, source_col: usize) -> usize {
561 self.byte_map
562 .partition_point(|&src_idx| src_idx < source_col)
563 .min(self.display_text.len())
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570
571 struct MockHighlighter;
572
573 impl SyntaxHighlighter for MockHighlighter {
574 fn highlight_line(
575 &self,
576 _buffer: &EditorBuffer,
577 _row: usize,
578 line_text: &str,
579 ) -> Vec<StyleSpan> {
580 if line_text.starts_with("# ") {
581 vec![StyleSpan::tag(0..line_text.len(), HighlightTag::Heading(1))]
582 } else {
583 vec![]
584 }
585 }
586 }
587
588 #[test]
589 fn test_syntax_highlighter_trait() {
590 let buffer = EditorBuffer::new("# Title\nBody");
591 let highlighter = MockHighlighter;
592
593 let spans_0 = highlighter.highlight_line(&buffer, 0, "# Title");
594 assert_eq!(spans_0.len(), 1);
595 assert_eq!(spans_0[0].range, 0..7);
596 assert_eq!(spans_0[0].style, StyleValue::Tag(HighlightTag::Heading(1)));
597
598 let spans_1 = highlighter.highlight_line(&buffer, 1, "Body");
599 assert!(spans_1.is_empty());
600 }
601
602 #[test]
603 fn test_rgba_hex_conversion() {
604 let red = Rgba::hex(0xFF0000);
605 assert_eq!(red, Rgba::new(255, 0, 0, 255));
606
607 let custom = Rgba::hex(0x123456);
608 assert_eq!(custom, Rgba::new(0x12, 0x34, 0x56, 255));
609 }
610
611 #[test]
612 fn test_split_line_empty() {
613 let segments = split_line_intervals(0, &[], None);
614 assert!(segments.is_empty());
615 }
616
617 #[test]
618 fn test_split_line_plain_text() {
619 let segments = split_line_intervals(11, &[], None);
620 assert_eq!(segments.len(), 1);
621 assert_eq!(segments[0].range, 0..11);
622 assert_eq!(segments[0].style, None);
623 assert!(!segments[0].is_selected);
624 }
625
626 #[test]
627 fn test_split_line_with_single_span() {
628 let spans = vec![StyleSpan::tag(0..5, HighlightTag::Keyword)];
629 let segments = split_line_intervals(11, &spans, None);
630
631 assert_eq!(segments.len(), 2);
632 assert_eq!(segments[0].range, 0..5);
633 assert_eq!(
634 segments[0].style,
635 Some(&StyleValue::Tag(HighlightTag::Keyword))
636 );
637 assert!(!segments[0].is_selected);
638
639 assert_eq!(segments[1].range, 5..11);
640 assert_eq!(segments[1].style, None);
641 assert!(!segments[1].is_selected);
642 }
643
644 #[test]
645 fn test_split_line_with_overlapping_selection() {
646 let spans = vec![StyleSpan::tag(0..5, HighlightTag::Keyword)];
647 let segments = split_line_intervals(11, &spans, Some((3, 8)));
648
649 assert_eq!(segments.len(), 4);
650
651 assert_eq!(segments[0].range, 0..3);
652 assert_eq!(
653 segments[0].style,
654 Some(&StyleValue::Tag(HighlightTag::Keyword))
655 );
656 assert!(!segments[0].is_selected);
657
658 assert_eq!(segments[1].range, 3..5);
659 assert_eq!(
660 segments[1].style,
661 Some(&StyleValue::Tag(HighlightTag::Keyword))
662 );
663 assert!(segments[1].is_selected);
664
665 assert_eq!(segments[2].range, 5..8);
666 assert_eq!(segments[2].style, None);
667 assert!(segments[2].is_selected);
668
669 assert_eq!(segments[3].range, 8..11);
670 assert_eq!(segments[3].style, None);
671 assert!(!segments[3].is_selected);
672 }
673
674 #[test]
675 fn test_concealed_line_headings_align_and_collapse() {
676 let line1 = "# hello";
677 let spans1 = vec![
678 StyleSpan::tag(0..2, HighlightTag::Hidden),
679 StyleSpan::tag(2..7, HighlightTag::Heading(1)),
680 ];
681 let concealed1 = ConcealedLine::build(line1, &spans1);
682 assert_eq!(concealed1.display_text, "hello");
683 assert_eq!(concealed1.spans.len(), 1);
684 assert_eq!(concealed1.spans[0].range, 0..5);
685 assert_eq!(
686 concealed1.spans[0].style,
687 StyleValue::Tag(HighlightTag::Heading(1))
688 );
689 assert_eq!(concealed1.display_to_source(0), 2);
690 assert_eq!(concealed1.source_to_display(2), 0);
691
692 let line2 = "## hello";
693 let spans2 = vec![
694 StyleSpan::tag(0..3, HighlightTag::Hidden),
695 StyleSpan::tag(3..8, HighlightTag::Heading(2)),
696 ];
697 let concealed2 = ConcealedLine::build(line2, &spans2);
698 assert_eq!(concealed2.display_text, "hello");
699 assert_eq!(concealed2.spans.len(), 1);
700 assert_eq!(concealed2.spans[0].range, 0..5);
701 assert_eq!(
702 concealed2.spans[0].style,
703 StyleValue::Tag(HighlightTag::Heading(2))
704 );
705 assert_eq!(concealed2.display_to_source(0), 3);
706 assert_eq!(concealed2.source_to_display(3), 0);
707
708 assert_eq!(concealed1.display_text, concealed2.display_text);
709
710 let line_inline = "Hi **bold**!";
711 let spans_inline = vec![
712 StyleSpan::tag(3..5, HighlightTag::Hidden),
713 StyleSpan::tag(5..9, HighlightTag::Bold),
714 StyleSpan::tag(9..11, HighlightTag::Hidden),
715 ];
716 let concealed_inline = ConcealedLine::build(line_inline, &spans_inline);
717 assert_eq!(concealed_inline.display_text, "Hi bold!");
718 assert_eq!(concealed_inline.spans.len(), 1);
719 assert_eq!(concealed_inline.spans[0].range, 3..7);
720 assert_eq!(
721 concealed_inline.spans[0].style,
722 StyleValue::Tag(HighlightTag::Bold)
723 );
724 assert_eq!(concealed_inline.display_to_source(3), 5);
725 assert_eq!(concealed_inline.source_to_display(5), 3);
726 }
727
728 #[test]
729 fn test_display_width_columns() {
730 assert_eq!(display_width(""), 0);
731 assert_eq!(display_width("abc |"), 5);
732 assert_eq!(display_width("日本"), 4);
733 assert_eq!(display_width("a日本b"), 6);
734 }
735
736 #[test]
737 fn test_expanded_line_pads_and_maps() {
738 let line = "| a | b |";
741 let spans = vec![
742 StyleSpan::tag(1..3, HighlightTag::Custom("cell")),
743 StyleSpan::tag(4..5, HighlightTag::Punctuation),
744 ];
745 let base = ConcealedLine::build(line, &spans);
746 let padded = base.expanded(&[DisplayPad {
747 display_at: 4,
748 fill: ' ',
749 len: 2,
750 }]);
751 assert_eq!(padded.display_text, "| a | b |");
752 assert!(
755 padded
756 .spans
757 .contains(&StyleSpan::tag(1..3, HighlightTag::Custom("cell")))
758 );
759 assert!(
760 padded
761 .spans
762 .contains(&StyleSpan::tag(6..7, HighlightTag::Punctuation))
763 );
764 assert_eq!(padded.display_to_source(4), 4);
766 assert_eq!(padded.display_to_source(5), 4);
767 assert_eq!(padded.display_to_source(6), 4);
768 assert_eq!(padded.source_to_display(4), 4);
770 assert_eq!(padded.source_to_display(5), 7);
771
772 let same = base.expanded(&[]);
774 assert_eq!(same.display_text, base.display_text);
775 assert_eq!(same.spans, base.spans);
776 }
777
778 #[test]
779 fn test_highlighter_expansion_defaults_are_noops() {
780 let buffer = EditorBuffer::new("hello");
781 let highlighter = MockHighlighter;
782 let concealed = ConcealedLine::build("hello", &[]);
783 assert!(highlighter.expand_line(&buffer, 0, &concealed).is_empty());
784 assert!(highlighter.should_wrap_line(&buffer, 0));
785 }
786}