Skip to main content

kimun_notes/components/note_browser/
file_finder_provider.rs

1use 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// ---------------------------------------------------------------------------
14// MatchEntry — adapts (index, haystack_str) for nucleo match_list
15// ---------------------------------------------------------------------------
16
17#[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
29// ---------------------------------------------------------------------------
30// FileFinderProvider
31// ---------------------------------------------------------------------------
32
33pub 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        // Non-empty query: nucleo fuzzy filter
87        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(&notes[e.idx].0, &notes[e.idx].1))
109            .collect();
110
111        // Prepend CreateNote entry so the user can create a note with this query as the path.
112        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}