1use crate::model::marker::{MarkerId, MarkerList};
2use ratatui::style::{Color, Style};
3use std::collections::HashMap;
4use std::ops::Range;
5
6pub use fresh_core::overlay::{OverlayHandle, OverlayNamespace};
8
9#[derive(Debug, Clone, PartialEq)]
11pub enum OverlayFace {
12 Underline { color: Color, style: UnderlineStyle },
14 Background { color: Color },
16 Foreground { color: Color },
18 Style { style: Style },
20 ThemedStyle {
26 fallback_style: Style,
28 fg_theme: Option<String>,
30 bg_theme: Option<String>,
32 fg_on_collision_only: bool,
35 },
36}
37
38impl OverlayFace {
39 pub fn from_options(options: &fresh_core::api::OverlayOptions) -> Self {
44 use crate::view::theme::named_color_from_str;
45 use ratatui::style::Modifier;
46
47 let mut style = Style::default();
48
49 if let Some(ref fg) = options.fg {
50 if let Some((r, g, b)) = fg.as_rgb() {
51 style = style.fg(Color::Rgb(r, g, b));
52 } else if let Some(key) = fg.as_theme_key() {
53 if let Some(color) = named_color_from_str(key) {
54 style = style.fg(color);
55 }
56 }
57 }
58
59 if let Some(ref bg) = options.bg {
60 if let Some((r, g, b)) = bg.as_rgb() {
61 style = style.bg(Color::Rgb(r, g, b));
62 } else if let Some(key) = bg.as_theme_key() {
63 if let Some(color) = named_color_from_str(key) {
64 style = style.bg(color);
65 }
66 }
67 }
68
69 let mut modifiers = Modifier::empty();
70 if options.bold {
71 modifiers |= Modifier::BOLD;
72 }
73 if options.italic {
74 modifiers |= Modifier::ITALIC;
75 }
76 if options.underline {
77 modifiers |= Modifier::UNDERLINED;
78 }
79 if options.strikethrough {
80 modifiers |= Modifier::CROSSED_OUT;
81 }
82 if !modifiers.is_empty() {
83 style = style.add_modifier(modifiers);
84 }
85
86 let fg_theme = options
89 .fg
90 .as_ref()
91 .and_then(|c| c.as_theme_key())
92 .filter(|key| named_color_from_str(key).is_none())
93 .map(String::from);
94 let bg_theme = options
95 .bg
96 .as_ref()
97 .and_then(|c| c.as_theme_key())
98 .filter(|key| named_color_from_str(key).is_none())
99 .map(String::from);
100
101 if fg_theme.is_some() || bg_theme.is_some() {
102 OverlayFace::ThemedStyle {
103 fallback_style: style,
104 fg_theme,
105 bg_theme,
106 fg_on_collision_only: options.fg_on_collision_only,
107 }
108 } else {
109 OverlayFace::Style { style }
110 }
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum UnderlineStyle {
117 Straight,
119 Wavy,
121 Dotted,
123 Dashed,
125}
126
127pub type Priority = i32;
130
131#[derive(Debug, Clone)]
134pub struct Overlay {
135 pub handle: OverlayHandle,
137
138 pub namespace: Option<OverlayNamespace>,
140
141 pub start_marker: MarkerId,
143
144 pub end_marker: MarkerId,
146
147 pub face: OverlayFace,
149
150 pub priority: Priority,
152
153 pub message: Option<String>,
155
156 pub extend_to_line_end: bool,
159
160 pub url: Option<String>,
163
164 pub theme_key: Option<&'static str>,
168}
169
170impl Overlay {
171 pub fn new(marker_list: &mut MarkerList, range: Range<usize>, face: OverlayFace) -> Self {
180 let start_marker = marker_list.create(range.start, true); let end_marker = marker_list.create(range.end, false); Self {
184 handle: OverlayHandle::new(),
185 namespace: None,
186 start_marker,
187 end_marker,
188 face,
189 priority: 0,
190 message: None,
191 extend_to_line_end: false,
192 url: None,
193 theme_key: None,
194 }
195 }
196
197 pub fn with_namespace(
199 marker_list: &mut MarkerList,
200 range: Range<usize>,
201 face: OverlayFace,
202 namespace: OverlayNamespace,
203 ) -> Self {
204 let mut overlay = Self::new(marker_list, range, face);
205 overlay.namespace = Some(namespace);
206 overlay
207 }
208
209 pub fn with_namespace_fixed_end(
217 marker_list: &mut MarkerList,
218 range: Range<usize>,
219 face: OverlayFace,
220 namespace: OverlayNamespace,
221 ) -> Self {
222 let start_marker = marker_list.create(range.start, true); let end_marker = marker_list.create_left_gravity(range.end);
224
225 Self {
226 handle: OverlayHandle::new(),
227 namespace: Some(namespace),
228 start_marker,
229 end_marker,
230 face,
231 priority: 0,
232 message: None,
233 extend_to_line_end: false,
234 url: None,
235 theme_key: None,
236 }
237 }
238
239 pub fn with_priority(
241 marker_list: &mut MarkerList,
242 range: Range<usize>,
243 face: OverlayFace,
244 priority: Priority,
245 ) -> Self {
246 let mut overlay = Self::new(marker_list, range, face);
247 overlay.priority = priority;
248 overlay
249 }
250
251 pub fn with_message(mut self, message: String) -> Self {
253 self.message = Some(message);
254 self
255 }
256
257 pub fn with_priority_value(mut self, priority: Priority) -> Self {
259 self.priority = priority;
260 self
261 }
262
263 pub fn with_namespace_value(mut self, namespace: OverlayNamespace) -> Self {
265 self.namespace = Some(namespace);
266 self
267 }
268
269 pub fn with_extend_to_line_end(mut self, extend: bool) -> Self {
271 self.extend_to_line_end = extend;
272 self
273 }
274
275 pub fn with_theme_key(mut self, key: &'static str) -> Self {
277 self.theme_key = Some(key);
278 self
279 }
280
281 pub fn range(&self, marker_list: &MarkerList) -> Range<usize> {
284 let start = marker_list.get_position(self.start_marker).unwrap_or(0);
285 let end = marker_list.get_position(self.end_marker).unwrap_or(0);
286 start..end
287 }
288
289 pub fn contains(&self, position: usize, marker_list: &MarkerList) -> bool {
291 self.range(marker_list).contains(&position)
292 }
293
294 pub fn overlaps(&self, range: &Range<usize>, marker_list: &MarkerList) -> bool {
296 let self_range = self.range(marker_list);
297 self_range.start < range.end && range.start < self_range.end
298 }
299}
300
301#[derive(Debug, Clone)]
304pub struct OverlayManager {
305 overlays: Vec<Overlay>,
307 marker_to_idx: HashMap<MarkerId, usize>,
311}
312
313impl OverlayManager {
314 pub fn new() -> Self {
316 Self {
317 overlays: Vec::new(),
318 marker_to_idx: HashMap::new(),
319 }
320 }
321
322 pub fn add(&mut self, overlay: Overlay) -> OverlayHandle {
324 let handle = overlay.handle.clone();
325 let priority = overlay.priority;
329 let pos = self.overlays.partition_point(|o| o.priority <= priority);
330 self.overlays.insert(pos, overlay);
331 for (i, o) in self.overlays.iter().enumerate().skip(pos) {
335 self.marker_to_idx.insert(o.start_marker, i);
336 self.marker_to_idx.insert(o.end_marker, i);
337 }
338 handle
339 }
340
341 pub fn extend<I: IntoIterator<Item = Overlay>>(&mut self, overlays: I) {
348 self.overlays.extend(overlays);
349 self.overlays.sort_by_key(|o| o.priority);
350 self.rebuild_marker_index();
351 }
352
353 pub fn remove_by_handle(
355 &mut self,
356 handle: &OverlayHandle,
357 marker_list: &mut MarkerList,
358 ) -> bool {
359 if let Some(pos) = self.overlays.iter().position(|o| &o.handle == handle) {
360 let overlay = self.overlays.remove(pos);
361 self.marker_to_idx.remove(&overlay.start_marker);
362 self.marker_to_idx.remove(&overlay.end_marker);
363 for (i, o) in self.overlays.iter().enumerate().skip(pos) {
365 self.marker_to_idx.insert(o.start_marker, i);
366 self.marker_to_idx.insert(o.end_marker, i);
367 }
368 marker_list.delete(overlay.start_marker);
369 marker_list.delete(overlay.end_marker);
370 true
371 } else {
372 false
373 }
374 }
375
376 pub fn clear_namespace(&mut self, namespace: &OverlayNamespace, marker_list: &mut MarkerList) {
378 let mut indices: Vec<usize> = self
379 .overlays
380 .iter()
381 .enumerate()
382 .filter_map(|(i, o)| (o.namespace.as_ref() == Some(namespace)).then_some(i))
383 .collect();
384 if indices.is_empty() {
385 return;
386 }
387 indices.sort_unstable_by(|a, b| b.cmp(a));
388 for idx in indices {
389 self.swap_remove_at(idx, marker_list);
390 }
391 self.overlays.sort_by_key(|o| o.priority);
393 self.rebuild_marker_index();
394 }
395
396 pub fn replace_range_in_namespace(
401 &mut self,
402 namespace: &OverlayNamespace,
403 range: &Range<usize>,
404 mut new_overlays: Vec<Overlay>,
405 marker_list: &mut MarkerList,
406 ) {
407 if range.start < range.end {
411 let hits = marker_list.query_range(range.start, range.end);
412 let mut candidates: Vec<usize> = hits
413 .iter()
414 .filter_map(|(mid, _, _)| self.marker_to_idx.get(mid).copied())
415 .collect();
416 candidates.sort_unstable();
417 candidates.dedup();
418 let mut to_remove: Vec<usize> = candidates
419 .into_iter()
420 .filter(|&idx| {
421 let o = &self.overlays[idx];
422 if o.namespace.as_ref() != Some(namespace) {
423 return false;
424 }
425 let start = marker_list.get_position(o.start_marker).unwrap_or(0);
426 let end = marker_list.get_position(o.end_marker).unwrap_or(0);
427 start < range.end && range.start < end
428 })
429 .collect();
430 to_remove.sort_unstable_by(|a, b| b.cmp(a));
431 for idx in to_remove {
432 self.swap_remove_at(idx, marker_list);
433 }
434 }
435
436 if !new_overlays.is_empty() {
437 self.overlays.append(&mut new_overlays);
438 }
439 self.overlays.sort_by_key(|o| o.priority);
440 self.rebuild_marker_index();
441 }
442
443 pub fn remove_in_range(&mut self, range: &Range<usize>, marker_list: &mut MarkerList) {
445 if range.start >= range.end {
453 return;
454 }
455 let hits = marker_list.query_range(range.start, range.end);
456 if hits.is_empty() {
457 return;
458 }
459 let mut candidates: Vec<usize> = hits
460 .iter()
461 .filter_map(|(mid, _, _)| self.marker_to_idx.get(mid).copied())
462 .collect();
463 candidates.sort_unstable();
464 candidates.dedup();
465
466 let mut to_remove: Vec<usize> = candidates
467 .into_iter()
468 .filter(|&idx| {
469 let o = &self.overlays[idx];
470 let start = marker_list.get_position(o.start_marker).unwrap_or(0);
471 let end = marker_list.get_position(o.end_marker).unwrap_or(0);
472 start < range.end && range.start < end
473 })
474 .collect();
475 if to_remove.is_empty() {
476 return;
477 }
478 to_remove.sort_unstable_by(|a, b| b.cmp(a));
479 for idx in to_remove {
480 self.swap_remove_at(idx, marker_list);
481 }
482 self.overlays.sort_by_key(|o| o.priority);
484 self.rebuild_marker_index();
485 }
486
487 pub fn remove_in_range_for_namespace(
493 &mut self,
494 range: &Range<usize>,
495 namespace: &OverlayNamespace,
496 marker_list: &mut MarkerList,
497 ) {
498 if range.start >= range.end {
499 return;
500 }
501 let hits = marker_list.query_range(range.start, range.end);
502 if hits.is_empty() {
503 return;
504 }
505 let mut candidates: Vec<usize> = hits
506 .iter()
507 .filter_map(|(mid, _, _)| self.marker_to_idx.get(mid).copied())
508 .collect();
509 candidates.sort_unstable();
510 candidates.dedup();
511
512 let mut to_remove: Vec<usize> = candidates
513 .into_iter()
514 .filter(|&idx| {
515 let o = &self.overlays[idx];
516 if o.namespace.as_ref() != Some(namespace) {
517 return false;
518 }
519 let start = marker_list.get_position(o.start_marker).unwrap_or(0);
520 let end = marker_list.get_position(o.end_marker).unwrap_or(0);
521 start < range.end && range.start < end
522 })
523 .collect();
524 if to_remove.is_empty() {
525 return;
526 }
527 to_remove.sort_unstable_by(|a, b| b.cmp(a));
528 for idx in to_remove {
529 self.swap_remove_at(idx, marker_list);
530 }
531 self.overlays.sort_by_key(|o| o.priority);
533 self.rebuild_marker_index();
534 }
535
536 pub fn clear(&mut self, marker_list: &mut MarkerList) {
538 for overlay in &self.overlays {
539 marker_list.delete(overlay.start_marker);
540 marker_list.delete(overlay.end_marker);
541 }
542 self.overlays.clear();
543 self.marker_to_idx.clear();
544 }
545
546 fn swap_remove_at(&mut self, idx: usize, marker_list: &mut MarkerList) {
550 let removed = self.overlays.swap_remove(idx);
551 self.marker_to_idx.remove(&removed.start_marker);
552 self.marker_to_idx.remove(&removed.end_marker);
553 marker_list.delete(removed.start_marker);
554 marker_list.delete(removed.end_marker);
555 if let Some(moved) = self.overlays.get(idx) {
556 self.marker_to_idx.insert(moved.start_marker, idx);
557 self.marker_to_idx.insert(moved.end_marker, idx);
558 }
559 }
560
561 fn rebuild_marker_index(&mut self) {
564 self.marker_to_idx.clear();
565 for (i, o) in self.overlays.iter().enumerate() {
566 self.marker_to_idx.insert(o.start_marker, i);
567 self.marker_to_idx.insert(o.end_marker, i);
568 }
569 }
570
571 pub fn at_position(&self, position: usize, marker_list: &MarkerList) -> Vec<&Overlay> {
573 self.overlays
574 .iter()
575 .filter(|o| {
576 let range = o.range(marker_list);
577 range.contains(&position)
578 })
579 .collect()
580 }
581
582 pub fn in_range(&self, range: &Range<usize>, marker_list: &MarkerList) -> Vec<&Overlay> {
584 self.overlays
585 .iter()
586 .filter(|o| o.overlaps(range, marker_list))
587 .collect()
588 }
589
590 pub fn query_viewport(
599 &self,
600 start: usize,
601 end: usize,
602 marker_list: &MarkerList,
603 ) -> Vec<(&Overlay, Range<usize>)> {
604 use std::collections::HashMap;
605
606 let visible_markers = marker_list.query_range(start, end);
609
610 let marker_positions: HashMap<_, _> = visible_markers
612 .into_iter()
613 .map(|(id, start, _end)| (id, start))
614 .collect();
615
616 self.overlays
622 .iter()
623 .filter_map(|overlay| {
624 let start_in_vp = marker_positions.get(&overlay.start_marker).copied();
625 let end_in_vp = marker_positions.get(&overlay.end_marker).copied();
626
627 if start_in_vp.is_none() && end_in_vp.is_none() {
630 return None;
631 }
632
633 let start_pos =
635 start_in_vp.or_else(|| marker_list.get_position(overlay.start_marker))?;
636 let end_pos = end_in_vp.or_else(|| marker_list.get_position(overlay.end_marker))?;
637
638 let range = start_pos..end_pos;
639
640 let included = if range.start == range.end {
645 range.start >= start && range.start <= end
646 } else {
647 range.start < end && range.end > start
648 };
649
650 if included {
651 Some((overlay, range))
652 } else {
653 None
654 }
655 })
656 .collect()
657 }
658
659 pub fn get_by_handle(&self, handle: &OverlayHandle) -> Option<&Overlay> {
661 self.overlays.iter().find(|o| &o.handle == handle)
662 }
663
664 pub fn get_by_handle_mut(&mut self, handle: &OverlayHandle) -> Option<&mut Overlay> {
666 self.overlays.iter_mut().find(|o| &o.handle == handle)
667 }
668
669 pub fn len(&self) -> usize {
671 self.overlays.len()
672 }
673
674 pub fn is_empty(&self) -> bool {
676 self.overlays.is_empty()
677 }
678
679 pub fn all(&self) -> &[Overlay] {
681 &self.overlays
682 }
683
684 #[cfg(test)]
688 fn check_invariants(&self) {
689 assert_eq!(
690 self.marker_to_idx.len(),
691 self.overlays.len() * 2,
692 "marker_to_idx size != 2 * overlays.len()"
693 );
694 for (i, o) in self.overlays.iter().enumerate() {
695 assert_eq!(
696 self.marker_to_idx.get(&o.start_marker).copied(),
697 Some(i),
698 "start_marker {:?} of overlay {} mismapped",
699 o.start_marker,
700 i,
701 );
702 assert_eq!(
703 self.marker_to_idx.get(&o.end_marker).copied(),
704 Some(i),
705 "end_marker {:?} of overlay {} mismapped",
706 o.end_marker,
707 i,
708 );
709 }
710 }
714
715 #[cfg(test)]
717 fn assert_priority_sorted(&self) {
718 for w in self.overlays.windows(2) {
719 assert!(
720 w[0].priority <= w[1].priority,
721 "priority order broken: {} after {}",
722 w[1].priority,
723 w[0].priority,
724 );
725 }
726 }
727}
728
729impl Default for OverlayManager {
730 fn default() -> Self {
731 Self::new()
732 }
733}
734
735impl Overlay {
737 pub fn error(
739 marker_list: &mut MarkerList,
740 range: Range<usize>,
741 message: Option<String>,
742 ) -> Self {
743 let mut overlay = Self::with_priority(
744 marker_list,
745 range,
746 OverlayFace::Underline {
747 color: Color::Red,
748 style: UnderlineStyle::Wavy,
749 },
750 10, );
752 overlay.message = message;
753 overlay
754 }
755
756 pub fn warning(
758 marker_list: &mut MarkerList,
759 range: Range<usize>,
760 message: Option<String>,
761 ) -> Self {
762 let mut overlay = Self::with_priority(
763 marker_list,
764 range,
765 OverlayFace::Underline {
766 color: Color::Yellow,
767 style: UnderlineStyle::Wavy,
768 },
769 5, );
771 overlay.message = message;
772 overlay
773 }
774
775 pub fn info(
777 marker_list: &mut MarkerList,
778 range: Range<usize>,
779 message: Option<String>,
780 ) -> Self {
781 let mut overlay = Self::with_priority(
782 marker_list,
783 range,
784 OverlayFace::Underline {
785 color: Color::Blue,
786 style: UnderlineStyle::Wavy,
787 },
788 3, );
790 overlay.message = message;
791 overlay
792 }
793
794 pub fn hint(
796 marker_list: &mut MarkerList,
797 range: Range<usize>,
798 message: Option<String>,
799 ) -> Self {
800 let mut overlay = Self::with_priority(
801 marker_list,
802 range,
803 OverlayFace::Underline {
804 color: Color::Gray,
805 style: UnderlineStyle::Dotted,
806 },
807 1, );
809 overlay.message = message;
810 overlay
811 }
812
813 pub fn selection(marker_list: &mut MarkerList, range: Range<usize>) -> Self {
815 let mut overlay = Self::with_priority(
816 marker_list,
817 range,
818 OverlayFace::Background {
819 color: Color::Rgb(38, 79, 120), },
821 -10, );
823 overlay.theme_key = Some("editor.selection_bg");
824 overlay
825 }
826
827 pub fn search_match(marker_list: &mut MarkerList, range: Range<usize>) -> Self {
829 let mut overlay = Self::with_priority(
830 marker_list,
831 range,
832 OverlayFace::Background {
833 color: Color::Rgb(72, 72, 0), },
835 -5, );
837 overlay.theme_key = Some("search.match_bg");
838 overlay
839 }
840}
841
842#[cfg(test)]
843mod tests {
844 use super::*;
845
846 #[test]
847 fn test_overlay_creation_with_markers() {
848 let mut marker_list = MarkerList::new();
849 marker_list.set_buffer_size(100);
850
851 let overlay = Overlay::new(
852 &mut marker_list,
853 5..10,
854 OverlayFace::Background { color: Color::Red },
855 );
856
857 assert_eq!(marker_list.get_position(overlay.start_marker), Some(5));
858 assert_eq!(marker_list.get_position(overlay.end_marker), Some(10));
859 assert_eq!(overlay.range(&marker_list), 5..10);
860 }
861
862 #[test]
863 fn test_overlay_adjusts_with_insert() {
864 let mut marker_list = MarkerList::new();
865 marker_list.set_buffer_size(100);
866
867 let overlay = Overlay::new(
868 &mut marker_list,
869 10..20,
870 OverlayFace::Background { color: Color::Red },
871 );
872
873 marker_list.adjust_for_insert(5, 10);
875
876 assert_eq!(overlay.range(&marker_list), 20..30);
878 }
879
880 #[test]
881 fn test_overlay_adjusts_with_delete() {
882 let mut marker_list = MarkerList::new();
883 marker_list.set_buffer_size(100);
884
885 let overlay = Overlay::new(
886 &mut marker_list,
887 20..30,
888 OverlayFace::Background { color: Color::Red },
889 );
890
891 marker_list.adjust_for_delete(5, 10);
893
894 assert_eq!(overlay.range(&marker_list), 10..20);
896 }
897
898 #[test]
899 fn test_overlay_manager_add_remove() {
900 let mut marker_list = MarkerList::new();
901 marker_list.set_buffer_size(100);
902 let mut manager = OverlayManager::new();
903
904 let overlay = Overlay::new(
905 &mut marker_list,
906 5..10,
907 OverlayFace::Background { color: Color::Red },
908 );
909
910 let handle = manager.add(overlay);
911 assert_eq!(manager.len(), 1);
912
913 manager.remove_by_handle(&handle, &mut marker_list);
914 assert_eq!(manager.len(), 0);
915 }
916
917 #[test]
918 fn test_overlay_namespace_clear() {
919 let mut marker_list = MarkerList::new();
920 marker_list.set_buffer_size(100);
921 let mut manager = OverlayManager::new();
922
923 let ns = OverlayNamespace::from_string("todo".to_string());
924
925 let overlay1 = Overlay::with_namespace(
927 &mut marker_list,
928 5..10,
929 OverlayFace::Background { color: Color::Red },
930 ns.clone(),
931 );
932 let overlay2 = Overlay::with_namespace(
933 &mut marker_list,
934 15..20,
935 OverlayFace::Background { color: Color::Blue },
936 ns.clone(),
937 );
938 let overlay3 = Overlay::new(
940 &mut marker_list,
941 25..30,
942 OverlayFace::Background {
943 color: Color::Green,
944 },
945 );
946
947 manager.add(overlay1);
948 manager.add(overlay2);
949 manager.add(overlay3);
950 assert_eq!(manager.len(), 3);
951
952 manager.clear_namespace(&ns, &mut marker_list);
954 assert_eq!(manager.len(), 1); }
956
957 #[test]
958 fn test_overlay_priority_sorting() {
959 let mut marker_list = MarkerList::new();
960 marker_list.set_buffer_size(100);
961 let mut manager = OverlayManager::new();
962
963 manager.add(Overlay::with_priority(
964 &mut marker_list,
965 5..10,
966 OverlayFace::Background { color: Color::Red },
967 10,
968 ));
969 manager.add(Overlay::with_priority(
970 &mut marker_list,
971 5..10,
972 OverlayFace::Background { color: Color::Blue },
973 5,
974 ));
975 manager.add(Overlay::with_priority(
976 &mut marker_list,
977 5..10,
978 OverlayFace::Background {
979 color: Color::Green,
980 },
981 15,
982 ));
983
984 let overlays = manager.at_position(7, &marker_list);
985 assert_eq!(overlays.len(), 3);
986 assert_eq!(overlays[0].priority, 5);
988 assert_eq!(overlays[1].priority, 10);
989 assert_eq!(overlays[2].priority, 15);
990 }
991
992 #[test]
993 fn test_overlay_contains_and_overlaps() {
994 let mut marker_list = MarkerList::new();
995 marker_list.set_buffer_size(100);
996
997 let overlay = Overlay::new(
998 &mut marker_list,
999 10..20,
1000 OverlayFace::Background { color: Color::Red },
1001 );
1002
1003 assert!(!overlay.contains(9, &marker_list));
1004 assert!(overlay.contains(10, &marker_list));
1005 assert!(overlay.contains(15, &marker_list));
1006 assert!(overlay.contains(19, &marker_list));
1007 assert!(!overlay.contains(20, &marker_list));
1008
1009 assert!(!overlay.overlaps(&(0..10), &marker_list));
1010 assert!(overlay.overlaps(&(5..15), &marker_list));
1011 assert!(overlay.overlaps(&(15..25), &marker_list));
1012 assert!(!overlay.overlaps(&(20..30), &marker_list));
1013 }
1014
1015 #[test]
1016 fn test_overlay_remove_in_range_keeps_only_disjoint() {
1017 let mut marker_list = MarkerList::new();
1018 marker_list.set_buffer_size(200);
1019 let mut manager = OverlayManager::new();
1020
1021 manager.add(Overlay::new(
1022 &mut marker_list,
1023 0..5,
1024 OverlayFace::Background { color: Color::Red },
1025 ));
1026 manager.add(Overlay::new(
1027 &mut marker_list,
1028 10..20,
1029 OverlayFace::Background { color: Color::Blue },
1030 ));
1031 manager.add(Overlay::new(
1032 &mut marker_list,
1033 30..40,
1034 OverlayFace::Background {
1035 color: Color::Green,
1036 },
1037 ));
1038 manager.add(Overlay::new(
1039 &mut marker_list,
1040 50..60,
1041 OverlayFace::Background {
1042 color: Color::Yellow,
1043 },
1044 ));
1045
1046 manager.remove_in_range(&(15..35), &mut marker_list);
1048
1049 let kept: Vec<_> = manager
1050 .all()
1051 .iter()
1052 .map(|o| o.range(&marker_list))
1053 .collect();
1054 assert_eq!(kept, vec![0..5, 50..60]);
1055 }
1056
1057 #[test]
1058 fn test_overlay_remove_in_range_deletes_markers() {
1059 let mut marker_list = MarkerList::new();
1060 marker_list.set_buffer_size(100);
1061 let mut manager = OverlayManager::new();
1062
1063 let overlay = Overlay::new(
1064 &mut marker_list,
1065 10..20,
1066 OverlayFace::Background { color: Color::Red },
1067 );
1068 let start_id = overlay.start_marker;
1069 let end_id = overlay.end_marker;
1070 manager.add(overlay);
1071
1072 manager.remove_in_range(&(0..50), &mut marker_list);
1073
1074 assert_eq!(manager.len(), 0);
1075 assert_eq!(marker_list.get_position(start_id), None);
1076 assert_eq!(marker_list.get_position(end_id), None);
1077 }
1078
1079 #[test]
1080 fn test_overlay_remove_in_range_endpoint_semantics() {
1081 let mut marker_list = MarkerList::new();
1083 marker_list.set_buffer_size(100);
1084 let mut manager = OverlayManager::new();
1085
1086 manager.add(Overlay::new(
1087 &mut marker_list,
1088 10..20,
1089 OverlayFace::Background { color: Color::Red },
1090 ));
1091
1092 manager.remove_in_range(&(20..30), &mut marker_list);
1093 assert_eq!(manager.len(), 1);
1094 manager.remove_in_range(&(0..10), &mut marker_list);
1095 assert_eq!(manager.len(), 1);
1096 manager.remove_in_range(&(19..21), &mut marker_list);
1097 assert_eq!(manager.len(), 0);
1098 }
1099
1100 #[test]
1109 fn perf_full_buffer_rebuild_pass() {
1110 const LINES: usize = 500;
1111 const LINE_BYTES: usize = 50;
1112 const OVERLAYS_PER_LINE: usize = 5;
1113
1114 let mut marker_list = MarkerList::new();
1115 marker_list.set_buffer_size(LINES * LINE_BYTES);
1116 let mut manager = OverlayManager::new();
1117
1118 let overlay_byte = |line: usize, k: usize| -> usize {
1119 line * LINE_BYTES + k * (LINE_BYTES / OVERLAYS_PER_LINE)
1120 };
1121 let make_overlay = |ml: &mut MarkerList, line: usize, k: usize| {
1122 let s = overlay_byte(line, k);
1123 Overlay::new(
1124 ml,
1125 s..(s + 2),
1126 OverlayFace::Background { color: Color::Red },
1127 )
1128 };
1129
1130 for line in 0..LINES {
1132 for k in 0..OVERLAYS_PER_LINE {
1133 let o = make_overlay(&mut marker_list, line, k);
1134 manager.add(o);
1135 }
1136 }
1137 let initial = LINES * OVERLAYS_PER_LINE;
1138
1139 let start = std::time::Instant::now();
1141 for line in 0..LINES {
1142 let line_range = (line * LINE_BYTES)..((line + 1) * LINE_BYTES);
1143 manager.remove_in_range(&line_range, &mut marker_list);
1144 for k in 0..OVERLAYS_PER_LINE {
1145 let o = make_overlay(&mut marker_list, line, k);
1146 manager.add(o);
1147 }
1148 }
1149 let elapsed = start.elapsed();
1150
1151 eprintln!(
1152 "[perf] overlay full-buffer rebuild ({LINES} lines, {} entries steady): \
1153 {:?} total, {:?}/line",
1154 initial,
1155 elapsed,
1156 elapsed / LINES as u32,
1157 );
1158 assert_eq!(manager.len(), initial);
1159 }
1160
1161 mod proptests {
1162 use super::*;
1163 use proptest::prelude::*;
1164
1165 #[derive(Debug, Clone)]
1166 enum Op {
1167 Add {
1168 start: usize,
1169 len: usize,
1170 priority: i32,
1171 ns_idx: u8,
1172 },
1173 RemoveInRange {
1174 start: usize,
1175 end: usize,
1176 },
1177 ClearNamespace {
1178 ns_idx: u8,
1179 },
1180 ReplaceRange {
1181 start: usize,
1182 end: usize,
1183 ns_idx: u8,
1184 new_overlays: Vec<(usize, usize, i32)>,
1187 },
1188 }
1189
1190 const BUFFER_SIZE: usize = 200;
1191 const MAX_OVERLAY_LEN: usize = 4;
1192 const MIN_QUERY_LEN: usize = MAX_OVERLAY_LEN + 1;
1193
1194 fn arb_overlay_spec() -> impl Strategy<Value = (usize, usize, i32)> {
1195 (
1196 0..(BUFFER_SIZE - MAX_OVERLAY_LEN),
1197 1..=MAX_OVERLAY_LEN,
1198 -5i32..=5i32,
1199 )
1200 }
1201
1202 fn arb_op() -> impl Strategy<Value = Op> {
1203 prop_oneof![
1204 3 => arb_overlay_spec().prop_flat_map(|(start, len, priority)| {
1205 (Just(start), Just(len), Just(priority), 0u8..3u8)
1206 }).prop_map(|(start, len, priority, ns_idx)| Op::Add {
1207 start, len, priority, ns_idx,
1208 }),
1209 2 => (0..BUFFER_SIZE, MIN_QUERY_LEN..=BUFFER_SIZE)
1210 .prop_map(|(start, qlen)| {
1211 let s = start.min(BUFFER_SIZE - 1);
1212 let e = (s + qlen).min(BUFFER_SIZE);
1213 Op::RemoveInRange { start: s, end: e }
1214 }),
1215 1 => (0u8..3u8).prop_map(|ns_idx| Op::ClearNamespace { ns_idx }),
1216 1 => (
1217 0..BUFFER_SIZE,
1218 MIN_QUERY_LEN..=BUFFER_SIZE,
1219 0u8..3u8,
1220 prop::collection::vec(arb_overlay_spec(), 0..4),
1221 )
1222 .prop_map(|(start, qlen, ns_idx, new_overlays)| {
1223 let s = start.min(BUFFER_SIZE - 1);
1224 let e = (s + qlen).min(BUFFER_SIZE);
1225 Op::ReplaceRange { start: s, end: e, ns_idx, new_overlays }
1226 }),
1227 ]
1228 }
1229
1230 fn nsf(idx: u8) -> OverlayNamespace {
1231 OverlayNamespace::from_string(format!("ns{idx}"))
1232 }
1233
1234 proptest! {
1235 #[test]
1243 fn prop_marker_index_consistent(ops in prop::collection::vec(arb_op(), 0..30)) {
1244 let mut marker_list = MarkerList::new();
1245 marker_list.set_buffer_size(BUFFER_SIZE);
1246 let mut manager = OverlayManager::new();
1247
1248 for op in ops {
1249 match op {
1250 Op::Add { start, len, priority, ns_idx } => {
1251 let o = Overlay::with_namespace(
1252 &mut marker_list,
1253 start..(start + len),
1254 OverlayFace::Background { color: Color::Red },
1255 nsf(ns_idx),
1256 );
1257 let mut o = o;
1258 o.priority = priority;
1259 manager.add(o);
1260 manager.check_invariants();
1261 manager.assert_priority_sorted();
1262 }
1263 Op::RemoveInRange { start, end } => {
1264 manager.remove_in_range(&(start..end), &mut marker_list);
1265 for (o, rng) in manager.query_viewport(start, end, &marker_list) {
1266 let overlaps = rng.start < end && start < rng.end;
1267 prop_assert!(
1268 !overlaps,
1269 "overlay {:?} (handle {:?}) survived remove_in_range({start}..{end})",
1270 rng, o.handle,
1271 );
1272 }
1273 manager.check_invariants();
1274 }
1275 Op::ClearNamespace { ns_idx } => {
1276 manager.clear_namespace(&nsf(ns_idx), &mut marker_list);
1277 manager.check_invariants();
1278 manager.assert_priority_sorted();
1279 }
1280 Op::ReplaceRange { start, end, ns_idx, new_overlays } => {
1281 let new: Vec<Overlay> = new_overlays.into_iter().map(|(s, l, p)| {
1282 let mut o = Overlay::with_namespace(
1283 &mut marker_list,
1284 s..(s + l),
1285 OverlayFace::Background { color: Color::Blue },
1286 nsf(ns_idx),
1287 );
1288 o.priority = p;
1289 o
1290 }).collect();
1291 manager.replace_range_in_namespace(
1292 &nsf(ns_idx),
1293 &(start..end),
1294 new,
1295 &mut marker_list,
1296 );
1297 manager.check_invariants();
1298 manager.assert_priority_sorted();
1299 }
1300 }
1301 }
1302 }
1303 }
1304 }
1305}