kimun_notes/components/note_browser/
file_finder_provider.rs1use std::sync::Arc;
2
3use async_trait::async_trait;
4use kimun_core::NoteVault;
5use kimun_core::nfs::{NoteEntryData, VaultPath};
6use kimun_core::note::NoteContentData;
7use nucleo::Matcher;
8use nucleo::pattern::{CaseMatching, Normalization, Pattern};
9
10use super::format_journal_date;
11use crate::components::file_list::FileListEntry;
12use crate::components::search_list::{Emit, RowSource};
13
14#[derive(Clone)]
19struct MatchEntry {
20 idx: usize,
21 text: String,
22}
23
24impl AsRef<str> for MatchEntry {
25 fn as_ref(&self) -> &str {
26 &self.text
27 }
28}
29
30pub struct FileFinderProvider {
35 vault: Arc<NoteVault>,
36 current_dir: VaultPath,
37 notes_cache: Arc<tokio::sync::OnceCell<Vec<(NoteEntryData, NoteContentData)>>>,
38}
39
40impl FileFinderProvider {
41 pub fn new(vault: Arc<NoteVault>, current_dir: VaultPath) -> Self {
42 Self {
43 vault,
44 current_dir,
45 notes_cache: Arc::new(tokio::sync::OnceCell::new()),
46 }
47 }
48
49 fn to_entry(&self, entry: &NoteEntryData, content: &NoteContentData) -> FileListEntry {
50 let filename = entry.path.get_parent_path().1;
51 let title = if content.title.trim().is_empty() {
52 "<no title>".to_string()
53 } else {
54 content.title.clone()
55 };
56 let journal_date = self
57 .vault
58 .journal_date(&entry.path)
59 .map(format_journal_date);
60 FileListEntry::Note {
61 path: entry.path.clone(),
62 title,
63 filename,
64 journal_date,
65 is_open: false,
66 }
67 }
68}
69
70#[async_trait]
71impl RowSource<FileListEntry> for FileFinderProvider {
72 async fn load(&self, query: &str, emit: Emit<FileListEntry>) {
73 let vault = Arc::clone(&self.vault);
74 let notes = self
75 .notes_cache
76 .get_or_init(|| async move { vault.get_all_notes().await.unwrap_or_default() })
77 .await;
78
79 if query.is_empty() {
80 let mut sorted = notes.clone();
81 sorted.sort_by_key(|(entry, _)| std::cmp::Reverse(entry.modified_secs));
82 let entries: Vec<FileListEntry> = sorted
83 .iter()
84 .map(|(entry, content)| self.to_entry(entry, content))
85 .collect();
86 emit.replace(entries);
87 return;
88 }
89
90 let candidates: Vec<MatchEntry> = notes
92 .iter()
93 .enumerate()
94 .map(|(i, (entry, content))| {
95 let filename = entry.path.get_parent_path().1;
96 let text = format!("{} {}", filename, content.title);
97 MatchEntry { idx: i, text }
98 })
99 .collect();
100
101 let query_str = query.to_string();
102 let matched = tokio::task::spawn_blocking(move || {
103 let mut matcher = Matcher::new(nucleo::Config::DEFAULT);
104 let pattern = Pattern::parse(&query_str, CaseMatching::Ignore, Normalization::Smart);
105 pattern.match_list(candidates, &mut matcher)
106 })
107 .await
108 .unwrap_or_default();
109
110 let result: Vec<FileListEntry> = matched
111 .into_iter()
112 .map(|(e, _score)| self.to_entry(¬es[e.idx].0, ¬es[e.idx].1))
113 .collect();
114
115 emit.replace(result);
118 }
119
120 fn leading_row(&self, query: &str) -> Option<FileListEntry> {
121 if query.is_empty() {
122 return None;
123 }
124 let resolved = self
126 .current_dir
127 .append(&VaultPath::note_path_from(query))
128 .flatten();
129 Some(FileListEntry::CreateNote {
130 filename: resolved.to_string(),
131 path: resolved,
132 })
133 }
134}