Skip to content

Commit 528eb20

Browse files
committed
feat: add acronym matching for directories (ajeetdsouza#1002)
1 parent 0f07314 commit 528eb20

File tree

1 file changed

+90
-18
lines changed

1 file changed

+90
-18
lines changed

src/db/stream.rs

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,32 +47,73 @@ impl<'a> Stream<'a> {
4747
None
4848
}
4949

50+
fn match_acronym(&self, path: &str, keywords_last: &str, keywords: &[String]) -> bool {
51+
let basename = match path.rsplit(path::is_separator).next() {
52+
Some(name) => name,
53+
None => return false,
54+
};
55+
56+
let words: Vec<&str> = basename
57+
.split(|c: char| c == '-' || c == '_' || c == ' ' || c == '.')
58+
.filter(|s| !s.is_empty())
59+
.collect();
60+
61+
if words.len() < 2 {
62+
return false;
63+
}
64+
65+
let acronym: String = words.iter()
66+
.filter_map(|word| word.chars().next())
67+
.collect();
68+
69+
let acronym_lower = util::to_lowercase(&acronym);
70+
71+
let mut user_input = String::new();
72+
for kw in keywords {
73+
user_input.push_str(kw);
74+
}
75+
user_input.push_str(keywords_last);
76+
77+
acronym_lower == util::to_lowercase(&user_input)
78+
}
79+
5080
fn filter_by_keywords(&self, path: &str) -> bool {
5181
let (keywords_last, keywords) = match self.options.keywords.split_last() {
5282
Some(split) => split,
5383
None => return true,
5484
};
55-
56-
let path = util::to_lowercase(path);
57-
let mut path = path.as_str();
58-
match path.rfind(keywords_last) {
59-
Some(idx) => {
60-
if path[idx + keywords_last.len()..].contains(path::is_separator) {
61-
return false;
85+
86+
let path_lower = util::to_lowercase(path);
87+
let mut path_str = path_lower.as_str();
88+
89+
let regular_match = {
90+
let mut matched = false;
91+
match path_str.rfind(keywords_last) {
92+
Some(idx) => {
93+
if path_str[idx + keywords_last.len()..].contains(path::is_separator) {
94+
return false;
95+
}
96+
path_str = &path_str[..idx];
97+
matched = true;
6298
}
63-
path = &path[..idx];
99+
None => {}
64100
}
65-
None => return false,
66-
}
67-
68-
for keyword in keywords.iter().rev() {
69-
match path.rfind(keyword) {
70-
Some(idx) => path = &path[..idx],
71-
None => return false,
101+
102+
if !matched {
103+
return self.match_acronym(path, keywords_last, keywords);
72104
}
73-
}
74-
75-
true
105+
106+
for keyword in keywords.iter().rev() {
107+
match path_str.rfind(keyword) {
108+
Some(idx) => path_str = &path_str[..idx],
109+
None => return self.match_acronym(path, keywords_last, keywords),
110+
}
111+
}
112+
113+
true
114+
};
115+
116+
regular_match
76117
}
77118

78119
fn filter_by_exclude(&self, path: &str) -> bool {
@@ -185,4 +226,35 @@ mod tests {
185226
let stream = Stream::new(db, options);
186227
assert_eq!(is_match, stream.filter_by_keywords(path));
187228
}
229+
230+
#[rstest]
231+
#[case(&["hick"], "/home/bachman/Documents/hooli-interactive-computer-keyboard", true)]
232+
#[case(&["HICK"], "/home/bachman/Documents/hooli-interactive-computer-keyboard", true)] // Case insensitive
233+
#[case(&["hick"], "/home/bachman/Documents/hooli_interactive_computer_keyboard", true)] // Different separators
234+
#[case(&["hick"], "/home/bachman/Documents/hooli interactive.computer-keyboard", true)] // Mixed separators
235+
#[case(&["hick"], "/home/bachman/Documents/hooli-interactive-keyboard", false)] // Incomplete acronym
236+
#[case(&["hik"], "/home/bachman/Documents/hooli-interactive-keyboard", true)] // Correct acronym for shorter name
237+
#[case(&["h"], "/home/bachman/Documents/hooli", false)] // Single letter - not an acronym
238+
#[case(&["abc"], "/home/bachman/Documents/a-b-c", true)] // Short words
239+
#[case(&["abc"], "/home/bachman/Documents/a-b", false)] // Partial match
240+
fn acronym_match(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) {
241+
let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false);
242+
let options = StreamOptions::new(0).with_keywords(keywords.iter());
243+
let stream = Stream::new(db, options);
244+
let last_keyword = keywords.last().unwrap();
245+
let other_keywords: Vec<String> = keywords[..keywords.len()-1].iter().map(|&s| s.to_string()).collect();
246+
assert_eq!(is_match, stream.match_acronym(path, last_keyword, &other_keywords));
247+
}
248+
249+
// Ensure the filter_by_keywords function correctly handles acronyms
250+
#[rstest]
251+
#[case(&["hick"], "/home/bachman/Documents/hooli-interactive-computer-keyboard", true)]
252+
#[case(&["hooli"], "/home/bachman/Documents/hooli-interactive-computer-keyboard", true)] // Regular match still works
253+
#[case(&["keyb"], "/home/bachman/Documents/hooli-interactive-computer-keyboard", true)] // Regular match still works
254+
fn integrated_acronym_keyword_filter(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) {
255+
let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false);
256+
let options = StreamOptions::new(0).with_keywords(keywords.iter());
257+
let stream = Stream::new(db, options);
258+
assert_eq!(is_match, stream.filter_by_keywords(path));
259+
}
188260
}

0 commit comments

Comments
 (0)