kimun_notes/components/search_list/
seams.rs1use std::sync::Arc;
6
7use async_trait::async_trait;
8use ratatui::widgets::ListItem;
9
10use crate::settings::icons::Icons;
11use crate::settings::themes::Theme;
12
13pub trait SearchRow: Clone + Send + Sync + 'static {
16 fn to_list_item(&self, theme: &Theme, icons: &Icons, selected: bool) -> ListItem<'static>;
18
19 fn visual_height(&self) -> u16 {
21 1
22 }
23
24 fn match_text(&self) -> Option<&str> {
28 None
29 }
30}
31
32pub enum Loaded<R> {
35 Replace(Vec<R>),
36 Push(R),
37 Done,
38}
39
40pub type RankFn<R> = std::sync::Arc<dyn Fn(&[R], &str) -> Vec<usize> + Send + Sync>;
43
44pub enum Filter<R: SearchRow> {
47 SourceOrder,
49 Fuzzy,
51 Rank(RankFn<R>),
53}
54
55#[derive(Clone)]
58pub struct Emit<R> {
59 tx: std::sync::mpsc::Sender<(u64, Loaded<R>)>,
60 generation: u64,
61 redraw: Arc<dyn Fn() + Send + Sync>,
62}
63
64impl<R> Emit<R> {
65 pub(super) fn new(
66 tx: std::sync::mpsc::Sender<(u64, Loaded<R>)>,
67 generation: u64,
68 redraw: Arc<dyn Fn() + Send + Sync>,
69 ) -> Self {
70 Self {
71 tx,
72 generation,
73 redraw,
74 }
75 }
76
77 pub fn replace(&self, rows: Vec<R>) {
79 let _ = self.tx.send((self.generation, Loaded::Replace(rows)));
80 (self.redraw)();
81 }
82
83 pub fn push(&self, row: R) {
85 let _ = self.tx.send((self.generation, Loaded::Push(row)));
86 (self.redraw)();
87 }
88
89 pub fn done(&self) {
91 let _ = self.tx.send((self.generation, Loaded::Done));
92 (self.redraw)();
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct SuggestionItem {
100 pub display: String,
101 pub secondary: Option<String>,
102}
103
104impl SuggestionItem {
105 pub fn plain(display: impl Into<String>) -> Self {
106 Self {
107 display: display.into(),
108 secondary: None,
109 }
110 }
111}
112
113#[async_trait]
116pub trait SuggestionSource: Send + Sync + 'static {
117 async fn notes_by_prefix(&self, prefix: &str, limit: usize) -> Vec<SuggestionItem>;
118 async fn tags_by_prefix(&self, prefix: &str, limit: usize) -> Vec<SuggestionItem>;
119
120 async fn saved_searches_by_prefix(&self, _prefix: &str, _limit: usize) -> Vec<SuggestionItem> {
125 Vec::new()
126 }
127}
128
129pub struct VaultSuggestions {
132 pub vault: std::sync::Arc<kimun_core::NoteVault>,
133}
134
135#[async_trait]
136impl SuggestionSource for VaultSuggestions {
137 async fn notes_by_prefix(&self, prefix: &str, limit: usize) -> Vec<SuggestionItem> {
138 self.vault
139 .suggest_notes_by_prefix(prefix, limit)
140 .await
141 .map(|v| {
142 v.into_iter()
143 .map(|n| SuggestionItem {
144 display: n.name,
145 secondary: Some(n.path.to_string()),
146 })
147 .collect()
148 })
149 .unwrap_or_default()
150 }
151 async fn tags_by_prefix(&self, prefix: &str, limit: usize) -> Vec<SuggestionItem> {
152 self.vault
153 .suggest_tags_by_prefix(prefix, limit)
154 .await
155 .map(|v| {
156 v.into_iter()
157 .map(|t| SuggestionItem {
158 display: t.label,
159 secondary: Some(format!("{}×", t.usage_count)),
160 })
161 .collect()
162 })
163 .unwrap_or_default()
164 }
165 async fn saved_searches_by_prefix(&self, prefix: &str, limit: usize) -> Vec<SuggestionItem> {
166 self.vault
171 .suggest_saved_searches_by_prefix(prefix, limit)
172 .await
173 .map(|v| {
174 v.into_iter()
175 .map(|s| SuggestionItem {
176 display: s.name,
177 secondary: Some(s.query),
178 })
179 .collect()
180 })
181 .unwrap_or_default()
182 }
183}
184
185#[async_trait]
188pub trait RowSource<R: SearchRow>: Send + Sync + 'static {
189 async fn load(&self, query: &str, emit: Emit<R>);
193
194 fn leading_row(&self, _query: &str) -> Option<R> {
197 None
198 }
199
200 fn reload_on_query(&self) -> bool {
204 true
205 }
206}
207
208#[cfg(test)]
209mod suggestion_tests {
210 use super::*;
211 struct Mem {
212 notes: Vec<SuggestionItem>,
213 tags: Vec<SuggestionItem>,
214 }
215 #[async_trait]
216 impl SuggestionSource for Mem {
217 async fn notes_by_prefix(&self, p: &str, _n: usize) -> Vec<SuggestionItem> {
218 self.notes
219 .iter()
220 .filter(|x| x.display.starts_with(p))
221 .cloned()
222 .collect()
223 }
224 async fn tags_by_prefix(&self, p: &str, _n: usize) -> Vec<SuggestionItem> {
225 self.tags
226 .iter()
227 .filter(|x| x.display.starts_with(p))
228 .cloned()
229 .collect()
230 }
231 }
232 #[tokio::test]
233 async fn mem_suggestions_filter_by_prefix() {
234 let m = Mem {
235 notes: vec![SuggestionItem {
236 display: "projects".into(),
237 secondary: Some("work/projects".into()),
238 }],
239 tags: vec![SuggestionItem::plain("todo")],
240 };
241 assert_eq!(m.notes_by_prefix("pro", 9).await.len(), 1);
242 assert_eq!(m.notes_by_prefix("pro", 9).await[0].display, "projects");
243 assert_eq!(m.tags_by_prefix("to", 9).await[0].display, "todo");
244 }
245}