matrix_sdk_ui/room_list_service/filters/
fuzzy_match_room_name.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15pub use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher as _};
16
17use super::{normalize_string, Filter};
18
19struct FuzzyMatcher {
20    matcher: SkimMatcherV2,
21    pattern: Option<String>,
22}
23
24impl FuzzyMatcher {
25    fn new() -> Self {
26        Self { matcher: SkimMatcherV2::default().smart_case().use_cache(true), pattern: None }
27    }
28
29    fn with_pattern(mut self, pattern: &str) -> Self {
30        self.pattern = Some(normalize_string(pattern));
31
32        self
33    }
34
35    fn matches(&self, subject: &str) -> bool {
36        // No pattern means there is a match.
37        let Some(pattern) = self.pattern.as_ref() else { return true };
38
39        self.matcher.fuzzy_match(&normalize_string(subject), pattern).is_some()
40    }
41}
42
43/// Create a new filter that will fuzzy match a pattern on room names.
44///
45/// Rooms are fetched from the `Client`. The pattern and the room names are
46/// normalized with `normalize_string`.
47pub fn new_filter(pattern: &str) -> impl Filter {
48    let searcher = FuzzyMatcher::new().with_pattern(pattern);
49
50    move |room| -> bool {
51        let Some(room_name) = room.cached_display_name() else { return false };
52
53        searcher.matches(&room_name)
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use std::ops::Not;
60
61    use super::*;
62
63    #[test]
64    fn test_no_pattern() {
65        let matcher = FuzzyMatcher::new();
66
67        assert!(matcher.matches("hello"));
68    }
69
70    #[test]
71    fn test_empty_pattern() {
72        let matcher = FuzzyMatcher::new();
73
74        assert!(matcher.matches("hello"));
75    }
76
77    #[test]
78    fn test_literal() {
79        let matcher = FuzzyMatcher::new();
80
81        let matcher = matcher.with_pattern("mtx");
82        assert!(matcher.matches("matrix"));
83
84        let matcher = matcher.with_pattern("mxt");
85        assert!(matcher.matches("matrix").not());
86    }
87
88    #[test]
89    fn test_ignore_case() {
90        let matcher = FuzzyMatcher::new();
91
92        let matcher = matcher.with_pattern("mtx");
93        assert!(matcher.matches("MaTrIX"));
94
95        let matcher = matcher.with_pattern("mxt");
96        assert!(matcher.matches("MaTrIX").not());
97    }
98
99    #[test]
100    fn test_smart_case() {
101        let matcher = FuzzyMatcher::new();
102
103        let matcher = matcher.with_pattern("mtx");
104        assert!(matcher.matches("matrix"));
105        assert!(matcher.matches("Matrix"));
106
107        let matcher = matcher.with_pattern("Mtx");
108        assert!(matcher.matches("matrix").not());
109        assert!(matcher.matches("Matrix"));
110    }
111
112    #[test]
113    fn test_normalization() {
114        let matcher = FuzzyMatcher::new();
115
116        let matcher = matcher.with_pattern("ubété");
117
118        // First, assert that the pattern has been normalized.
119        assert_eq!(matcher.pattern, Some("ubete".to_owned()));
120
121        // Second, assert that the subject is normalized too.
122        assert!(matcher.matches("un bel été"));
123
124        // Another concrete test.
125        let matcher = matcher.with_pattern("stf");
126        assert!(matcher.matches("Ștefan"));
127    }
128}