1use std::ops::Range;
2
3use crate::EditorBuffer;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub struct Rgba {
8 pub r: u8,
10 pub g: u8,
12 pub b: u8,
14 pub a: u8,
16}
17
18impl Rgba {
19 pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
21 Self { r, g, b, a }
22 }
23
24 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
26 Self { r, g, b, a: 255 }
27 }
28
29 pub const fn hex(hex: u32) -> Self {
31 let r = ((hex >> 16) & 0xFF) as u8;
32 let g = ((hex >> 8) & 0xFF) as u8;
33 let b = (hex & 0xFF) as u8;
34 Self { r, g, b, a: 255 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum UnderlineDecoration {
41 Solid,
43 Wavy,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
54pub enum HighlightTag {
55 Keyword,
57 Function,
59 Type,
61 String,
63 Number,
65 Comment,
67 Operator,
69 Punctuation,
71 Heading(u8),
74 Bold,
76 Italic,
78 Highlight,
80 Code,
82 Link,
84 Blockquote,
86 HorizontalRule,
88 TaskUnchecked,
90 TaskChecked,
92 Dimmed,
94 Hidden,
96 Custom(&'static str),
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
105pub struct TextStyle {
106 pub color: Option<Rgba>,
108 pub background: Option<Rgba>,
110 pub bold: bool,
112 pub italic: bool,
114 pub underline: Option<UnderlineDecoration>,
116 pub strikethrough: bool,
118}
119
120impl TextStyle {
121 pub const fn new() -> Self {
123 Self {
124 color: None,
125 background: None,
126 bold: false,
127 italic: false,
128 underline: None,
129 strikethrough: false,
130 }
131 }
132
133 pub const fn color(mut self, color: Rgba) -> Self {
135 self.color = Some(color);
136 self
137 }
138
139 pub const fn background(mut self, background: Rgba) -> Self {
141 self.background = Some(background);
142 self
143 }
144
145 pub const fn bold(mut self) -> Self {
147 self.bold = true;
148 self
149 }
150
151 pub const fn italic(mut self) -> Self {
153 self.italic = true;
154 self
155 }
156
157 pub const fn underline(mut self, underline: UnderlineDecoration) -> Self {
159 self.underline = Some(underline);
160 self
161 }
162
163 pub const fn strikethrough(mut self) -> Self {
165 self.strikethrough = true;
166 self
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
172pub enum StyleValue {
173 Tag(HighlightTag),
175 Direct(TextStyle),
177}
178
179impl From<HighlightTag> for StyleValue {
180 fn from(tag: HighlightTag) -> Self {
181 Self::Tag(tag)
182 }
183}
184
185impl From<TextStyle> for StyleValue {
186 fn from(style: TextStyle) -> Self {
187 Self::Direct(style)
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct StyleSpan {
194 pub range: Range<usize>,
196 pub style: StyleValue,
198}
199
200impl StyleSpan {
201 pub fn new(range: Range<usize>, style: impl Into<StyleValue>) -> Self {
203 Self {
204 range,
205 style: style.into(),
206 }
207 }
208
209 pub fn tag(range: Range<usize>, tag: HighlightTag) -> Self {
211 Self {
212 range,
213 style: StyleValue::Tag(tag),
214 }
215 }
216
217 pub fn direct(range: Range<usize>, style: TextStyle) -> Self {
219 Self {
220 range,
221 style: StyleValue::Direct(style),
222 }
223 }
224}
225
226pub trait SyntaxHighlighter: Send + Sync + 'static {
228 fn highlight_line(&self, buffer: &EditorBuffer, row: usize, line_text: &str) -> Vec<StyleSpan>;
232
233 fn extract_links(
238 &self,
239 _buffer: &EditorBuffer,
240 _row: usize,
241 _line_text: &str,
242 ) -> Vec<(Range<usize>, String)> {
243 Vec::new()
244 }
245
246 fn expand_line(
252 &self,
253 _buffer: &EditorBuffer,
254 _row: usize,
255 _concealed: &ConcealedLine,
256 ) -> Vec<DisplayPad> {
257 Vec::new()
258 }
259
260 fn should_wrap_line(&self, _buffer: &EditorBuffer, _row: usize) -> bool {
265 true
266 }
267}
268
269#[derive(Debug, Clone, PartialEq, Eq)]
271pub struct StyledSegment<'a> {
272 pub range: Range<usize>,
274 pub style: Option<&'a StyleValue>,
276 pub is_selected: bool,
278}
279
280pub fn split_line_intervals<'a>(
282 line_len: usize,
283 spans: &'a [StyleSpan],
284 selection_range: Option<(usize, usize)>,
285) -> Vec<StyledSegment<'a>> {
286 if line_len == 0 {
287 return Vec::new();
288 }
289
290 let mut boundaries = Vec::with_capacity(spans.len() * 2 + 4);
291 boundaries.push(0);
292 boundaries.push(line_len);
293
294 if let Some((s_start, s_end)) = selection_range {
295 boundaries.push(s_start.min(line_len));
296 boundaries.push(s_end.min(line_len));
297 }
298
299 for span in spans {
300 boundaries.push(span.range.start.min(line_len));
301 boundaries.push(span.range.end.min(line_len));
302 }
303
304 boundaries.sort_unstable();
305 boundaries.dedup();
306
307 let mut segments = Vec::with_capacity(boundaries.len());
308
309 for window in boundaries.windows(2) {
310 let start = window[0];
311 let end = window[1];
312 if start >= end {
313 continue;
314 }
315
316 let is_selected = if let Some((s_start, s_end)) = selection_range {
317 start >= s_start && end <= s_end
318 } else {
319 false
320 };
321
322 let style = spans
323 .iter()
324 .rev()
325 .find(|s| s.range.start <= start && end <= s.range.end)
326 .map(|s| &s.style);
327
328 segments.push(StyledSegment {
329 range: start..end,
330 style,
331 is_selected,
332 });
333 }
334
335 segments
336}
337
338pub fn display_width(s: &str) -> usize {
345 use unicode_width::UnicodeWidthStr;
346 s.width()
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359pub struct DisplayPad {
360 pub display_at: usize,
363 pub fill: char,
365 pub len: usize,
367}
368
369#[derive(Debug, Clone)]
372pub struct ConcealedLine {
373 pub display_text: String,
375 pub spans: Vec<StyleSpan>,
377 byte_map: Vec<usize>,
379}
380
381impl ConcealedLine {
382 pub fn build(line_text: &str, spans: &[StyleSpan]) -> Self {
384 let has_hidden = spans
385 .iter()
386 .any(|s| matches!(s.style, StyleValue::Tag(HighlightTag::Hidden)));
387
388 if !has_hidden {
389 let byte_map = (0..=line_text.len()).collect();
390 return Self {
391 display_text: line_text.to_string(),
392 spans: spans.to_vec(),
393 byte_map,
394 };
395 }
396
397 let mut display_text = String::with_capacity(line_text.len());
398 let mut byte_map = Vec::with_capacity(line_text.len() + 1);
399
400 for (byte_idx, ch) in line_text.char_indices() {
401 let is_hidden = spans.iter().any(|s| {
402 matches!(s.style, StyleValue::Tag(HighlightTag::Hidden))
403 && s.range.contains(&byte_idx)
404 });
405
406 if !is_hidden {
407 let ch_len = ch.len_utf8();
408 for b in 0..ch_len {
409 byte_map.push(byte_idx + b);
410 }
411 display_text.push(ch);
412 }
413 }
414 byte_map.push(line_text.len());
415
416 let mut new_spans = Vec::new();
417 for span in spans {
418 if matches!(span.style, StyleValue::Tag(HighlightTag::Hidden)) {
419 continue;
420 }
421
422 let new_start = byte_map
423 .iter()
424 .position(|&src_idx| src_idx >= span.range.start)
425 .unwrap_or(display_text.len());
426 let new_end = byte_map
427 .iter()
428 .position(|&src_idx| src_idx >= span.range.end)
429 .unwrap_or(display_text.len());
430
431 if new_start < new_end {
432 new_spans.push(StyleSpan {
433 range: new_start..new_end,
434 style: span.style.clone(),
435 });
436 }
437 }
438
439 Self {
440 display_text,
441 spans: new_spans,
442 byte_map,
443 }
444 }
445
446 pub fn expanded(&self, pads: &[DisplayPad]) -> Self {
454 if pads.is_empty() {
455 return self.clone();
456 }
457 let mut sorted: Vec<DisplayPad> = pads.to_vec();
458 sorted.sort_by_key(|p| p.display_at);
459
460 let total_pad: usize = sorted.iter().map(|p| p.len * p.fill.len_utf8()).sum();
461 let mut display_text = String::with_capacity(self.display_text.len() + total_pad);
462 let mut byte_map = Vec::with_capacity(self.byte_map.len() + total_pad);
463
464 let mut consumed = 0;
466
467 for pad in &sorted {
468 if pad.len == 0 {
469 continue;
470 }
471 let mut at = pad.display_at.min(self.display_text.len());
472 while at < self.display_text.len() && !self.display_text.is_char_boundary(at) {
473 at += 1;
474 }
475 if at < consumed {
476 continue;
477 }
478 display_text.push_str(&self.display_text[consumed..at]);
479 byte_map.extend_from_slice(&self.byte_map[consumed..at]);
480 let anchor = self.byte_map[at];
481 let fill: String = std::iter::repeat_n(pad.fill, pad.len).collect();
482 display_text.push_str(&fill);
483 byte_map.extend(std::iter::repeat_n(anchor, fill.len()));
484 consumed = at;
485 }
486 display_text.push_str(&self.display_text[consumed..]);
487 byte_map.extend_from_slice(&self.byte_map[consumed..]);
488
489 let spans = self
492 .spans
493 .iter()
494 .map(|span| {
495 let shift = |b: usize| {
496 let mut out = b;
497 for pad in &sorted {
498 if pad.display_at <= b {
499 out += pad.len * pad.fill.len_utf8();
500 } else {
501 break;
502 }
503 }
504 out
505 };
506 StyleSpan {
507 range: shift(span.range.start)..shift(span.range.end),
508 style: span.style.clone(),
509 }
510 })
511 .collect();
512
513 Self {
514 display_text,
515 spans,
516 byte_map,
517 }
518 }
519
520 pub fn display_to_source(&self, display_col: usize) -> usize {
522 if display_col >= self.byte_map.len() {
523 *self.byte_map.last().unwrap_or(&0)
524 } else {
525 self.byte_map[display_col]
526 }
527 }
528
529 pub fn source_to_display(&self, source_col: usize) -> usize {
531 self.byte_map
532 .partition_point(|&src_idx| src_idx < source_col)
533 .min(self.display_text.len())
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540
541 struct MockHighlighter;
542
543 impl SyntaxHighlighter for MockHighlighter {
544 fn highlight_line(
545 &self,
546 _buffer: &EditorBuffer,
547 _row: usize,
548 line_text: &str,
549 ) -> Vec<StyleSpan> {
550 if line_text.starts_with("# ") {
551 vec![StyleSpan::tag(0..line_text.len(), HighlightTag::Heading(1))]
552 } else {
553 vec![]
554 }
555 }
556 }
557
558 #[test]
559 fn test_syntax_highlighter_trait() {
560 let buffer = EditorBuffer::new("# Title\nBody");
561 let highlighter = MockHighlighter;
562
563 let spans_0 = highlighter.highlight_line(&buffer, 0, "# Title");
564 assert_eq!(spans_0.len(), 1);
565 assert_eq!(spans_0[0].range, 0..7);
566 assert_eq!(spans_0[0].style, StyleValue::Tag(HighlightTag::Heading(1)));
567
568 let spans_1 = highlighter.highlight_line(&buffer, 1, "Body");
569 assert!(spans_1.is_empty());
570 }
571
572 #[test]
573 fn test_rgba_hex_conversion() {
574 let red = Rgba::hex(0xFF0000);
575 assert_eq!(red, Rgba::new(255, 0, 0, 255));
576
577 let custom = Rgba::hex(0x123456);
578 assert_eq!(custom, Rgba::new(0x12, 0x34, 0x56, 255));
579 }
580
581 #[test]
582 fn test_split_line_empty() {
583 let segments = split_line_intervals(0, &[], None);
584 assert!(segments.is_empty());
585 }
586
587 #[test]
588 fn test_split_line_plain_text() {
589 let segments = split_line_intervals(11, &[], None);
590 assert_eq!(segments.len(), 1);
591 assert_eq!(segments[0].range, 0..11);
592 assert_eq!(segments[0].style, None);
593 assert!(!segments[0].is_selected);
594 }
595
596 #[test]
597 fn test_split_line_with_single_span() {
598 let spans = vec![StyleSpan::tag(0..5, HighlightTag::Keyword)];
599 let segments = split_line_intervals(11, &spans, None);
600
601 assert_eq!(segments.len(), 2);
602 assert_eq!(segments[0].range, 0..5);
603 assert_eq!(
604 segments[0].style,
605 Some(&StyleValue::Tag(HighlightTag::Keyword))
606 );
607 assert!(!segments[0].is_selected);
608
609 assert_eq!(segments[1].range, 5..11);
610 assert_eq!(segments[1].style, None);
611 assert!(!segments[1].is_selected);
612 }
613
614 #[test]
615 fn test_split_line_with_overlapping_selection() {
616 let spans = vec![StyleSpan::tag(0..5, HighlightTag::Keyword)];
617 let segments = split_line_intervals(11, &spans, Some((3, 8)));
618
619 assert_eq!(segments.len(), 4);
620
621 assert_eq!(segments[0].range, 0..3);
622 assert_eq!(
623 segments[0].style,
624 Some(&StyleValue::Tag(HighlightTag::Keyword))
625 );
626 assert!(!segments[0].is_selected);
627
628 assert_eq!(segments[1].range, 3..5);
629 assert_eq!(
630 segments[1].style,
631 Some(&StyleValue::Tag(HighlightTag::Keyword))
632 );
633 assert!(segments[1].is_selected);
634
635 assert_eq!(segments[2].range, 5..8);
636 assert_eq!(segments[2].style, None);
637 assert!(segments[2].is_selected);
638
639 assert_eq!(segments[3].range, 8..11);
640 assert_eq!(segments[3].style, None);
641 assert!(!segments[3].is_selected);
642 }
643
644 #[test]
645 fn test_concealed_line_headings_align_and_collapse() {
646 let line1 = "# hello";
647 let spans1 = vec![
648 StyleSpan::tag(0..2, HighlightTag::Hidden),
649 StyleSpan::tag(2..7, HighlightTag::Heading(1)),
650 ];
651 let concealed1 = ConcealedLine::build(line1, &spans1);
652 assert_eq!(concealed1.display_text, "hello");
653 assert_eq!(concealed1.spans.len(), 1);
654 assert_eq!(concealed1.spans[0].range, 0..5);
655 assert_eq!(
656 concealed1.spans[0].style,
657 StyleValue::Tag(HighlightTag::Heading(1))
658 );
659 assert_eq!(concealed1.display_to_source(0), 2);
660 assert_eq!(concealed1.source_to_display(2), 0);
661
662 let line2 = "## hello";
663 let spans2 = vec![
664 StyleSpan::tag(0..3, HighlightTag::Hidden),
665 StyleSpan::tag(3..8, HighlightTag::Heading(2)),
666 ];
667 let concealed2 = ConcealedLine::build(line2, &spans2);
668 assert_eq!(concealed2.display_text, "hello");
669 assert_eq!(concealed2.spans.len(), 1);
670 assert_eq!(concealed2.spans[0].range, 0..5);
671 assert_eq!(
672 concealed2.spans[0].style,
673 StyleValue::Tag(HighlightTag::Heading(2))
674 );
675 assert_eq!(concealed2.display_to_source(0), 3);
676 assert_eq!(concealed2.source_to_display(3), 0);
677
678 assert_eq!(concealed1.display_text, concealed2.display_text);
679
680 let line_inline = "Hi **bold**!";
681 let spans_inline = vec![
682 StyleSpan::tag(3..5, HighlightTag::Hidden),
683 StyleSpan::tag(5..9, HighlightTag::Bold),
684 StyleSpan::tag(9..11, HighlightTag::Hidden),
685 ];
686 let concealed_inline = ConcealedLine::build(line_inline, &spans_inline);
687 assert_eq!(concealed_inline.display_text, "Hi bold!");
688 assert_eq!(concealed_inline.spans.len(), 1);
689 assert_eq!(concealed_inline.spans[0].range, 3..7);
690 assert_eq!(
691 concealed_inline.spans[0].style,
692 StyleValue::Tag(HighlightTag::Bold)
693 );
694 assert_eq!(concealed_inline.display_to_source(3), 5);
695 assert_eq!(concealed_inline.source_to_display(5), 3);
696 }
697
698 #[test]
699 fn test_display_width_columns() {
700 assert_eq!(display_width(""), 0);
701 assert_eq!(display_width("abc |"), 5);
702 assert_eq!(display_width("日本"), 4);
703 assert_eq!(display_width("a日本b"), 6);
704 }
705
706 #[test]
707 fn test_expanded_line_pads_and_maps() {
708 let line = "| a | b |";
711 let spans = vec![
712 StyleSpan::tag(1..3, HighlightTag::Custom("cell")),
713 StyleSpan::tag(4..5, HighlightTag::Punctuation),
714 ];
715 let base = ConcealedLine::build(line, &spans);
716 let padded = base.expanded(&[DisplayPad {
717 display_at: 4,
718 fill: ' ',
719 len: 2,
720 }]);
721 assert_eq!(padded.display_text, "| a | b |");
722 assert!(
725 padded
726 .spans
727 .contains(&StyleSpan::tag(1..3, HighlightTag::Custom("cell")))
728 );
729 assert!(
730 padded
731 .spans
732 .contains(&StyleSpan::tag(6..7, HighlightTag::Punctuation))
733 );
734 assert_eq!(padded.display_to_source(4), 4);
736 assert_eq!(padded.display_to_source(5), 4);
737 assert_eq!(padded.display_to_source(6), 4);
738 assert_eq!(padded.source_to_display(4), 4);
740 assert_eq!(padded.source_to_display(5), 7);
741
742 let same = base.expanded(&[]);
744 assert_eq!(same.display_text, base.display_text);
745 assert_eq!(same.spans, base.spans);
746 }
747
748 #[test]
749 fn test_highlighter_expansion_defaults_are_noops() {
750 let buffer = EditorBuffer::new("hello");
751 let highlighter = MockHighlighter;
752 let concealed = ConcealedLine::build("hello", &[]);
753 assert!(highlighter.expand_line(&buffer, 0, &concealed).is_empty());
754 assert!(highlighter.should_wrap_line(&buffer, 0));
755 }
756}