use std::sync::Arc;
use async_trait::async_trait;
use ratatui::widgets::ListItem;
use crate::settings::icons::Icons;
use crate::settings::themes::Theme;
pub trait SearchRow: Clone + Send + Sync + 'static {
fn to_list_item(&self, theme: &Theme, icons: &Icons, selected: bool) -> ListItem<'static>;
fn visual_height(&self) -> u16 {
1
}
fn match_text(&self) -> Option<&str> {
None
}
}
pub enum Loaded<R> {
Replace(Vec<R>),
Push(R),
Done,
}
pub type RankFn<R> = std::sync::Arc<dyn Fn(&[R], &str) -> Vec<usize> + Send + Sync>;
pub enum Filter<R: SearchRow> {
SourceOrder,
Fuzzy,
Rank(RankFn<R>),
}
#[derive(Clone)]
pub struct Emit<R> {
tx: std::sync::mpsc::Sender<(u64, Loaded<R>)>,
generation: u64,
redraw: Arc<dyn Fn() + Send + Sync>,
}
impl<R> Emit<R> {
pub(super) fn new(
tx: std::sync::mpsc::Sender<(u64, Loaded<R>)>,
generation: u64,
redraw: Arc<dyn Fn() + Send + Sync>,
) -> Self {
Self {
tx,
generation,
redraw,
}
}
pub fn replace(&self, rows: Vec<R>) {
let _ = self.tx.send((self.generation, Loaded::Replace(rows)));
(self.redraw)();
}
pub fn push(&self, row: R) {
let _ = self.tx.send((self.generation, Loaded::Push(row)));
(self.redraw)();
}
pub fn done(&self) {
let _ = self.tx.send((self.generation, Loaded::Done));
(self.redraw)();
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SuggestionItem {
pub display: String,
pub secondary: Option<String>,
}
impl SuggestionItem {
pub fn plain(display: impl Into<String>) -> Self {
Self {
display: display.into(),
secondary: None,
}
}
}
#[async_trait]
pub trait SuggestionSource: Send + Sync + 'static {
async fn notes_by_prefix(&self, prefix: &str, limit: usize) -> Vec<SuggestionItem>;
async fn tags_by_prefix(&self, prefix: &str, limit: usize) -> Vec<SuggestionItem>;
async fn saved_searches_by_prefix(&self, _prefix: &str, _limit: usize) -> Vec<SuggestionItem> {
Vec::new()
}
}
pub struct VaultSuggestions {
pub vault: std::sync::Arc<kimun_core::NoteVault>,
}
#[async_trait]
impl SuggestionSource for VaultSuggestions {
async fn notes_by_prefix(&self, prefix: &str, limit: usize) -> Vec<SuggestionItem> {
self.vault
.suggest_notes_by_prefix(prefix, limit)
.await
.map(|v| {
v.into_iter()
.map(|n| SuggestionItem {
display: n.name,
secondary: Some(n.path.to_string()),
})
.collect()
})
.unwrap_or_default()
}
async fn tags_by_prefix(&self, prefix: &str, limit: usize) -> Vec<SuggestionItem> {
self.vault
.suggest_tags_by_prefix(prefix, limit)
.await
.map(|v| {
v.into_iter()
.map(|t| SuggestionItem {
display: t.label,
secondary: Some(format!("{}×", t.usage_count)),
})
.collect()
})
.unwrap_or_default()
}
async fn saved_searches_by_prefix(&self, prefix: &str, limit: usize) -> Vec<SuggestionItem> {
self.vault
.suggest_saved_searches_by_prefix(prefix, limit)
.await
.map(|v| {
v.into_iter()
.map(|s| SuggestionItem {
display: s.name,
secondary: Some(s.query),
})
.collect()
})
.unwrap_or_default()
}
}
#[async_trait]
pub trait RowSource<R: SearchRow>: Send + Sync + 'static {
async fn load(&self, query: &str, emit: Emit<R>);
fn leading_row(&self, _query: &str) -> Option<R> {
None
}
fn reload_on_query(&self) -> bool {
true
}
}
#[cfg(test)]
mod suggestion_tests {
use super::*;
struct Mem {
notes: Vec<SuggestionItem>,
tags: Vec<SuggestionItem>,
}
#[async_trait]
impl SuggestionSource for Mem {
async fn notes_by_prefix(&self, p: &str, _n: usize) -> Vec<SuggestionItem> {
self.notes
.iter()
.filter(|x| x.display.starts_with(p))
.cloned()
.collect()
}
async fn tags_by_prefix(&self, p: &str, _n: usize) -> Vec<SuggestionItem> {
self.tags
.iter()
.filter(|x| x.display.starts_with(p))
.cloned()
.collect()
}
}
#[tokio::test]
async fn mem_suggestions_filter_by_prefix() {
let m = Mem {
notes: vec![SuggestionItem {
display: "projects".into(),
secondary: Some("work/projects".into()),
}],
tags: vec![SuggestionItem::plain("todo")],
};
assert_eq!(m.notes_by_prefix("pro", 9).await.len(), 1);
assert_eq!(m.notes_by_prefix("pro", 9).await[0].display, "projects");
assert_eq!(m.tags_by_prefix("to", 9).await[0].display, "todo");
}
}