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 pub in_one_row: bool,
44}
45
46impl PickerItem {
47 pub fn new(display: impl Into<Cow<'static, str>>, data: PickerData) -> Self {
48 Self {
49 display: display.into(),
50 data,
51 in_one_row: true,
52 }
53 }
54
55 pub(crate) fn only_in_list(mut self) -> Self {
57 self.in_one_row = false;
58 self
59 }
60}
61
62#[derive(Debug, Clone)]
64struct MatchResult {
65 index: usize,
66 score: i64,
67}
68
69#[derive(Debug, Clone, PartialEq)]
71pub enum PickerStatus {
72 Active,
74 Done,
76 Cancelled,
78 KeySet(char),
80}
81
82pub struct PickerState {
84 items: Vec<PickerItem>,
86 filtered_indices: Vec<usize>,
88 cursor: usize,
90 pub(crate) input_state: TextInput,
92 matcher: SkimMatcherV2,
94 pub prompt_text: Cow<'static, str>,
96 status: PickerStatus,
98 allow_custom_input: bool,
100 custom_input_item: Option<PickerItem>,
102 assigns_keys: bool,
104 one_row: Option<bool>,
108 grid: Option<Grid>,
110}
111
112pub(crate) struct Grid {
114 pub(crate) headings: Vec<&'static str>,
115 columns: Vec<usize>,
117}
118
119impl Grid {
120 pub(crate) fn column_of(&self, item_i: usize) -> usize {
121 self.columns[item_i]
122 }
123}
124
125pub(crate) struct PickerParams<'a> {
126 pub(crate) prompt: Cow<'static, str>,
127 pub(crate) refs: &'a [Ref],
128 pub(crate) exclude_ref: Option<Ref>,
129 pub(crate) default: Option<Rev>,
130 pub(crate) allow_custom_input: bool,
131}
132
133impl PickerState {
134 pub fn new(
136 prompt: impl Into<Cow<'static, str>>,
137 items: Vec<PickerItem>,
138 allow_custom_input: bool,
139 ) -> Self {
140 let mut state = Self {
141 items: items.clone(),
142 filtered_indices: Vec::new(),
143 cursor: 0,
144 input_state: TextInput::default(),
145 matcher: SkimMatcherV2::default(),
146 prompt_text: prompt.into(),
147 status: PickerStatus::Active,
148 allow_custom_input,
149 custom_input_item: None,
150 assigns_keys: false,
151 one_row: None,
152 grid: None,
153 };
154 state.update_filter();
155 state
156 }
157
158 pub(crate) fn assigning_keys(mut self) -> Self {
161 self.assigns_keys = true;
162 self
163 }
164
165 pub(crate) fn one_row(mut self) -> Self {
168 self.one_row = Some(true);
169 self
170 }
171
172 pub(crate) fn in_columns(mut self, headings: Vec<&'static str>, columns: Vec<usize>) -> Self {
176 self.grid = Some(Grid { headings, columns });
177 self
178 }
179
180 pub(crate) fn grid(&self) -> Option<&Grid> {
181 self.grid.as_ref()
182 }
183
184 pub(crate) fn column_positions(&self, column: usize) -> Vec<usize> {
186 let Some(grid) = &self.grid else {
187 return vec![];
188 };
189 (0..self.filtered_indices.len())
190 .filter(|&position| grid.column_of(self.filtered_indices[position]) == column)
191 .collect()
192 }
193
194 pub(crate) fn cursor_column(&self) -> Option<usize> {
196 let grid = self.grid.as_ref()?;
197 let item_i = *self.filtered_indices.get(self.cursor)?;
198 Some(grid.column_of(item_i))
199 }
200
201 pub(crate) fn move_column(&mut self, step: isize) {
205 let (Some(grid), Some(column)) = (&self.grid, self.cursor_column()) else {
206 return;
207 };
208 let row = self
209 .column_positions(column)
210 .iter()
211 .position(|&position| position == self.cursor)
212 .unwrap_or(0);
213 let mut target = column;
214 loop {
215 let Some(next) = target
216 .checked_add_signed(step)
217 .filter(|&next| next < grid.headings.len())
218 else {
219 return;
220 };
221 target = next;
222 let positions = self.column_positions(target);
223 if let Some(&position) = positions.get(row).or(positions.last()) {
224 self.cursor = position;
225 return;
226 }
227 }
228 }
229
230 pub(crate) fn is_one_row(&self) -> bool {
231 self.one_row == Some(true)
232 }
233
234 pub(crate) fn toggle_one_row(&mut self) {
238 if let Some(one_row) = &mut self.one_row {
239 *one_row = !*one_row;
240 }
241 }
242
243 pub(crate) fn with_cursor(mut self, cursor: usize) -> Self {
244 if cursor < self.filtered_indices.len() {
245 self.cursor = cursor;
246 }
247 self
248 }
249
250 pub(crate) fn assigns_keys(&self) -> bool {
251 self.assigns_keys
252 }
253
254 pub(crate) fn set_key(&mut self, key: char) {
255 self.status = PickerStatus::KeySet(key);
256 }
257
258 pub(crate) fn with_branches(params: PickerParams) -> Self {
261 let default_name = params.default.as_ref().map(|r| r.shorthand().to_string());
262 let exclude_name = params
263 .exclude_ref
264 .as_ref()
265 .map(|r| r.shorthand().to_string());
266
267 let items = default_name
268 .iter()
269 .map(|name| PickerItem::new(name.clone(), PickerData::Item(name.clone())))
270 .chain(
271 params
272 .refs
273 .iter()
274 .filter(|r| matches!(r, Ref::Head(_)))
275 .filter(|r| exclude_name.as_deref().is_none_or(|e| e != r.shorthand()))
276 .filter(|r| default_name.as_deref().is_none_or(|d| d != r.shorthand()))
277 .map(|r| {
278 let name = r.shorthand().to_string();
279 PickerItem::new(name.clone(), PickerData::Item(name))
280 }),
281 )
282 .collect();
283
284 Self::new(params.prompt, items, params.allow_custom_input)
285 }
286
287 pub(crate) fn with_refs(params: PickerParams) -> Self {
290 let ref_to_item = |r: &Ref| {
291 let (display, refname) = match r {
292 Ref::Tag(name) => (format!("tag: {}", name), r.to_full_refname()),
293 _ => (r.shorthand().to_string(), r.shorthand().to_string()),
294 };
295 PickerItem::new(display, PickerData::Item(refname))
296 };
297
298 let default_item = params.default.as_ref().map(|default| match default {
299 Rev::Ref(r) => ref_to_item(r),
300 Rev::Commit(c) => PickerItem::new(c.clone(), PickerData::Item(c.clone())),
301 });
302
303 let items = default_item
304 .into_iter()
305 .chain(
306 params
307 .refs
308 .iter()
309 .filter(|r| {
310 params.exclude_ref.as_ref().is_none_or(|e| e != *r)
311 && params.default.as_ref().is_none_or(|d| match d {
312 Rev::Ref(d) => d != *r,
313 _ => true,
314 })
315 })
316 .map(ref_to_item),
317 )
318 .collect();
319
320 Self::new(params.prompt, items, params.allow_custom_input)
321 }
322
323 pub fn pattern(&self) -> &str {
325 &self.input_state.value
326 }
327
328 pub fn update_filter(&mut self) {
330 let pattern = self.pattern().to_string();
331
332 if pattern.is_empty() {
333 self.filtered_indices = (0..self.items.len()).collect();
335 self.custom_input_item = None;
336 } else {
337 let mut matches: Vec<MatchResult> = self
339 .items
340 .iter()
341 .enumerate()
342 .filter_map(|(i, item)| {
343 self.matcher
344 .fuzzy_match(&item.display, &pattern)
345 .map(|score| MatchResult { index: i, score })
346 })
347 .collect();
348
349 matches.sort_by_key(|b| std::cmp::Reverse(b.score));
351
352 self.filtered_indices = matches.into_iter().map(|m| m.index).collect();
353
354 if self.allow_custom_input {
356 self.custom_input_item = Some(PickerItem::new(
357 pattern.clone(),
358 PickerData::CustomInput(pattern),
359 ));
360 } else {
361 self.custom_input_item = None;
362 }
363 }
364
365 let total_count = self.filtered_indices.len()
367 + if self.custom_input_item.is_some() {
368 1
369 } else {
370 0
371 };
372 if self.cursor >= total_count {
373 self.cursor = 0;
374 }
375 }
376
377 pub fn selected(&self) -> Option<&PickerItem> {
379 if self.cursor == self.filtered_indices.len() {
381 return self.custom_input_item.as_ref();
382 }
383
384 self.filtered_indices
385 .get(self.cursor)
386 .and_then(|&i| self.items.get(i))
387 }
388
389 pub fn filtered_items(&self) -> impl Iterator<Item = (usize, &PickerItem)> {
392 self.filtered_indices
393 .iter()
394 .filter_map(|&i| self.items.get(i).map(|item| (i, item)))
395 .chain(
396 self.custom_input_item
397 .as_ref()
398 .map(|item| (usize::MAX, item)),
399 )
400 }
401
402 pub fn next(&mut self) {
404 if !self.step_in_column(1) {
405 self.step(|cursor, total| (cursor + 1) % total);
406 }
407 }
408
409 pub fn previous(&mut self) {
411 if !self.step_in_column(-1) {
412 self.step(|cursor, total| (cursor + total - 1) % total);
413 }
414 }
415
416 fn step_in_column(&mut self, step: isize) -> bool {
419 let Some(column) = self.cursor_column() else {
420 return false;
421 };
422 let positions = self.column_positions(column);
423 let row = positions
424 .iter()
425 .position(|&position| position == self.cursor)
426 .unwrap_or(0);
427 let len = positions.len() as isize;
428 self.cursor = positions[(row as isize + step).rem_euclid(len) as usize];
429 true
430 }
431
432 fn step(&mut self, step: impl Fn(usize, usize) -> usize) {
436 let total = self.filtered_indices.len() + usize::from(self.custom_input_item.is_some());
437 let mut cursor = self.cursor;
438 for _ in 0..total {
439 cursor = step(cursor, total);
440 let stops = !self.is_one_row()
441 || self
442 .filtered_indices
443 .get(cursor)
444 .is_none_or(|&i| self.items[i].in_one_row);
445 if stops {
446 self.cursor = cursor;
447 return;
448 }
449 }
450 }
451
452 fn one_row_stops(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
455 self.filtered_indices
456 .iter()
457 .enumerate()
458 .filter(|(_, i)| self.items[**i].in_one_row)
459 .map(|(position, _)| position)
460 .enumerate()
461 }
462
463 pub(crate) fn one_row_number(&self) -> (Option<usize>, usize) {
466 let mut number = None;
467 let mut last = 0;
468 for (n, position) in self.one_row_stops() {
469 if position == self.cursor {
470 number = Some(n);
471 }
472 last = n;
473 }
474 (number, last)
475 }
476
477 pub(crate) fn select_number(&mut self, number: usize) {
479 let position = self.one_row_stops().find(|(n, _)| *n == number);
480 if let Some((_, position)) = position {
481 self.cursor = position;
482 }
483 }
484
485 pub fn cursor(&self) -> usize {
487 self.cursor
488 }
489
490 pub fn total_items(&self) -> usize {
492 self.items.len()
493 }
494
495 pub fn filtered_count(&self) -> usize {
497 self.filtered_indices.len()
498 }
499
500 pub fn match_indices(&self, item_index: usize) -> Option<Vec<usize>> {
502 let pattern = self.pattern();
503 if pattern.is_empty() {
504 return None;
505 }
506
507 if item_index == usize::MAX {
509 return None;
510 }
511
512 self.items
513 .get(item_index)
514 .and_then(|item| self.matcher.fuzzy_indices(&item.display, pattern))
515 .map(|(_, indices)| indices)
516 }
517
518 pub fn status(&self) -> &PickerStatus {
520 &self.status
521 }
522
523 pub fn done(&mut self) {
525 self.status = PickerStatus::Done;
526 }
527
528 pub fn cancel(&mut self) {
530 self.status = PickerStatus::Cancelled;
531 }
532
533 pub fn is_done(&self) -> bool {
535 self.status == PickerStatus::Done
536 }
537
538 pub fn is_cancelled(&self) -> bool {
540 self.status == PickerStatus::Cancelled
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
548
549 fn create_test_items() -> Vec<PickerItem> {
550 vec![
551 PickerItem::new("main", PickerData::Item("main".to_string())),
552 PickerItem::new("develop", PickerData::Item("develop".to_string())),
553 PickerItem::new("feature/test", PickerData::Item("feature/test".to_string())),
554 PickerItem::new("feature/new", PickerData::Item("feature/new".to_string())),
555 PickerItem::new("bugfix/123", PickerData::Item("bugfix/123".to_string())),
556 ]
557 }
558
559 #[test]
560 fn test_picker_data_display() {
561 let revision = PickerData::Item("main".to_string());
562 assert_eq!(revision.display(), "main");
563
564 let custom = PickerData::CustomInput("custom".to_string());
565 assert_eq!(custom.display(), "custom");
566 }
567
568 #[test]
569 fn test_picker_item_new() {
570 let item = PickerItem::new("main", PickerData::Item("main".to_string()));
571 assert_eq!(item.display.as_ref(), "main");
572 assert_eq!(item.data.display(), "main");
573 }
574
575 #[test]
576 fn test_picker_state_new_without_custom_input() {
577 let items = create_test_items();
578 let state = PickerState::new("Select branch", items.clone(), false);
579
580 assert_eq!(state.prompt_text.as_ref(), "Select branch");
581 assert_eq!(state.total_items(), 5);
582 assert_eq!(state.filtered_count(), 5);
583 assert_eq!(state.cursor(), 0);
584 assert_eq!(state.pattern(), "");
585 assert_eq!(state.status(), &PickerStatus::Active);
586 assert!(!state.allow_custom_input);
587 }
588
589 #[test]
590 fn test_picker_state_new_with_custom_input() {
591 let items = create_test_items();
592 let state = PickerState::new("Select branch", items, true);
593
594 assert!(state.allow_custom_input);
595 assert_eq!(state.custom_input_item, None); }
597
598 #[test]
599 fn test_empty_pattern_shows_all_items() {
600 let items = create_test_items();
601 let state = PickerState::new("Select", items, false);
602
603 assert_eq!(state.filtered_count(), 5);
604 assert_eq!(state.filtered_indices, vec![0, 1, 2, 3, 4]);
605 }
606
607 #[test]
608 fn test_fuzzy_filtering() {
609 let items = create_test_items();
610 let mut state = PickerState::new("Select", items, false);
611
612 state
613 .input_state
614 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
615 state.update_filter();
616
617 assert_eq!(state.filtered_count(), 3);
619
620 let filtered: Vec<_> = state.filtered_items().collect();
621 assert!(
622 filtered
623 .iter()
624 .any(|(_, item)| item.display == "feature/test")
625 );
626 assert!(
627 filtered
628 .iter()
629 .any(|(_, item)| item.display == "feature/new")
630 );
631 assert!(
632 filtered
633 .iter()
634 .any(|(_, item)| item.display == "bugfix/123")
635 );
636 }
637
638 #[test]
639 fn test_fuzzy_filtering_sorts_by_score() {
640 let items = vec![
642 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())), ];
647 let mut state = PickerState::new("Select", items, false);
648
649 state
651 .input_state
652 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
653 state
654 .input_state
655 .handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
656 state
657 .input_state
658 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
659 state
660 .input_state
661 .handle_key_event(KeyEvent::new(KeyCode::Char('t'), KeyModifiers::empty()));
662 state.update_filter();
663
664 assert_eq!(state.filtered_count(), 4);
666
667 let filtered: Vec<_> = state
669 .filtered_items()
670 .map(|(_, item)| item.display.as_ref())
671 .collect();
672
673 assert_eq!(filtered[0], "feat");
675 assert_eq!(filtered[filtered.len() - 1], "fix-eat-bug");
676 }
677
678 #[test]
679 fn test_case_insensitive_matching() {
680 let items = vec![
681 PickerItem::new("Feature", PickerData::Item("Feature".to_string())),
682 PickerItem::new("feature", PickerData::Item("feature".to_string())),
683 PickerItem::new("FEATURE", PickerData::Item("FEATURE".to_string())),
684 ];
685
686 let mut state = PickerState::new("Select item", items, false);
687
688 state
689 .input_state
690 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
691 state
692 .input_state
693 .handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
694 state
695 .input_state
696 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
697 state.update_filter();
698
699 assert_eq!(state.filtered_count(), 3);
701 }
702
703 #[test]
704 fn test_no_matches() {
705 let items = create_test_items();
706 let mut state = PickerState::new("Select", items, false);
707
708 state
709 .input_state
710 .handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::empty()));
711 state
712 .input_state
713 .handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty()));
714 state
715 .input_state
716 .handle_key_event(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::empty()));
717 state.update_filter();
718
719 assert_eq!(state.filtered_count(), 0);
720 assert!(state.selected().is_none());
721 }
722
723 #[test]
724 fn test_custom_input_creation() {
725 let items = create_test_items();
726 let mut state = PickerState::new("Select", items, true);
727
728 state
730 .input_state
731 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
732 state
733 .input_state
734 .handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
735 state
736 .input_state
737 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
738 state.update_filter();
739
740 assert!(state.custom_input_item.is_some());
742
743 let filtered: Vec<_> = state.filtered_items().collect();
745 assert!(filtered.len() >= 3); let last = filtered.last().unwrap();
749 assert_eq!(last.0, usize::MAX);
750 assert_eq!(last.1.display.as_ref(), "fea");
751 }
752
753 #[test]
754 fn test_custom_input_not_created_when_disabled() {
755 let items = create_test_items();
756 let mut state = PickerState::new("Select", items, false);
757
758 state
759 .input_state
760 .handle_key_event(KeyEvent::new(KeyCode::Char('t'), KeyModifiers::empty()));
761 state
762 .input_state
763 .handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
764 state.update_filter();
765
766 assert!(state.custom_input_item.is_none());
767 }
768
769 #[test]
770 fn test_custom_input_not_created_on_empty_pattern() {
771 let items = create_test_items();
772 let mut state = PickerState::new("Select", items, true);
773
774 assert!(state.custom_input_item.is_none());
776
777 state
779 .input_state
780 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
781 state.update_filter();
782 assert!(state.custom_input_item.is_some());
783
784 state
785 .input_state
786 .handle_key_event(KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty()));
787 state.update_filter();
788 assert!(state.custom_input_item.is_none());
789 }
790
791 #[test]
792 fn test_cursor_next() {
793 let items = create_test_items();
794 let mut state = PickerState::new("Select", items, false);
795
796 assert_eq!(state.cursor(), 0);
797
798 state.next();
799 assert_eq!(state.cursor(), 1);
800
801 state.next();
802 assert_eq!(state.cursor(), 2);
803 }
804
805 #[test]
806 fn test_cursor_next_wraps_around() {
807 let items = create_test_items();
808 let mut state = PickerState::new("Select", items, false);
809
810 for _ in 0..5 {
812 state.next();
813 }
814
815 assert_eq!(state.cursor(), 0);
817 }
818
819 #[test]
820 fn test_cursor_previous() {
821 let items = create_test_items();
822 let mut state = PickerState::new("Select", items, false);
823
824 state.next();
825 state.next();
826 assert_eq!(state.cursor(), 2);
827
828 state.previous();
829 assert_eq!(state.cursor(), 1);
830
831 state.previous();
832 assert_eq!(state.cursor(), 0);
833 }
834
835 #[test]
836 fn test_cursor_previous_wraps_around() {
837 let items = create_test_items();
838 let mut state = PickerState::new("Select", items, false);
839
840 assert_eq!(state.cursor(), 0);
841
842 state.previous();
843 assert_eq!(state.cursor(), 4);
845 }
846
847 #[test]
848 fn test_cursor_with_custom_input() {
849 let items = create_test_items();
850 let mut state = PickerState::new("Select", items, true);
851
852 state
853 .input_state
854 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
855 state.update_filter();
856
857 assert_eq!(state.cursor(), 0);
859
860 state.next();
861 assert_eq!(state.cursor(), 1);
862
863 state.next();
864 assert_eq!(state.cursor(), 2);
865
866 state.next();
867 assert_eq!(state.cursor(), 3); state.next();
870 assert_eq!(state.cursor(), 0); state.previous();
873 assert_eq!(state.cursor(), 3); }
875
876 #[test]
877 fn test_navigation_with_custom_input_at_end() {
878 let items = vec![
879 PickerItem::new("feature/a", PickerData::Item("feature/a".to_string())),
880 PickerItem::new("feature/b", PickerData::Item("feature/b".to_string())),
881 ];
882
883 let mut state = PickerState::new("Select", items, true);
884
885 state
887 .input_state
888 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
889 state
890 .input_state
891 .handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
892 state
893 .input_state
894 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
895 state
896 .input_state
897 .handle_key_event(KeyEvent::new(KeyCode::Char('t'), KeyModifiers::empty()));
898 state.update_filter();
899
900 assert_eq!(state.filtered_count(), 2);
902 assert_eq!(state.cursor(), 0);
903
904 let selected = state.selected().unwrap();
906 match &selected.data {
907 PickerData::Item(_) => {}
908 _ => panic!("Expected first item to be a revision"),
909 }
910
911 state.next();
913 assert_eq!(state.cursor(), 1);
914
915 state.next();
917 assert_eq!(state.cursor(), 2);
918 let selected = state.selected().unwrap();
919 match &selected.data {
920 PickerData::CustomInput(s) => assert_eq!(s, "feat"),
921 _ => panic!("Expected custom input at end"),
922 }
923
924 state.next();
926 assert_eq!(state.cursor(), 0);
927 }
928
929 #[test]
930 fn test_cursor_resets_when_filter_reduces_items() {
931 let items = create_test_items();
932 let mut state = PickerState::new("Select", items, false);
933
934 for _ in 0..4 {
936 state.next();
937 }
938 assert_eq!(state.cursor(), 4);
939
940 state
942 .input_state
943 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
944 state.update_filter();
945
946 assert_eq!(state.cursor(), 0);
948 }
949
950 #[test]
951 fn test_scroll_through_many_items() {
952 let items: Vec<_> = (0..20)
954 .map(|i| {
955 PickerItem::new(
956 format!("branch-{:02}", i),
957 PickerData::Item(format!("branch-{:02}", i)),
958 )
959 })
960 .collect();
961
962 let mut state = PickerState::new("Select", items, false);
963
964 assert_eq!(state.cursor(), 0);
966
967 for _ in 0..10 {
969 state.next();
970 }
971 assert_eq!(state.cursor(), 10);
972
973 for _ in 0..9 {
975 state.next();
976 }
977 assert_eq!(state.cursor(), 19);
978
979 state.next();
981 assert_eq!(state.cursor(), 0);
982
983 state.previous();
985 assert_eq!(state.cursor(), 19);
986 }
987
988 #[test]
989 fn test_navigation_after_filtering() {
990 let items = vec![
991 PickerItem::new("feature/a", PickerData::Item("feature/a".to_string())),
992 PickerItem::new("feature/b", PickerData::Item("feature/b".to_string())),
993 PickerItem::new("main", PickerData::Item("main".to_string())),
994 PickerItem::new("develop", PickerData::Item("develop".to_string())),
995 ];
996
997 let mut state = PickerState::new("Select", items, false);
998
999 state
1001 .input_state
1002 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
1003 state
1004 .input_state
1005 .handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
1006 state
1007 .input_state
1008 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
1009 state.update_filter();
1010
1011 assert_eq!(state.filtered_count(), 2);
1013 assert_eq!(state.cursor(), 0);
1014
1015 state.next();
1017 assert_eq!(state.cursor(), 1);
1018
1019 state.next();
1021 assert_eq!(state.cursor(), 0);
1022
1023 state.previous();
1025 assert_eq!(state.cursor(), 1);
1026 }
1027
1028 #[test]
1029 fn test_selected_returns_correct_item() {
1030 let items = create_test_items();
1031 let mut state = PickerState::new("Select", items, false);
1032
1033 let selected = state.selected().unwrap();
1034 assert_eq!(selected.display.as_ref(), "main");
1035
1036 state.next();
1037 state.next();
1038 let selected = state.selected().unwrap();
1039 assert_eq!(selected.display.as_ref(), "feature/test");
1040 }
1041
1042 #[test]
1043 fn test_selected_with_filter() {
1044 let items = create_test_items();
1045 let mut state = PickerState::new("Select", items, false);
1046
1047 state
1048 .input_state
1049 .handle_key_event(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()));
1050 state.update_filter();
1051
1052 let selected = state.selected().unwrap();
1054 assert_eq!(selected.display.as_ref(), "bugfix/123");
1055 }
1056
1057 #[test]
1058 fn test_selected_returns_custom_input() {
1059 let items = create_test_items();
1060 let mut state = PickerState::new("Select", items, true);
1061
1062 state
1064 .input_state
1065 .handle_key_event(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty()));
1066 state
1067 .input_state
1068 .handle_key_event(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty()));
1069 state.update_filter();
1070
1071 assert_eq!(state.cursor(), 0);
1073 assert_eq!(state.selected().unwrap().display.as_ref(), "qq");
1074 }
1075
1076 #[test]
1077 fn test_select_custom_input() {
1078 let items = create_test_items();
1079 let mut state = PickerState::new("Select", items, true);
1080
1081 state
1083 .input_state
1084 .handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::empty()));
1085 state
1086 .input_state
1087 .handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty()));
1088 state
1089 .input_state
1090 .handle_key_event(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::empty()));
1091 state.update_filter();
1092
1093 assert_eq!(state.filtered_count(), 0);
1095 assert!(state.custom_input_item.is_some());
1096
1097 assert_eq!(state.cursor(), 0);
1099
1100 let selected = state.selected().unwrap();
1102 assert_eq!(selected.display, "xyz");
1103 match &selected.data {
1104 PickerData::CustomInput(s) => assert_eq!(s, "xyz"),
1105 _ => panic!("Expected CustomInput"),
1106 }
1107
1108 state.done();
1110 assert!(state.is_done());
1111 assert_eq!(state.status(), &PickerStatus::Done);
1112 }
1113
1114 #[test]
1115 fn test_filtered_items_order() {
1116 let items = create_test_items();
1117 let mut state = PickerState::new("Select", items, true);
1118
1119 state
1120 .input_state
1121 .handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
1122 state.update_filter();
1123
1124 let indices: Vec<_> = state.filtered_items().map(|(idx, _)| idx).collect();
1125
1126 assert_eq!(indices[0], 2); assert_eq!(indices[1], 3); assert_eq!(indices[2], 4); assert_eq!(indices[3], usize::MAX); }
1132
1133 #[test]
1134 fn test_match_indices_with_pattern() {
1135 let items = create_test_items();
1136 let mut state = PickerState::new("Select", items, false);
1137
1138 state
1139 .input_state
1140 .handle_key_event(KeyEvent::new(KeyCode::Char('m'), KeyModifiers::empty()));
1141 state
1142 .input_state
1143 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
1144 state
1145 .input_state
1146 .handle_key_event(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::empty()));
1147 state.update_filter();
1148
1149 let indices = state.match_indices(0);
1151 assert!(indices.is_some());
1152
1153 let indices = indices.unwrap();
1154 assert_eq!(indices.len(), 3); }
1156
1157 #[test]
1158 fn test_match_indices_empty_pattern() {
1159 let items = create_test_items();
1160 let state = PickerState::new("Select", items, false);
1161
1162 let indices = state.match_indices(0);
1163 assert!(indices.is_none());
1164 }
1165
1166 #[test]
1167 fn test_match_indices_custom_input_returns_none() {
1168 let items = create_test_items();
1169 let mut state = PickerState::new("Select", items, true);
1170
1171 state
1172 .input_state
1173 .handle_key_event(KeyEvent::new(KeyCode::Char('t'), KeyModifiers::empty()));
1174 state.update_filter();
1175
1176 let indices = state.match_indices(usize::MAX);
1178 assert!(indices.is_none());
1179 }
1180
1181 #[test]
1182 fn test_status_transitions() {
1183 let items = create_test_items();
1184 let mut state = PickerState::new("Select", items, false);
1185
1186 assert_eq!(state.status(), &PickerStatus::Active);
1187 assert!(!state.is_done());
1188 assert!(!state.is_cancelled());
1189
1190 state.done();
1191 assert_eq!(state.status(), &PickerStatus::Done);
1192 assert!(state.is_done());
1193 assert!(!state.is_cancelled());
1194 }
1195
1196 #[test]
1197 fn test_status_cancelled() {
1198 let items = create_test_items();
1199 let mut state = PickerState::new("Select", items, false);
1200
1201 state.cancel();
1202 assert_eq!(state.status(), &PickerStatus::Cancelled);
1203 assert!(!state.is_done());
1204 assert!(state.is_cancelled());
1205 }
1206
1207 #[test]
1208 fn test_empty_items_list() {
1209 let state = PickerState::new("Select", vec![], false);
1210
1211 assert_eq!(state.total_items(), 0);
1212 assert_eq!(state.filtered_count(), 0);
1213 assert_eq!(state.cursor(), 0);
1214 assert!(state.selected().is_none());
1215 }
1216
1217 #[test]
1218 fn test_empty_items_with_custom_input() {
1219 let mut state = PickerState::new("Select", vec![], true);
1220
1221 state
1222 .input_state
1223 .handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
1224 state.update_filter();
1225
1226 assert_eq!(state.total_items(), 0);
1227 assert_eq!(state.filtered_count(), 0);
1228 assert!(state.custom_input_item.is_some());
1229
1230 assert_eq!(state.selected().unwrap().display.as_ref(), "a");
1231 }
1232
1233 #[test]
1234 fn test_cursor_navigation_empty_list() {
1235 let mut state = PickerState::new("Select", vec![], false);
1236
1237 state.next();
1238 assert_eq!(state.cursor(), 0);
1239
1240 state.previous();
1241 assert_eq!(state.cursor(), 0);
1242 }
1243
1244 #[test]
1245 fn test_single_item_navigation() {
1246 let items = vec![PickerItem::new(
1247 "only",
1248 PickerData::Item("only".to_string()),
1249 )];
1250 let mut state = PickerState::new("Select", items, false);
1251
1252 assert_eq!(state.cursor(), 0);
1253
1254 state.next();
1255 assert_eq!(state.cursor(), 0); state.previous();
1258 assert_eq!(state.cursor(), 0); }
1260
1261 #[test]
1262 fn test_with_refs_basic_sorting() {
1263 let refs = &[
1265 Ref::Tag("v1.0.0".to_string()),
1266 Ref::Remote("origin/main".to_string()),
1267 Ref::Head("feature".to_string()),
1268 Ref::Head("main".to_string()),
1269 Ref::Tag("v2.0.0".to_string()),
1270 ];
1271
1272 let state = PickerState::with_refs(PickerParams {
1273 prompt: "Select".into(),
1274 refs,
1275 exclude_ref: None,
1276 default: None,
1277 allow_custom_input: false,
1278 });
1279
1280 let items: Vec<_> = state
1281 .filtered_items()
1282 .map(|(_, item)| item.display.as_ref())
1283 .collect();
1284
1285 assert_eq!(
1287 items,
1288 &[
1289 "tag: v1.0.0",
1290 "origin/main",
1291 "feature",
1292 "main",
1293 "tag: v2.0.0"
1294 ]
1295 )
1296 }
1297
1298 #[test]
1299 fn test_with_refs_empty_list() {
1300 let state = PickerState::with_refs(PickerParams {
1301 prompt: "Select".into(),
1302 refs: &[],
1303 exclude_ref: None,
1304 default: None,
1305 allow_custom_input: false,
1306 });
1307
1308 assert_eq!(state.total_items(), 0);
1309 assert_eq!(state.filtered_count(), 0);
1310 assert!(state.selected().is_none());
1311 }
1312
1313 #[test]
1314 fn test_with_refs_with_default() {
1315 let refs = &[
1317 Ref::Head("feature".to_string()),
1318 Ref::Head("main".to_string()),
1319 Ref::Tag("v1.0.0".to_string()),
1320 ];
1321
1322 let default = Some(Rev::Ref(Ref::Head("main".to_string())));
1323 let state = PickerState::with_refs(PickerParams {
1324 prompt: "Select".into(),
1325 refs,
1326 exclude_ref: None,
1327 default,
1328 allow_custom_input: false,
1329 });
1330
1331 let items: Vec<_> = state
1332 .filtered_items()
1333 .map(|(_, item)| item.display.as_ref())
1334 .collect();
1335
1336 assert_eq!(&items, &["main", "feature", "tag: v1.0.0"]);
1338 }
1339
1340 #[test]
1341 fn test_with_refs_exclude_ref() {
1342 let refs = &[
1344 Ref::Head("feature".to_string()),
1345 Ref::Head("main".to_string()),
1346 Ref::Tag("v1.0.0".to_string()),
1347 ];
1348
1349 let state = PickerState::with_refs(PickerParams {
1350 prompt: "Select".into(),
1351 refs,
1352 exclude_ref: Some(Ref::Head("main".to_string())),
1353 default: None,
1354 allow_custom_input: false,
1355 });
1356
1357 let items: Vec<_> = state
1358 .filtered_items()
1359 .map(|(_, item)| item.display.as_ref())
1360 .collect();
1361
1362 assert_eq!(&items, &["feature", "tag: v1.0.0"])
1364 }
1365
1366 #[test]
1367 fn test_with_refs_duplicate_names() {
1368 let refs = &[
1370 Ref::Head("v1.0.0".to_string()),
1371 Ref::Tag("v1.0.0".to_string()),
1372 Ref::Head("main".to_string()),
1373 ];
1374
1375 let state = PickerState::with_refs(PickerParams {
1376 prompt: "Select".into(),
1377 refs,
1378 exclude_ref: None,
1379 default: None,
1380 allow_custom_input: false,
1381 });
1382
1383 let items: Vec<_> = state
1384 .filtered_items()
1385 .map(|(_, item)| item.display.as_ref())
1386 .collect();
1387
1388 assert_eq!(&items, &["v1.0.0", "tag: v1.0.0", "main"])
1389 }
1390
1391 #[test]
1392 fn test_with_refs_duplicate_with_default() {
1393 let refs = &[
1395 Ref::Head("v1.0.0".to_string()),
1396 Ref::Tag("v1.0.0".to_string()),
1397 Ref::Head("main".to_string()),
1398 ];
1399
1400 let state = PickerState::with_refs(PickerParams {
1401 prompt: "Select".into(),
1402 refs,
1403 exclude_ref: None,
1404 default: Some(Rev::Ref(Ref::Tag("v1.0.0".to_string()))),
1405 allow_custom_input: false,
1406 });
1407
1408 let items: Vec<_> = state
1409 .filtered_items()
1410 .map(|(_, item)| item.display.as_ref())
1411 .collect();
1412
1413 assert_eq!(items, &["tag: v1.0.0", "v1.0.0", "main"])
1414 }
1415
1416 #[test]
1417 fn test_with_refs_exclude_and_default_same() {
1418 let refs = &[
1422 Ref::Head("feature".to_string()),
1423 Ref::Head("main".to_string()),
1424 ];
1425
1426 let state = PickerState::with_refs(PickerParams {
1427 prompt: "Select".into(),
1428 refs,
1429 exclude_ref: Some(Ref::Head("main".to_string())),
1430 default: Some(Rev::Ref(Ref::Head("main".to_string()))),
1431 allow_custom_input: false,
1432 });
1433
1434 let items: Vec<_> = state
1435 .filtered_items()
1436 .map(|(_, item)| item.display.as_ref())
1437 .collect();
1438
1439 assert_eq!(&items, &["main", "feature"])
1442 }
1443
1444 #[test]
1445 fn test_with_refs_with_custom_input() {
1446 let refs = &[
1447 Ref::Head("main".to_string()),
1448 Ref::Tag("v1.0.0".to_string()),
1449 ];
1450
1451 let state = PickerState::with_refs(PickerParams {
1452 prompt: "Select".into(),
1453 refs,
1454 exclude_ref: None,
1455 default: None,
1456 allow_custom_input: true,
1457 });
1458
1459 assert!(state.allow_custom_input);
1461
1462 assert!(state.custom_input_item.is_none());
1464 }
1465
1466 #[test]
1467 fn test_with_refs_exclude_tag_sibling() {
1468 let refs = &[
1470 Ref::Head("v1.0.0".to_string()),
1471 Ref::Tag("v1.0.0".to_string()),
1472 ];
1473
1474 let state = PickerState::with_refs(PickerParams {
1475 prompt: "Select".into(),
1476 refs,
1477 exclude_ref: Some(Ref::Head("v1.0.0".to_string())),
1478 default: None,
1479 allow_custom_input: false,
1480 });
1481
1482 let items: Vec<_> = state
1483 .filtered_items()
1484 .map(|(_, item)| item.display.as_ref())
1485 .collect();
1486
1487 assert_eq!(&items, &["tag: v1.0.0"]);
1488 }
1489
1490 #[test]
1491 fn test_with_refs_remote_tracking_default() {
1492 let refs = &[
1493 Ref::Head("main".to_string()),
1494 Ref::Remote("origin/main".to_string()),
1495 Ref::Remote("origin/feature".to_string()),
1496 ];
1497
1498 let state = PickerState::with_refs(PickerParams {
1499 prompt: "Select".into(),
1500 refs,
1501 exclude_ref: None,
1502 default: Some(Rev::Ref(Ref::Remote("origin/main".to_string()))),
1503 allow_custom_input: false,
1504 });
1505
1506 let items: Vec<_> = state
1507 .filtered_items()
1508 .map(|(_, item)| item.display.as_ref())
1509 .collect();
1510
1511 assert_eq!(&items, &["origin/main", "main", "origin/feature"]);
1513 }
1514}