From 35a889e1ed8174e7e64c40358d48a8e09b82e8df Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 22 Jul 2025 19:11:33 +0800 Subject: [PATCH] Prioritise prefix matches in tag autocomplete results (#4212) * prioritise prefix matches in tag autocomplete results * update/add tests * fix lint was fine locally though? --- rslib/src/tags/complete.rs | 58 ++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/rslib/src/tags/complete.rs b/rslib/src/tags/complete.rs index 1093017b0..f995b63a2 100644 --- a/rslib/src/tags/complete.rs +++ b/rslib/src/tags/complete.rs @@ -12,14 +12,20 @@ impl Collection { .map(component_to_regex) .collect::>()?; let mut tags = vec![]; + let mut priority = vec![]; self.storage.get_tags_by_predicate(|tag| { - if tags.len() <= limit && filters_match(&filters, tag) { - tags.push(tag.to_string()); + if priority.len() + tags.len() <= limit { + match filters_match(&filters, tag) { + Some(true) => priority.push(tag.to_string()), + Some(_) => tags.push(tag.to_string()), + _ => {} + } } // we only need the tag name false })?; - Ok(tags) + priority.append(&mut tags); + Ok(priority) } } @@ -27,20 +33,26 @@ fn component_to_regex(component: &str) -> Result { Regex::new(&format!("(?i){}", regex::escape(component))).map_err(Into::into) } -fn filters_match(filters: &[Regex], tag: &str) -> bool { +/// Returns None if tag wasn't a match, otherwise whether it was a consecutive +/// prefix match +fn filters_match(filters: &[Regex], tag: &str) -> Option { let mut remaining_tag_components = tag.split("::"); + let mut is_prefix = true; 'outer: for filter in filters { loop { if let Some(component) = remaining_tag_components.next() { - if filter.is_match(component) { + if let Some(m) = filter.find(component) { + is_prefix &= m.start() == 0; continue 'outer; + } else { + is_prefix = false; } } else { - return false; + return None; } } } - true + Some(is_prefix) } #[cfg(test)] @@ -50,28 +62,32 @@ mod test { #[test] fn matching() -> Result<()> { let filters = &[component_to_regex("b")?]; - assert!(filters_match(filters, "ABC")); - assert!(filters_match(filters, "ABC::def")); - assert!(filters_match(filters, "def::abc")); - assert!(!filters_match(filters, "def")); + assert!(filters_match(filters, "ABC").is_some()); + assert!(filters_match(filters, "ABC::def").is_some()); + assert!(filters_match(filters, "def::abc").is_some()); + assert!(filters_match(filters, "def").is_none()); let filters = &[component_to_regex("b")?, component_to_regex("E")?]; - assert!(!filters_match(filters, "ABC")); - assert!(filters_match(filters, "ABC::def")); - assert!(!filters_match(filters, "def::abc")); - assert!(!filters_match(filters, "def")); + assert!(filters_match(filters, "ABC").is_none()); + assert!(filters_match(filters, "ABC::def").is_some()); + assert!(filters_match(filters, "def::abc").is_none()); + assert!(filters_match(filters, "def").is_none()); let filters = &[ component_to_regex("a")?, component_to_regex("c")?, component_to_regex("e")?, ]; - assert!(!filters_match(filters, "ace")); - assert!(!filters_match(filters, "a::c")); - assert!(!filters_match(filters, "c::e")); - assert!(filters_match(filters, "a::c::e")); - assert!(filters_match(filters, "a::b::c::d::e")); - assert!(filters_match(filters, "1::a::b::c::d::e::f")); + assert!(filters_match(filters, "ace").is_none()); + assert!(filters_match(filters, "a::c").is_none()); + assert!(filters_match(filters, "c::e").is_none()); + assert!(filters_match(filters, "a::c::e").is_some()); + assert!(filters_match(filters, "a::b::c::d::e").is_some()); + assert!(filters_match(filters, "1::a::b::c::d::e::f").is_some()); + + assert_eq!(filters_match(filters, "a1::c2::e3"), Some(true)); + assert_eq!(filters_match(filters, "a1::c2::?::e4"), Some(false)); + assert_eq!(filters_match(filters, "a1::c2::3e"), Some(false)); Ok(()) }