diff options
Diffstat (limited to 'makima/src/daemon/tui/fuzzy.rs')
| -rw-r--r-- | makima/src/daemon/tui/fuzzy.rs | 217 |
1 files changed, 0 insertions, 217 deletions
diff --git a/makima/src/daemon/tui/fuzzy.rs b/makima/src/daemon/tui/fuzzy.rs deleted file mode 100644 index 44c27ad..0000000 --- a/makima/src/daemon/tui/fuzzy.rs +++ /dev/null @@ -1,217 +0,0 @@ -//! Fuzzy matching wrapper for search functionality. -//! -//! This module provides a wrapper around the `fuzzy-matcher` crate's -//! `SkimMatcherV2` algorithm, offering: -//! -//! - Single-term fuzzy matching with score and matched indices -//! - Multi-term search (space-separated patterns) -//! - Recency-adjusted scoring for time-aware results -//! - Case-insensitive matching by default -//! -//! # Examples -//! -//! ``` -//! use makima::daemon::tui::fuzzy::FuzzyMatcher; -//! -//! let matcher = FuzzyMatcher::new(); -//! -//! // Single pattern matching -//! if let Some((score, indices)) = matcher.fuzzy_match("hello world", "hlo") { -//! println!("Score: {}, Matched positions: {:?}", score, indices); -//! } -//! -//! // Multi-term search -//! if let Some(score) = matcher.fuzzy_match_all("fix authentication bug", "fix bug") { -//! println!("All terms matched with score: {}", score); -//! } -//! ``` - -use fuzzy_matcher::skim::SkimMatcherV2; -use fuzzy_matcher::FuzzyMatcher as FuzzyMatcherTrait; - -/// Fuzzy matcher wrapper providing search functionality. -/// -/// Wraps the `SkimMatcherV2` algorithm which provides: -/// - Smart case matching (case-insensitive unless pattern has uppercase) -/// - Word boundary bonuses -/// - Consecutive character bonuses -pub struct FuzzyMatcher { - matcher: SkimMatcherV2, -} - -impl FuzzyMatcher { - /// Create a new fuzzy matcher with default settings. - pub fn new() -> Self { - Self { - matcher: SkimMatcherV2::default(), - } - } - - /// Match a pattern against a string, returning score and matched indices. - /// - /// Returns `Some((score, indices))` if the pattern matches, where: - /// - `score` is a relevance score (higher is better) - /// - `indices` are the positions of matched characters in the text - /// - /// Returns `None` if the pattern doesn't match the text. - /// - /// # Arguments - /// - /// * `text` - The text to search in - /// * `pattern` - The pattern to search for - pub fn fuzzy_match(&self, text: &str, pattern: &str) -> Option<(i64, Vec<usize>)> { - self.matcher.fuzzy_indices(text, pattern) - } - - /// Match multiple patterns (space-separated) against a string. - /// - /// All patterns must match for the function to return a score. - /// The returned score is the sum of individual pattern scores. - /// - /// # Arguments - /// - /// * `text` - The text to search in - /// * `patterns` - Space-separated patterns (e.g., "fix bug" matches both "fix" and "bug") - /// - /// # Returns - /// - /// `Some(total_score)` if all patterns match, `None` otherwise. - pub fn fuzzy_match_all(&self, text: &str, patterns: &str) -> Option<i64> { - let patterns: Vec<&str> = patterns.split_whitespace().collect(); - - if patterns.is_empty() { - return Some(0); - } - - let mut total_score = 0i64; - - for pattern in patterns { - if let Some((score, _)) = self.matcher.fuzzy_indices(text, pattern) { - total_score += score; - } else { - return None; - } - } - - Some(total_score) - } - - /// Calculate a recency-adjusted score for time-aware sorting. - /// - /// Items with lower indices (more recent) receive a bonus to their score, - /// making them rank higher in search results. - /// - /// # Arguments - /// - /// * `base_score` - The original fuzzy match score - /// * `index` - The item's position in the list (0 = most recent) - /// * `total_items` - Total number of items in the list - /// - /// # Returns - /// - /// An adjusted score that factors in recency. - pub fn recency_adjusted_score(base_score: i64, index: usize, total_items: usize) -> i64 { - if total_items == 0 { - return base_score; - } - - // Recency bonus: items at the beginning get up to 20% bonus - // Formula: bonus = base_score * 0.2 * (1 - index/total_items) - let recency_factor = 1.0 - (index as f64 / total_items as f64); - let bonus = (base_score as f64 * 0.2 * recency_factor) as i64; - - base_score + bonus - } -} - -impl Default for FuzzyMatcher { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_fuzzy_match_exact() { - let matcher = FuzzyMatcher::new(); - let result = matcher.fuzzy_match("hello world", "hello"); - assert!(result.is_some()); - } - - #[test] - fn test_fuzzy_match_partial() { - let matcher = FuzzyMatcher::new(); - let result = matcher.fuzzy_match("authentication", "auth"); - assert!(result.is_some()); - } - - #[test] - fn test_fuzzy_match_no_match() { - let matcher = FuzzyMatcher::new(); - let result = matcher.fuzzy_match("hello", "xyz"); - assert!(result.is_none()); - } - - #[test] - fn test_multi_term_search() { - let matcher = FuzzyMatcher::new(); - let result = matcher.fuzzy_match_all("fix authentication bug", "fix bug"); - assert!(result.is_some()); - } - - #[test] - fn test_case_insensitive() { - let matcher = FuzzyMatcher::new(); - let result = matcher.fuzzy_match("Hello World", "hello"); - assert!(result.is_some()); - } - - #[test] - fn test_recency_bonus() { - // Earlier items (lower index) should get higher recency bonus - let score1 = FuzzyMatcher::recency_adjusted_score(100, 0, 50); - let score2 = FuzzyMatcher::recency_adjusted_score(100, 10, 50); - assert!(score1 > score2); - } - - #[test] - fn test_fuzzy_match_returns_indices() { - let matcher = FuzzyMatcher::new(); - let result = matcher.fuzzy_match("hello world", "hlo"); - assert!(result.is_some()); - let (_, indices) = result.unwrap(); - // Should have matched 3 characters - assert_eq!(indices.len(), 3); - } - - #[test] - fn test_multi_term_empty_pattern() { - let matcher = FuzzyMatcher::new(); - let result = matcher.fuzzy_match_all("hello world", ""); - assert!(result.is_some()); - assert_eq!(result.unwrap(), 0); - } - - #[test] - fn test_multi_term_partial_match_fails() { - let matcher = FuzzyMatcher::new(); - // "xyz" doesn't match, so the whole search should fail - let result = matcher.fuzzy_match_all("fix authentication bug", "fix xyz"); - assert!(result.is_none()); - } - - #[test] - fn test_recency_bonus_edge_cases() { - // Zero total items should return base score - let score = FuzzyMatcher::recency_adjusted_score(100, 0, 0); - assert_eq!(score, 100); - - // Last item should get minimal bonus - let score_last = FuzzyMatcher::recency_adjusted_score(100, 49, 50); - let score_first = FuzzyMatcher::recency_adjusted_score(100, 0, 50); - assert!(score_first > score_last); - } -} |
