use crate::file::FileEntry;
#[derive(Debug, Clone)]
pub struct MatchIndex {
display_lc: String,
path_stem_lc: String,
corpus: String,
}
impl MatchIndex {
pub fn from_entry(entry: &dyn FileEntry) -> Self {
let display_lc = entry.display_name().to_lowercase();
let path_stem_lc = entry
.path()
.file_stem()
.map(|s| s.to_string_lossy().to_lowercase())
.unwrap_or_default();
let link_stem_lc = entry
.link_path()
.and_then(|p| p.file_stem())
.map(|s| s.to_string_lossy().to_lowercase())
.unwrap_or_default();
let mut pieces: Vec<String> = Vec::with_capacity(8);
push_unique(&mut pieces, display_lc.clone());
push_unique(&mut pieces, path_stem_lc.clone());
push_unique(&mut pieces, link_stem_lc);
if let Ok(info) = entry.version_info()
&& let Some(fi) = info.english()
{
for s in [
fi.file_description(),
fi.company_name(),
fi.product_name(),
fi.original_filename(),
fi.file_version(),
]
.into_iter()
.flatten()
{
push_unique(&mut pieces, s.to_lowercase());
}
}
let corpus = pieces.join("\n");
Self {
display_lc,
path_stem_lc,
corpus,
}
}
#[must_use]
pub fn score(&self, needle: &str) -> Option<f32> {
if needle.is_empty() {
return Some(1.0);
}
if self.display_lc == needle {
return Some(10.0);
}
if self.display_lc.starts_with(needle) {
return Some(8.0);
}
if contains_at_word_boundary(&self.display_lc, needle) {
return Some(3.0);
}
if contains_at_word_boundary(&self.path_stem_lc, needle) {
return Some(2.0);
}
if self.display_lc.contains(needle) {
return Some(1.0);
}
if self.corpus.contains(needle) {
return Some(0.5);
}
None
}
}
fn push_unique(pieces: &mut Vec<String>, s: String) {
if s.is_empty() {
return;
}
if pieces.last().is_some_and(|prev| prev == &s) {
return;
}
pieces.push(s);
}
fn contains_at_word_boundary(haystack: &str, needle: &str) -> bool {
if needle.is_empty() {
return false;
}
let mut prev: Option<char> = None;
for (i, c) in haystack.char_indices() {
if haystack[i..].starts_with(needle) && prev.is_none_or(|p| !p.is_alphanumeric()) {
return true;
}
prev = Some(c);
}
false
}