Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): Implement the “fuzzy match room name” filter #2335

Merged
merged 4 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ WeeChat = "WeeChat"
[files]
# Our json files contain a bunch of base64 encoded ed25519 keys which aren't
# automatically ignored, we ignore them here.
extend-exclude = ["*.json"]
extend-exclude = [
"*.json",
# We are using some fuzzy match patterns that can be understood as typos confusingly.
"crates/matrix-sdk-ui/tests/integration/room_list_service.rs",
]
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/matrix-sdk-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ eyeball-im = { workspace = true }
eyeball-im-util = { workspace = true }
futures-core = { workspace = true }
futures-util = { workspace = true }
fuzzy-matcher = "0.3.7"
imbl = { version = "2.0.0", features = ["serde"] }
indexmap = "2.0.0"
itertools = { workspace = true }
Expand All @@ -40,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 }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
pub use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher as _};
use matrix_sdk::{Client, RoomListEntry};

use super::normalize_string;

struct FuzzyMatcher {
matcher: SkimMatcherV2,
pattern: Option<String>,
}

impl FuzzyMatcher {
fn new() -> Self {
Self { matcher: SkimMatcherV2::default().smart_case().use_cache(true), pattern: None }
}

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()
}
}

/// Create a new filter that will fuzzy match a pattern on room names.
///
/// Rooms are fetched from the `Client`. The pattern and the room names are
/// normalized with `normalize_string`.
pub fn new_filter(
client: &Client,
pattern: &str,
) -> impl Fn(&RoomListEntry) -> bool + Send + Sync + 'static {
let searcher = FuzzyMatcher::new().with_pattern(pattern);

let client = client.clone();

move |room_list_entry| -> bool {
let Some(room_id) = room_list_entry.as_room_id() else { return false };
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)
}
}

#[cfg(test)]
mod tests {
use std::ops::Not;

use super::*;

#[test]
fn test_no_pattern() {
let matcher = FuzzyMatcher::new();

assert!(matcher.fuzzy_match("hello"));
}

#[test]
fn test_literal() {
let matcher = FuzzyMatcher::new();

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();

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();

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());
}

#[test]
fn test_normalization() {
let matcher = FuzzyMatcher::new();

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"));
}
}
24 changes: 24 additions & 0 deletions crates/matrix-sdk-ui/src/room_list_service/filters/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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};

/// Normalize a string, i.e. decompose it into NFD (Normalization Form D, i.e. a
/// canonical decomposition, see http://www.unicode.org/reports/tr15/) and
/// filter out the combining marks.
fn normalize_string(str: &str) -> String {
str.nfd().filter(|c| !is_combining_mark(*c)).collect::<String>()
}

#[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("هند"), "هند");
}
}
1 change: 1 addition & 0 deletions crates/matrix-sdk-ui/src/room_list_service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
//! [`RoomListService::state`] provides a way to get a stream of the state
//! machine's state, which can be pretty helpful for the client app.

pub mod filters;
mod room;
mod room_list;
mod state;
Expand Down
Loading