1use regex::Regex;
25use serde::{Deserialize, Serialize};
26use std::fmt;
27use std::sync::LazyLock;
28
29use super::{BindingEntry, BindingSpec};
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub enum Mention {
34 Number(u32),
36 Last,
38 All,
40 Range { start: u32, end: u32 },
42}
43
44impl fmt::Display for Mention {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 Mention::Number(n) => write!(f, "@{}", n),
48 Mention::Last => write!(f, "@last"),
49 Mention::All => write!(f, "@all"),
50 Mention::Range { start, end } => write!(f, "@{}..{}", start, end),
51 }
52 }
53}
54
55static MENTION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
69 Regex::new(r"(?:^|[\s\[\](),:;])@(?:(\d+)\.\.(\d+)|(\d+)|(last|all))")
72 .expect("Invalid mention regex")
73});
74
75pub fn parse_mentions(text: &str) -> Vec<Mention> {
97 let mut mentions = Vec::new();
98
99 for cap in MENTION_REGEX.captures_iter(text) {
100 if let (Some(start), Some(end)) = (cap.get(1), cap.get(2)) {
101 let Ok(start) = start.as_str().parse::<u32>() else {
103 continue;
104 };
105 let Ok(end) = end.as_str().parse::<u32>() else {
106 continue;
107 };
108 mentions.push(Mention::Range { start, end });
109 } else if let Some(num) = cap.get(3) {
110 let Ok(n) = num.as_str().parse::<u32>() else {
112 continue;
113 };
114 mentions.push(Mention::Number(n));
115 } else if let Some(keyword) = cap.get(4) {
116 match keyword.as_str() {
118 "last" => mentions.push(Mention::Last),
119 "all" => mentions.push(Mention::All),
120 _ => {}
121 }
122 }
123 }
124
125 mentions
126}
127
128pub fn has_parallel_marker(text: &str) -> bool {
148 text.trim_start().starts_with("//")
149}
150
151pub fn strip_parallel_marker(text: &str) -> &str {
166 let trimmed = text.trim_start();
167 if let Some(stripped) = trimmed.strip_prefix("//") {
168 stripped.trim_start()
169 } else {
170 text
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
182pub enum ResolvedMention {
183 Single(u32),
185 Multiple(Vec<u32>),
187 Empty,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
193pub enum MentionResolutionError {
194 MessageNotFound { index: u32, max: u32 },
196 InvalidRange { start: u32, end: u32 },
198 NoMessages,
200}
201
202impl std::fmt::Display for MentionResolutionError {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 match self {
205 Self::MessageNotFound { index, max } => {
206 write!(f, "Message @{} not found (max: @{})", index, max)
207 }
208 Self::InvalidRange { start, end } => {
209 write!(f, "Invalid range @{}..{} (start > end)", start, end)
210 }
211 Self::NoMessages => write!(f, "No messages to reference"),
212 }
213 }
214}
215
216impl std::error::Error for MentionResolutionError {}
217
218pub fn resolve_mention(
242 mention: &Mention,
243 message_count: u32,
244) -> Result<ResolvedMention, MentionResolutionError> {
245 match mention {
246 Mention::Last => {
247 if message_count == 0 {
248 Err(MentionResolutionError::NoMessages)
249 } else {
250 Ok(ResolvedMention::Single(message_count))
251 }
252 }
253 Mention::Number(n) => {
254 if *n == 0 || *n > message_count {
255 Err(MentionResolutionError::MessageNotFound {
256 index: *n,
257 max: message_count,
258 })
259 } else {
260 Ok(ResolvedMention::Single(*n))
261 }
262 }
263 Mention::All => {
264 if message_count == 0 {
265 Ok(ResolvedMention::Empty)
266 } else {
267 Ok(ResolvedMention::Multiple((1..=message_count).collect()))
268 }
269 }
270 Mention::Range { start, end } => {
271 if start > end {
272 return Err(MentionResolutionError::InvalidRange {
273 start: *start,
274 end: *end,
275 });
276 }
277 if *start == 0 || *end > message_count {
278 return Err(MentionResolutionError::MessageNotFound {
279 index: if *start == 0 { 0 } else { *end },
280 max: message_count,
281 });
282 }
283 Ok(ResolvedMention::Multiple((*start..=*end).collect()))
284 }
285 }
286}
287
288pub fn mentions_to_bindings(resolved: &ResolvedMention) -> BindingSpec {
320 let mut spec = BindingSpec::default();
321
322 match resolved {
323 ResolvedMention::Single(n) => {
324 let alias = format!("ref_{}", n);
325 let path = format!("msg-{:03}.output", n);
326 spec.insert(alias, BindingEntry::new(path));
327 }
328 ResolvedMention::Multiple(indices) => {
329 for n in indices {
330 let alias = format!("ref_{}", n);
331 let path = format!("msg-{:03}.output", n);
332 spec.insert(alias, BindingEntry::new(path));
333 }
334 }
335 ResolvedMention::Empty => {
336 }
338 }
339
340 spec
341}
342
343pub fn text_to_bindings(
354 text: &str,
355 message_count: u32,
356) -> Result<BindingSpec, MentionResolutionError> {
357 let mut spec = BindingSpec::default();
358
359 for mention in parse_mentions(text) {
360 let resolved = resolve_mention(&mention, message_count)?;
361 spec.extend(mentions_to_bindings(&resolved));
362 }
363
364 Ok(spec)
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[test]
376 fn test_mention_module_exists() {
377 let _: Mention = Mention::Number(1);
378 }
379
380 #[test]
381 fn test_mention_number_stores_value() {
382 let m = Mention::Number(42);
383 if let Mention::Number(n) = m {
384 assert_eq!(n, 42);
385 } else {
386 panic!("Expected Number variant");
387 }
388 }
389
390 #[test]
391 fn test_mention_range_stores_bounds() {
392 let m = Mention::Range { start: 1, end: 5 };
393 if let Mention::Range { start, end } = m {
394 assert_eq!(start, 1);
395 assert_eq!(end, 5);
396 } else {
397 panic!("Expected Range variant");
398 }
399 }
400
401 #[test]
402 fn test_mention_equality() {
403 assert_eq!(Mention::Number(1), Mention::Number(1));
404 assert_ne!(Mention::Number(1), Mention::Number(2));
405 assert_eq!(Mention::Last, Mention::Last);
406 assert_eq!(Mention::All, Mention::All);
407 }
408
409 #[test]
410 fn test_mention_clone() {
411 let m = Mention::Range { start: 1, end: 10 };
412 let cloned = m.clone();
413 assert_eq!(m, cloned);
414 }
415
416 #[test]
417 fn test_mention_serialization() {
418 let m = Mention::Number(5);
419 let json = serde_json::to_string(&m).unwrap();
420 let restored: Mention = serde_json::from_str(&json).unwrap();
421 assert_eq!(m, restored);
422 }
423
424 #[test]
429 fn test_mention_display_number() {
430 assert_eq!(format!("{}", Mention::Number(1)), "@1");
431 assert_eq!(format!("{}", Mention::Number(42)), "@42");
432 assert_eq!(format!("{}", Mention::Number(999)), "@999");
433 }
434
435 #[test]
436 fn test_mention_display_last() {
437 assert_eq!(format!("{}", Mention::Last), "@last");
438 }
439
440 #[test]
441 fn test_mention_display_all() {
442 assert_eq!(format!("{}", Mention::All), "@all");
443 }
444
445 #[test]
446 fn test_mention_display_range() {
447 assert_eq!(format!("{}", Mention::Range { start: 1, end: 3 }), "@1..3");
448 assert_eq!(
449 format!("{}", Mention::Range { start: 10, end: 20 }),
450 "@10..20"
451 );
452 }
453
454 #[test]
459 fn test_parse_mentions_number() {
460 let text = "Look at @1 and @2";
461 let mentions = parse_mentions(text);
462 assert_eq!(mentions, vec![Mention::Number(1), Mention::Number(2)]);
463 }
464
465 #[test]
466 fn test_parse_mentions_at_start() {
467 let text = "@1 is the first message";
468 let mentions = parse_mentions(text);
469 assert_eq!(mentions, vec![Mention::Number(1)]);
470 }
471
472 #[test]
473 fn test_parse_mentions_last() {
474 let text = "Continue from @last";
475 let mentions = parse_mentions(text);
476 assert_eq!(mentions, vec![Mention::Last]);
477 }
478
479 #[test]
480 fn test_parse_mentions_all() {
481 let text = "Summarize @all";
482 let mentions = parse_mentions(text);
483 assert_eq!(mentions, vec![Mention::All]);
484 }
485
486 #[test]
487 fn test_parse_mentions_range() {
488 let text = "Combine @1..3";
489 let mentions = parse_mentions(text);
490 assert_eq!(mentions, vec![Mention::Range { start: 1, end: 3 }]);
491 }
492
493 #[test]
494 fn test_parse_mentions_mixed() {
495 let text = "Based on @1, @last, and @all";
496 let mentions = parse_mentions(text);
497 assert_eq!(
498 mentions,
499 vec![Mention::Number(1), Mention::Last, Mention::All,]
500 );
501 }
502
503 #[test]
504 fn test_parse_mentions_no_matches() {
505 let text = "No mentions here";
506 let mentions = parse_mentions(text);
507 assert!(mentions.is_empty());
508 }
509
510 #[test]
511 fn test_parse_mentions_after_punctuation() {
512 let text = "See (@1) and [@2]";
513 let mentions = parse_mentions(text);
514 assert_eq!(mentions, vec![Mention::Number(1), Mention::Number(2)]);
515 }
516
517 #[test]
518 fn test_parse_mentions_email_not_matched() {
519 let text = "Contact user@123.com for help";
521 let mentions = parse_mentions(text);
522 assert!(mentions.is_empty(), "Email should not be parsed as mention");
523 }
524
525 #[test]
526 fn test_parse_mentions_mixed_with_email() {
527 let text = "Check @123 after emailing user@456.com";
529 let mentions = parse_mentions(text);
530 assert_eq!(mentions, vec![Mention::Number(123)]);
531 }
532
533 #[test]
538 fn test_resolve_last_with_messages() {
539 let result = resolve_mention(&Mention::Last, 3);
541 assert_eq!(result, Ok(ResolvedMention::Single(3)));
542 }
543
544 #[test]
545 fn test_resolve_last_with_one_message() {
546 let result = resolve_mention(&Mention::Last, 1);
548 assert_eq!(result, Ok(ResolvedMention::Single(1)));
549 }
550
551 #[test]
552 fn test_resolve_last_with_no_messages() {
553 let result = resolve_mention(&Mention::Last, 0);
555 assert_eq!(result, Err(MentionResolutionError::NoMessages));
556 }
557
558 #[test]
559 fn test_resolve_number_valid() {
560 let result = resolve_mention(&Mention::Number(2), 3);
562 assert_eq!(result, Ok(ResolvedMention::Single(2)));
563 }
564
565 #[test]
566 fn test_resolve_number_first_message() {
567 let result = resolve_mention(&Mention::Number(1), 5);
569 assert_eq!(result, Ok(ResolvedMention::Single(1)));
570 }
571
572 #[test]
573 fn test_resolve_number_out_of_bounds() {
574 let result = resolve_mention(&Mention::Number(5), 3);
576 assert_eq!(
577 result,
578 Err(MentionResolutionError::MessageNotFound { index: 5, max: 3 })
579 );
580 }
581
582 #[test]
583 fn test_resolve_number_zero() {
584 let result = resolve_mention(&Mention::Number(0), 3);
586 assert_eq!(
587 result,
588 Err(MentionResolutionError::MessageNotFound { index: 0, max: 3 })
589 );
590 }
591
592 #[test]
597 fn test_resolve_all_with_messages() {
598 let result = resolve_mention(&Mention::All, 3);
600 assert_eq!(result, Ok(ResolvedMention::Multiple(vec![1, 2, 3])));
601 }
602
603 #[test]
604 fn test_resolve_all_with_one_message() {
605 let result = resolve_mention(&Mention::All, 1);
607 assert_eq!(result, Ok(ResolvedMention::Multiple(vec![1])));
608 }
609
610 #[test]
611 fn test_resolve_all_with_no_messages() {
612 let result = resolve_mention(&Mention::All, 0);
614 assert_eq!(result, Ok(ResolvedMention::Empty));
615 }
616
617 #[test]
622 fn test_resolve_range_valid() {
623 let result = resolve_mention(&Mention::Range { start: 1, end: 3 }, 5);
625 assert_eq!(result, Ok(ResolvedMention::Multiple(vec![1, 2, 3])));
626 }
627
628 #[test]
629 fn test_resolve_range_single() {
630 let result = resolve_mention(&Mention::Range { start: 2, end: 2 }, 3);
632 assert_eq!(result, Ok(ResolvedMention::Multiple(vec![2])));
633 }
634
635 #[test]
636 fn test_resolve_range_full() {
637 let result = resolve_mention(&Mention::Range { start: 1, end: 3 }, 3);
639 assert_eq!(result, Ok(ResolvedMention::Multiple(vec![1, 2, 3])));
640 }
641
642 #[test]
643 fn test_resolve_range_out_of_bounds() {
644 let result = resolve_mention(&Mention::Range { start: 1, end: 5 }, 3);
646 assert_eq!(
647 result,
648 Err(MentionResolutionError::MessageNotFound { index: 5, max: 3 })
649 );
650 }
651
652 #[test]
653 fn test_resolve_range_invalid_order() {
654 let result = resolve_mention(&Mention::Range { start: 3, end: 1 }, 5);
656 assert_eq!(
657 result,
658 Err(MentionResolutionError::InvalidRange { start: 3, end: 1 })
659 );
660 }
661
662 #[test]
663 fn test_resolve_range_zero_start() {
664 let result = resolve_mention(&Mention::Range { start: 0, end: 3 }, 5);
666 assert_eq!(
667 result,
668 Err(MentionResolutionError::MessageNotFound { index: 0, max: 5 })
669 );
670 }
671
672 #[test]
673 fn test_error_display() {
674 let err = MentionResolutionError::MessageNotFound { index: 5, max: 3 };
675 assert_eq!(format!("{}", err), "Message @5 not found (max: @3)");
676
677 let err = MentionResolutionError::InvalidRange { start: 3, end: 1 };
678 assert_eq!(format!("{}", err), "Invalid range @3..1 (start > end)");
679
680 let err = MentionResolutionError::NoMessages;
681 assert_eq!(format!("{}", err), "No messages to reference");
682 }
683
684 #[test]
689 fn test_has_parallel_marker_basic() {
690 assert!(has_parallel_marker("// Independent task"));
691 assert!(has_parallel_marker("//Task"));
692 }
693
694 #[test]
695 fn test_has_parallel_marker_with_whitespace() {
696 assert!(has_parallel_marker(" // Also parallel"));
697 assert!(has_parallel_marker("\t// Tab prefixed"));
698 }
699
700 #[test]
701 fn test_has_parallel_marker_false() {
702 assert!(!has_parallel_marker("Normal message"));
703 assert!(!has_parallel_marker("@1 Reference"));
704 assert!(!has_parallel_marker("/ Single slash"));
705 assert!(!has_parallel_marker(""));
706 }
707
708 #[test]
709 fn test_has_parallel_marker_not_url() {
710 assert!(!has_parallel_marker("https://example.com"));
712 assert!(!has_parallel_marker("http://localhost"));
713 }
714
715 #[test]
716 fn test_strip_parallel_marker_basic() {
717 assert_eq!(strip_parallel_marker("// Task"), "Task");
718 assert_eq!(strip_parallel_marker("//Task"), "Task");
719 }
720
721 #[test]
722 fn test_strip_parallel_marker_with_whitespace() {
723 assert_eq!(
724 strip_parallel_marker(" // Parallel work"),
725 "Parallel work"
726 );
727 assert_eq!(strip_parallel_marker("\t// Tab"), "Tab");
728 }
729
730 #[test]
731 fn test_strip_parallel_marker_no_marker() {
732 assert_eq!(strip_parallel_marker("Normal message"), "Normal message");
733 assert_eq!(strip_parallel_marker("@1 Reference"), "@1 Reference");
734 }
735
736 #[test]
737 fn test_strip_parallel_marker_empty() {
738 assert_eq!(strip_parallel_marker("//"), "");
739 assert_eq!(strip_parallel_marker("// "), "");
740 }
741
742 #[test]
747 fn test_mentions_to_bindings_single() {
748 let spec = mentions_to_bindings(&ResolvedMention::Single(2));
749 assert_eq!(spec.len(), 1);
750 assert!(spec.contains_key("ref_2"));
751 assert_eq!(spec["ref_2"].path, "msg-002.output");
752 }
753
754 #[test]
755 fn test_mentions_to_bindings_single_large_number() {
756 let spec = mentions_to_bindings(&ResolvedMention::Single(123));
757 assert_eq!(spec.len(), 1);
758 assert!(spec.contains_key("ref_123"));
759 assert_eq!(spec["ref_123"].path, "msg-123.output");
760 }
761
762 #[test]
763 fn test_mentions_to_bindings_multiple() {
764 let spec = mentions_to_bindings(&ResolvedMention::Multiple(vec![1, 2, 3]));
765 assert_eq!(spec.len(), 3);
766 assert_eq!(spec["ref_1"].path, "msg-001.output");
767 assert_eq!(spec["ref_2"].path, "msg-002.output");
768 assert_eq!(spec["ref_3"].path, "msg-003.output");
769 }
770
771 #[test]
772 fn test_mentions_to_bindings_empty() {
773 let spec = mentions_to_bindings(&ResolvedMention::Empty);
774 assert!(spec.is_empty());
775 }
776
777 #[test]
778 fn test_mentions_to_bindings_entry_is_eager() {
779 let spec = mentions_to_bindings(&ResolvedMention::Single(1));
781 assert!(!spec["ref_1"].lazy);
782 assert!(spec["ref_1"].default.is_none());
783 }
784
785 #[test]
786 fn test_text_to_bindings_simple() {
787 let spec = text_to_bindings("Based on @1", 3).unwrap();
788 assert_eq!(spec.len(), 1);
789 assert!(spec.contains_key("ref_1"));
790 }
791
792 #[test]
793 fn test_text_to_bindings_multiple() {
794 let spec = text_to_bindings("Combine @1 and @2", 3).unwrap();
795 assert_eq!(spec.len(), 2);
796 assert!(spec.contains_key("ref_1"));
797 assert!(spec.contains_key("ref_2"));
798 }
799
800 #[test]
801 fn test_text_to_bindings_with_last() {
802 let spec = text_to_bindings("Continue from @last", 5).unwrap();
803 assert_eq!(spec.len(), 1);
804 assert!(spec.contains_key("ref_5")); }
806
807 #[test]
808 fn test_text_to_bindings_with_range() {
809 let spec = text_to_bindings("Summarize @1..3", 5).unwrap();
810 assert_eq!(spec.len(), 3);
811 assert!(spec.contains_key("ref_1"));
812 assert!(spec.contains_key("ref_2"));
813 assert!(spec.contains_key("ref_3"));
814 }
815
816 #[test]
817 fn test_text_to_bindings_with_all() {
818 let spec = text_to_bindings("Based on @all", 3).unwrap();
819 assert_eq!(spec.len(), 3);
820 assert!(spec.contains_key("ref_1"));
821 assert!(spec.contains_key("ref_2"));
822 assert!(spec.contains_key("ref_3"));
823 }
824
825 #[test]
826 fn test_text_to_bindings_no_mentions() {
827 let spec = text_to_bindings("Just a normal message", 5).unwrap();
828 assert!(spec.is_empty());
829 }
830
831 #[test]
832 fn test_text_to_bindings_error_out_of_bounds() {
833 let result = text_to_bindings("Reference @10", 3);
834 assert!(result.is_err());
835 assert_eq!(
836 result.unwrap_err(),
837 MentionResolutionError::MessageNotFound { index: 10, max: 3 }
838 );
839 }
840
841 #[test]
842 fn test_text_to_bindings_dedup() {
843 let spec = text_to_bindings("See @1 and again @1", 3).unwrap();
845 assert_eq!(spec.len(), 1); assert!(spec.contains_key("ref_1"));
847 }
848}