1use ratatui::style::{Color, Modifier, Style};
28use ratatui::text::Span;
29
30pub fn display_line_to_source_spans(
53 line: &lattice_host::display_matrix::DisplayLine,
54 resolved: &lattice_host::ui::theme::ResolvedTheme,
55 ids: &lattice_host::ui::theme::BuiltinElementIds,
56) -> Vec<Span<'static>> {
57 use lattice_cells::cell_flags;
58 use lattice_host::ui::theme::resolve_syntax_style;
59 let default_fg = resolve_syntax_style(resolved, ids, lattice_syntax::Style::Default)
63 .fg
64 .map(|c| c.to_rgb_u32(0))
65 .unwrap_or(0);
66 let trailing_fg = resolved
67 .get(ids.whitespace_trailing)
68 .fg
69 .map(|c| c.to_rgb_u32(default_fg))
70 .unwrap_or(default_fg);
71
72 let mut spans: Vec<Span<'static>> = Vec::new();
73 let mut current_text = String::new();
74 let mut current_style: Option<Style> = None;
75 let mut byte_off = 0usize;
76 for run in line.runs.iter() {
77 let end = byte_off + run.len as usize;
78 let slice = &line.text[byte_off..end];
79 byte_off = end;
80 if run.flags & cell_flags::INLAY != 0 {
82 continue;
83 }
84 let style = display_run_to_style(run, resolved, ids, trailing_fg);
85 if current_style == Some(style) {
86 current_text.push_str(slice);
87 } else {
88 if !current_text.is_empty() {
89 spans.push(Span::styled(
90 std::mem::take(&mut current_text),
91 current_style.unwrap_or_default(),
92 ));
93 }
94 current_text.push_str(slice);
95 current_style = Some(style);
96 }
97 }
98 if !current_text.is_empty() {
99 spans.push(Span::styled(
100 current_text,
101 current_style.unwrap_or_default(),
102 ));
103 }
104 spans
105}
106
107fn display_run_to_style(
113 run: &lattice_host::display_matrix::DisplayRun,
114 resolved: &lattice_host::ui::theme::ResolvedTheme,
115 ids: &lattice_host::ui::theme::BuiltinElementIds,
116 trailing_fg: u32,
117) -> Style {
118 use lattice_cells::cell_flags;
119 let is_ws_marker = run.flags & cell_flags::WS_MARKER != 0;
120 let is_trailing = run.flags & cell_flags::WS_TRAILING != 0;
121 let host = lattice_host::ui::theme::resolve_syntax_style(resolved, ids, run.style);
122 let style_fg = host.fg.map(|c| c.to_rgb_u32(0)).unwrap_or(0);
125 let fg = if is_ws_marker && is_trailing {
126 trailing_fg
127 } else {
128 style_fg
129 };
130 let mut style = Style::default();
131 if fg != 0 {
132 style = style.fg(rgb_u32_to_color(fg));
133 }
134 if let Some(kind) = run.refine {
141 let element = match kind {
142 lattice_cells::RefineKind::Added => ids.diff_add_refine_bg,
143 lattice_cells::RefineKind::Removed => ids.diff_remove_refine_bg,
144 };
145 if let Some(bg) = resolved.get(element).bg {
146 style = style.bg(rgb_u32_to_color(bg.to_rgb_u32(0)));
147 }
148 }
149 let m = &host.modifiers;
150 let mut mods = Modifier::empty();
151 if m.bold {
152 mods |= Modifier::BOLD;
153 }
154 if m.italic {
155 mods |= Modifier::ITALIC;
156 }
157 if m.underline {
158 mods |= Modifier::UNDERLINED;
159 }
160 if m.dim {
161 mods |= Modifier::DIM;
162 }
163 if m.reverse {
164 mods |= Modifier::REVERSED;
165 }
166 if !mods.is_empty() {
167 style = style.add_modifier(mods);
168 }
169 style
170}
171
172pub(crate) fn rgb_u32_to_color(rgb: u32) -> Color {
176 let r = ((rgb >> 16) & 0xff) as u8;
177 let g = ((rgb >> 8) & 0xff) as u8;
178 let b = (rgb & 0xff) as u8;
179 Color::Rgb(r, g, b)
180}
181
182#[cfg(test)]
183mod tests {
184 #![allow(clippy::unwrap_used)]
185 use super::*;
186 use lattice_cells::cell_flags;
187
188 fn body(parts: &[(&str, u32)]) -> Vec<Span<'static>> {
196 parts
197 .iter()
198 .map(|(text, fg)| {
199 let mut style = Style::default();
200 if *fg != 0 {
201 style = style.fg(rgb_u32_to_color(*fg));
202 }
203 Span::styled(text.to_string(), style)
204 })
205 .collect()
206 }
207
208 #[test]
224 fn a_refined_run_paints_the_refine_background() {
225 let (resolved, ids) = defaults();
226 let plain = DisplayRun {
227 len: 3,
228 style: lattice_syntax::Style::Default,
229 flags: 0,
230 refine: None,
231 };
232 let refined = DisplayRun {
233 refine: Some(lattice_cells::RefineKind::Removed),
234 ..plain.clone()
235 };
236
237 let plain_style = display_run_to_style(&plain, &resolved, &ids, 0);
238 let refined_style = display_run_to_style(&refined, &resolved, &ids, 0);
239
240 assert!(
241 plain_style.bg.is_none(),
242 "an unrefined run sets no background of its own"
243 );
244 assert!(
245 refined_style.bg.is_some(),
246 "a refined run must paint a background — the theme element \
247 resolves, and this is the assertion the rest of the DR \
248 suite could not make"
249 );
250 assert_eq!(
251 refined_style.fg, plain_style.fg,
252 "refinement is a background axis; the syntax fg survives"
253 );
254 }
255
256 #[test]
260 fn the_two_refine_sides_paint_different_backgrounds() {
261 let (resolved, ids) = defaults();
262 let run = |kind| DisplayRun {
263 len: 3,
264 style: lattice_syntax::Style::Default,
265 flags: 0,
266 refine: Some(kind),
267 };
268 let added =
269 display_run_to_style(&run(lattice_cells::RefineKind::Added), &resolved, &ids, 0);
270 let removed =
271 display_run_to_style(&run(lattice_cells::RefineKind::Removed), &resolved, &ids, 0);
272 assert!(added.bg.is_some() && removed.bg.is_some());
273 assert_ne!(
274 added.bg, removed.bg,
275 "added and removed refinement must be distinguishable"
276 );
277 }
278
279 use lattice_host::display_matrix::{DisplayLine, DisplayRun};
288 use lattice_host::ui::theme::{
289 BuiltinElementIds, InMemoryThemeRegistry, ResolvedTheme, ThemeRegistry as _,
290 resolve_syntax_style,
291 };
292
293 fn defaults() -> (std::sync::Arc<ResolvedTheme>, BuiltinElementIds) {
298 let reg = InMemoryThemeRegistry::with_defaults();
299 let resolved = reg.resolved();
300 let ids = BuiltinElementIds::capture(®);
301 (resolved, ids)
302 }
303
304 fn dline(specs: &[(&str, lattice_syntax::Style, u16)]) -> DisplayLine {
306 let mut text = String::new();
307 let mut runs = Vec::new();
308 for (s, style, flags) in specs {
309 runs.push(DisplayRun {
310 len: s.len() as u32,
311 style: *style,
312 flags: *flags,
313 refine: None,
314 });
315 text.push_str(s);
316 }
317 let col_count = text.chars().count() as u32;
318 DisplayLine {
319 source_line: 0,
320 text: std::sync::Arc::from(text.as_str()),
321 runs: std::sync::Arc::from(runs.into_boxed_slice()),
322 col_map: std::sync::Arc::from([] as [(u32, u32); 0]),
323 conceals: std::sync::Arc::from([] as [lattice_cells::ConcealRange; 0]),
324 col_count,
325 fold: None,
326 }
327 }
328
329 fn expect_color(rgb: u32) -> Option<Color> {
332 if rgb == 0 {
333 None
334 } else {
335 Some(Color::Rgb(
336 ((rgb >> 16) & 0xff) as u8,
337 ((rgb >> 8) & 0xff) as u8,
338 (rgb & 0xff) as u8,
339 ))
340 }
341 }
342
343 #[test]
346 fn display_keyword_run_takes_theme_keyword_fg() {
347 let (resolved, ids) = defaults();
348 let kw_fg = resolve_syntax_style(&resolved, &ids, lattice_syntax::Style::Keyword)
349 .fg
350 .map(|c| c.to_rgb_u32(0))
351 .unwrap_or(0);
352 let line = dline(&[
353 ("fn", lattice_syntax::Style::Keyword, 0),
354 (" x", lattice_syntax::Style::Default, 0),
355 ]);
356 let spans = display_line_to_source_spans(&line, &resolved, &ids);
357 assert_eq!(spans[0].content.as_ref(), "fn");
358 assert_eq!(spans[0].style.fg, expect_color(kw_fg));
359 }
360
361 #[test]
364 fn display_source_spans_drop_inlay_runs() {
365 let (resolved, ids) = defaults();
366 let line = dline(&[
367 ("hi", lattice_syntax::Style::Default, 0),
368 (": i32", lattice_syntax::Style::Default, cell_flags::INLAY),
369 ]);
370 let text: String = display_line_to_source_spans(&line, &resolved, &ids)
371 .iter()
372 .map(|s| s.content.as_ref().to_string())
373 .collect();
374 assert_eq!(text, "hi");
375 }
376
377 #[test]
380 fn display_trailing_ws_run_takes_trailing_fg() {
381 let (resolved, ids) = defaults();
382 let default_fg = resolve_syntax_style(&resolved, &ids, lattice_syntax::Style::Default)
383 .fg
384 .map(|c| c.to_rgb_u32(0))
385 .unwrap_or(0);
386 let trailing_fg = resolved
387 .get(ids.whitespace_trailing)
388 .fg
389 .map(|c| c.to_rgb_u32(default_fg))
390 .unwrap_or(default_fg);
391 let line = dline(&[
392 ("x", lattice_syntax::Style::Default, 0),
393 (
394 "·",
395 lattice_syntax::Style::Default,
396 cell_flags::WS_MARKER | cell_flags::WS_TRAILING,
397 ),
398 ]);
399 let spans = display_line_to_source_spans(&line, &resolved, &ids);
400 let last = spans.last().unwrap();
401 assert_eq!(last.content.as_ref(), "·");
402 assert_eq!(last.style.fg, expect_color(trailing_fg));
403 }
404
405 #[test]
409 fn display_same_style_runs_merge_across_dropped_inlay() {
410 let (resolved, ids) = defaults();
411 let line = dline(&[
412 ("ab", lattice_syntax::Style::Default, 0),
413 ("INLAY", lattice_syntax::Style::Default, cell_flags::INLAY),
414 ("cd", lattice_syntax::Style::Default, 0),
415 ]);
416 let spans = display_line_to_source_spans(&line, &resolved, &ids);
417 assert_eq!(
418 spans.len(),
419 1,
420 "same-style runs merge across a dropped inlay"
421 );
422 assert_eq!(spans[0].content.as_ref(), "abcd");
423 }
424
425 #[test]
427 fn display_all_inlay_line_yields_no_source_spans() {
428 let (resolved, ids) = defaults();
429 let line = dline(&[(": T", lattice_syntax::Style::Default, cell_flags::INLAY)]);
430 assert!(display_line_to_source_spans(&line, &resolved, &ids).is_empty());
431 }
432
433 use crate::render::{WhitespaceDecoration, apply_whitespace_decoration};
444 use ratatui::style::Style as TuiStyle;
445
446 fn ws_deco_all_off() -> WhitespaceDecoration {
447 WhitespaceDecoration {
448 tab: None,
449 trailing: None,
450 leading: None,
451 space: None,
452 eol: None,
453 style_normal: TuiStyle::default(),
454 style_trailing: TuiStyle::default(),
455 }
456 }
457
458 fn ws_deco(
459 tab: Option<char>,
460 trailing: Option<char>,
461 leading: Option<char>,
462 space: Option<char>,
463 eol: Option<char>,
464 ) -> WhitespaceDecoration {
465 WhitespaceDecoration {
466 tab,
467 trailing,
468 leading,
469 space,
470 eol,
471 style_normal: TuiStyle::default(),
472 style_trailing: TuiStyle::default(),
473 }
474 }
475
476 fn collect_text(spans: &[Span<'static>]) -> String {
480 spans.iter().map(|s| s.content.as_ref()).collect()
481 }
482
483 #[test]
487 fn s3c1_mid_line_space_substituted() {
488 let fg = 0xcdd6f4;
489 let body = body(&[("a b", fg)]);
490 let line_text = "a b";
491 let d = ws_deco(None, None, None, Some('·'), None);
492 let out = apply_whitespace_decoration(body, line_text, &d);
493 assert_eq!(collect_text(&out), "a·b");
494 }
495
496 #[test]
501 fn s3c1_leading_whitespace_substituted() {
502 let fg = 0xcdd6f4;
503 let body = body(&[(" hi", fg)]);
505 let line_text = " hi";
506 let d = ws_deco(None, None, Some('›'), None, None);
507 let out = apply_whitespace_decoration(body, line_text, &d);
508 assert_eq!(collect_text(&out), "››hi");
509 }
510
511 #[test]
515 fn s3c1_trailing_whitespace_substituted() {
516 let fg = 0xcdd6f4;
517 let body = body(&[("hi ", fg)]);
519 let line_text = "hi ";
520 let d = ws_deco(None, Some('▷'), None, None, None);
521 let out = apply_whitespace_decoration(body, line_text, &d);
522 assert_eq!(collect_text(&out), "hi▷▷");
523 }
524
525 #[test]
529 fn s3c1_tab_cell_substituted() {
530 let fg = 0xcdd6f4;
531 let body = body(&[("x\ty", fg)]);
532 let line_text = "x\ty";
533 let d = ws_deco(Some('→'), None, None, None, None);
534 let out = apply_whitespace_decoration(body, line_text, &d);
535 assert_eq!(collect_text(&out), "x→y");
536 }
537
538 #[test]
542 fn s3c1_eol_marker_appends_after_cells() {
543 let fg = 0xcdd6f4;
544 let body = body(&[("hi", fg)]);
545 let line_text = "hi";
546 let d = ws_deco(None, None, None, None, Some('¶'));
547 let out = apply_whitespace_decoration(body, line_text, &d);
548 assert_eq!(collect_text(&out), "hi¶");
549 }
550
551 #[test]
556 fn s3c1_no_op_decoration_preserves_cell_spans() {
557 let fg = 0xcdd6f4;
558 let body_before = body(&[("a b", fg)]);
559 let line_text = "a b";
560 let d = ws_deco_all_off();
561 let body_after = apply_whitespace_decoration(body_before.clone(), line_text, &d);
562 assert_eq!(body_after.len(), body_before.len());
564 for (a, b) in body_after.iter().zip(body_before.iter()) {
565 assert_eq!(a.content.as_ref(), b.content.as_ref());
566 assert_eq!(a.style, b.style);
567 }
568 }
569
570 #[test]
576 fn s3c1_substitution_across_span_boundary() {
577 let fg_a = 0xff0000;
578 let fg_b = 0x00ff00;
579 let body = body(&[("a ", fg_a), ("b", fg_b)]);
582 assert_eq!(body.len(), 2);
583 let line_text = "a b";
584 let d = ws_deco(None, None, None, Some('·'), None);
585 let out = apply_whitespace_decoration(body, line_text, &d);
586 assert_eq!(collect_text(&out), "a·b");
587 }
588
589 use crate::render::apply_semantic_token_overlay;
604
605 fn flat_body(text: &str, fg: u32) -> Vec<Span<'static>> {
608 body(&[(text, fg)])
609 }
610
611 #[test]
615 fn s3c2_overlay_splits_span_when_partial() {
616 let body = flat_body("abcdefgh", 0xcdd6f4);
617 let overlay_fg = Color::Rgb(0xff, 0x00, 0x00);
618 let overlay_mods = Modifier::ITALIC;
619 let out = apply_semantic_token_overlay(body, 2, 6, overlay_fg, overlay_mods);
620 assert_eq!(out.len(), 3);
621 assert_eq!(out[0].content.as_ref(), "ab");
622 assert_eq!(out[1].content.as_ref(), "cdef");
623 assert_eq!(out[2].content.as_ref(), "gh");
624 assert_eq!(out[1].style.fg, Some(overlay_fg));
626 assert!(out[1].style.add_modifier.contains(Modifier::ITALIC));
627 assert_eq!(out[0].style.fg, Some(Color::Rgb(0xcd, 0xd6, 0xf4)));
629 assert_eq!(out[2].style.fg, Some(Color::Rgb(0xcd, 0xd6, 0xf4)));
630 }
631
632 #[test]
635 fn s3c2_overlay_full_coverage() {
636 let body = flat_body("hi", 0xcdd6f4);
637 let overlay_fg = Color::Rgb(0xff, 0xa5, 0x00);
638 let out = apply_semantic_token_overlay(body, 0, 2, overlay_fg, Modifier::empty());
639 let combined: String = out.iter().map(|s| s.content.as_ref()).collect();
641 assert_eq!(combined, "hi");
642 for s in &out {
643 assert_eq!(s.style.fg, Some(overlay_fg));
644 }
645 }
646
647 #[test]
650 fn s3c2_overlay_outside_range_is_noop() {
651 let body = flat_body("abc", 0xcdd6f4);
652 let pre_text: String = body.iter().map(|s| s.content.as_ref()).collect();
653 let pre_styles: Vec<_> = body.iter().map(|s| s.style).collect();
654 let out = apply_semantic_token_overlay(body, 10, 20, Color::Red, Modifier::ITALIC);
655 let post_text: String = out.iter().map(|s| s.content.as_ref()).collect();
656 let post_styles: Vec<_> = out.iter().map(|s| s.style).collect();
657 assert_eq!(post_text, pre_text);
658 assert_eq!(post_styles, pre_styles);
659 }
660
661 #[test]
665 fn s3c2_overlay_preserves_existing_modifiers() {
666 let fg = 0xcba6f7;
668 let body = vec![Span::styled(
669 "kw".to_string(),
670 Style::default()
671 .fg(rgb_u32_to_color(fg))
672 .add_modifier(Modifier::BOLD),
673 )];
674 let out = apply_semantic_token_overlay(body, 0, 2, Color::Cyan, Modifier::ITALIC);
676 for s in &out {
679 assert!(s.style.add_modifier.contains(Modifier::BOLD));
680 assert!(s.style.add_modifier.contains(Modifier::ITALIC));
681 }
682 }
683
684 #[test]
687 fn s3c2_overlay_replaces_fg_keeps_bg() {
688 let body = vec![Span::styled(
690 "x".to_string(),
691 Style::default()
692 .fg(rgb_u32_to_color(0xcdd6f4))
693 .bg(rgb_u32_to_color(0x1e1e2e)),
694 )];
695 assert_eq!(body[0].style.bg, Some(Color::Rgb(0x1e, 0x1e, 0x2e)));
696 let out = apply_semantic_token_overlay(body, 0, 1, Color::Magenta, Modifier::empty());
697 assert_eq!(out[0].style.fg, Some(Color::Magenta));
699 assert_eq!(out[0].style.bg, Some(Color::Rgb(0x1e, 0x1e, 0x2e)));
700 }
701
702 use crate::render::{apply_match_overlay, apply_underline_overlay};
718
719 fn match_style_yellow_bg() -> TuiStyle {
722 TuiStyle::default()
723 .bg(Color::Yellow)
724 .fg(Color::Black)
725 .add_modifier(Modifier::BOLD)
726 }
727
728 #[test]
732 fn s3c3_match_overlay_splits_single_span_body() {
733 let body = flat_body("abcdefgh", 0xcdd6f4);
734 let overlay = match_style_yellow_bg();
735 let out = apply_match_overlay(body, 2, 6, overlay);
736 assert_eq!(out.len(), 3);
737 assert_eq!(out[0].content.as_ref(), "ab");
738 assert_eq!(out[1].content.as_ref(), "cdef");
739 assert_eq!(out[2].content.as_ref(), "gh");
740 assert_eq!(out[1].style.fg, Some(Color::Black));
742 assert_eq!(out[1].style.bg, Some(Color::Yellow));
743 assert!(out[1].style.add_modifier.contains(Modifier::BOLD));
744 assert_eq!(out[0].style.fg, Some(Color::Rgb(0xcd, 0xd6, 0xf4)));
746 assert_eq!(out[0].style.bg, None);
747 assert_eq!(out[2].style.fg, Some(Color::Rgb(0xcd, 0xd6, 0xf4)));
748 }
749
750 #[test]
754 fn s3c3_match_overlay_full_coverage() {
755 let body = flat_body("hi", 0xcdd6f4);
756 let overlay = match_style_yellow_bg();
757 let out = apply_match_overlay(body, 0, 2, overlay);
758 assert_eq!(collect_text(&out), "hi");
759 for s in &out {
760 assert_eq!(s.style.fg, Some(Color::Black));
761 assert_eq!(s.style.bg, Some(Color::Yellow));
762 }
763 }
764
765 #[test]
768 fn s3c3_match_overlay_outside_range_noop() {
769 let body = flat_body("abc", 0xcdd6f4);
770 let pre_text = collect_text(&body);
771 let pre_styles: Vec<_> = body.iter().map(|s| s.style).collect();
772 let out = apply_match_overlay(body, 10, 20, match_style_yellow_bg());
773 assert_eq!(collect_text(&out), pre_text);
774 let post_styles: Vec<_> = out.iter().map(|s| s.style).collect();
775 assert_eq!(post_styles, pre_styles);
776 }
777
778 #[test]
783 fn s3c3_match_overlay_spans_multi_span_body() {
784 let fg_a = 0xff0000;
785 let fg_b = 0x00ff00;
786 let body = body(&[("aaa", fg_a), ("bbb", fg_b)]);
787 assert_eq!(body.len(), 2);
788 let overlay = match_style_yellow_bg();
789 let out = apply_match_overlay(body, 2, 5, overlay);
790 let mut cursor = 0usize;
794 for s in &out {
795 let len = s.content.len();
796 let span_start = cursor;
797 let span_end = cursor + len;
798 if span_start >= 2 && span_end <= 5 {
799 assert_eq!(
800 s.style.bg,
801 Some(Color::Yellow),
802 "overlap span '{}' must carry overlay bg",
803 s.content.as_ref()
804 );
805 }
806 cursor = span_end;
807 }
808 }
809
810 #[test]
817 fn s3c3_match_overlay_replaces_modifiers() {
818 let body = vec![Span::styled(
820 "x".to_string(),
821 Style::default()
822 .fg(rgb_u32_to_color(0xcba6f7))
823 .add_modifier(Modifier::BOLD),
824 )];
825 let overlay = TuiStyle::default()
827 .bg(Color::Yellow)
828 .add_modifier(Modifier::ITALIC);
829 let out = apply_match_overlay(body, 0, 1, overlay);
830 assert!(out[0].style.add_modifier.contains(Modifier::ITALIC));
832 assert!(
833 !out[0].style.add_modifier.contains(Modifier::BOLD),
834 "match overlay must REPLACE the style — BOLD from syntax must be dropped"
835 );
836 }
837
838 #[test]
843 fn s3c3_underline_overlay_adds_only_underline() {
844 let body = vec![Span::styled(
846 "err".to_string(),
847 Style::default()
848 .fg(rgb_u32_to_color(0xcdd6f4))
849 .bg(rgb_u32_to_color(0x1e1e2e))
850 .add_modifier(Modifier::BOLD),
851 )];
852 assert_eq!(body.len(), 1);
853 let out = apply_underline_overlay(body, 0, 3, Color::Red );
854 for s in &out {
856 assert!(s.style.add_modifier.contains(Modifier::UNDERLINED));
857 assert!(s.style.add_modifier.contains(Modifier::BOLD));
858 assert_eq!(s.style.fg, Some(Color::Rgb(0xcd, 0xd6, 0xf4)));
859 assert_eq!(s.style.bg, Some(Color::Rgb(0x1e, 0x1e, 0x2e)));
860 }
861 }
862
863 #[test]
867 fn s3c3_underline_overlay_partial_coverage_keeps_outer_style() {
868 let body = flat_body("abcdef", 0xcdd6f4);
869 let out = apply_underline_overlay(body, 2, 4, Color::Red);
870 assert_eq!(out.len(), 3);
871 assert_eq!(out[0].content.as_ref(), "ab");
872 assert!(!out[0].style.add_modifier.contains(Modifier::UNDERLINED));
873 assert_eq!(out[1].content.as_ref(), "cd");
874 assert!(out[1].style.add_modifier.contains(Modifier::UNDERLINED));
875 assert_eq!(out[1].style.fg, Some(Color::Rgb(0xcd, 0xd6, 0xf4)));
878 assert_eq!(out[2].content.as_ref(), "ef");
879 assert!(!out[2].style.add_modifier.contains(Modifier::UNDERLINED));
880 }
881
882 #[test]
888 fn s3c3_match_overlay_composes_by_sequence() {
889 let body = flat_body("abcdef", 0xcdd6f4);
890 let yellow = TuiStyle::default().bg(Color::Yellow).fg(Color::Black);
891 let cyan = TuiStyle::default().bg(Color::Cyan).fg(Color::Black);
892 let after_dh = apply_match_overlay(body, 1, 5, yellow);
894 let out = apply_match_overlay(after_dh, 2, 4, cyan);
897 let mut cursor = 0usize;
900 for s in &out {
901 let len = s.content.len();
902 let mid = cursor + len / 2;
903 match mid {
904 0 => assert_eq!(s.style.bg, None),
905 1 => assert_eq!(s.style.bg, Some(Color::Yellow)),
906 2 | 3 => assert_eq!(s.style.bg, Some(Color::Cyan)),
907 4 => assert_eq!(s.style.bg, Some(Color::Yellow)),
908 5 => assert_eq!(s.style.bg, None),
909 _ => {}
910 }
911 cursor += len;
912 }
913 }
914
915 use crate::render::splice_virtual_text_into_spans;
932
933 fn dim_gray_style() -> TuiStyle {
934 TuiStyle::default()
935 .fg(Color::DarkGray)
936 .add_modifier(Modifier::ITALIC)
937 }
938
939 #[test]
943 fn s3c4_inlay_splice_at_byte_zero_prepends() {
944 let body = flat_body("hi", 0xcdd6f4);
945 let out = splice_virtual_text_into_spans(body, 0, ": ".to_string(), dim_gray_style());
946 assert_eq!(collect_text(&out), ": hi");
947 assert_eq!(out[0].content.as_ref(), ": ");
949 assert_eq!(out[0].style.fg, Some(Color::DarkGray));
950 assert_eq!(out[1].content.as_ref(), "hi");
952 assert_eq!(out[1].style.fg, Some(Color::Rgb(0xcd, 0xd6, 0xf4)));
953 }
954
955 #[test]
959 fn s3c4_inlay_splice_mid_span_splits_the_span() {
960 let body = flat_body("abcdef", 0xcdd6f4);
961 let out = splice_virtual_text_into_spans(body, 3, "[i]".to_string(), dim_gray_style());
963 assert_eq!(out.len(), 3);
965 assert_eq!(out[0].content.as_ref(), "abc");
966 assert_eq!(out[1].content.as_ref(), "[i]");
967 assert_eq!(out[1].style.fg, Some(Color::DarkGray));
968 assert!(out[1].style.add_modifier.contains(Modifier::ITALIC));
969 assert_eq!(out[2].content.as_ref(), "def");
970 assert_eq!(out[0].style.fg, Some(Color::Rgb(0xcd, 0xd6, 0xf4)));
972 assert_eq!(out[2].style.fg, Some(Color::Rgb(0xcd, 0xd6, 0xf4)));
973 }
974
975 #[test]
981 fn s3c4_inlay_splice_at_span_boundary_does_not_split() {
982 let fg_a = 0xff0000;
983 let fg_b = 0x00ff00;
984 let body = body(&[("ab", fg_a), ("cd", fg_b)]);
985 assert_eq!(body.len(), 2);
986 let out = splice_virtual_text_into_spans(body, 2, "/*X*/".to_string(), dim_gray_style());
989 assert_eq!(out.len(), 3);
992 assert_eq!(out[0].content.as_ref(), "ab");
993 assert_eq!(out[0].style.fg, Some(Color::Rgb(0xff, 0x00, 0x00)));
994 assert_eq!(out[1].content.as_ref(), "/*X*/");
995 assert_eq!(out[2].content.as_ref(), "cd");
996 assert_eq!(out[2].style.fg, Some(Color::Rgb(0x00, 0xff, 0x00)));
997 }
998
999 #[test]
1003 fn s3c4_inlay_splice_past_end_appends() {
1004 let body = flat_body("hi", 0xcdd6f4);
1005 let out =
1006 splice_virtual_text_into_spans(body, 999, " → unit".to_string(), dim_gray_style());
1007 assert_eq!(collect_text(&out), "hi → unit");
1009 let last = out.last().unwrap();
1010 assert_eq!(last.content.as_ref(), " → unit");
1011 assert_eq!(last.style.fg, Some(Color::DarkGray));
1012 }
1013
1014 #[test]
1020 fn s3c4_multiple_inlays_in_reverse_byte_order() {
1021 let body = flat_body("xy", 0xcdd6f4);
1022 let after_second =
1026 splice_virtual_text_into_spans(body, 2, "/B/".to_string(), dim_gray_style());
1027 let after_first =
1028 splice_virtual_text_into_spans(after_second, 1, "/A/".to_string(), dim_gray_style());
1029 assert_eq!(collect_text(&after_first), "x/A/y/B/");
1032 }
1033
1034 #[test]
1038 fn s3c4_fold_suffix_appends_after_cell_body() {
1039 let body = flat_body("fn main() {}", 0xcdd6f4);
1040 let pre_count = body.len();
1041 let mut out = body;
1042 out.push(Span::styled(
1045 " ┄ 3 lines folded".to_string(),
1046 TuiStyle::default().fg(Color::DarkGray),
1047 ));
1048 assert_eq!(out.len(), pre_count + 1);
1050 let last = out.last().unwrap();
1051 assert_eq!(last.content.as_ref(), " ┄ 3 lines folded");
1052 assert_eq!(last.style.fg, Some(Color::DarkGray));
1053 }
1054
1055 #[test]
1060 fn s3c4_inlay_splice_then_fold_suffix_order() {
1061 let body = flat_body("ab", 0xcdd6f4);
1062 let mut after_inlay =
1064 splice_virtual_text_into_spans(body, 2, ": T".to_string(), dim_gray_style());
1065 after_inlay.push(Span::styled(
1067 " ┄ 5 lines folded".to_string(),
1068 TuiStyle::default().fg(Color::DarkGray),
1069 ));
1070 assert_eq!(collect_text(&after_inlay), "ab: T ┄ 5 lines folded");
1071 let last = after_inlay.last().unwrap();
1073 assert!(last.content.as_ref().starts_with(" ┄"));
1074 }
1075
1076 #[test]
1080 fn s3c2_overlay_spans_multi_span_body() {
1081 let fg_a = 0xff0000;
1082 let fg_b = 0x00ff00;
1083 let body = body(&[("aaa", fg_a), ("bbb", fg_b)]);
1085 assert_eq!(body.len(), 2);
1086 let overlay_fg = Color::Yellow;
1089 let out = apply_semantic_token_overlay(body, 2, 5, overlay_fg, Modifier::empty());
1090 let combined: String = out.iter().map(|s| s.content.as_ref()).collect();
1096 assert_eq!(combined, "aaabbb");
1097 let mut cursor = 0usize;
1100 for s in &out {
1101 let len = s.content.len();
1102 let overlap_start = cursor.max(2);
1103 let overlap_end = (cursor + len).min(5);
1104 if overlap_start < overlap_end {
1105 if cursor >= 2 && cursor + len <= 5 {
1108 assert_eq!(
1109 s.style.fg,
1110 Some(overlay_fg),
1111 "span '{}' at byte {cursor} must carry overlay fg",
1112 s.content.as_ref()
1113 );
1114 }
1115 }
1116 cursor += len;
1117 }
1118 }
1119}