Skip to main content

steer_tui/tui/state/
file_cache.rs

1use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2};
2use std::sync::Arc;
3use tokio::sync::RwLock;
4
5/// Cache for workspace files to enable fast fuzzy searching
6#[derive(Clone, Debug)]
7pub struct FileCache {
8    /// Cached file paths from the workspace
9    files: Arc<RwLock<Vec<String>>>,
10    /// Session ID this cache belongs to
11    session_id: String,
12}
13
14impl FileCache {
15    /// Create a new empty file cache
16    pub fn new(session_id: String) -> Self {
17        Self {
18            files: Arc::new(RwLock::new(Vec::new())),
19            session_id,
20        }
21    }
22
23    /// Update the cache with new file paths
24    pub async fn update(&self, files: Vec<String>) {
25        let mut cache = self.files.write().await;
26        *cache = files;
27    }
28
29    /// Clear the cache
30    pub async fn clear(&self) {
31        let mut cache = self.files.write().await;
32        cache.clear();
33    }
34
35    /// Check if the cache is empty
36    pub async fn is_empty(&self) -> bool {
37        let cache = self.files.read().await;
38        cache.is_empty()
39    }
40
41    /// Get the number of files in the cache
42    pub async fn len(&self) -> usize {
43        let cache = self.files.read().await;
44        cache.len()
45    }
46
47    /// Search files with fuzzy matching
48    pub async fn fuzzy_search(&self, query: &str, max_results: Option<usize>) -> Vec<String> {
49        if query.is_empty() {
50            // If no query, return all files up to limit
51            let cache = self.files.read().await;
52            let max = max_results.unwrap_or(cache.len());
53            return cache.iter().take(max).cloned().collect();
54        }
55
56        let cache = self.files.read().await;
57        let matcher = SkimMatcherV2::default();
58
59        let mut scored_files: Vec<(i64, String)> = cache
60            .iter()
61            .filter_map(|file| {
62                matcher
63                    .fuzzy_match(file, query)
64                    .map(|score| (score, file.clone()))
65            })
66            .collect();
67
68        // Sort by score (highest first)
69        scored_files.sort_by(|a, b| b.0.cmp(&a.0));
70
71        // Apply limit if specified
72
73        if let Some(max) = max_results {
74            scored_files
75                .into_iter()
76                .take(max)
77                .map(|(_, file)| file)
78                .collect()
79        } else {
80            scored_files.into_iter().map(|(_, file)| file).collect()
81        }
82    }
83
84    /// Get the session ID
85    pub fn session_id(&self) -> &str {
86        &self.session_id
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::FileCache;
93
94    #[tokio::test]
95    async fn test_fuzzy_search_matches_query() {
96        let cache = FileCache::new("session-1".to_string());
97        cache
98            .update(vec![
99                "src/main.rs".to_string(),
100                "src/lib.rs".to_string(),
101                "README.md".to_string(),
102            ])
103            .await;
104
105        let results = cache.fuzzy_search("main", None).await;
106        assert!(results.iter().any(|path| path == "src/main.rs"));
107        assert!(!results.is_empty());
108    }
109
110    #[tokio::test]
111    async fn test_fuzzy_search_empty_query_limit() {
112        let cache = FileCache::new("session-2".to_string());
113        cache
114            .update(vec![
115                "src/main.rs".to_string(),
116                "src/lib.rs".to_string(),
117                "README.md".to_string(),
118            ])
119            .await;
120
121        let results = cache.fuzzy_search("", Some(2)).await;
122        assert_eq!(results.len(), 2);
123    }
124}