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