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