listary 0.0.1

A fast command-line file search utility inspired by Listary, with fuzzy search, application launching, and smart auto-open features
use crate::types::{EntryType, FileEntry, SearchResult};
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;

pub struct SearchEngine {
    matcher: SkimMatcherV2,
}

impl SearchEngine {
    pub fn new() -> Self {
        Self {
            matcher: SkimMatcherV2::default(),
        }
    }

    pub fn search(&self, entries: &[FileEntry], query: &str, limit: usize) -> Vec<SearchResult> {
        if query.is_empty() {
            return Vec::new();
        }

        let mut results = Vec::new();

        for entry in entries {
            // 在文件名中搜索
            if let Some((score, indices)) = self.matcher.fuzzy_indices(&entry.name, query) {
                results.push(SearchResult {
                    entry: entry.clone(),
                    score,
                    matched_indices: indices,
                });
            }
        }

        // 按分数排序(分数越高越好)
        results.sort_by(|a, b| b.score.cmp(&a.score));
        results.truncate(limit);
        results
    }

    pub fn is_exact_match(&self, entry: &FileEntry, query: &str) -> bool {
        let entry_name = entry.name.to_lowercase();
        let query_lower = query.to_lowercase();
        
        // Check for exact name match or exact match without extension
        entry_name == query_lower || 
        entry.path.file_stem()
            .and_then(|stem| stem.to_str())
            .map(|stem| stem.to_lowercase() == query_lower)
            .unwrap_or(false)
    }

    pub fn find_single_application_match<'a>(&self, entries: &'a [FileEntry], query: &str) -> Option<&'a FileEntry> {
        // Get all application entries that have any fuzzy match
        let app_matches: Vec<&FileEntry> = entries
            .iter()
            .filter(|entry| matches!(entry.entry_type, EntryType::Application))
            .filter(|entry| self.matcher.fuzzy_match(&entry.name, query).is_some())
            .collect();

        // Return the match if there's only one application match
        if app_matches.len() == 1 {
            Some(app_matches[0])
        } else {
            None
        }
    }

    // Keep the exact match function for backward compatibility
    pub fn find_exact_application_match<'a>(&self, entries: &'a [FileEntry], query: &str) -> Option<&'a FileEntry> {
        let exact_matches: Vec<&FileEntry> = entries
            .iter()
            .filter(|entry| matches!(entry.entry_type, EntryType::Application))
            .filter(|entry| self.is_exact_match(entry, query))
            .collect();

        if exact_matches.len() == 1 {
            Some(exact_matches[0])
        } else {
            None
        }
    }
}