use std::{fmt, iter::FromIterator, sync::Arc};
use radix_trie::{Trie, TrieCommon};
#[derive(Clone)]
pub struct PrefixSearch {
candidates: Arc<[String]>,
index: Trie<String, usize>,
}
impl Default for PrefixSearch {
fn default() -> Self {
Self {
candidates: Arc::default(),
index: Trie::new(),
}
}
}
impl<T: fmt::Display> FromIterator<T> for PrefixSearch {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
let candidates = iter
.into_iter()
.map(|item| item.to_string())
.collect::<Arc<[_]>>();
let index = Trie::from_iter(
candidates
.iter()
.enumerate()
.map(|(index, candidate)| (candidate.clone(), index)),
);
Self { candidates, index }
}
}
impl PrefixSearch {
pub fn query(&self, query: impl AsRef<str>) -> PrefixSearchResult {
let candidates = self
.index
.get_raw_descendant(query.as_ref())
.into_iter()
.flat_map(|subtrie| subtrie.iter().map(|(_, index)| *index))
.collect::<Vec<_>>();
let selected = (!candidates.is_empty()).then_some(0);
PrefixSearchResult {
source: Arc::clone(&self.candidates),
candidates,
selected,
}
}
}
#[derive(Clone, Default)]
pub struct PrefixSearchResult {
source: Arc<[String]>,
candidates: Vec<usize>,
selected: Option<usize>,
}
impl PrefixSearchResult {
pub fn clear(&mut self) {
self.candidates.clear();
self.selected = None;
}
pub fn candidates(&self) -> impl Iterator<Item = &str> {
self.candidates
.iter()
.filter_map(|index| self.source.get(*index))
.map(String::as_str)
}
pub fn candidate_at(&self, index: usize) -> Option<&str> {
self.candidates
.get(index)
.and_then(|index| self.source.get(*index))
.map(String::as_str)
}
pub fn selected(&self) -> Option<usize> {
self.selected
}
pub fn get(&self) -> Option<&str> {
self.selected
.and_then(|selected| self.candidate_at(selected))
}
pub fn is_empty(&self) -> bool {
self.candidates.is_empty()
}
pub fn backward(&mut self) -> bool {
let Some(selected) = self.selected.filter(|selected| *selected > 0) else {
return false;
};
self.selected = Some(selected - 1);
true
}
pub fn forward(&mut self) -> bool {
let Some(selected) = self
.selected
.filter(|selected| selected.saturating_add(1) < self.candidates.len())
else {
return false;
};
self.selected = Some(selected + 1);
true
}
pub fn move_to(&mut self, index: usize) -> bool {
if index < self.candidates.len() {
self.selected = Some(index);
true
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::{PrefixSearch, PrefixSearchResult};
fn prefix_search() -> PrefixSearch {
["apple", "applet", "application", "banana"]
.into_iter()
.collect()
}
mod prefix_search {
use super::*;
mod query {
use super::*;
#[test]
fn returns_an_independently_selectable_snapshot() {
let prefix_search = prefix_search();
let mut application = prefix_search.query("app");
let banana = prefix_search.query("ban");
application.forward();
assert_eq!(
application.candidates().collect::<Vec<_>>(),
vec!["apple", "applet", "application"]
);
assert_eq!(application.get(), Some("applet"));
assert_eq!(banana.candidates().collect::<Vec<_>>(), vec!["banana"]);
assert_eq!(banana.get(), Some("banana"));
}
#[test]
fn returns_an_empty_snapshot_for_a_missing_prefix() {
let result = prefix_search().query("orange");
assert!(result.is_empty());
assert_eq!(result.selected(), None);
assert_eq!(result.get(), None);
}
}
}
mod prefix_search_result {
use super::*;
fn result() -> PrefixSearchResult {
prefix_search().query("app")
}
mod backward {
use super::*;
#[test]
fn stops_at_the_first_candidate() {
let mut result = result();
assert!(!result.backward());
assert_eq!(result.get(), Some("apple"));
}
}
mod forward {
use super::*;
#[test]
fn stops_at_the_last_candidate() {
let mut result = result();
assert!(result.forward());
assert_eq!(result.get(), Some("applet"));
assert!(result.forward());
assert_eq!(result.get(), Some("application"));
assert!(!result.forward());
}
}
mod move_to {
use super::*;
#[test]
fn rejects_an_out_of_bounds_index() {
let mut result = result();
result.move_to(2);
assert!(!result.move_to(3));
assert_eq!(result.selected(), Some(2));
}
#[test]
fn selects_the_given_candidate() {
let mut result = result();
result.move_to(2);
assert!(result.move_to(0));
assert_eq!(result.get(), Some("apple"));
}
}
mod clear {
use super::*;
#[test]
fn removes_candidates_and_selection_from_the_snapshot() {
let mut result = result();
result.clear();
assert!(result.is_empty());
assert_eq!(result.selected(), None);
assert_eq!(result.get(), None);
}
}
}
}