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 Callout(CalloutKind),
89 HorizontalRule,
91 TaskUnchecked,
93 TaskChecked,
95 Dimmed,
97 Hidden,
99 Custom(&'static str),
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109pub enum CalloutKind {
110 Note,
112 Tip,
114 Warning,
116 Caution,
118 Important,
120 Other,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
126pub struct TextStyle {
127 pub color: Option<Rgba>,
129 pub background: Option<Rgba>,
131 pub bold: bool,
133 pub italic: bool,
135 pub underline: Option<UnderlineDecoration>,
137 pub strikethrough: bool,
139}
140
141impl TextStyle {
142 pub const fn new() -> Self {
144 Self {
145 color: None,
146 background: None,
147 bold: false,
148 italic: false,
149 underline: None,
150 strikethrough: false,
151 }
152 }
153
154 pub const fn color(mut self, color: Rgba) -> Self {
156 self.color = Some(color);
157 self
158 }
159
160 pub const fn background(mut self, background: Rgba) -> Self {
162 self.background = Some(background);
163 self
164 }
165
166 pub const fn bold(mut self) -> Self {
168 self.bold = true;
169 self
170 }
171
172 pub const fn italic(mut self) -> Self {
174 self.italic = true;
175 self
176 }
177
178 pub const fn underline(mut self, underline: UnderlineDecoration) -> Self {
180 self.underline = Some(underline);
181 self
182 }
183
184 pub const fn strikethrough(mut self) -> Self {
186 self.strikethrough = true;
187 self
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
193pub enum StyleValue {
194 Tag(HighlightTag),
196 Direct(TextStyle),
198}
199
200impl From<HighlightTag> for StyleValue {
201 fn from(tag: HighlightTag) -> Self {
202 Self::Tag(tag)
203 }
204}
205
206impl From<TextStyle> for StyleValue {
207 fn from(style: TextStyle) -> Self {
208 Self::Direct(style)
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct StyleSpan {
215 pub range: Range<usize>,
217 pub style: StyleValue,
219}
220
221impl StyleSpan {
222 pub fn new(range: Range<usize>, style: impl Into<StyleValue>) -> Self {
224 Self {
225 range,
226 style: style.into(),
227 }
228 }
229
230 pub fn tag(range: Range<usize>, tag: HighlightTag) -> Self {
232 Self {
233 range,
234 style: StyleValue::Tag(tag),
235 }
236 }
237
238 pub fn direct(range: Range<usize>, style: TextStyle) -> Self {
240 Self {
241 range,
242 style: StyleValue::Direct(style),
243 }
244 }
245}
246
247pub trait SyntaxHighlighter: Send + Sync + 'static {
249 fn highlight_line(&self, buffer: &EditorBuffer, row: usize, line_text: &str) -> Vec<StyleSpan>;
253
254 fn extract_links(
259 &self,
260 _buffer: &EditorBuffer,
261 _row: usize,
262 _line_text: &str,
263 ) -> Vec<(Range<usize>, String)> {
264 Vec::new()
265 }
266
267 fn expand_line(
273 &self,
274 _buffer: &EditorBuffer,
275 _row: usize,
276 _concealed: &ConcealedLine,
277 ) -> Vec<DisplayPad> {
278 Vec::new()
279 }
280
281 fn should_wrap_line(&self, _buffer: &EditorBuffer, _row: usize) -> bool {
286 true
287 }
288}
289
290#[derive(Debug, Clone, PartialEq, Eq)]
292pub struct StyledSegment<'a> {
293 pub range: Range<usize>,
295 pub style: Option<&'a StyleValue>,
297 pub is_selected: bool,
299}
300
301pub fn split_line_intervals<'a>(
303 line_len: usize,
304 spans: &'a [StyleSpan],
305 selection_range: Option<(usize, usize)>,
306) -> Vec<StyledSegment<'a>> {
307 if line_len == 0 {
308 return Vec::new();
309 }
310
311 let mut boundaries = Vec::with_capacity(spans.len() * 2 + 4);
312 boundaries.push(0);
313 boundaries.push(line_len);
314
315 if let Some((s_start, s_end)) = selection_range {
316 boundaries.push(s_start.min(line_len));
317 boundaries.push(s_end.min(line_len));
318 }
319
320 for span in spans {
321 boundaries.push(span.range.start.min(line_len));
322 boundaries.push(span.range.end.min(line_len));
323 }
324
325 boundaries.sort_unstable();
326 boundaries.dedup();
327
328 let mut segments = Vec::with_capacity(boundaries.len());
329
330 for window in boundaries.windows(2) {
331 let start = window[0];
332 let end = window[1];
333 if start >= end {
334 continue;
335 }
336
337 let is_selected = if let Some((s_start, s_end)) = selection_range {
338 start >= s_start && end <= s_end
339 } else {
340 false
341 };
342
343 let style = spans
344 .iter()
345 .rev()
346 .find(|s| s.range.start <= start && end <= s.range.end)
347 .map(|s| &s.style);
348
349 segments.push(StyledSegment {
350 range: start..end,
351 style,
352 is_selected,
353 });
354 }
355
356 segments
357}
358
359pub fn display_width(s: &str) -> usize {
366 use unicode_width::UnicodeWidthStr;
367 s.width()
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
380pub struct DisplayPad {
381 pub display_at: usize,
384 pub fill: char,
386 pub len: usize,
388}
389
390#[derive(Debug, Clone)]
393pub struct ConcealedLine {
394 pub display_text: String,
396 pub spans: Vec<StyleSpan>,
398 byte_map: Vec<usize>,
400}
401
402impl ConcealedLine {
403 pub fn build(line_text: &str, spans: &[StyleSpan]) -> Self {
405 let has_hidden = spans
406 .iter()
407 .any(|s| matches!(s.style, StyleValue::Tag(HighlightTag::Hidden)));
408
409 if !has_hidden {
410 let byte_map = (0..=line_text.len()).collect();
411 return Self {
412 display_text: line_text.to_string(),
413 spans: spans.to_vec(),
414 byte_map,
415 };
416 }
417
418 let mut display_text = String::with_capacity(line_text.len());
419 let mut byte_map = Vec::with_capacity(line_text.len() + 1);
420
421 for (byte_idx, ch) in line_text.char_indices() {
422 let is_hidden = spans.iter().any(|s| {
423 matches!(s.style, StyleValue::Tag(HighlightTag::Hidden))
424 && s.range.contains(&byte_idx)
425 });
426
427 if !is_hidden {
428 let ch_len = ch.len_utf8();
429 for b in 0..ch_len {
430 byte_map.push(byte_idx + b);
431 }
432 display_text.push(ch);
433 }
434 }
435 byte_map.push(line_text.len());
436
437 let mut new_spans = Vec::new();
438 for span in spans {
439 if matches!(span.style, StyleValue::Tag(HighlightTag::Hidden)) {
440 continue;
441 }
442
443 let new_start = byte_map
444 .iter()
445 .position(|&src_idx| src_idx >= span.range.start)
446 .unwrap_or(display_text.len());
447 let new_end = byte_map
448 .iter()
449 .position(|&src_idx| src_idx >= span.range.end)
450 .unwrap_or(display_text.len());
451
452 if new_start < new_end {
453 new_spans.push(StyleSpan {
454 range: new_start..new_end,
455 style: span.style.clone(),
456 });
457 }
458 }
459
460 Self {
461 display_text,
462 spans: new_spans,
463 byte_map,
464 }
465 }
466
467 pub fn expanded(&self, pads: &[DisplayPad]) -> Self {
475 if pads.is_empty() {
476 return self.clone();
477 }
478 let mut sorted: Vec<DisplayPad> = pads.to_vec();
479 sorted.sort_by_key(|p| p.display_at);
480
481 let total_pad: usize = sorted.iter().map(|p| p.len * p.fill.len_utf8()).sum();
482 let mut display_text = String::with_capacity(self.display_text.len() + total_pad);
483 let mut byte_map = Vec::with_capacity(self.byte_map.len() + total_pad);
484
485 let mut consumed = 0;
487
488 for pad in &sorted {
489 if pad.len == 0 {
490 continue;
491 }
492 let mut at = pad.display_at.min(self.display_text.len());
493 while at < self.display_text.len() && !self.display_text.is_char_boundary(at) {
494 at += 1;
495 }
496 if at < consumed {
497 continue;
498 }
499 display_text.push_str(&self.display_text[consumed..at]);
500 byte_map.extend_from_slice(&self.byte_map[consumed..at]);
501 let anchor = self.byte_map[at];
502 let fill: String = std::iter::repeat_n(pad.fill, pad.len).collect();
503 display_text.push_str(&fill);
504 byte_map.extend(std::iter::repeat_n(anchor, fill.len()));
505 consumed = at;
506 }
507 display_text.push_str(&self.display_text[consumed..]);
508 byte_map.extend_from_slice(&self.byte_map[consumed..]);
509
510 let spans = self
513 .spans
514 .iter()
515 .map(|span| {
516 let shift = |b: usize| {
517 let mut out = b;
518 for pad in &sorted {
519 if pad.display_at <= b {
520 out += pad.len * pad.fill.len_utf8();
521 } else {
522 break;
523 }
524 }
525 out
526 };
527 StyleSpan {
528 range: shift(span.range.start)..shift(span.range.end),
529 style: span.style.clone(),
530 }
531 })
532 .collect();
533
534 Self {
535 display_text,
536 spans,
537 byte_map,
538 }
539 }
540
541 pub fn display_to_source(&self, display_col: usize) -> usize {
543 if display_col >= self.byte_map.len() {
544 *self.byte_map.last().unwrap_or(&0)
545 } else {
546 self.byte_map[display_col]
547 }
548 }
549
550 pub fn source_to_display(&self, source_col: usize) -> usize {
552 self.byte_map
553 .partition_point(|&src_idx| src_idx < source_col)
554 .min(self.display_text.len())
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 struct MockHighlighter;
563
564 impl SyntaxHighlighter for MockHighlighter {
565 fn highlight_line(
566 &self,
567 _buffer: &EditorBuffer,
568 _row: usize,
569 line_text: &str,
570 ) -> Vec<StyleSpan> {
571 if line_text.starts_with("# ") {
572 vec![StyleSpan::tag(0..line_text.len(), HighlightTag::Heading(1))]
573 } else {
574 vec![]
575 }
576 }
577 }
578
579 #[test]
580 fn test_syntax_highlighter_trait() {
581 let buffer = EditorBuffer::new("# Title\nBody");
582 let highlighter = MockHighlighter;
583
584 let spans_0 = highlighter.highlight_line(&buffer, 0, "# Title");
585 assert_eq!(spans_0.len(), 1);
586 assert_eq!(spans_0[0].range, 0..7);
587 assert_eq!(spans_0[0].style, StyleValue::Tag(HighlightTag::Heading(1)));
588
589 let spans_1 = highlighter.highlight_line(&buffer, 1, "Body");
590 assert!(spans_1.is_empty());
591 }
592
593 #[test]
594 fn test_rgba_hex_conversion() {
595 let red = Rgba::hex(0xFF0000);
596 assert_eq!(red, Rgba::new(255, 0, 0, 255));
597
598 let custom = Rgba::hex(0x123456);
599 assert_eq!(custom, Rgba::new(0x12, 0x34, 0x56, 255));
600 }
601
602 #[test]
603 fn test_split_line_empty() {
604 let segments = split_line_intervals(0, &[], None);
605 assert!(segments.is_empty());
606 }
607
608 #[test]
609 fn test_split_line_plain_text() {
610 let segments = split_line_intervals(11, &[], None);
611 assert_eq!(segments.len(), 1);
612 assert_eq!(segments[0].range, 0..11);
613 assert_eq!(segments[0].style, None);
614 assert!(!segments[0].is_selected);
615 }
616
617 #[test]
618 fn test_split_line_with_single_span() {
619 let spans = vec![StyleSpan::tag(0..5, HighlightTag::Keyword)];
620 let segments = split_line_intervals(11, &spans, None);
621
622 assert_eq!(segments.len(), 2);
623 assert_eq!(segments[0].range, 0..5);
624 assert_eq!(
625 segments[0].style,
626 Some(&StyleValue::Tag(HighlightTag::Keyword))
627 );
628 assert!(!segments[0].is_selected);
629
630 assert_eq!(segments[1].range, 5..11);
631 assert_eq!(segments[1].style, None);
632 assert!(!segments[1].is_selected);
633 }
634
635 #[test]
636 fn test_split_line_with_overlapping_selection() {
637 let spans = vec![StyleSpan::tag(0..5, HighlightTag::Keyword)];
638 let segments = split_line_intervals(11, &spans, Some((3, 8)));
639
640 assert_eq!(segments.len(), 4);
641
642 assert_eq!(segments[0].range, 0..3);
643 assert_eq!(
644 segments[0].style,
645 Some(&StyleValue::Tag(HighlightTag::Keyword))
646 );
647 assert!(!segments[0].is_selected);
648
649 assert_eq!(segments[1].range, 3..5);
650 assert_eq!(
651 segments[1].style,
652 Some(&StyleValue::Tag(HighlightTag::Keyword))
653 );
654 assert!(segments[1].is_selected);
655
656 assert_eq!(segments[2].range, 5..8);
657 assert_eq!(segments[2].style, None);
658 assert!(segments[2].is_selected);
659
660 assert_eq!(segments[3].range, 8..11);
661 assert_eq!(segments[3].style, None);
662 assert!(!segments[3].is_selected);
663 }
664
665 #[test]
666 fn test_concealed_line_headings_align_and_collapse() {
667 let line1 = "# hello";
668 let spans1 = vec![
669 StyleSpan::tag(0..2, HighlightTag::Hidden),
670 StyleSpan::tag(2..7, HighlightTag::Heading(1)),
671 ];
672 let concealed1 = ConcealedLine::build(line1, &spans1);
673 assert_eq!(concealed1.display_text, "hello");
674 assert_eq!(concealed1.spans.len(), 1);
675 assert_eq!(concealed1.spans[0].range, 0..5);
676 assert_eq!(
677 concealed1.spans[0].style,
678 StyleValue::Tag(HighlightTag::Heading(1))
679 );
680 assert_eq!(concealed1.display_to_source(0), 2);
681 assert_eq!(concealed1.source_to_display(2), 0);
682
683 let line2 = "## hello";
684 let spans2 = vec![
685 StyleSpan::tag(0..3, HighlightTag::Hidden),
686 StyleSpan::tag(3..8, HighlightTag::Heading(2)),
687 ];
688 let concealed2 = ConcealedLine::build(line2, &spans2);
689 assert_eq!(concealed2.display_text, "hello");
690 assert_eq!(concealed2.spans.len(), 1);
691 assert_eq!(concealed2.spans[0].range, 0..5);
692 assert_eq!(
693 concealed2.spans[0].style,
694 StyleValue::Tag(HighlightTag::Heading(2))
695 );
696 assert_eq!(concealed2.display_to_source(0), 3);
697 assert_eq!(concealed2.source_to_display(3), 0);
698
699 assert_eq!(concealed1.display_text, concealed2.display_text);
700
701 let line_inline = "Hi **bold**!";
702 let spans_inline = vec![
703 StyleSpan::tag(3..5, HighlightTag::Hidden),
704 StyleSpan::tag(5..9, HighlightTag::Bold),
705 StyleSpan::tag(9..11, HighlightTag::Hidden),
706 ];
707 let concealed_inline = ConcealedLine::build(line_inline, &spans_inline);
708 assert_eq!(concealed_inline.display_text, "Hi bold!");
709 assert_eq!(concealed_inline.spans.len(), 1);
710 assert_eq!(concealed_inline.spans[0].range, 3..7);
711 assert_eq!(
712 concealed_inline.spans[0].style,
713 StyleValue::Tag(HighlightTag::Bold)
714 );
715 assert_eq!(concealed_inline.display_to_source(3), 5);
716 assert_eq!(concealed_inline.source_to_display(5), 3);
717 }
718
719 #[test]
720 fn test_display_width_columns() {
721 assert_eq!(display_width(""), 0);
722 assert_eq!(display_width("abc |"), 5);
723 assert_eq!(display_width("日本"), 4);
724 assert_eq!(display_width("a日本b"), 6);
725 }
726
727 #[test]
728 fn test_expanded_line_pads_and_maps() {
729 let line = "| a | b |";
732 let spans = vec![
733 StyleSpan::tag(1..3, HighlightTag::Custom("cell")),
734 StyleSpan::tag(4..5, HighlightTag::Punctuation),
735 ];
736 let base = ConcealedLine::build(line, &spans);
737 let padded = base.expanded(&[DisplayPad {
738 display_at: 4,
739 fill: ' ',
740 len: 2,
741 }]);
742 assert_eq!(padded.display_text, "| a | b |");
743 assert!(
746 padded
747 .spans
748 .contains(&StyleSpan::tag(1..3, HighlightTag::Custom("cell")))
749 );
750 assert!(
751 padded
752 .spans
753 .contains(&StyleSpan::tag(6..7, HighlightTag::Punctuation))
754 );
755 assert_eq!(padded.display_to_source(4), 4);
757 assert_eq!(padded.display_to_source(5), 4);
758 assert_eq!(padded.display_to_source(6), 4);
759 assert_eq!(padded.source_to_display(4), 4);
761 assert_eq!(padded.source_to_display(5), 7);
762
763 let same = base.expanded(&[]);
765 assert_eq!(same.display_text, base.display_text);
766 assert_eq!(same.spans, base.spans);
767 }
768
769 #[test]
770 fn test_highlighter_expansion_defaults_are_noops() {
771 let buffer = EditorBuffer::new("hello");
772 let highlighter = MockHighlighter;
773 let concealed = ConcealedLine::build("hello", &[]);
774 assert!(highlighter.expand_line(&buffer, 0, &concealed).is_empty());
775 assert!(highlighter.should_wrap_line(&buffer, 0));
776 }
777}