1use fuzzy_matcher::FuzzyMatcher;
2use fuzzy_matcher::skim::SkimMatcherV2;
3use std::borrow::Cow;
4
5use crate::item_data::Ref;
6use crate::item_data::Rev;
7use crate::text_input::TextInput;
8
9#[derive(Debug, Clone, PartialEq)]
11pub enum PickerData {
12 Item(String),
14 CustomInput(String),
16}
17
18impl PickerData {
19 pub fn display(&self) -> &str {
20 match self {
21 PickerData::Item(s) | PickerData::CustomInput(s) => s,
22 }
23 }
24}
25
26#[derive(Debug, Clone, PartialEq)]
28pub struct PickerItem {
29 pub display: Cow<'static, str>,
31 pub data: PickerData,
33}
34
35impl PickerItem {
36 pub fn new(display: impl Into<Cow<'static, str>>, data: PickerData) -> Self {
37 Self {
38 display: display.into(),
39 data,
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
46struct MatchResult {
47 index: usize,
48 score: i64,
49}
50
51#[derive(Debug, Clone, PartialEq)]
53pub enum PickerStatus {
54 Active,
56 Done,
58 Cancelled,
60}
61
62pub struct PickerState {
64 items: Vec<PickerItem>,
66 filtered_indices: Vec<usize>,
68 cursor: usize,
70 pub(crate) input_state: TextInput,
72 matcher: SkimMatcherV2,
74 pub prompt_text: Cow<'static, str>,
76 status: PickerStatus,
78 allow_custom_input: bool,
80 custom_input_item: Option<PickerItem>,
82}
83
84pub(crate) struct PickerParams<'a> {
85 pub(crate) prompt: Cow<'static, str>,
86 pub(crate) refs: &'a [Ref],
87 pub(crate) exclude_ref: Option<Ref>,
88 pub(crate) default: Option<Rev>,
89 pub(crate) allow_custom_input: bool,
90}
91
92impl PickerState {
93 pub fn new(
95 prompt: impl Into<Cow<'static, str>>,
96 items: Vec<PickerItem>,
97 allow_custom_input: bool,
98 ) -> Self {
99 let mut state = Self {
100 items: items.clone(),
101 filtered_indices: Vec::new(),
102 cursor: 0,
103 input_state: TextInput::default(),
104 matcher: SkimMatcherV2::default(),
105 prompt_text: prompt.into(),
106 status: PickerStatus::Active,
107 allow_custom_input,
108 custom_input_item: None,
109 };
110 state.update_filter();
111 state
112 }
113
114 pub(crate) fn with_branches(params: PickerParams) -> Self {
117 let default_name = params.default.as_ref().map(|r| r.shorthand().to_string());
118 let exclude_name = params
119 .exclude_ref
120 .as_ref()
121 .map(|r| r.shorthand().to_string());
122
123 let items = default_name
124 .iter()
125 .map(|name| PickerItem::new(name.clone(), PickerData::Item(name.clone())))
126 .chain(
127 params
128 .refs
129 .iter()
130 .filter(|r| matches!(r, Ref::Head(_)))
131 .filter(|r| exclude_name.as_deref().is_none_or(|e| e != r.shorthand()))
132 .filter(|r| default_name.as_deref().is_none_or(|d| d != r.shorthand()))
133 .map(|r| {
134 let name = r.shorthand().to_string();
135 PickerItem::new(name.clone(), PickerData::Item(name))
136 }),
137 )
138 .collect();
139
140 Self::new(params.prompt, items, params.allow_custom_input)
141 }
142
143 pub(crate) fn with_refs(params: PickerParams) -> Self {
146 let ref_to_item = |r: &Ref| {
147 let (display, refname) = match r {
148 Ref::Tag(name) => (format!("tag: {}", name), r.to_full_refname()),
149 _ => (r.shorthand().to_string(), r.shorthand().to_string()),
150 };
151 PickerItem::new(display, PickerData::Item(refname))
152 };
153
154 let default_item = params.default.as_ref().map(|default| match default {
155 Rev::Ref(r) => ref_to_item(r),
156 Rev::Commit(c) => PickerItem::new(c.clone(), PickerData::Item(c.clone())),
157 });
158
159 let items = default_item
160 .into_iter()
161 .chain(
162 params
163 .refs
164 .iter()
165 .filter(|r| {
166 params.exclude_ref.as_ref().is_none_or(|e| e != *r)
167 && params.default.as_ref().is_none_or(|d| match d {
168 Rev::Ref(d) => d != *r,
169 _ => true,
170 })
171 })
172 .map(ref_to_item),
173 )
174 .collect();
175
176 Self::new(params.prompt, items, params.allow_custom_input)
177 }
178
179 pub fn pattern(&self) -> &str {
181 &self.input_state.value
182 }
183
184 pub fn update_filter(&mut self) {
186 let pattern = self.pattern().to_string();
187
188 if pattern.is_empty() {
189 self.filtered_indices = (0..self.items.len()).collect();
191 self.custom_input_item = None;
192 } else {
193 let mut matches: Vec<MatchResult> = self
195 .items
196 .iter()
197 .enumerate()
198 .filter_map(|(i, item)| {
199 self.matcher
200 .fuzzy_match(&item.display, &pattern)
201 .map(|score| MatchResult { index: i, score })
202 })
203 .collect();
204
205 matches.sort_by_key(|b| std::cmp::Reverse(b.score));
207
208 self.filtered_indices = matches.into_iter().map(|m| m.index).collect();
209
210 if self.allow_custom_input {
212 self.custom_input_item = Some(PickerItem::new(
213 pattern.clone(),
214 PickerData::CustomInput(pattern),
215 ));
216 } else {
217 self.custom_input_item = None;
218 }
219 }
220
221 let total_count = self.filtered_indices.len()
223 + if self.custom_input_item.is_some() {
224 1
225 } else {
226 0
227 };
228 if self.cursor >= total_count {
229 self.cursor = 0;
230 }
231 }
232
233 pub fn selected(&self) -> Option<&PickerItem> {
235 if self.cursor == self.filtered_indices.len() {
237 return self.custom_input_item.as_ref();
238 }
239
240 self.filtered_indices
241 .get(self.cursor)
242 .and_then(|&i| self.items.get(i))
243 }
244
245 pub fn filtered_items(&self) -> impl Iterator<Item = (usize, &PickerItem)> {
248 self.filtered_indices
249 .iter()
250 .filter_map(|&i| self.items.get(i).map(|item| (i, item)))
251 .chain(
252 self.custom_input_item
253 .as_ref()
254 .map(|item| (usize::MAX, item)),
255 )
256 }
257
258 pub fn next(&mut self) {
260 let total_count = self.filtered_indices.len()
261 + if self.custom_input_item.is_some() {
262 1
263 } else {
264 0
265 };
266 if total_count > 0 {
267 self.cursor = (self.cursor + 1) % total_count;
268 }
269 }
270
271 pub fn previous(&mut self) {
273 let total_count = self.filtered_indices.len()
274 + if self.custom_input_item.is_some() {
275 1
276 } else {
277 0
278 };
279 if total_count > 0 {
280 self.cursor = if self.cursor == 0 {
281 total_count - 1
282 } else {
283 self.cursor - 1
284 };
285 }
286 }
287
288 pub fn cursor(&self) -> usize {
290 self.cursor
291 }
292
293 pub fn total_items(&self) -> usize {
295 self.items.len()
296 }
297
298 pub fn filtered_count(&self) -> usize {
300 self.filtered_indices.len()
301 }
302
303 pub fn match_indices(&self, item_index: usize) -> Option<Vec<usize>> {
305 let pattern = self.pattern();
306 if pattern.is_empty() {
307 return None;
308 }
309
310 if item_index == usize::MAX {
312 return None;
313 }
314
315 self.items
316 .get(item_index)
317 .and_then(|item| self.matcher.fuzzy_indices(&item.display, pattern))
318 .map(|(_, indices)| indices)
319 }
320
321 pub fn status(&self) -> &PickerStatus {
323 &self.status
324 }
325
326 pub fn done(&mut self) {
328 self.status = PickerStatus::Done;
329 }
330
331 pub fn cancel(&mut self) {
333 self.status = PickerStatus::Cancelled;
334 }
335
336 pub fn is_done(&self) -> bool {
338 self.status == PickerStatus::Done
339 }
340
341 pub fn is_cancelled(&self) -> bool {
343 self.status == PickerStatus::Cancelled
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
351
352 fn create_test_items() -> Vec<PickerItem> {
353 vec![
354 PickerItem::new("main", PickerData::Item("main".to_string())),
355 PickerItem::new("develop", PickerData::Item("develop".to_string())),
356 PickerItem::new("feature/test", PickerData::Item("feature/test".to_string())),
357 PickerItem::new("feature/new", PickerData::Item("feature/new".to_string())),
358 PickerItem::new("bugfix/123", PickerData::Item("bugfix/123".to_string())),
359 ]
360 }
361
362 #[test]
363 fn test_picker_data_display() {
364 let revision = PickerData::Item("main".to_string());
365 assert_eq!(revision.display(), "main");
366
367 let custom = PickerData::CustomInput("custom".to_string());
368 assert_eq!(custom.display(), "custom");
369 }
370
371 #[test]
372 fn test_picker_item_new() {
373 let item = PickerItem::new("main", PickerData::Item("main".to_string()));
374 assert_eq!(item.display.as_ref(), "main");
375 assert_eq!(item.data.display(), "main");
376 }
377
378 #[test]
379 fn test_picker_state_new_without_custom_input() {
380 let items = create_test_items();
381 let state = PickerState::new("Select branch", items.clone(), false);
382
383 assert_eq!(state.prompt_text.as_ref(), "Select branch");
384 assert_eq!(state.total_items(), 5);
385 assert_eq!(state.filtered_count(), 5);
386 assert_eq!(state.cursor(), 0);
387 assert_eq!(state.pattern(), "");
388 assert_eq!(state.status(), &PickerStatus::Active);
389 assert!(!state.allow_custom_input);
390 }
391
392 #[test]
393 fn test_picker_state_new_with_custom_input() {
394 let items = create_test_items();
395 let state = PickerState::new("Select branch", items, true);
396
397 assert!(state.allow_custom_input);
398 assert_eq!(state.custom_input_item, None); }
400
401 #[test]
402 fn test_empty_pattern_shows_all_items() {
403 let items = create_test_items();
404 let state = PickerState::new("Select", items, false);
405
406 assert_eq!(state.filtered_count(), 5);
407 assert_eq!(state.filtered_indices, vec![0, 1, 2, 3, 4]);
408 }
409
410 #[test]
411 fn test_fuzzy_filtering() {
412 let items = create_test_items();
413 let mut state = PickerState::new("Select", items, false);
414
415 state
416 .input_state
417 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
418 state.update_filter();
419
420 assert_eq!(state.filtered_count(), 3);
422
423 let filtered: Vec<_> = state.filtered_items().collect();
424 assert!(
425 filtered
426 .iter()
427 .any(|(_, item)| item.display == "feature/test")
428 );
429 assert!(
430 filtered
431 .iter()
432 .any(|(_, item)| item.display == "feature/new")
433 );
434 assert!(
435 filtered
436 .iter()
437 .any(|(_, item)| item.display == "bugfix/123")
438 );
439 }
440
441 #[test]
442 fn test_fuzzy_filtering_sorts_by_score() {
443 let items = vec![
445 PickerItem::new("feat", PickerData::Item("feat".to_string())), PickerItem::new("feature", PickerData::Item("feature".to_string())), PickerItem::new("feature/test", PickerData::Item("feature/test".to_string())), PickerItem::new("fix-eat-bug", PickerData::Item("fix-eat-bug".to_string())), ];
450 let mut state = PickerState::new("Select", items, false);
451
452 state
454 .input_state
455 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
456 state
457 .input_state
458 .handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
459 state
460 .input_state
461 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
462 state
463 .input_state
464 .handle_key_event(KeyEvent::new(KeyCode::Char('t'), KeyModifiers::empty()));
465 state.update_filter();
466
467 assert_eq!(state.filtered_count(), 4);
469
470 let filtered: Vec<_> = state
472 .filtered_items()
473 .map(|(_, item)| item.display.as_ref())
474 .collect();
475
476 assert_eq!(filtered[0], "feat");
478 assert_eq!(filtered[filtered.len() - 1], "fix-eat-bug");
479 }
480
481 #[test]
482 fn test_case_insensitive_matching() {
483 let items = vec![
484 PickerItem::new("Feature", PickerData::Item("Feature".to_string())),
485 PickerItem::new("feature", PickerData::Item("feature".to_string())),
486 PickerItem::new("FEATURE", PickerData::Item("FEATURE".to_string())),
487 ];
488
489 let mut state = PickerState::new("Select item", items, false);
490
491 state
492 .input_state
493 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
494 state
495 .input_state
496 .handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
497 state
498 .input_state
499 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
500 state.update_filter();
501
502 assert_eq!(state.filtered_count(), 3);
504 }
505
506 #[test]
507 fn test_no_matches() {
508 let items = create_test_items();
509 let mut state = PickerState::new("Select", items, false);
510
511 state
512 .input_state
513 .handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::empty()));
514 state
515 .input_state
516 .handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty()));
517 state
518 .input_state
519 .handle_key_event(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::empty()));
520 state.update_filter();
521
522 assert_eq!(state.filtered_count(), 0);
523 assert!(state.selected().is_none());
524 }
525
526 #[test]
527 fn test_custom_input_creation() {
528 let items = create_test_items();
529 let mut state = PickerState::new("Select", items, true);
530
531 state
533 .input_state
534 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
535 state
536 .input_state
537 .handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
538 state
539 .input_state
540 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
541 state.update_filter();
542
543 assert!(state.custom_input_item.is_some());
545
546 let filtered: Vec<_> = state.filtered_items().collect();
548 assert!(filtered.len() >= 3); let last = filtered.last().unwrap();
552 assert_eq!(last.0, usize::MAX);
553 assert_eq!(last.1.display.as_ref(), "fea");
554 }
555
556 #[test]
557 fn test_custom_input_not_created_when_disabled() {
558 let items = create_test_items();
559 let mut state = PickerState::new("Select", items, false);
560
561 state
562 .input_state
563 .handle_key_event(KeyEvent::new(KeyCode::Char('t'), KeyModifiers::empty()));
564 state
565 .input_state
566 .handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
567 state.update_filter();
568
569 assert!(state.custom_input_item.is_none());
570 }
571
572 #[test]
573 fn test_custom_input_not_created_on_empty_pattern() {
574 let items = create_test_items();
575 let mut state = PickerState::new("Select", items, true);
576
577 assert!(state.custom_input_item.is_none());
579
580 state
582 .input_state
583 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
584 state.update_filter();
585 assert!(state.custom_input_item.is_some());
586
587 state
588 .input_state
589 .handle_key_event(KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty()));
590 state.update_filter();
591 assert!(state.custom_input_item.is_none());
592 }
593
594 #[test]
595 fn test_cursor_next() {
596 let items = create_test_items();
597 let mut state = PickerState::new("Select", items, false);
598
599 assert_eq!(state.cursor(), 0);
600
601 state.next();
602 assert_eq!(state.cursor(), 1);
603
604 state.next();
605 assert_eq!(state.cursor(), 2);
606 }
607
608 #[test]
609 fn test_cursor_next_wraps_around() {
610 let items = create_test_items();
611 let mut state = PickerState::new("Select", items, false);
612
613 for _ in 0..5 {
615 state.next();
616 }
617
618 assert_eq!(state.cursor(), 0);
620 }
621
622 #[test]
623 fn test_cursor_previous() {
624 let items = create_test_items();
625 let mut state = PickerState::new("Select", items, false);
626
627 state.next();
628 state.next();
629 assert_eq!(state.cursor(), 2);
630
631 state.previous();
632 assert_eq!(state.cursor(), 1);
633
634 state.previous();
635 assert_eq!(state.cursor(), 0);
636 }
637
638 #[test]
639 fn test_cursor_previous_wraps_around() {
640 let items = create_test_items();
641 let mut state = PickerState::new("Select", items, false);
642
643 assert_eq!(state.cursor(), 0);
644
645 state.previous();
646 assert_eq!(state.cursor(), 4);
648 }
649
650 #[test]
651 fn test_cursor_with_custom_input() {
652 let items = create_test_items();
653 let mut state = PickerState::new("Select", items, true);
654
655 state
656 .input_state
657 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
658 state.update_filter();
659
660 assert_eq!(state.cursor(), 0);
662
663 state.next();
664 assert_eq!(state.cursor(), 1);
665
666 state.next();
667 assert_eq!(state.cursor(), 2);
668
669 state.next();
670 assert_eq!(state.cursor(), 3); state.next();
673 assert_eq!(state.cursor(), 0); state.previous();
676 assert_eq!(state.cursor(), 3); }
678
679 #[test]
680 fn test_navigation_with_custom_input_at_end() {
681 let items = vec![
682 PickerItem::new("feature/a", PickerData::Item("feature/a".to_string())),
683 PickerItem::new("feature/b", PickerData::Item("feature/b".to_string())),
684 ];
685
686 let mut state = PickerState::new("Select", items, true);
687
688 state
690 .input_state
691 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
692 state
693 .input_state
694 .handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
695 state
696 .input_state
697 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
698 state
699 .input_state
700 .handle_key_event(KeyEvent::new(KeyCode::Char('t'), KeyModifiers::empty()));
701 state.update_filter();
702
703 assert_eq!(state.filtered_count(), 2);
705 assert_eq!(state.cursor(), 0);
706
707 let selected = state.selected().unwrap();
709 match &selected.data {
710 PickerData::Item(_) => {}
711 _ => panic!("Expected first item to be a revision"),
712 }
713
714 state.next();
716 assert_eq!(state.cursor(), 1);
717
718 state.next();
720 assert_eq!(state.cursor(), 2);
721 let selected = state.selected().unwrap();
722 match &selected.data {
723 PickerData::CustomInput(s) => assert_eq!(s, "feat"),
724 _ => panic!("Expected custom input at end"),
725 }
726
727 state.next();
729 assert_eq!(state.cursor(), 0);
730 }
731
732 #[test]
733 fn test_cursor_resets_when_filter_reduces_items() {
734 let items = create_test_items();
735 let mut state = PickerState::new("Select", items, false);
736
737 for _ in 0..4 {
739 state.next();
740 }
741 assert_eq!(state.cursor(), 4);
742
743 state
745 .input_state
746 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
747 state.update_filter();
748
749 assert_eq!(state.cursor(), 0);
751 }
752
753 #[test]
754 fn test_scroll_through_many_items() {
755 let items: Vec<_> = (0..20)
757 .map(|i| {
758 PickerItem::new(
759 format!("branch-{:02}", i),
760 PickerData::Item(format!("branch-{:02}", i)),
761 )
762 })
763 .collect();
764
765 let mut state = PickerState::new("Select", items, false);
766
767 assert_eq!(state.cursor(), 0);
769
770 for _ in 0..10 {
772 state.next();
773 }
774 assert_eq!(state.cursor(), 10);
775
776 for _ in 0..9 {
778 state.next();
779 }
780 assert_eq!(state.cursor(), 19);
781
782 state.next();
784 assert_eq!(state.cursor(), 0);
785
786 state.previous();
788 assert_eq!(state.cursor(), 19);
789 }
790
791 #[test]
792 fn test_navigation_after_filtering() {
793 let items = vec![
794 PickerItem::new("feature/a", PickerData::Item("feature/a".to_string())),
795 PickerItem::new("feature/b", PickerData::Item("feature/b".to_string())),
796 PickerItem::new("main", PickerData::Item("main".to_string())),
797 PickerItem::new("develop", PickerData::Item("develop".to_string())),
798 ];
799
800 let mut state = PickerState::new("Select", items, false);
801
802 state
804 .input_state
805 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
806 state
807 .input_state
808 .handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
809 state
810 .input_state
811 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
812 state.update_filter();
813
814 assert_eq!(state.filtered_count(), 2);
816 assert_eq!(state.cursor(), 0);
817
818 state.next();
820 assert_eq!(state.cursor(), 1);
821
822 state.next();
824 assert_eq!(state.cursor(), 0);
825
826 state.previous();
828 assert_eq!(state.cursor(), 1);
829 }
830
831 #[test]
832 fn test_selected_returns_correct_item() {
833 let items = create_test_items();
834 let mut state = PickerState::new("Select", items, false);
835
836 let selected = state.selected().unwrap();
837 assert_eq!(selected.display.as_ref(), "main");
838
839 state.next();
840 state.next();
841 let selected = state.selected().unwrap();
842 assert_eq!(selected.display.as_ref(), "feature/test");
843 }
844
845 #[test]
846 fn test_selected_with_filter() {
847 let items = create_test_items();
848 let mut state = PickerState::new("Select", items, false);
849
850 state
851 .input_state
852 .handle_key_event(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()));
853 state.update_filter();
854
855 let selected = state.selected().unwrap();
857 assert_eq!(selected.display.as_ref(), "bugfix/123");
858 }
859
860 #[test]
861 fn test_selected_returns_custom_input() {
862 let items = create_test_items();
863 let mut state = PickerState::new("Select", items, true);
864
865 state
867 .input_state
868 .handle_key_event(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty()));
869 state
870 .input_state
871 .handle_key_event(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty()));
872 state.update_filter();
873
874 assert_eq!(state.cursor(), 0);
876 assert_eq!(state.selected().unwrap().display.as_ref(), "qq");
877 }
878
879 #[test]
880 fn test_select_custom_input() {
881 let items = create_test_items();
882 let mut state = PickerState::new("Select", items, true);
883
884 state
886 .input_state
887 .handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::empty()));
888 state
889 .input_state
890 .handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty()));
891 state
892 .input_state
893 .handle_key_event(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::empty()));
894 state.update_filter();
895
896 assert_eq!(state.filtered_count(), 0);
898 assert!(state.custom_input_item.is_some());
899
900 assert_eq!(state.cursor(), 0);
902
903 let selected = state.selected().unwrap();
905 assert_eq!(selected.display, "xyz");
906 match &selected.data {
907 PickerData::CustomInput(s) => assert_eq!(s, "xyz"),
908 _ => panic!("Expected CustomInput"),
909 }
910
911 state.done();
913 assert!(state.is_done());
914 assert_eq!(state.status(), &PickerStatus::Done);
915 }
916
917 #[test]
918 fn test_filtered_items_order() {
919 let items = create_test_items();
920 let mut state = PickerState::new("Select", items, true);
921
922 state
923 .input_state
924 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
925 state.update_filter();
926
927 let indices: Vec<_> = state.filtered_items().map(|(idx, _)| idx).collect();
928
929 assert_eq!(indices[0], 2); assert_eq!(indices[1], 3); assert_eq!(indices[2], 4); assert_eq!(indices[3], usize::MAX); }
935
936 #[test]
937 fn test_match_indices_with_pattern() {
938 let items = create_test_items();
939 let mut state = PickerState::new("Select", items, false);
940
941 state
942 .input_state
943 .handle_key_event(KeyEvent::new(KeyCode::Char('m'), KeyModifiers::empty()));
944 state
945 .input_state
946 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
947 state
948 .input_state
949 .handle_key_event(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::empty()));
950 state.update_filter();
951
952 let indices = state.match_indices(0);
954 assert!(indices.is_some());
955
956 let indices = indices.unwrap();
957 assert_eq!(indices.len(), 3); }
959
960 #[test]
961 fn test_match_indices_empty_pattern() {
962 let items = create_test_items();
963 let state = PickerState::new("Select", items, false);
964
965 let indices = state.match_indices(0);
966 assert!(indices.is_none());
967 }
968
969 #[test]
970 fn test_match_indices_custom_input_returns_none() {
971 let items = create_test_items();
972 let mut state = PickerState::new("Select", items, true);
973
974 state
975 .input_state
976 .handle_key_event(KeyEvent::new(KeyCode::Char('t'), KeyModifiers::empty()));
977 state.update_filter();
978
979 let indices = state.match_indices(usize::MAX);
981 assert!(indices.is_none());
982 }
983
984 #[test]
985 fn test_status_transitions() {
986 let items = create_test_items();
987 let mut state = PickerState::new("Select", items, false);
988
989 assert_eq!(state.status(), &PickerStatus::Active);
990 assert!(!state.is_done());
991 assert!(!state.is_cancelled());
992
993 state.done();
994 assert_eq!(state.status(), &PickerStatus::Done);
995 assert!(state.is_done());
996 assert!(!state.is_cancelled());
997 }
998
999 #[test]
1000 fn test_status_cancelled() {
1001 let items = create_test_items();
1002 let mut state = PickerState::new("Select", items, false);
1003
1004 state.cancel();
1005 assert_eq!(state.status(), &PickerStatus::Cancelled);
1006 assert!(!state.is_done());
1007 assert!(state.is_cancelled());
1008 }
1009
1010 #[test]
1011 fn test_empty_items_list() {
1012 let state = PickerState::new("Select", vec![], false);
1013
1014 assert_eq!(state.total_items(), 0);
1015 assert_eq!(state.filtered_count(), 0);
1016 assert_eq!(state.cursor(), 0);
1017 assert!(state.selected().is_none());
1018 }
1019
1020 #[test]
1021 fn test_empty_items_with_custom_input() {
1022 let mut state = PickerState::new("Select", vec![], true);
1023
1024 state
1025 .input_state
1026 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
1027 state.update_filter();
1028
1029 assert_eq!(state.total_items(), 0);
1030 assert_eq!(state.filtered_count(), 0);
1031 assert!(state.custom_input_item.is_some());
1032
1033 assert_eq!(state.selected().unwrap().display.as_ref(), "a");
1034 }
1035
1036 #[test]
1037 fn test_cursor_navigation_empty_list() {
1038 let mut state = PickerState::new("Select", vec![], false);
1039
1040 state.next();
1041 assert_eq!(state.cursor(), 0);
1042
1043 state.previous();
1044 assert_eq!(state.cursor(), 0);
1045 }
1046
1047 #[test]
1048 fn test_single_item_navigation() {
1049 let items = vec![PickerItem::new(
1050 "only",
1051 PickerData::Item("only".to_string()),
1052 )];
1053 let mut state = PickerState::new("Select", items, false);
1054
1055 assert_eq!(state.cursor(), 0);
1056
1057 state.next();
1058 assert_eq!(state.cursor(), 0); state.previous();
1061 assert_eq!(state.cursor(), 0); }
1063
1064 #[test]
1065 fn test_with_refs_basic_sorting() {
1066 let refs = &[
1068 Ref::Tag("v1.0.0".to_string()),
1069 Ref::Remote("origin/main".to_string()),
1070 Ref::Head("feature".to_string()),
1071 Ref::Head("main".to_string()),
1072 Ref::Tag("v2.0.0".to_string()),
1073 ];
1074
1075 let state = PickerState::with_refs(PickerParams {
1076 prompt: "Select".into(),
1077 refs,
1078 exclude_ref: None,
1079 default: None,
1080 allow_custom_input: false,
1081 });
1082
1083 let items: Vec<_> = state
1084 .filtered_items()
1085 .map(|(_, item)| item.display.as_ref())
1086 .collect();
1087
1088 assert_eq!(
1090 items,
1091 &[
1092 "tag: v1.0.0",
1093 "origin/main",
1094 "feature",
1095 "main",
1096 "tag: v2.0.0"
1097 ]
1098 )
1099 }
1100
1101 #[test]
1102 fn test_with_refs_empty_list() {
1103 let state = PickerState::with_refs(PickerParams {
1104 prompt: "Select".into(),
1105 refs: &[],
1106 exclude_ref: None,
1107 default: None,
1108 allow_custom_input: false,
1109 });
1110
1111 assert_eq!(state.total_items(), 0);
1112 assert_eq!(state.filtered_count(), 0);
1113 assert!(state.selected().is_none());
1114 }
1115
1116 #[test]
1117 fn test_with_refs_with_default() {
1118 let refs = &[
1120 Ref::Head("feature".to_string()),
1121 Ref::Head("main".to_string()),
1122 Ref::Tag("v1.0.0".to_string()),
1123 ];
1124
1125 let default = Some(Rev::Ref(Ref::Head("main".to_string())));
1126 let state = PickerState::with_refs(PickerParams {
1127 prompt: "Select".into(),
1128 refs,
1129 exclude_ref: None,
1130 default,
1131 allow_custom_input: false,
1132 });
1133
1134 let items: Vec<_> = state
1135 .filtered_items()
1136 .map(|(_, item)| item.display.as_ref())
1137 .collect();
1138
1139 assert_eq!(&items, &["main", "feature", "tag: v1.0.0"]);
1141 }
1142
1143 #[test]
1144 fn test_with_refs_exclude_ref() {
1145 let refs = &[
1147 Ref::Head("feature".to_string()),
1148 Ref::Head("main".to_string()),
1149 Ref::Tag("v1.0.0".to_string()),
1150 ];
1151
1152 let state = PickerState::with_refs(PickerParams {
1153 prompt: "Select".into(),
1154 refs,
1155 exclude_ref: Some(Ref::Head("main".to_string())),
1156 default: None,
1157 allow_custom_input: false,
1158 });
1159
1160 let items: Vec<_> = state
1161 .filtered_items()
1162 .map(|(_, item)| item.display.as_ref())
1163 .collect();
1164
1165 assert_eq!(&items, &["feature", "tag: v1.0.0"])
1167 }
1168
1169 #[test]
1170 fn test_with_refs_duplicate_names() {
1171 let refs = &[
1173 Ref::Head("v1.0.0".to_string()),
1174 Ref::Tag("v1.0.0".to_string()),
1175 Ref::Head("main".to_string()),
1176 ];
1177
1178 let state = PickerState::with_refs(PickerParams {
1179 prompt: "Select".into(),
1180 refs,
1181 exclude_ref: None,
1182 default: None,
1183 allow_custom_input: false,
1184 });
1185
1186 let items: Vec<_> = state
1187 .filtered_items()
1188 .map(|(_, item)| item.display.as_ref())
1189 .collect();
1190
1191 assert_eq!(&items, &["v1.0.0", "tag: v1.0.0", "main"])
1192 }
1193
1194 #[test]
1195 fn test_with_refs_duplicate_with_default() {
1196 let refs = &[
1198 Ref::Head("v1.0.0".to_string()),
1199 Ref::Tag("v1.0.0".to_string()),
1200 Ref::Head("main".to_string()),
1201 ];
1202
1203 let state = PickerState::with_refs(PickerParams {
1204 prompt: "Select".into(),
1205 refs,
1206 exclude_ref: None,
1207 default: Some(Rev::Ref(Ref::Tag("v1.0.0".to_string()))),
1208 allow_custom_input: false,
1209 });
1210
1211 let items: Vec<_> = state
1212 .filtered_items()
1213 .map(|(_, item)| item.display.as_ref())
1214 .collect();
1215
1216 assert_eq!(items, &["tag: v1.0.0", "v1.0.0", "main"])
1217 }
1218
1219 #[test]
1220 fn test_with_refs_exclude_and_default_same() {
1221 let refs = &[
1225 Ref::Head("feature".to_string()),
1226 Ref::Head("main".to_string()),
1227 ];
1228
1229 let state = PickerState::with_refs(PickerParams {
1230 prompt: "Select".into(),
1231 refs,
1232 exclude_ref: Some(Ref::Head("main".to_string())),
1233 default: Some(Rev::Ref(Ref::Head("main".to_string()))),
1234 allow_custom_input: false,
1235 });
1236
1237 let items: Vec<_> = state
1238 .filtered_items()
1239 .map(|(_, item)| item.display.as_ref())
1240 .collect();
1241
1242 assert_eq!(&items, &["main", "feature"])
1245 }
1246
1247 #[test]
1248 fn test_with_refs_with_custom_input() {
1249 let refs = &[
1250 Ref::Head("main".to_string()),
1251 Ref::Tag("v1.0.0".to_string()),
1252 ];
1253
1254 let state = PickerState::with_refs(PickerParams {
1255 prompt: "Select".into(),
1256 refs,
1257 exclude_ref: None,
1258 default: None,
1259 allow_custom_input: true,
1260 });
1261
1262 assert!(state.allow_custom_input);
1264
1265 assert!(state.custom_input_item.is_none());
1267 }
1268
1269 #[test]
1270 fn test_with_refs_exclude_tag_sibling() {
1271 let refs = &[
1273 Ref::Head("v1.0.0".to_string()),
1274 Ref::Tag("v1.0.0".to_string()),
1275 ];
1276
1277 let state = PickerState::with_refs(PickerParams {
1278 prompt: "Select".into(),
1279 refs,
1280 exclude_ref: Some(Ref::Head("v1.0.0".to_string())),
1281 default: None,
1282 allow_custom_input: false,
1283 });
1284
1285 let items: Vec<_> = state
1286 .filtered_items()
1287 .map(|(_, item)| item.display.as_ref())
1288 .collect();
1289
1290 assert_eq!(&items, &["tag: v1.0.0"]);
1291 }
1292
1293 #[test]
1294 fn test_with_refs_remote_tracking_default() {
1295 let refs = &[
1296 Ref::Head("main".to_string()),
1297 Ref::Remote("origin/main".to_string()),
1298 Ref::Remote("origin/feature".to_string()),
1299 ];
1300
1301 let state = PickerState::with_refs(PickerParams {
1302 prompt: "Select".into(),
1303 refs,
1304 exclude_ref: None,
1305 default: Some(Rev::Ref(Ref::Remote("origin/main".to_string()))),
1306 allow_custom_input: false,
1307 });
1308
1309 let items: Vec<_> = state
1310 .filtered_items()
1311 .map(|(_, item)| item.display.as_ref())
1312 .collect();
1313
1314 assert_eq!(&items, &["origin/main", "main", "origin/feature"]);
1316 }
1317}