summaryrefslogtreecommitdiff
path: root/makima/src/daemon/tui/fuzzy.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/daemon/tui/fuzzy.rs')
-rw-r--r--makima/src/daemon/tui/fuzzy.rs217
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);
- }
-}