use std::sync::Arc;
use async_trait::async_trait;
use kimun_core::NoteVault;
use kimun_core::nfs::{NoteEntryData, VaultPath};
use kimun_core::note::NoteContentData;
use nucleo::Matcher;
use nucleo::pattern::{CaseMatching, Normalization, Pattern};
use crate::components::file_list::FileListEntry;
use super::{NoteBrowserProvider, format_journal_date};
#[derive(Clone)]
struct MatchEntry {
idx: usize,
text: String,
}
impl AsRef<str> for MatchEntry {
fn as_ref(&self) -> &str {
&self.text
}
}
pub struct FileFinderProvider {
vault: Arc<NoteVault>,
current_dir: VaultPath,
notes_cache: Arc<tokio::sync::OnceCell<Vec<(NoteEntryData, NoteContentData)>>>,
}
impl FileFinderProvider {
pub fn new(vault: Arc<NoteVault>, current_dir: VaultPath) -> Self {
Self {
vault,
current_dir,
notes_cache: Arc::new(tokio::sync::OnceCell::new()),
}
}
fn into_entry(&self, entry: &NoteEntryData, content: &NoteContentData) -> FileListEntry {
let filename = entry.path.get_parent_path().1;
let title = if content.title.trim().is_empty() {
"<no title>".to_string()
} else {
content.title.clone()
};
let journal_date = self.vault.journal_date(&entry.path).map(format_journal_date);
FileListEntry::Note {
path: entry.path.clone(),
title,
filename,
journal_date,
}
}
}
#[async_trait]
impl NoteBrowserProvider for FileFinderProvider {
async fn load(&self, query: &str) -> Vec<FileListEntry> {
let vault = Arc::clone(&self.vault);
let notes = self
.notes_cache
.get_or_init(|| async move {
vault.get_all_notes().await.unwrap_or_default()
})
.await;
if query.is_empty() {
let mut sorted = notes.clone();
sorted.sort_by(|(a, _), (b, _)| b.modified_secs.cmp(&a.modified_secs));
return sorted
.iter()
.map(|(entry, content)| self.into_entry(entry, content))
.collect();
}
let candidates: Vec<MatchEntry> = notes
.iter()
.enumerate()
.map(|(i, (entry, content))| {
let filename = entry.path.get_parent_path().1;
let text = format!("{} {}", filename, content.title);
MatchEntry { idx: i, text }
})
.collect();
let query_str = query.to_string();
let matched = tokio::task::spawn_blocking(move || {
let mut matcher = Matcher::new(nucleo::Config::DEFAULT);
let pattern = Pattern::parse(&query_str, CaseMatching::Ignore, Normalization::Smart);
pattern.match_list(candidates, &mut matcher)
})
.await
.unwrap_or_default();
let mut result: Vec<FileListEntry> = matched
.into_iter()
.map(|(e, _score)| self.into_entry(¬es[e.idx].0, ¬es[e.idx].1))
.collect();
let resolved = self
.current_dir
.append(&VaultPath::note_path_from(query))
.flatten();
result.insert(
0,
FileListEntry::CreateNote {
filename: resolved.to_string(),
path: resolved,
},
);
result
}
fn allows_create(&self) -> bool {
true
}
}