1use ratatui::style::Style;
23use std::collections::HashMap;
24
25use crate::model::marker::{MarkerId, MarkerList};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum VirtualTextPosition {
30 BeforeChar,
33 AfterChar,
35
36 LineAbove,
41 LineBelow,
45}
46
47impl VirtualTextPosition {
48 pub fn is_line(&self) -> bool {
50 matches!(self, Self::LineAbove | Self::LineBelow)
51 }
52
53 pub fn is_inline(&self) -> bool {
55 matches!(self, Self::BeforeChar | Self::AfterChar)
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Hash)]
62pub struct VirtualTextNamespace(pub String);
63
64impl VirtualTextNamespace {
65 pub fn from_string(s: String) -> Self {
67 Self(s)
68 }
69
70 pub fn as_str(&self) -> &str {
72 &self.0
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct VirtualText {
79 pub marker_id: MarkerId,
81 pub text: String,
83 pub style: Style,
85 pub position: VirtualTextPosition,
87 pub priority: i32,
89 pub string_id: Option<String>,
91 pub namespace: Option<VirtualTextNamespace>,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97pub struct VirtualTextId(pub u64);
98
99pub struct VirtualTextManager {
104 texts: HashMap<VirtualTextId, VirtualText>,
106 next_id: u64,
108}
109
110impl VirtualTextManager {
111 pub fn new() -> Self {
113 Self {
114 texts: HashMap::new(),
115 next_id: 0,
116 }
117 }
118
119 pub fn add(
132 &mut self,
133 marker_list: &mut MarkerList,
134 position: usize,
135 text: String,
136 style: Style,
137 vtext_position: VirtualTextPosition,
138 priority: i32,
139 ) -> VirtualTextId {
140 let marker_id = marker_list.create(position, false);
143
144 let id = VirtualTextId(self.next_id);
145 self.next_id += 1;
146
147 self.texts.insert(
148 id,
149 VirtualText {
150 marker_id,
151 text,
152 style,
153 position: vtext_position,
154 priority,
155 string_id: None,
156 namespace: None,
157 },
158 );
159
160 id
161 }
162
163 #[allow(clippy::too_many_arguments)]
167 pub fn add_with_id(
168 &mut self,
169 marker_list: &mut MarkerList,
170 position: usize,
171 text: String,
172 style: Style,
173 vtext_position: VirtualTextPosition,
174 priority: i32,
175 string_id: String,
176 ) -> VirtualTextId {
177 let marker_id = marker_list.create(position, false);
178
179 let id = VirtualTextId(self.next_id);
180 self.next_id += 1;
181
182 self.texts.insert(
183 id,
184 VirtualText {
185 marker_id,
186 text,
187 style,
188 position: vtext_position,
189 priority,
190 string_id: Some(string_id),
191 namespace: None,
192 },
193 );
194
195 id
196 }
197
198 #[allow(clippy::too_many_arguments)]
211 pub fn add_line(
212 &mut self,
213 marker_list: &mut MarkerList,
214 position: usize,
215 text: String,
216 style: Style,
217 placement: VirtualTextPosition,
218 namespace: VirtualTextNamespace,
219 priority: i32,
220 ) -> VirtualTextId {
221 debug_assert!(
222 placement.is_line(),
223 "add_line requires LineAbove or LineBelow"
224 );
225
226 let marker_id = marker_list.create(position, false);
227
228 let id = VirtualTextId(self.next_id);
229 self.next_id += 1;
230
231 self.texts.insert(
232 id,
233 VirtualText {
234 marker_id,
235 text,
236 style,
237 position: placement,
238 priority,
239 string_id: None,
240 namespace: Some(namespace),
241 },
242 );
243
244 id
245 }
246
247 pub fn remove_by_id(&mut self, marker_list: &mut MarkerList, string_id: &str) -> bool {
249 let to_remove: Vec<VirtualTextId> = self
251 .texts
252 .iter()
253 .filter_map(|(id, vtext)| {
254 if vtext.string_id.as_deref() == Some(string_id) {
255 Some(*id)
256 } else {
257 None
258 }
259 })
260 .collect();
261
262 let mut removed = false;
263 for id in to_remove {
264 if let Some(vtext) = self.texts.remove(&id) {
265 marker_list.delete(vtext.marker_id);
266 removed = true;
267 }
268 }
269 removed
270 }
271
272 pub fn remove_by_prefix(&mut self, marker_list: &mut MarkerList, prefix: &str) {
274 let markers_to_delete: Vec<(VirtualTextId, MarkerId)> = self
276 .texts
277 .iter()
278 .filter_map(|(id, vtext)| {
279 if let Some(ref sid) = vtext.string_id {
280 if sid.starts_with(prefix) {
281 return Some((*id, vtext.marker_id));
282 }
283 }
284 None
285 })
286 .collect();
287
288 for (id, marker_id) in markers_to_delete {
290 marker_list.delete(marker_id);
291 self.texts.remove(&id);
292 }
293 }
294
295 pub fn remove(&mut self, marker_list: &mut MarkerList, id: VirtualTextId) -> bool {
297 if let Some(vtext) = self.texts.remove(&id) {
298 marker_list.delete(vtext.marker_id);
299 true
300 } else {
301 false
302 }
303 }
304
305 pub fn clear(&mut self, marker_list: &mut MarkerList) {
307 for vtext in self.texts.values() {
308 marker_list.delete(vtext.marker_id);
309 }
310 self.texts.clear();
311 }
312
313 pub fn len(&self) -> usize {
315 self.texts.len()
316 }
317
318 pub fn is_empty(&self) -> bool {
320 self.texts.is_empty()
321 }
322
323 pub fn query_range(
334 &self,
335 marker_list: &MarkerList,
336 start: usize,
337 end: usize,
338 ) -> Vec<(usize, &VirtualText)> {
339 let mut results: Vec<(usize, &VirtualText)> = self
340 .texts
341 .values()
342 .filter_map(|vtext| {
343 let pos = marker_list.get_position(vtext.marker_id)?;
344 if pos >= start && pos < end {
345 Some((pos, vtext))
346 } else {
347 None
348 }
349 })
350 .collect();
351
352 results.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.priority.cmp(&b.1.priority)));
354
355 results
356 }
357
358 pub fn build_lookup(
363 &self,
364 marker_list: &MarkerList,
365 start: usize,
366 end: usize,
367 ) -> HashMap<usize, Vec<&VirtualText>> {
368 let mut lookup: HashMap<usize, Vec<&VirtualText>> = HashMap::new();
369
370 for vtext in self.texts.values() {
371 if let Some(pos) = marker_list.get_position(vtext.marker_id) {
372 if pos >= start && pos < end {
373 lookup.entry(pos).or_default().push(vtext);
374 }
375 }
376 }
377
378 for texts in lookup.values_mut() {
380 texts.sort_by_key(|vt| vt.priority);
381 }
382
383 lookup
384 }
385
386 pub fn clear_namespace(
390 &mut self,
391 marker_list: &mut MarkerList,
392 namespace: &VirtualTextNamespace,
393 ) {
394 let to_remove: Vec<VirtualTextId> = self
395 .texts
396 .iter()
397 .filter_map(|(id, vtext)| {
398 if vtext.namespace.as_ref() == Some(namespace) {
399 Some(*id)
400 } else {
401 None
402 }
403 })
404 .collect();
405
406 for id in to_remove {
407 if let Some(vtext) = self.texts.remove(&id) {
408 marker_list.delete(vtext.marker_id);
409 }
410 }
411 }
412
413 pub fn query_lines_in_range(
418 &self,
419 marker_list: &MarkerList,
420 start: usize,
421 end: usize,
422 ) -> Vec<(usize, &VirtualText)> {
423 let mut results: Vec<(usize, &VirtualText)> = self
424 .texts
425 .values()
426 .filter(|vtext| vtext.position.is_line())
427 .filter_map(|vtext| {
428 let pos = marker_list.get_position(vtext.marker_id)?;
429 if pos >= start && pos < end {
430 Some((pos, vtext))
431 } else {
432 None
433 }
434 })
435 .collect();
436
437 results.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.priority.cmp(&b.1.priority)));
439
440 results
441 }
442
443 pub fn query_inline_in_range(
447 &self,
448 marker_list: &MarkerList,
449 start: usize,
450 end: usize,
451 ) -> Vec<(usize, &VirtualText)> {
452 let mut results: Vec<(usize, &VirtualText)> = self
453 .texts
454 .values()
455 .filter(|vtext| vtext.position.is_inline())
456 .filter_map(|vtext| {
457 let pos = marker_list.get_position(vtext.marker_id)?;
458 if pos >= start && pos < end {
459 Some((pos, vtext))
460 } else {
461 None
462 }
463 })
464 .collect();
465
466 results.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.priority.cmp(&b.1.priority)));
468
469 results
470 }
471
472 pub fn build_lines_lookup(
477 &self,
478 marker_list: &MarkerList,
479 start: usize,
480 end: usize,
481 ) -> HashMap<usize, Vec<&VirtualText>> {
482 let mut lookup: HashMap<usize, Vec<&VirtualText>> = HashMap::new();
483
484 for vtext in self.texts.values() {
485 if !vtext.position.is_line() {
486 continue;
487 }
488 if let Some(pos) = marker_list.get_position(vtext.marker_id) {
489 if pos >= start && pos < end {
490 lookup.entry(pos).or_default().push(vtext);
491 }
492 }
493 }
494
495 for texts in lookup.values_mut() {
497 texts.sort_by_key(|vt| vt.priority);
498 }
499
500 lookup
501 }
502}
503
504impl Default for VirtualTextManager {
505 fn default() -> Self {
506 Self::new()
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use ratatui::style::Color;
514
515 fn hint_style() -> Style {
516 Style::default().fg(Color::DarkGray)
517 }
518
519 #[test]
520 fn test_new_manager() {
521 let manager = VirtualTextManager::new();
522 assert_eq!(manager.len(), 0);
523 assert!(manager.is_empty());
524 }
525
526 #[test]
527 fn test_add_virtual_text() {
528 let mut marker_list = MarkerList::new();
529 let mut manager = VirtualTextManager::new();
530
531 let id = manager.add(
532 &mut marker_list,
533 10,
534 ": i32".to_string(),
535 hint_style(),
536 VirtualTextPosition::AfterChar,
537 0,
538 );
539
540 assert_eq!(manager.len(), 1);
541 assert!(!manager.is_empty());
542 assert_eq!(id.0, 0);
543 }
544
545 #[test]
546 fn test_remove_virtual_text() {
547 let mut marker_list = MarkerList::new();
548 let mut manager = VirtualTextManager::new();
549
550 let id = manager.add(
551 &mut marker_list,
552 10,
553 ": i32".to_string(),
554 hint_style(),
555 VirtualTextPosition::AfterChar,
556 0,
557 );
558
559 assert_eq!(manager.len(), 1);
560
561 let removed = manager.remove(&mut marker_list, id);
562 assert!(removed);
563 assert_eq!(manager.len(), 0);
564
565 assert_eq!(marker_list.marker_count(), 0);
567 }
568
569 #[test]
570 fn test_remove_nonexistent() {
571 let mut marker_list = MarkerList::new();
572 let mut manager = VirtualTextManager::new();
573
574 let removed = manager.remove(&mut marker_list, VirtualTextId(999));
575 assert!(!removed);
576 }
577
578 #[test]
579 fn test_clear() {
580 let mut marker_list = MarkerList::new();
581 let mut manager = VirtualTextManager::new();
582
583 manager.add(
584 &mut marker_list,
585 10,
586 ": i32".to_string(),
587 hint_style(),
588 VirtualTextPosition::AfterChar,
589 0,
590 );
591 manager.add(
592 &mut marker_list,
593 20,
594 ": String".to_string(),
595 hint_style(),
596 VirtualTextPosition::AfterChar,
597 0,
598 );
599
600 assert_eq!(manager.len(), 2);
601 assert_eq!(marker_list.marker_count(), 2);
602
603 manager.clear(&mut marker_list);
604
605 assert_eq!(manager.len(), 0);
606 assert_eq!(marker_list.marker_count(), 0);
607 }
608
609 #[test]
610 fn test_query_range() {
611 let mut marker_list = MarkerList::new();
612 let mut manager = VirtualTextManager::new();
613
614 manager.add(
616 &mut marker_list,
617 10,
618 ": i32".to_string(),
619 hint_style(),
620 VirtualTextPosition::AfterChar,
621 0,
622 );
623 manager.add(
624 &mut marker_list,
625 20,
626 ": String".to_string(),
627 hint_style(),
628 VirtualTextPosition::AfterChar,
629 0,
630 );
631 manager.add(
632 &mut marker_list,
633 30,
634 ": bool".to_string(),
635 hint_style(),
636 VirtualTextPosition::AfterChar,
637 0,
638 );
639
640 let results = manager.query_range(&marker_list, 15, 35);
642 assert_eq!(results.len(), 2);
643 assert_eq!(results[0].0, 20);
644 assert_eq!(results[0].1.text, ": String");
645 assert_eq!(results[1].0, 30);
646 assert_eq!(results[1].1.text, ": bool");
647
648 let results = manager.query_range(&marker_list, 0, 15);
650 assert_eq!(results.len(), 1);
651 assert_eq!(results[0].0, 10);
652 assert_eq!(results[0].1.text, ": i32");
653 }
654
655 #[test]
656 fn test_query_empty_range() {
657 let mut marker_list = MarkerList::new();
658 let mut manager = VirtualTextManager::new();
659
660 manager.add(
661 &mut marker_list,
662 10,
663 ": i32".to_string(),
664 hint_style(),
665 VirtualTextPosition::AfterChar,
666 0,
667 );
668
669 let results = manager.query_range(&marker_list, 100, 200);
671 assert!(results.is_empty());
672 }
673
674 #[test]
675 fn test_priority_ordering() {
676 let mut marker_list = MarkerList::new();
677 let mut manager = VirtualTextManager::new();
678
679 manager.add(
681 &mut marker_list,
682 10,
683 "low".to_string(),
684 hint_style(),
685 VirtualTextPosition::AfterChar,
686 0,
687 );
688 manager.add(
689 &mut marker_list,
690 10,
691 "high".to_string(),
692 hint_style(),
693 VirtualTextPosition::AfterChar,
694 10,
695 );
696 manager.add(
697 &mut marker_list,
698 10,
699 "medium".to_string(),
700 hint_style(),
701 VirtualTextPosition::AfterChar,
702 5,
703 );
704
705 let results = manager.query_range(&marker_list, 0, 20);
706 assert_eq!(results.len(), 3);
707 assert_eq!(results[0].1.text, "low");
709 assert_eq!(results[1].1.text, "medium");
710 assert_eq!(results[2].1.text, "high");
711 }
712
713 #[test]
714 fn test_build_lookup() {
715 let mut marker_list = MarkerList::new();
716 let mut manager = VirtualTextManager::new();
717
718 manager.add(
719 &mut marker_list,
720 10,
721 ": i32".to_string(),
722 hint_style(),
723 VirtualTextPosition::AfterChar,
724 0,
725 );
726 manager.add(
727 &mut marker_list,
728 10,
729 " = 5".to_string(),
730 hint_style(),
731 VirtualTextPosition::AfterChar,
732 1,
733 );
734 manager.add(
735 &mut marker_list,
736 20,
737 ": String".to_string(),
738 hint_style(),
739 VirtualTextPosition::AfterChar,
740 0,
741 );
742
743 let lookup = manager.build_lookup(&marker_list, 0, 30);
744
745 assert_eq!(lookup.len(), 2); let at_10 = lookup.get(&10).unwrap();
748 assert_eq!(at_10.len(), 2);
749 assert_eq!(at_10[0].text, ": i32"); assert_eq!(at_10[1].text, " = 5"); let at_20 = lookup.get(&20).unwrap();
753 assert_eq!(at_20.len(), 1);
754 assert_eq!(at_20[0].text, ": String");
755 }
756
757 #[test]
758 fn test_position_tracking_after_insert() {
759 let mut marker_list = MarkerList::new();
760 let mut manager = VirtualTextManager::new();
761
762 manager.add(
763 &mut marker_list,
764 10,
765 ": i32".to_string(),
766 hint_style(),
767 VirtualTextPosition::AfterChar,
768 0,
769 );
770
771 marker_list.adjust_for_insert(5, 5);
773
774 let results = manager.query_range(&marker_list, 0, 20);
776 assert_eq!(results.len(), 1);
777 assert_eq!(results[0].0, 15);
778 }
779
780 #[test]
781 fn test_position_tracking_after_delete() {
782 let mut marker_list = MarkerList::new();
783 let mut manager = VirtualTextManager::new();
784
785 manager.add(
786 &mut marker_list,
787 20,
788 ": i32".to_string(),
789 hint_style(),
790 VirtualTextPosition::AfterChar,
791 0,
792 );
793
794 marker_list.adjust_for_delete(10, 5);
796
797 let results = manager.query_range(&marker_list, 0, 20);
799 assert_eq!(results.len(), 1);
800 assert_eq!(results[0].0, 15);
801 }
802
803 #[test]
804 fn test_before_and_after_positions() {
805 let mut marker_list = MarkerList::new();
806 let mut manager = VirtualTextManager::new();
807
808 manager.add(
809 &mut marker_list,
810 10,
811 "/*param=*/".to_string(),
812 hint_style(),
813 VirtualTextPosition::BeforeChar,
814 0,
815 );
816 manager.add(
817 &mut marker_list,
818 10,
819 ": Type".to_string(),
820 hint_style(),
821 VirtualTextPosition::AfterChar,
822 0,
823 );
824
825 let lookup = manager.build_lookup(&marker_list, 0, 20);
826 let at_10 = lookup.get(&10).unwrap();
827
828 assert_eq!(at_10.len(), 2);
829 let before = at_10
831 .iter()
832 .find(|vt| vt.position == VirtualTextPosition::BeforeChar);
833 let after = at_10
834 .iter()
835 .find(|vt| vt.position == VirtualTextPosition::AfterChar);
836
837 assert!(before.is_some());
838 assert!(after.is_some());
839 assert_eq!(before.unwrap().text, "/*param=*/");
840 assert_eq!(after.unwrap().text, ": Type");
841 }
842}