1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
use std::{cell::RefCell, rc::Rc};
use console::Key;
use crate::prompt::{cursor::StringCursor, interaction::State};
pub(crate) trait LabeledItem {
fn label(&self) -> &str;
}
/// The list of items gathered (filtered) by interactive input using
/// `FilteredView::on` event in a selection prompt.
pub(crate) struct FilteredView<I: LabeledItem> {
/// Enables the filtered view.
enabled: bool,
/// Collects the input from the user.
input: StringCursor,
/// Represents a view of the filtered items.
items: Vec<Rc<RefCell<I>>>,
}
impl<I: LabeledItem> Default for FilteredView<I> {
fn default() -> Self {
Self {
enabled: false,
input: StringCursor::default(),
items: vec![],
}
}
}
impl<I: LabeledItem + Clone> FilteredView<I> {
/// Enables the filtered view.
pub fn enable(&mut self) {
self.enabled = true;
}
/// Sets a predefined set of items for the view.
pub fn set(&mut self, items: Vec<Rc<RefCell<I>>>) {
self.items = items;
}
/// Returns the items in the view.
pub fn items(&self) -> &[Rc<RefCell<I>>] {
&self.items
}
/// Collects the input and filters the items from the list of all items.
///
/// Uses the Jaro-Winkler similarity algorithm to score the items
/// ([`strsim::jaro_winkler`]).
pub fn on<T>(&mut self, key: &Key, all_items: Vec<Rc<RefCell<I>>>) -> Option<State<T>> {
if !self.enabled {
// Pass over the control.
return None;
}
match key {
// Need further processing of simple "up" and "down" actions.
Key::ArrowDown | Key::ArrowUp => None,
// Need moving up and down if no input provided.
Key::ArrowLeft | Key::ArrowRight if self.input.is_empty() => None,
// Need to submit the selected item.
Key::Enter if !self.items.is_empty() => None,
// Otherwise, no items found.
Key::Enter => Some(State::Error("No items".into())),
// Ignore spaces passing through.
Key::Char(' ') => {
self.input.delete_left();
None
}
// Refresh the filtered items for the rest of the keys.
_ if !self.input.is_empty() => {
let input_lower = self.input.to_string();
let filter_words: Vec<_> = input_lower.split_whitespace().collect();
let mut filtered_and_scored_items: Vec<_> = all_items
.into_iter()
.map(|item| {
let label = item.borrow().label().to_lowercase();
let input = self.input.to_string().to_lowercase();
let similarity = strsim::jaro_winkler(&label, &input);
let bonus = filter_words
.iter()
.all(|word| label.contains(&word.to_lowercase()))
as usize as f64;
(similarity + bonus, item)
})
.filter(|(similarity, _)| *similarity > 0.6)
.collect();
filtered_and_scored_items.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
self.items = filtered_and_scored_items
.into_iter()
.map(|(_, item)| item)
.collect();
Some(State::Active)
}
// Reset the items to the original list.
_ => {
self.items = all_items.to_vec();
Some(State::Active)
}
}
}
/// Returns the input cursor if the filter is enabled.
/// It makes the outer code to handle the input.
pub fn input(&mut self) -> Option<&mut StringCursor> {
if !self.enabled {
return None;
}
Some(&mut self.input)
}
}