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