From 528eb20af7e0f12cb0eee494023c05db2e797c3c Mon Sep 17 00:00:00 2001 From: Erio-Harrison Date: Fri, 28 Feb 2025 11:56:27 +1100 Subject: [PATCH 1/5] feat: add acronym matching for directories (#1002) --- src/db/stream.rs | 108 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 90 insertions(+), 18 deletions(-) diff --git a/src/db/stream.rs b/src/db/stream.rs index 4af7d7a..c430d44 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -47,32 +47,73 @@ impl<'a> Stream<'a> { None } + fn match_acronym(&self, path: &str, keywords_last: &str, keywords: &[String]) -> bool { + let basename = match path.rsplit(path::is_separator).next() { + Some(name) => name, + None => return false, + }; + + let words: Vec<&str> = basename + .split(|c: char| c == '-' || c == '_' || c == ' ' || c == '.') + .filter(|s| !s.is_empty()) + .collect(); + + if words.len() < 2 { + return false; + } + + let acronym: String = words.iter() + .filter_map(|word| word.chars().next()) + .collect(); + + let acronym_lower = util::to_lowercase(&acronym); + + let mut user_input = String::new(); + for kw in keywords { + user_input.push_str(kw); + } + user_input.push_str(keywords_last); + + acronym_lower == util::to_lowercase(&user_input) + } + fn filter_by_keywords(&self, path: &str) -> bool { let (keywords_last, keywords) = match self.options.keywords.split_last() { Some(split) => split, None => return true, }; - - let path = util::to_lowercase(path); - let mut path = path.as_str(); - match path.rfind(keywords_last) { - Some(idx) => { - if path[idx + keywords_last.len()..].contains(path::is_separator) { - return false; + + let path_lower = util::to_lowercase(path); + let mut path_str = path_lower.as_str(); + + let regular_match = { + let mut matched = false; + match path_str.rfind(keywords_last) { + Some(idx) => { + if path_str[idx + keywords_last.len()..].contains(path::is_separator) { + return false; + } + path_str = &path_str[..idx]; + matched = true; } - path = &path[..idx]; + None => {} } - None => return false, - } - - for keyword in keywords.iter().rev() { - match path.rfind(keyword) { - Some(idx) => path = &path[..idx], - None => return false, + + if !matched { + return self.match_acronym(path, keywords_last, keywords); } - } - - true + + for keyword in keywords.iter().rev() { + match path_str.rfind(keyword) { + Some(idx) => path_str = &path_str[..idx], + None => return self.match_acronym(path, keywords_last, keywords), + } + } + + true + }; + + regular_match } fn filter_by_exclude(&self, path: &str) -> bool { @@ -185,4 +226,35 @@ mod tests { let stream = Stream::new(db, options); assert_eq!(is_match, stream.filter_by_keywords(path)); } + + #[rstest] + #[case(&["hick"], "/home/bachman/Documents/hooli-interactive-computer-keyboard", true)] + #[case(&["HICK"], "/home/bachman/Documents/hooli-interactive-computer-keyboard", true)] // Case insensitive + #[case(&["hick"], "/home/bachman/Documents/hooli_interactive_computer_keyboard", true)] // Different separators + #[case(&["hick"], "/home/bachman/Documents/hooli interactive.computer-keyboard", true)] // Mixed separators + #[case(&["hick"], "/home/bachman/Documents/hooli-interactive-keyboard", false)] // Incomplete acronym + #[case(&["hik"], "/home/bachman/Documents/hooli-interactive-keyboard", true)] // Correct acronym for shorter name + #[case(&["h"], "/home/bachman/Documents/hooli", false)] // Single letter - not an acronym + #[case(&["abc"], "/home/bachman/Documents/a-b-c", true)] // Short words + #[case(&["abc"], "/home/bachman/Documents/a-b", false)] // Partial match + fn acronym_match(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) { + let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false); + let options = StreamOptions::new(0).with_keywords(keywords.iter()); + let stream = Stream::new(db, options); + let last_keyword = keywords.last().unwrap(); + let other_keywords: Vec = keywords[..keywords.len()-1].iter().map(|&s| s.to_string()).collect(); + assert_eq!(is_match, stream.match_acronym(path, last_keyword, &other_keywords)); + } + + // Ensure the filter_by_keywords function correctly handles acronyms + #[rstest] + #[case(&["hick"], "/home/bachman/Documents/hooli-interactive-computer-keyboard", true)] + #[case(&["hooli"], "/home/bachman/Documents/hooli-interactive-computer-keyboard", true)] // Regular match still works + #[case(&["keyb"], "/home/bachman/Documents/hooli-interactive-computer-keyboard", true)] // Regular match still works + fn integrated_acronym_keyword_filter(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) { + let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false); + let options = StreamOptions::new(0).with_keywords(keywords.iter()); + let stream = Stream::new(db, options); + assert_eq!(is_match, stream.filter_by_keywords(path)); + } } From e04b897c54e6ab1ba24700289a47411f48f99cbb Mon Sep 17 00:00:00 2001 From: Erio-Harrison Date: Fri, 28 Feb 2025 11:58:29 +1100 Subject: [PATCH 2/5] keep format --- src/db/stream.rs | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/db/stream.rs b/src/db/stream.rs index c430d44..d9849da 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -57,23 +57,21 @@ impl<'a> Stream<'a> { .split(|c: char| c == '-' || c == '_' || c == ' ' || c == '.') .filter(|s| !s.is_empty()) .collect(); - + if words.len() < 2 { return false; } - - let acronym: String = words.iter() - .filter_map(|word| word.chars().next()) - .collect(); - + + let acronym: String = words.iter().filter_map(|word| word.chars().next()).collect(); + let acronym_lower = util::to_lowercase(&acronym); - + let mut user_input = String::new(); for kw in keywords { user_input.push_str(kw); } user_input.push_str(keywords_last); - + acronym_lower == util::to_lowercase(&user_input) } @@ -82,10 +80,10 @@ impl<'a> Stream<'a> { Some(split) => split, None => return true, }; - + let path_lower = util::to_lowercase(path); let mut path_str = path_lower.as_str(); - + let regular_match = { let mut matched = false; match path_str.rfind(keywords_last) { @@ -98,21 +96,21 @@ impl<'a> Stream<'a> { } None => {} } - + if !matched { return self.match_acronym(path, keywords_last, keywords); } - + for keyword in keywords.iter().rev() { match path_str.rfind(keyword) { Some(idx) => path_str = &path_str[..idx], None => return self.match_acronym(path, keywords_last, keywords), } } - + true }; - + regular_match } @@ -242,16 +240,21 @@ mod tests { let options = StreamOptions::new(0).with_keywords(keywords.iter()); let stream = Stream::new(db, options); let last_keyword = keywords.last().unwrap(); - let other_keywords: Vec = keywords[..keywords.len()-1].iter().map(|&s| s.to_string()).collect(); + let other_keywords: Vec = + keywords[..keywords.len() - 1].iter().map(|&s| s.to_string()).collect(); assert_eq!(is_match, stream.match_acronym(path, last_keyword, &other_keywords)); } - + // Ensure the filter_by_keywords function correctly handles acronyms #[rstest] #[case(&["hick"], "/home/bachman/Documents/hooli-interactive-computer-keyboard", true)] #[case(&["hooli"], "/home/bachman/Documents/hooli-interactive-computer-keyboard", true)] // Regular match still works #[case(&["keyb"], "/home/bachman/Documents/hooli-interactive-computer-keyboard", true)] // Regular match still works - fn integrated_acronym_keyword_filter(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) { + fn integrated_acronym_keyword_filter( + #[case] keywords: &[&str], + #[case] path: &str, + #[case] is_match: bool, + ) { let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false); let options = StreamOptions::new(0).with_keywords(keywords.iter()); let stream = Stream::new(db, options); From 9dffc8c5cc7f3e1674f0173c3ded191ab92c85e2 Mon Sep 17 00:00:00 2001 From: Erio-Harrison Date: Fri, 28 Feb 2025 12:10:46 +1100 Subject: [PATCH 3/5] fix CI error --- build.rs | 1 + src/db/stream.rs | 71 +++++++++++++++++++++--------------------------- 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/build.rs b/build.rs index bf6dbbf..55b4216 100644 --- a/build.rs +++ b/build.rs @@ -11,6 +11,7 @@ use cmd::Cmd; fn main() -> io::Result<()> { // Since we are generating completions in the package directory, we need to // set this so that Cargo doesn't rebuild every time. + println!("cargo:rustc-check-cfg=cfg(test)"); println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=src/"); println!("cargo:rerun-if-changed=templates/"); diff --git a/src/db/stream.rs b/src/db/stream.rs index d9849da..ef4397b 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -52,26 +52,24 @@ impl<'a> Stream<'a> { Some(name) => name, None => return false, }; - + let words: Vec<&str> = basename - .split(|c: char| c == '-' || c == '_' || c == ' ' || c == '.') + .split(|c| ['-', '_', ' ', '.'].contains(&c)) .filter(|s| !s.is_empty()) .collect(); - + if words.len() < 2 { return false; } - + let acronym: String = words.iter().filter_map(|word| word.chars().next()).collect(); - let acronym_lower = util::to_lowercase(&acronym); - - let mut user_input = String::new(); - for kw in keywords { - user_input.push_str(kw); - } - user_input.push_str(keywords_last); - + + let user_input: String = keywords.iter() + .map(String::as_str) + .chain(std::iter::once(keywords_last)) + .collect(); + acronym_lower == util::to_lowercase(&user_input) } @@ -80,38 +78,31 @@ impl<'a> Stream<'a> { Some(split) => split, None => return true, }; - + let path_lower = util::to_lowercase(path); let mut path_str = path_lower.as_str(); - - let regular_match = { - let mut matched = false; - match path_str.rfind(keywords_last) { - Some(idx) => { - if path_str[idx + keywords_last.len()..].contains(path::is_separator) { - return false; - } - path_str = &path_str[..idx]; - matched = true; - } - None => {} + + let mut matched = false; + if let Some(idx) = path_str.rfind(keywords_last) { + if path_str[idx + keywords_last.len()..].contains(path::is_separator) { + return false; } - - if !matched { - return self.match_acronym(path, keywords_last, keywords); + path_str = &path_str[..idx]; + matched = true; + } + + if !matched { + return self.match_acronym(path, keywords_last, keywords); + } + + for keyword in keywords.iter().rev() { + match path_str.rfind(keyword) { + Some(idx) => path_str = &path_str[..idx], + None => return self.match_acronym(path, keywords_last, keywords), } - - for keyword in keywords.iter().rev() { - match path_str.rfind(keyword) { - Some(idx) => path_str = &path_str[..idx], - None => return self.match_acronym(path, keywords_last, keywords), - } - } - - true - }; - - regular_match + } + + true } fn filter_by_exclude(&self, path: &str) -> bool { From 6db25b0d3fa49f617b684e846f261dcc1c2cd844 Mon Sep 17 00:00:00 2001 From: Erio-Harrison Date: Fri, 28 Feb 2025 12:15:20 +1100 Subject: [PATCH 4/5] fix fmt --- src/db/stream.rs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/db/stream.rs b/src/db/stream.rs index ef4397b..c0495da 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -52,24 +52,22 @@ impl<'a> Stream<'a> { Some(name) => name, None => return false, }; - + let words: Vec<&str> = basename .split(|c| ['-', '_', ' ', '.'].contains(&c)) .filter(|s| !s.is_empty()) .collect(); - + if words.len() < 2 { return false; } - + let acronym: String = words.iter().filter_map(|word| word.chars().next()).collect(); let acronym_lower = util::to_lowercase(&acronym); - - let user_input: String = keywords.iter() - .map(String::as_str) - .chain(std::iter::once(keywords_last)) - .collect(); - + + let user_input: String = + keywords.iter().map(String::as_str).chain(std::iter::once(keywords_last)).collect(); + acronym_lower == util::to_lowercase(&user_input) } @@ -78,10 +76,10 @@ impl<'a> Stream<'a> { Some(split) => split, None => return true, }; - + let path_lower = util::to_lowercase(path); let mut path_str = path_lower.as_str(); - + let mut matched = false; if let Some(idx) = path_str.rfind(keywords_last) { if path_str[idx + keywords_last.len()..].contains(path::is_separator) { @@ -90,18 +88,18 @@ impl<'a> Stream<'a> { path_str = &path_str[..idx]; matched = true; } - + if !matched { return self.match_acronym(path, keywords_last, keywords); } - + for keyword in keywords.iter().rev() { match path_str.rfind(keyword) { Some(idx) => path_str = &path_str[..idx], None => return self.match_acronym(path, keywords_last, keywords), } } - + true } From 39d07a550903b3867c4741e1b0850416ceda38dc Mon Sep 17 00:00:00 2001 From: Erio-Harrison Date: Tue, 19 Aug 2025 21:41:54 +1000 Subject: [PATCH 5/5] cargo fmt --- src/db/stream.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/db/stream.rs b/src/db/stream.rs index 59ce54a..8622dcc 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -76,7 +76,7 @@ impl<'a> Stream<'a> { acronym_lower == util::to_lowercase(&user_input) } - + fn filter_by_base_dir(&self, path: &str) -> bool { match &self.options.base_dir { Some(base_dir) => Path::new(path).starts_with(base_dir), @@ -272,4 +272,4 @@ mod tests { let stream = Stream::new(db, options); assert_eq!(is_match, stream.filter_by_keywords(path)); } -} \ No newline at end of file +}