rust_filesearch/px/
search.rs1use crate::px::project::Project;
7use fuzzy_matcher::skim::SkimMatcherV2;
8use fuzzy_matcher::FuzzyMatcher;
9
10pub struct ProjectSearcher {
12 matcher: SkimMatcherV2,
13}
14
15impl ProjectSearcher {
16 pub fn new() -> Self {
18 Self {
19 matcher: SkimMatcherV2::default(),
20 }
21 }
22
23 pub fn search<'a>(&self, projects: &'a [Project], query: &str) -> Vec<&'a Project> {
34 if query.trim().is_empty() {
35 return self.sort_by_frecency(projects);
37 }
38
39 let mut matches: Vec<(&Project, i64)> = projects
40 .iter()
41 .filter_map(|project| {
42 let name_score = self.matcher.fuzzy_match(&project.name, query).unwrap_or(0);
44 let path_score = self
45 .matcher
46 .fuzzy_match(&project.path.to_string_lossy(), query)
47 .unwrap_or(0);
48
49 let fuzzy_score = name_score.max(path_score);
51
52 if fuzzy_score > 0 {
53 let combined_score =
57 (fuzzy_score as f64 * 0.7) + (project.frecency_score * 0.3);
58
59 Some((project, combined_score as i64))
60 } else {
61 None
62 }
63 })
64 .collect();
65
66 matches.sort_by(|a, b| b.1.cmp(&a.1));
68
69 matches.into_iter().map(|(project, _)| project).collect()
70 }
71
72 pub fn exact_search<'a>(&self, projects: &'a [Project], query: &str) -> Vec<&'a Project> {
76 let query_lower = query.to_lowercase();
77 let mut matches: Vec<&Project> = projects
78 .iter()
79 .filter(|p| {
80 p.name.to_lowercase().contains(&query_lower)
81 || p.path
82 .to_string_lossy()
83 .to_lowercase()
84 .contains(&query_lower)
85 })
86 .collect();
87
88 matches.sort_by(|a, b| {
90 b.frecency_score
91 .partial_cmp(&a.frecency_score)
92 .unwrap_or(std::cmp::Ordering::Equal)
93 });
94
95 matches
96 }
97
98 fn sort_by_frecency<'a>(&self, projects: &'a [Project]) -> Vec<&'a Project> {
100 let mut sorted: Vec<&Project> = projects.iter().collect();
101 sorted.sort_by(|a, b| {
102 b.frecency_score
103 .partial_cmp(&a.frecency_score)
104 .unwrap_or(std::cmp::Ordering::Equal)
105 });
106 sorted
107 }
108}
109
110impl Default for ProjectSearcher {
111 fn default() -> Self {
112 Self::new()
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::px::project::{Project, ProjectGitStatus};
120 use chrono::Utc;
121 use std::path::PathBuf;
122
123 fn create_test_project(name: &str, frecency: f64) -> Project {
124 Project {
125 path: PathBuf::from(format!("/test/{}", name)),
126 name: name.to_string(),
127 last_modified: Utc::now(),
128 git_status: ProjectGitStatus {
129 current_branch: "main".to_string(),
130 has_uncommitted: false,
131 ahead: 0,
132 behind: 0,
133 last_commit: None,
134 },
135 frecency_score: frecency,
136 last_accessed: None,
137 access_count: 0,
138 readme_excerpt: None,
139 }
140 }
141
142 #[test]
143 fn test_empty_query() {
144 let searcher = ProjectSearcher::new();
145 let projects = vec![
146 create_test_project("low-frecency", 10.0),
147 create_test_project("high-frecency", 100.0),
148 ];
149
150 let results = searcher.search(&projects, "");
151 assert_eq!(results.len(), 2);
152 assert_eq!(results[0].name, "high-frecency");
154 }
155
156 #[test]
157 fn test_fuzzy_match() {
158 let searcher = ProjectSearcher::new();
159 let projects = vec![
160 create_test_project("rust-filesearch", 50.0),
161 create_test_project("python-script", 50.0),
162 create_test_project("rust-analyzer", 50.0),
163 ];
164
165 let results = searcher.search(&projects, "rust");
166 assert!(results.len() >= 2);
168 assert!(results.iter().any(|p| p.name.contains("rust")));
169 }
170
171 #[test]
172 fn test_frecency_influences_ranking() {
173 let searcher = ProjectSearcher::new();
174 let projects = vec![
175 create_test_project("rust-project", 10.0), create_test_project("rust-awesome", 100.0), ];
178
179 let results = searcher.search(&projects, "rust");
180 assert_eq!(results.len(), 2);
181 assert_eq!(results[0].name, "rust-awesome");
183 }
184
185 #[test]
186 fn test_exact_search() {
187 let searcher = ProjectSearcher::new();
188 let projects = vec![
189 create_test_project("whatsgood-homepage", 50.0),
190 create_test_project("rust-filesearch", 50.0),
191 ];
192
193 let results = searcher.exact_search(&projects, "whatsgood");
194 assert_eq!(results.len(), 1);
195 assert_eq!(results[0].name, "whatsgood-homepage");
196 }
197}
198