zerolaunch_plugin_api/plugin/
cached_candidate.rs1use crate::plugin::types::{CandidateId, ExecutionTarget, SearchCandidate};
2use dashmap::DashMap;
3use dashmap::Entry;
4use serde::{Deserialize, Serialize};
5use std::collections::HashSet;
6use tracing::{debug, warn};
7
8#[derive(Clone)]
9pub struct CachedCandidateData {
10 candidates: Vec<SearchCandidate>,
12 index: DashMap<CandidateId, usize>,
14 cached_targets: HashSet<ExecutionTarget>,
17 cached_display_names: HashSet<String>,
20 next_candidate_id: CandidateId,
22 generation: u64,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct CandidateCacheSnapshot {
36 #[serde(rename = "candidates")]
38 pub candidates: Vec<SearchCandidate>,
39 #[serde(rename = "nextCandidateId")]
41 pub next_candidate_id: CandidateId,
42}
43
44impl Default for CachedCandidateData {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl CachedCandidateData {
51 pub fn new() -> Self {
52 Self {
53 candidates: Vec::new(),
54 index: DashMap::new(),
55 cached_targets: HashSet::new(),
56 cached_display_names: HashSet::new(),
57 next_candidate_id: 1,
58 generation: 0,
59 }
60 }
61
62 pub fn generation(&self) -> u64 {
64 self.generation
65 }
66
67 pub fn bump_generation(&mut self) {
69 self.generation += 1;
70 }
71
72 pub fn to_data(&self) -> CandidateCacheSnapshot {
75 CandidateCacheSnapshot {
76 candidates: self.candidates.clone(),
77 next_candidate_id: self.next_candidate_id,
78 }
79 }
80
81 pub fn from_data(data: CandidateCacheSnapshot) -> Self {
85 let index = DashMap::new();
86 let mut cached_targets = HashSet::new();
87 let mut cached_display_names = HashSet::new();
88 for (pos, candidate) in data.candidates.iter().enumerate() {
89 debug_assert_eq!(
90 candidate.id,
91 pos as CandidateId + 1,
92 "快照候选 id 与位置不对应: id = {}, pos = {}",
93 candidate.id,
94 pos
95 );
96 index.insert(candidate.id, pos);
97 debug_assert!(
98 cached_targets.insert(candidate.target.clone()),
99 "快照候选中存在重复执行目标: {:?}",
100 candidate.target
101 );
102 debug_assert!(
103 cached_display_names.insert(candidate.name.to_lowercase()),
104 "快照候选中存在重复显示名: {}",
105 candidate.name
106 );
107 }
108 debug_assert_eq!(
109 data.next_candidate_id,
110 data.candidates.len() as CandidateId + 1,
111 "快照 next_candidate_id 与候选数量不对应: next = {}, len = {}",
112 data.next_candidate_id,
113 data.candidates.len()
114 );
115 Self {
116 candidates: data.candidates,
117 index,
118 cached_targets,
119 cached_display_names,
120 next_candidate_id: data.next_candidate_id,
121 generation: 0,
122 }
123 }
124
125 pub fn add_candidate(&mut self, mut candidate: SearchCandidate) {
127 if self.has_target(&candidate.target) || self.has_display_name(&candidate.name) {
128 debug!(
129 "候选项已存在,丢弃重复的候选项: target = {:?}, name = {}",
130 candidate.target, candidate.name
131 );
132 return;
133 }
134 let candidate_id = self.next_candidate_id;
135 candidate.id = candidate_id;
136 self.cached_targets.insert(candidate.target.clone());
137 self.cached_display_names
138 .insert(candidate.name.to_lowercase());
139 self.candidates.push(candidate);
140 self.index.insert(candidate_id, self.candidates.len() - 1);
141 self.next_candidate_id += 1;
142 }
143
144 pub fn add_plugin_candidate(&mut self, mut candidate: SearchCandidate) {
148 if self.has_target(&candidate.target) {
149 warn!(
150 "插件候选项已存在,丢弃重复项: target = {:?}",
151 candidate.target
152 );
153 return;
154 }
155 let candidate_id: u64 = self.next_candidate_id;
156 candidate.id = candidate_id;
157 self.cached_targets.insert(candidate.target.clone());
158 self.candidates.push(candidate);
159 self.index.insert(candidate_id, self.candidates.len() - 1);
160 self.next_candidate_id += 1;
161 }
162
163 pub fn get_candidate(&self, id: CandidateId) -> Option<&SearchCandidate> {
165 match self.index.entry(id) {
166 Entry::Occupied(entry) => Some(&self.candidates[*entry.get()]),
167 Entry::Vacant(_) => None,
168 }
169 }
170
171 pub fn add_candidates(&mut self, candidates: CachedCandidateData) {
173 for candidate in candidates.candidates.iter() {
174 self.add_candidate(candidate.clone());
175 }
176 }
177
178 pub fn get_candidates(&self) -> &Vec<SearchCandidate> {
180 &self.candidates
181 }
182
183 pub fn get_candidates_mut(&mut self) -> &mut Vec<SearchCandidate> {
185 &mut self.candidates
186 }
187
188 fn has_target(&self, target: &ExecutionTarget) -> bool {
190 self.cached_targets.contains(target)
191 }
192
193 fn has_display_name(&self, display_name: &str) -> bool {
195 self.cached_display_names
196 .contains(&display_name.to_lowercase())
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::services::IconRequest;
204
205 fn sample_candidate(i: u64) -> SearchCandidate {
206 SearchCandidate {
207 id: i,
208 name: format!("候选{}", i),
209 icon: IconRequest::Path(format!("C:\\item{}.exe", i)),
210 target: ExecutionTarget::Path(format!("C:\\item{}.exe", i)),
211 keywords: vec!["kw".into()],
212 bias: 0.0,
213 trigger_keywords: Vec::new(),
214 }
215 }
216
217 #[test]
220 fn test_snapshot_roundtrip() {
221 let mut cache = CachedCandidateData::new();
222 for i in 1..=10u64 {
223 cache.add_candidate(sample_candidate(i));
224 }
225 let mut restored = CachedCandidateData::from_data(cache.to_data());
226 assert_eq!(restored.get_candidates().len(), 10);
227 for i in 1..=10u64 {
228 let c = restored.get_candidate(i).expect("id 保真");
229 assert_eq!(c.name, format!("候选{}", i));
230 assert_eq!(
231 c.target,
232 ExecutionTarget::Path(format!("C:\\item{}.exe", i))
233 );
234 }
235 restored.add_candidate(sample_candidate(0));
237 assert_eq!(restored.get_candidates().len(), 11);
238 assert_eq!(
239 restored.get_candidate(11).expect("next_id 保真").name,
240 "候选0"
241 );
242 }
243
244 #[test]
246 fn test_snapshot_json_roundtrip() {
247 let mut cache = CachedCandidateData::new();
248 for i in 1..=5u64 {
249 cache.add_candidate(sample_candidate(i));
250 }
251 let json = serde_json::to_value(cache.to_data()).unwrap();
252 let data: CandidateCacheSnapshot = serde_json::from_value(json).unwrap();
253 let restored = CachedCandidateData::from_data(data);
254 assert_eq!(restored.get_candidates().len(), 5);
255 for i in 1..=5u64 {
256 assert_eq!(
257 restored.get_candidate(i).expect("id 保真").name,
258 format!("候选{}", i)
259 );
260 }
261 }
262
263 #[test]
265 #[should_panic(expected = "id 与位置不对应")]
266 fn test_from_data_detects_mismatched_id() {
267 let data = CandidateCacheSnapshot {
268 candidates: vec![sample_candidate(5)],
269 next_candidate_id: 2,
270 };
271 CachedCandidateData::from_data(data);
272 }
273}