use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2;
use std::borrow::Cow;
use tui_prompts::State as _;
use tui_prompts::TextState;
use crate::item_data::Ref;
use crate::item_data::Rev;
#[derive(Debug, Clone, PartialEq)]
pub enum PickerData {
Item(String),
CustomInput(String),
}
impl PickerData {
pub fn display(&self) -> &str {
match self {
PickerData::Item(s) | PickerData::CustomInput(s) => s,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PickerItem {
pub display: Cow<'static, str>,
pub data: PickerData,
}
impl PickerItem {
pub fn new(display: impl Into<Cow<'static, str>>, data: PickerData) -> Self {
Self {
display: display.into(),
data,
}
}
}
#[derive(Debug, Clone)]
struct MatchResult {
index: usize,
score: i64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PickerStatus {
Active,
Done,
Cancelled,
}
pub struct PickerState {
items: Vec<PickerItem>,
filtered_indices: Vec<usize>,
cursor: usize,
pub input_state: TextState<'static>,
matcher: SkimMatcherV2,
pub prompt_text: Cow<'static, str>,
status: PickerStatus,
allow_custom_input: bool,
custom_input_item: Option<PickerItem>,
}
pub(crate) struct PickerParams<'a> {
pub(crate) prompt: Cow<'static, str>,
pub(crate) refs: &'a [Ref],
pub(crate) exclude_ref: Option<Ref>,
pub(crate) default: Option<Rev>,
pub(crate) allow_custom_input: bool,
}
impl PickerState {
pub fn new(
prompt: impl Into<Cow<'static, str>>,
items: Vec<PickerItem>,
allow_custom_input: bool,
) -> Self {
let mut state = Self {
items: items.clone(),
filtered_indices: Vec::new(),
cursor: 0,
input_state: TextState::default(),
matcher: SkimMatcherV2::default(),
prompt_text: prompt.into(),
status: PickerStatus::Active,
allow_custom_input,
custom_input_item: None,
};
state.update_filter();
state
}
pub(crate) fn with_branches(params: PickerParams) -> Self {
let default_name = params.default.as_ref().map(|r| r.shorthand().to_string());
let exclude_name = params
.exclude_ref
.as_ref()
.map(|r| r.shorthand().to_string());
let items = default_name
.iter()
.map(|name| PickerItem::new(name.clone(), PickerData::Item(name.clone())))
.chain(
params
.refs
.iter()
.filter(|r| matches!(r, Ref::Head(_)))
.filter(|r| exclude_name.as_deref().is_none_or(|e| e != r.shorthand()))
.filter(|r| default_name.as_deref().is_none_or(|d| d != r.shorthand()))
.map(|r| {
let name = r.shorthand().to_string();
PickerItem::new(name.clone(), PickerData::Item(name))
}),
)
.collect();
Self::new(params.prompt, items, params.allow_custom_input)
}
pub(crate) fn with_refs(params: PickerParams) -> Self {
let ref_to_item = |r: &Ref| {
let (display, refname) = match r {
Ref::Tag(name) => (format!("tag: {}", name), r.to_full_refname()),
_ => (r.shorthand().to_string(), r.shorthand().to_string()),
};
PickerItem::new(display, PickerData::Item(refname))
};
let default_item = params.default.as_ref().map(|default| match default {
Rev::Ref(r) => ref_to_item(r),
Rev::Commit(c) => PickerItem::new(c.clone(), PickerData::Item(c.clone())),
});
let items = default_item
.into_iter()
.chain(
params
.refs
.iter()
.filter(|r| {
params.exclude_ref.as_ref().is_none_or(|e| e != *r)
&& params.default.as_ref().is_none_or(|d| match d {
Rev::Ref(d) => d != *r,
_ => true,
})
})
.map(ref_to_item),
)
.collect();
Self::new(params.prompt, items, params.allow_custom_input)
}
pub fn pattern(&self) -> &str {
self.input_state.value()
}
pub fn update_filter(&mut self) {
let pattern = self.pattern().to_string();
if pattern.is_empty() {
self.filtered_indices = (0..self.items.len()).collect();
self.custom_input_item = None;
} else {
let mut matches: Vec<MatchResult> = self
.items
.iter()
.enumerate()
.filter_map(|(i, item)| {
self.matcher
.fuzzy_match(&item.display, &pattern)
.map(|score| MatchResult { index: i, score })
})
.collect();
matches.sort_by(|a, b| b.score.cmp(&a.score));
self.filtered_indices = matches.into_iter().map(|m| m.index).collect();
if self.allow_custom_input {
self.custom_input_item = Some(PickerItem::new(
pattern.clone(),
PickerData::CustomInput(pattern),
));
} else {
self.custom_input_item = None;
}
}
let total_count = self.filtered_indices.len()
+ if self.custom_input_item.is_some() {
1
} else {
0
};
if self.cursor >= total_count {
self.cursor = 0;
}
}
pub fn selected(&self) -> Option<&PickerItem> {
if self.cursor == self.filtered_indices.len() {
return self.custom_input_item.as_ref();
}
self.filtered_indices
.get(self.cursor)
.and_then(|&i| self.items.get(i))
}
pub fn filtered_items(&self) -> impl Iterator<Item = (usize, &PickerItem)> {
self.filtered_indices
.iter()
.filter_map(|&i| self.items.get(i).map(|item| (i, item)))
.chain(
self.custom_input_item
.as_ref()
.map(|item| (usize::MAX, item)),
)
}
pub fn next(&mut self) {
let total_count = self.filtered_indices.len()
+ if self.custom_input_item.is_some() {
1
} else {
0
};
if total_count > 0 {
self.cursor = (self.cursor + 1) % total_count;
}
}
pub fn previous(&mut self) {
let total_count = self.filtered_indices.len()
+ if self.custom_input_item.is_some() {
1
} else {
0
};
if total_count > 0 {
self.cursor = if self.cursor == 0 {
total_count - 1
} else {
self.cursor - 1
};
}
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn total_items(&self) -> usize {
self.items.len()
}
pub fn filtered_count(&self) -> usize {
self.filtered_indices.len()
}
pub fn match_indices(&self, item_index: usize) -> Option<Vec<usize>> {
let pattern = self.pattern();
if pattern.is_empty() {
return None;
}
if item_index == usize::MAX {
return None;
}
self.items
.get(item_index)
.and_then(|item| self.matcher.fuzzy_indices(&item.display, pattern))
.map(|(_, indices)| indices)
}
pub fn status(&self) -> &PickerStatus {
&self.status
}
pub fn done(&mut self) {
self.status = PickerStatus::Done;
}
pub fn cancel(&mut self) {
self.status = PickerStatus::Cancelled;
}
pub fn is_done(&self) -> bool {
self.status == PickerStatus::Done
}
pub fn is_cancelled(&self) -> bool {
self.status == PickerStatus::Cancelled
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
fn create_test_items() -> Vec<PickerItem> {
vec![
PickerItem::new("main", PickerData::Item("main".to_string())),
PickerItem::new("develop", PickerData::Item("develop".to_string())),
PickerItem::new("feature/test", PickerData::Item("feature/test".to_string())),
PickerItem::new("feature/new", PickerData::Item("feature/new".to_string())),
PickerItem::new("bugfix/123", PickerData::Item("bugfix/123".to_string())),
]
}
#[test]
fn test_picker_data_display() {
let revision = PickerData::Item("main".to_string());
assert_eq!(revision.display(), "main");
let custom = PickerData::CustomInput("custom".to_string());
assert_eq!(custom.display(), "custom");
}
#[test]
fn test_picker_item_new() {
let item = PickerItem::new("main", PickerData::Item("main".to_string()));
assert_eq!(item.display.as_ref(), "main");
assert_eq!(item.data.display(), "main");
}
#[test]
fn test_picker_state_new_without_custom_input() {
let items = create_test_items();
let state = PickerState::new("Select branch", items.clone(), false);
assert_eq!(state.prompt_text.as_ref(), "Select branch");
assert_eq!(state.total_items(), 5);
assert_eq!(state.filtered_count(), 5);
assert_eq!(state.cursor(), 0);
assert_eq!(state.pattern(), "");
assert_eq!(state.status(), &PickerStatus::Active);
assert!(!state.allow_custom_input);
}
#[test]
fn test_picker_state_new_with_custom_input() {
let items = create_test_items();
let state = PickerState::new("Select branch", items, true);
assert!(state.allow_custom_input);
assert_eq!(state.custom_input_item, None); }
#[test]
fn test_empty_pattern_shows_all_items() {
let items = create_test_items();
let state = PickerState::new("Select", items, false);
assert_eq!(state.filtered_count(), 5);
assert_eq!(state.filtered_indices, vec![0, 1, 2, 3, 4]);
}
#[test]
fn test_fuzzy_filtering() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, false);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
state.update_filter();
assert_eq!(state.filtered_count(), 3);
let filtered: Vec<_> = state.filtered_items().collect();
assert!(
filtered
.iter()
.any(|(_, item)| item.display == "feature/test")
);
assert!(
filtered
.iter()
.any(|(_, item)| item.display == "feature/new")
);
assert!(
filtered
.iter()
.any(|(_, item)| item.display == "bugfix/123")
);
}
#[test]
fn test_fuzzy_filtering_sorts_by_score() {
let items = vec![
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())), ];
let mut state = PickerState::new("Select", items, false);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('t'), KeyModifiers::empty()));
state.update_filter();
assert_eq!(state.filtered_count(), 4);
let filtered: Vec<_> = state
.filtered_items()
.map(|(_, item)| item.display.as_ref())
.collect();
assert_eq!(filtered[0], "feat");
assert_eq!(filtered[filtered.len() - 1], "fix-eat-bug");
}
#[test]
fn test_case_insensitive_matching() {
let items = vec![
PickerItem::new("Feature", PickerData::Item("Feature".to_string())),
PickerItem::new("feature", PickerData::Item("feature".to_string())),
PickerItem::new("FEATURE", PickerData::Item("FEATURE".to_string())),
];
let mut state = PickerState::new("Select item", items, false);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
state.update_filter();
assert_eq!(state.filtered_count(), 3);
}
#[test]
fn test_no_matches() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, false);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::empty()));
state.update_filter();
assert_eq!(state.filtered_count(), 0);
assert!(state.selected().is_none());
}
#[test]
fn test_custom_input_creation() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, true);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
state.update_filter();
assert!(state.custom_input_item.is_some());
let filtered: Vec<_> = state.filtered_items().collect();
assert!(filtered.len() >= 3);
let last = filtered.last().unwrap();
assert_eq!(last.0, usize::MAX);
assert_eq!(last.1.display.as_ref(), "fea");
}
#[test]
fn test_custom_input_not_created_when_disabled() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, false);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('t'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
state.update_filter();
assert!(state.custom_input_item.is_none());
}
#[test]
fn test_custom_input_not_created_on_empty_pattern() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, true);
assert!(state.custom_input_item.is_none());
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
state.update_filter();
assert!(state.custom_input_item.is_some());
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty()));
state.update_filter();
assert!(state.custom_input_item.is_none());
}
#[test]
fn test_cursor_next() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, false);
assert_eq!(state.cursor(), 0);
state.next();
assert_eq!(state.cursor(), 1);
state.next();
assert_eq!(state.cursor(), 2);
}
#[test]
fn test_cursor_next_wraps_around() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, false);
for _ in 0..5 {
state.next();
}
assert_eq!(state.cursor(), 0);
}
#[test]
fn test_cursor_previous() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, false);
state.next();
state.next();
assert_eq!(state.cursor(), 2);
state.previous();
assert_eq!(state.cursor(), 1);
state.previous();
assert_eq!(state.cursor(), 0);
}
#[test]
fn test_cursor_previous_wraps_around() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, false);
assert_eq!(state.cursor(), 0);
state.previous();
assert_eq!(state.cursor(), 4);
}
#[test]
fn test_cursor_with_custom_input() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, true);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
state.update_filter();
assert_eq!(state.cursor(), 0);
state.next();
assert_eq!(state.cursor(), 1);
state.next();
assert_eq!(state.cursor(), 2);
state.next();
assert_eq!(state.cursor(), 3);
state.next();
assert_eq!(state.cursor(), 0);
state.previous();
assert_eq!(state.cursor(), 3); }
#[test]
fn test_navigation_with_custom_input_at_end() {
let items = vec![
PickerItem::new("feature/a", PickerData::Item("feature/a".to_string())),
PickerItem::new("feature/b", PickerData::Item("feature/b".to_string())),
];
let mut state = PickerState::new("Select", items, true);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('t'), KeyModifiers::empty()));
state.update_filter();
assert_eq!(state.filtered_count(), 2);
assert_eq!(state.cursor(), 0);
let selected = state.selected().unwrap();
match &selected.data {
PickerData::Item(_) => {}
_ => panic!("Expected first item to be a revision"),
}
state.next();
assert_eq!(state.cursor(), 1);
state.next();
assert_eq!(state.cursor(), 2);
let selected = state.selected().unwrap();
match &selected.data {
PickerData::CustomInput(s) => assert_eq!(s, "feat"),
_ => panic!("Expected custom input at end"),
}
state.next();
assert_eq!(state.cursor(), 0);
}
#[test]
fn test_cursor_resets_when_filter_reduces_items() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, false);
for _ in 0..4 {
state.next();
}
assert_eq!(state.cursor(), 4);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
state.update_filter();
assert_eq!(state.cursor(), 0);
}
#[test]
fn test_scroll_through_many_items() {
let items: Vec<_> = (0..20)
.map(|i| {
PickerItem::new(
format!("branch-{:02}", i),
PickerData::Item(format!("branch-{:02}", i)),
)
})
.collect();
let mut state = PickerState::new("Select", items, false);
assert_eq!(state.cursor(), 0);
for _ in 0..10 {
state.next();
}
assert_eq!(state.cursor(), 10);
for _ in 0..9 {
state.next();
}
assert_eq!(state.cursor(), 19);
state.next();
assert_eq!(state.cursor(), 0);
state.previous();
assert_eq!(state.cursor(), 19);
}
#[test]
fn test_navigation_after_filtering() {
let items = vec![
PickerItem::new("feature/a", PickerData::Item("feature/a".to_string())),
PickerItem::new("feature/b", PickerData::Item("feature/b".to_string())),
PickerItem::new("main", PickerData::Item("main".to_string())),
PickerItem::new("develop", PickerData::Item("develop".to_string())),
];
let mut state = PickerState::new("Select", items, false);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
state.update_filter();
assert_eq!(state.filtered_count(), 2);
assert_eq!(state.cursor(), 0);
state.next();
assert_eq!(state.cursor(), 1);
state.next();
assert_eq!(state.cursor(), 0);
state.previous();
assert_eq!(state.cursor(), 1);
}
#[test]
fn test_selected_returns_correct_item() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, false);
let selected = state.selected().unwrap();
assert_eq!(selected.display.as_ref(), "main");
state.next();
state.next();
let selected = state.selected().unwrap();
assert_eq!(selected.display.as_ref(), "feature/test");
}
#[test]
fn test_selected_with_filter() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, false);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()));
state.update_filter();
let selected = state.selected().unwrap();
assert_eq!(selected.display.as_ref(), "bugfix/123");
}
#[test]
fn test_selected_returns_custom_input() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, true);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty()));
state.update_filter();
assert_eq!(state.cursor(), 0);
assert_eq!(state.selected().unwrap().display.as_ref(), "qq");
}
#[test]
fn test_select_custom_input() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, true);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::empty()));
state.update_filter();
assert_eq!(state.filtered_count(), 0);
assert!(state.custom_input_item.is_some());
assert_eq!(state.cursor(), 0);
let selected = state.selected().unwrap();
assert_eq!(selected.display, "xyz");
match &selected.data {
PickerData::CustomInput(s) => assert_eq!(s, "xyz"),
_ => panic!("Expected CustomInput"),
}
state.done();
assert!(state.is_done());
assert_eq!(state.status(), &PickerStatus::Done);
}
#[test]
fn test_filtered_items_order() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, true);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()));
state.update_filter();
let indices: Vec<_> = state.filtered_items().map(|(idx, _)| idx).collect();
assert_eq!(indices[0], 2); assert_eq!(indices[1], 3); assert_eq!(indices[2], 4); assert_eq!(indices[3], usize::MAX); }
#[test]
fn test_match_indices_with_pattern() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, false);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('m'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::empty()));
state.update_filter();
let indices = state.match_indices(0);
assert!(indices.is_some());
let indices = indices.unwrap();
assert_eq!(indices.len(), 3); }
#[test]
fn test_match_indices_empty_pattern() {
let items = create_test_items();
let state = PickerState::new("Select", items, false);
let indices = state.match_indices(0);
assert!(indices.is_none());
}
#[test]
fn test_match_indices_custom_input_returns_none() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, true);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('t'), KeyModifiers::empty()));
state.update_filter();
let indices = state.match_indices(usize::MAX);
assert!(indices.is_none());
}
#[test]
fn test_status_transitions() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, false);
assert_eq!(state.status(), &PickerStatus::Active);
assert!(!state.is_done());
assert!(!state.is_cancelled());
state.done();
assert_eq!(state.status(), &PickerStatus::Done);
assert!(state.is_done());
assert!(!state.is_cancelled());
}
#[test]
fn test_status_cancelled() {
let items = create_test_items();
let mut state = PickerState::new("Select", items, false);
state.cancel();
assert_eq!(state.status(), &PickerStatus::Cancelled);
assert!(!state.is_done());
assert!(state.is_cancelled());
}
#[test]
fn test_empty_items_list() {
let state = PickerState::new("Select", vec![], false);
assert_eq!(state.total_items(), 0);
assert_eq!(state.filtered_count(), 0);
assert_eq!(state.cursor(), 0);
assert!(state.selected().is_none());
}
#[test]
fn test_empty_items_with_custom_input() {
let mut state = PickerState::new("Select", vec![], true);
state
.input_state
.handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
state.update_filter();
assert_eq!(state.total_items(), 0);
assert_eq!(state.filtered_count(), 0);
assert!(state.custom_input_item.is_some());
assert_eq!(state.selected().unwrap().display.as_ref(), "a");
}
#[test]
fn test_cursor_navigation_empty_list() {
let mut state = PickerState::new("Select", vec![], false);
state.next();
assert_eq!(state.cursor(), 0);
state.previous();
assert_eq!(state.cursor(), 0);
}
#[test]
fn test_single_item_navigation() {
let items = vec![PickerItem::new(
"only",
PickerData::Item("only".to_string()),
)];
let mut state = PickerState::new("Select", items, false);
assert_eq!(state.cursor(), 0);
state.next();
assert_eq!(state.cursor(), 0);
state.previous();
assert_eq!(state.cursor(), 0); }
#[test]
fn test_with_refs_basic_sorting() {
let refs = &[
Ref::Tag("v1.0.0".to_string()),
Ref::Remote("origin/main".to_string()),
Ref::Head("feature".to_string()),
Ref::Head("main".to_string()),
Ref::Tag("v2.0.0".to_string()),
];
let state = PickerState::with_refs(PickerParams {
prompt: "Select".into(),
refs,
exclude_ref: None,
default: None,
allow_custom_input: false,
});
let items: Vec<_> = state
.filtered_items()
.map(|(_, item)| item.display.as_ref())
.collect();
assert_eq!(
items,
&[
"tag: v1.0.0",
"origin/main",
"feature",
"main",
"tag: v2.0.0"
]
)
}
#[test]
fn test_with_refs_empty_list() {
let state = PickerState::with_refs(PickerParams {
prompt: "Select".into(),
refs: &[],
exclude_ref: None,
default: None,
allow_custom_input: false,
});
assert_eq!(state.total_items(), 0);
assert_eq!(state.filtered_count(), 0);
assert!(state.selected().is_none());
}
#[test]
fn test_with_refs_with_default() {
let refs = &[
Ref::Head("feature".to_string()),
Ref::Head("main".to_string()),
Ref::Tag("v1.0.0".to_string()),
];
let default = Some(Rev::Ref(Ref::Head("main".to_string())));
let state = PickerState::with_refs(PickerParams {
prompt: "Select".into(),
refs,
exclude_ref: None,
default,
allow_custom_input: false,
});
let items: Vec<_> = state
.filtered_items()
.map(|(_, item)| item.display.as_ref())
.collect();
assert_eq!(&items, &["main", "feature", "tag: v1.0.0"]);
}
#[test]
fn test_with_refs_exclude_ref() {
let refs = &[
Ref::Head("feature".to_string()),
Ref::Head("main".to_string()),
Ref::Tag("v1.0.0".to_string()),
];
let state = PickerState::with_refs(PickerParams {
prompt: "Select".into(),
refs,
exclude_ref: Some(Ref::Head("main".to_string())),
default: None,
allow_custom_input: false,
});
let items: Vec<_> = state
.filtered_items()
.map(|(_, item)| item.display.as_ref())
.collect();
assert_eq!(&items, &["feature", "tag: v1.0.0"])
}
#[test]
fn test_with_refs_duplicate_names() {
let refs = &[
Ref::Head("v1.0.0".to_string()),
Ref::Tag("v1.0.0".to_string()),
Ref::Head("main".to_string()),
];
let state = PickerState::with_refs(PickerParams {
prompt: "Select".into(),
refs,
exclude_ref: None,
default: None,
allow_custom_input: false,
});
let items: Vec<_> = state
.filtered_items()
.map(|(_, item)| item.display.as_ref())
.collect();
assert_eq!(&items, &["v1.0.0", "tag: v1.0.0", "main"])
}
#[test]
fn test_with_refs_duplicate_with_default() {
let refs = &[
Ref::Head("v1.0.0".to_string()),
Ref::Tag("v1.0.0".to_string()),
Ref::Head("main".to_string()),
];
let state = PickerState::with_refs(PickerParams {
prompt: "Select".into(),
refs,
exclude_ref: None,
default: Some(Rev::Ref(Ref::Tag("v1.0.0".to_string()))),
allow_custom_input: false,
});
let items: Vec<_> = state
.filtered_items()
.map(|(_, item)| item.display.as_ref())
.collect();
assert_eq!(items, &["tag: v1.0.0", "v1.0.0", "main"])
}
#[test]
fn test_with_refs_exclude_and_default_same() {
let refs = &[
Ref::Head("feature".to_string()),
Ref::Head("main".to_string()),
];
let state = PickerState::with_refs(PickerParams {
prompt: "Select".into(),
refs,
exclude_ref: Some(Ref::Head("main".to_string())),
default: Some(Rev::Ref(Ref::Head("main".to_string()))),
allow_custom_input: false,
});
let items: Vec<_> = state
.filtered_items()
.map(|(_, item)| item.display.as_ref())
.collect();
assert_eq!(&items, &["main", "feature"])
}
#[test]
fn test_with_refs_with_custom_input() {
let refs = &[
Ref::Head("main".to_string()),
Ref::Tag("v1.0.0".to_string()),
];
let state = PickerState::with_refs(PickerParams {
prompt: "Select".into(),
refs,
exclude_ref: None,
default: None,
allow_custom_input: true,
});
assert!(state.allow_custom_input);
assert!(state.custom_input_item.is_none());
}
#[test]
fn test_with_refs_exclude_tag_sibling() {
let refs = &[
Ref::Head("v1.0.0".to_string()),
Ref::Tag("v1.0.0".to_string()),
];
let state = PickerState::with_refs(PickerParams {
prompt: "Select".into(),
refs,
exclude_ref: Some(Ref::Head("v1.0.0".to_string())),
default: None,
allow_custom_input: false,
});
let items: Vec<_> = state
.filtered_items()
.map(|(_, item)| item.display.as_ref())
.collect();
assert_eq!(&items, &["tag: v1.0.0"]);
}
#[test]
fn test_with_refs_remote_tracking_default() {
let refs = &[
Ref::Head("main".to_string()),
Ref::Remote("origin/main".to_string()),
Ref::Remote("origin/feature".to_string()),
];
let state = PickerState::with_refs(PickerParams {
prompt: "Select".into(),
refs,
exclude_ref: None,
default: Some(Rev::Ref(Ref::Remote("origin/main".to_string()))),
allow_custom_input: false,
});
let items: Vec<_> = state
.filtered_items()
.map(|(_, item)| item.display.as_ref())
.collect();
assert_eq!(&items, &["origin/main", "main", "origin/feature"]);
}
}