From 3e93bdbc3f19a03cfaee892e4f93371df6936c4a Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 27 Jul 2023 08:33:41 +0200 Subject: [PATCH] feat(ui): Normallize strings when doing fuzzy matching. --- Cargo.lock | 1 + crates/matrix-sdk-ui/Cargo.toml | 1 + .../filters/fuzzy_match_room_name.rs | 76 +++++++++++++------ .../src/room_list_service/filters/mod.rs | 18 +++++ .../tests/integration/room_list_service.rs | 2 +- 5 files changed, 75 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17a58e470..6d6b57fca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2939,6 +2939,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "unicode-normalization", "wiremock", ] diff --git a/crates/matrix-sdk-ui/Cargo.toml b/crates/matrix-sdk-ui/Cargo.toml index 72ce7edee..8e121bbea 100644 --- a/crates/matrix-sdk-ui/Cargo.toml +++ b/crates/matrix-sdk-ui/Cargo.toml @@ -41,6 +41,7 @@ serde_json = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true, features = ["attributes"] } +unicode-normalization = "0.1.22" [dev-dependencies] anyhow = { workspace = true } diff --git a/crates/matrix-sdk-ui/src/room_list_service/filters/fuzzy_match_room_name.rs b/crates/matrix-sdk-ui/src/room_list_service/filters/fuzzy_match_room_name.rs index b889c93b4..5d1e65c62 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/filters/fuzzy_match_room_name.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/filters/fuzzy_match_room_name.rs @@ -1,25 +1,38 @@ pub use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher as _}; use matrix_sdk::{Client, RoomListEntry}; +use super::normalize_string; + struct FuzzyMatcher { matcher: SkimMatcherV2, + pattern: Option, } impl FuzzyMatcher { fn new() -> Self { - Self { matcher: SkimMatcherV2::default().smart_case().use_cache(true) } + Self { matcher: SkimMatcherV2::default().smart_case().use_cache(true), pattern: None } } - fn fuzzy_match(&self, subject: &str, pattern: &str) -> bool { - self.matcher.fuzzy_match(subject, pattern).is_some() + fn with_pattern(mut self, pattern: &str) -> Self { + self.pattern = Some(normalize_string(pattern)); + + self + } + + fn fuzzy_match(&self, subject: &str) -> bool { + // No pattern means there is a match. + let Some(pattern) = self.pattern.as_ref() else { return true }; + + self.matcher.fuzzy_match(&normalize_string(subject), pattern).is_some() } } pub fn new_filter( client: &Client, - pattern: String, + pattern: &str, ) -> impl Fn(&RoomListEntry) -> bool + Send + Sync + 'static { - let searcher = FuzzyMatcher::new(); + let searcher = FuzzyMatcher::new().with_pattern(pattern); + let client = client.clone(); move |room_list_entry| -> bool { @@ -27,7 +40,7 @@ pub fn new_filter( let Some(room) = client.get_room(room_id) else { return false }; let Some(room_name) = room.name() else { return false }; - searcher.fuzzy_match(&room_name, &pattern) + searcher.fuzzy_match(&room_name) } } @@ -37,42 +50,61 @@ mod tests { use super::*; + #[test] + fn test_no_pattern() { + let matcher = FuzzyMatcher::new(); + + assert!(matcher.fuzzy_match("hello")); + } + #[test] fn test_literal() { let matcher = FuzzyMatcher::new(); - assert!(matcher.fuzzy_match("matrix", "mtx")); - assert!(matcher.fuzzy_match("matrix", "mxt").not()); + let matcher = matcher.with_pattern("mtx"); + assert!(matcher.fuzzy_match("matrix")); + + let matcher = matcher.with_pattern("mxt"); + assert!(matcher.fuzzy_match("matrix").not()); } #[test] fn test_ignore_case() { let matcher = FuzzyMatcher::new(); - assert!(matcher.fuzzy_match("MaTrIX", "mtx")); - assert!(matcher.fuzzy_match("MaTrIX", "mxt").not()); + let matcher = matcher.with_pattern("mtx"); + assert!(matcher.fuzzy_match("MaTrIX")); + + let matcher = matcher.with_pattern("mxt"); + assert!(matcher.fuzzy_match("MaTrIX").not()); } #[test] fn test_smart_case() { let matcher = FuzzyMatcher::new(); - assert!(matcher.fuzzy_match("Matrix", "mtx")); - assert!(matcher.fuzzy_match("Matrix", "mtx")); - assert!(matcher.fuzzy_match("MatriX", "Mtx").not()); + let matcher = matcher.with_pattern("mtx"); + assert!(matcher.fuzzy_match("Matrix")); + assert!(matcher.fuzzy_match("Matrix")); + + let matcher = matcher.with_pattern("Mtx"); + assert!(matcher.fuzzy_match("MatriX").not()); } - // This is not supported yet. - /* #[test] - fn test_transliteration_and_normalization() { + fn test_normalization() { let matcher = FuzzyMatcher::new(); - assert!(matcher.fuzzy_match("un bel été", "été")); - assert!(matcher.fuzzy_match("un bel été", "ete")); - assert!(matcher.fuzzy_match("un bel été", "éte")); - assert!(matcher.fuzzy_match("un bel été", "étè").not()); - assert!(matcher.fuzzy_match("Ștefan", "stef")); + let matcher = matcher.with_pattern("ubété"); + + // First, assert that the pattern has been normalized. + assert_eq!(matcher.pattern, Some("ubete".to_string())); + + // Second, assert that the subject is normalized too. + assert!(matcher.fuzzy_match("un bel été")); + + // Another concrete test. + let matcher = matcher.with_pattern("stf"); + assert!(matcher.fuzzy_match("Ștefan")); } - */ } diff --git a/crates/matrix-sdk-ui/src/room_list_service/filters/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/filters/mod.rs index 5b9b55589..bc4eff73e 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/filters/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/filters/mod.rs @@ -1,3 +1,21 @@ mod fuzzy_match_room_name; pub use fuzzy_match_room_name::new_filter as new_filter_fuzzy_match_room_name; +use unicode_normalization::{char::is_combining_mark, UnicodeNormalization}; + +fn normalize_string(str: &str) -> String { + str.nfd().filter(|c| !is_combining_mark(*c)).collect::() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_normalize_string() { + assert_eq!(&normalize_string("abc"), "abc"); + assert_eq!(&normalize_string("Ștefan Été"), "Stefan Ete"); + assert_eq!(&normalize_string("Ç ṩ ḋ Å"), "C s d A"); + assert_eq!(&normalize_string("هند"), "هند"); + } +} diff --git a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs index 1aaa73de3..cdf76cbd5 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -1428,7 +1428,7 @@ async fn test_entries_stream_with_updated_filter() -> Result<(), Error> { }; let (previous_entries, entries_stream) = - all_rooms.entries_filtered(new_filter_fuzzy_match_room_name(&client, "mat ba".to_string())); + all_rooms.entries_filtered(new_filter_fuzzy_match_room_name(&client, "mat ba")); pin_mut!(entries_stream); sync_then_assert_request_and_fake_response! {