use gpui::{App, SharedString, Task, Window};
use crate::IndexPath;
use super::delegate::{SearchableListDelegate, SearchableListItem};
impl SearchableListItem for String {
type Value = Self;
fn title(&self) -> SharedString {
SharedString::from(self.clone())
}
fn value(&self) -> &Self::Value {
self
}
}
impl SearchableListItem for SharedString {
type Value = Self;
fn title(&self) -> SharedString {
self.clone()
}
fn value(&self) -> &Self::Value {
self
}
}
impl SearchableListItem for &'static str {
type Value = Self;
fn title(&self) -> SharedString {
SharedString::from(*self)
}
fn value(&self) -> &Self::Value {
self
}
}
impl<T: SearchableListItem + 'static> SearchableListDelegate for Vec<T> {
type Item = T;
fn items_count(&self, _: usize) -> usize {
self.len()
}
fn item(&self, ix: IndexPath) -> Option<&Self::Item> {
self.as_slice().get(ix.row)
}
fn position<V>(&self, value: &V) -> Option<IndexPath>
where
Self::Item: SearchableListItem<Value = V>,
V: PartialEq,
{
self.iter()
.position(|v| v.value() == value)
.map(|ix| IndexPath::default().row(ix))
}
}
#[derive(Debug, Clone)]
pub struct SearchableVec<T> {
items: Vec<T>,
matched: Vec<Matched>,
}
#[derive(Debug, Clone)]
struct Matched {
ix: usize,
rows: Option<Vec<usize>>,
}
impl Matched {
fn all(ix: usize) -> Self {
Self { ix, rows: None }
}
fn len(&self, all: usize) -> usize {
self.rows.as_ref().map_or(all, Vec::len)
}
fn row(&self, row: usize) -> Option<usize> {
match &self.rows {
Some(rows) => rows.get(row).copied(),
None => Some(row),
}
}
}
impl<T> SearchableVec<T> {
fn from_items(items: Vec<T>) -> Self {
Self {
matched: (0..items.len()).map(Matched::all).collect(),
items,
}
}
fn matched_item(&self, ix: usize) -> Option<(&Matched, &T)> {
let matched = self.matched.get(ix)?;
Some((matched, self.items.get(matched.ix)?))
}
fn matched_items(&self) -> impl Iterator<Item = (&Matched, &T)> {
self.matched
.iter()
.filter_map(|matched| Some((matched, self.items.get(matched.ix)?)))
}
}
impl<T: Clone> SearchableVec<T> {
pub fn new(items: impl Into<Vec<T>>) -> Self {
Self::from_items(items.into())
}
pub fn push(&mut self, item: T) {
self.matched.push(Matched::all(self.items.len()));
self.items.push(item);
}
}
impl<T: SearchableListItem> From<Vec<T>> for SearchableVec<T> {
fn from(items: Vec<T>) -> Self {
Self::from_items(items)
}
}
impl<I: SearchableListItem + 'static> SearchableListDelegate for SearchableVec<I> {
type Item = I;
fn items_count(&self, _: usize) -> usize {
self.matched.len()
}
fn item(&self, ix: IndexPath) -> Option<&Self::Item> {
self.matched_item(ix.row).map(|(_, item)| item)
}
fn position<V>(&self, value: &V) -> Option<IndexPath>
where
Self::Item: SearchableListItem<Value = V>,
V: PartialEq,
{
self.matched_items()
.position(|(_, v)| v.value() == value)
.map(|ix| IndexPath::default().row(ix))
}
fn perform_search(&mut self, query: &str, _: &mut Window, _: &mut App) -> Task<()> {
self.matched = self
.items
.iter()
.enumerate()
.filter(|(_, item)| item.matches(query))
.map(|(ix, _)| Matched::all(ix))
.collect();
Task::ready(())
}
}
#[derive(Debug, Clone)]
pub struct SearchableGroup<I: SearchableListItem> {
pub title: SharedString,
pub items: Vec<I>,
}
impl<I: SearchableListItem> SearchableGroup<I> {
pub fn new(title: impl Into<SharedString>) -> Self {
Self {
title: title.into(),
items: vec![],
}
}
pub fn item(mut self, item: I) -> Self {
self.items.push(item);
self
}
pub fn items(mut self, items: impl IntoIterator<Item = I>) -> Self {
self.items.extend(items);
self
}
fn matched_rows(&self, query: &str) -> Option<Vec<usize>> {
let rows: Vec<usize> = self
.items
.iter()
.enumerate()
.filter(|(_, item)| item.matches(query))
.map(|(ix, _)| ix)
.collect();
(!rows.is_empty() || self.title.to_lowercase().contains(&query.to_lowercase()))
.then_some(rows)
}
}
impl<I: SearchableListItem + 'static> SearchableListDelegate for SearchableVec<SearchableGroup<I>> {
type Item = I;
fn sections_count(&self, _: &App) -> usize {
self.matched.len()
}
fn items_count(&self, section: usize) -> usize {
self.matched_item(section)
.map_or(0, |(matched, group)| matched.len(group.items.len()))
}
fn section(&self, section: usize) -> Option<gpui::AnyElement> {
use gpui::IntoElement as _;
let (_, group) = self.matched_item(section)?;
Some(group.title.clone().into_any_element())
}
fn item(&self, ix: IndexPath) -> Option<&Self::Item> {
let (matched, group) = self.matched_item(ix.section)?;
group.items.get(matched.row(ix.row)?)
}
fn position<V>(&self, value: &V) -> Option<IndexPath>
where
Self::Item: SearchableListItem<Value = V>,
V: PartialEq,
{
for (ix, (matched, group)) in self.matched_items().enumerate() {
for row_ix in 0..matched.len(group.items.len()) {
let item = matched.row(row_ix).and_then(|row| group.items.get(row));
if item.is_some_and(|item| item.value() == value) {
return Some(IndexPath::default().section(ix).row(row_ix));
}
}
}
None
}
fn perform_search(&mut self, query: &str, _: &mut Window, _: &mut App) -> Task<()> {
self.matched = self
.items
.iter()
.enumerate()
.filter_map(|(ix, group)| {
let rows = group.matched_rows(query)?;
Some(Matched {
ix,
rows: Some(rows),
})
})
.collect();
Task::ready(())
}
}
#[cfg(test)]
mod tests {
use gpui::TestAppContext;
use super::*;
#[gpui::test]
fn test_searchable_vec_maps_matched_rows_to_items(cx: &mut TestAppContext) {
let cx = cx.add_empty_window();
cx.update(|window, cx| {
let mut items = SearchableVec::new(vec!["Rust", "Go", "Ruby"]);
_ = items.perform_search("ru", window, cx);
assert_eq!(items.items_count(0), 2);
assert_eq!(items.item(IndexPath::new(1)), Some(&"Ruby"));
assert_eq!(items.position(&"Ruby"), Some(IndexPath::new(1)));
assert_eq!(items.position(&"Go"), None);
items.push("Rune");
assert_eq!(items.item(IndexPath::new(2)), Some(&"Rune"));
_ = items.perform_search("", window, cx);
assert_eq!(items.items_count(0), 4);
});
}
#[gpui::test]
fn test_searchable_group_keeps_matched_rows(cx: &mut TestAppContext) {
let cx = cx.add_empty_window();
cx.update(|window, cx| {
let mut groups = SearchableVec::new(vec![
SearchableGroup::new("Fruits").items(["Apple", "Banana"]),
SearchableGroup::new("Berries").items(["Blueberry", "Cranberry"]),
SearchableGroup::new("Nuts").items(["Almond"]),
]);
_ = groups.perform_search("cran", window, cx);
assert_eq!(groups.sections_count(cx), 1);
assert_eq!(groups.items_count(0), 1);
assert_eq!(groups.item(IndexPath::new(0)), Some(&"Cranberry"));
assert_eq!(groups.position(&"Cranberry"), Some(IndexPath::new(0)));
assert_eq!(groups.position(&"Blueberry"), None);
_ = groups.perform_search("nuts", window, cx);
assert_eq!(groups.sections_count(cx), 1);
assert_eq!(groups.items_count(0), 0);
_ = groups.perform_search("b", window, cx);
assert_eq!(groups.sections_count(cx), 2);
assert_eq!(groups.items_count(1), 2);
assert_eq!(
groups.position(&"Cranberry"),
Some(IndexPath::new(1).section(1))
);
});
}
}