use crate::plugin::types::{CandidateId, ExecutionTarget, SearchCandidate};
use dashmap::DashMap;
use dashmap::Entry;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use tracing::{debug, warn};
#[derive(Clone)]
pub struct CachedCandidateData {
candidates: Vec<SearchCandidate>,
index: DashMap<CandidateId, usize>,
cached_targets: HashSet<ExecutionTarget>,
cached_display_names: HashSet<String>,
next_candidate_id: CandidateId,
generation: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CandidateCacheSnapshot {
#[serde(rename = "candidates")]
pub candidates: Vec<SearchCandidate>,
#[serde(rename = "nextCandidateId")]
pub next_candidate_id: CandidateId,
}
impl Default for CachedCandidateData {
fn default() -> Self {
Self::new()
}
}
impl CachedCandidateData {
pub fn new() -> Self {
Self {
candidates: Vec::new(),
index: DashMap::new(),
cached_targets: HashSet::new(),
cached_display_names: HashSet::new(),
next_candidate_id: 1,
generation: 0,
}
}
pub fn generation(&self) -> u64 {
self.generation
}
pub fn bump_generation(&mut self) {
self.generation += 1;
}
pub fn to_data(&self) -> CandidateCacheSnapshot {
CandidateCacheSnapshot {
candidates: self.candidates.clone(),
next_candidate_id: self.next_candidate_id,
}
}
pub fn from_data(data: CandidateCacheSnapshot) -> Self {
let index = DashMap::new();
let mut cached_targets = HashSet::new();
let mut cached_display_names = HashSet::new();
for (pos, candidate) in data.candidates.iter().enumerate() {
debug_assert_eq!(
candidate.id,
pos as CandidateId + 1,
"快照候选 id 与位置不对应: id = {}, pos = {}",
candidate.id,
pos
);
index.insert(candidate.id, pos);
debug_assert!(
cached_targets.insert(candidate.target.clone()),
"快照候选中存在重复执行目标: {:?}",
candidate.target
);
debug_assert!(
cached_display_names.insert(candidate.name.to_lowercase()),
"快照候选中存在重复显示名: {}",
candidate.name
);
}
debug_assert_eq!(
data.next_candidate_id,
data.candidates.len() as CandidateId + 1,
"快照 next_candidate_id 与候选数量不对应: next = {}, len = {}",
data.next_candidate_id,
data.candidates.len()
);
Self {
candidates: data.candidates,
index,
cached_targets,
cached_display_names,
next_candidate_id: data.next_candidate_id,
generation: 0,
}
}
pub fn add_candidate(&mut self, mut candidate: SearchCandidate) {
if self.has_target(&candidate.target) || self.has_display_name(&candidate.name) {
debug!(
"候选项已存在,丢弃重复的候选项: target = {:?}, name = {}",
candidate.target, candidate.name
);
return;
}
let candidate_id = self.next_candidate_id;
candidate.id = candidate_id;
self.cached_targets.insert(candidate.target.clone());
self.cached_display_names
.insert(candidate.name.to_lowercase());
self.candidates.push(candidate);
self.index.insert(candidate_id, self.candidates.len() - 1);
self.next_candidate_id += 1;
}
pub fn add_plugin_candidate(&mut self, mut candidate: SearchCandidate) {
if self.has_target(&candidate.target) {
warn!(
"插件候选项已存在,丢弃重复项: target = {:?}",
candidate.target
);
return;
}
let candidate_id: u64 = self.next_candidate_id;
candidate.id = candidate_id;
self.cached_targets.insert(candidate.target.clone());
self.candidates.push(candidate);
self.index.insert(candidate_id, self.candidates.len() - 1);
self.next_candidate_id += 1;
}
pub fn get_candidate(&self, id: CandidateId) -> Option<&SearchCandidate> {
match self.index.entry(id) {
Entry::Occupied(entry) => Some(&self.candidates[*entry.get()]),
Entry::Vacant(_) => None,
}
}
pub fn add_candidates(&mut self, candidates: CachedCandidateData) {
for candidate in candidates.candidates.iter() {
self.add_candidate(candidate.clone());
}
}
pub fn get_candidates(&self) -> &Vec<SearchCandidate> {
&self.candidates
}
pub fn get_candidates_mut(&mut self) -> &mut Vec<SearchCandidate> {
&mut self.candidates
}
fn has_target(&self, target: &ExecutionTarget) -> bool {
self.cached_targets.contains(target)
}
fn has_display_name(&self, display_name: &str) -> bool {
self.cached_display_names
.contains(&display_name.to_lowercase())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::services::IconRequest;
fn sample_candidate(i: u64) -> SearchCandidate {
SearchCandidate {
id: i,
name: format!("候选{}", i),
icon: IconRequest::Path(format!("C:\\item{}.exe", i)),
target: ExecutionTarget::Path(format!("C:\\item{}.exe", i)),
keywords: vec!["kw".into()],
bias: 0.0,
trigger_keywords: Vec::new(),
}
}
#[test]
fn test_snapshot_roundtrip() {
let mut cache = CachedCandidateData::new();
for i in 1..=10u64 {
cache.add_candidate(sample_candidate(i));
}
let mut restored = CachedCandidateData::from_data(cache.to_data());
assert_eq!(restored.get_candidates().len(), 10);
for i in 1..=10u64 {
let c = restored.get_candidate(i).expect("id 保真");
assert_eq!(c.name, format!("候选{}", i));
assert_eq!(
c.target,
ExecutionTarget::Path(format!("C:\\item{}.exe", i))
);
}
restored.add_candidate(sample_candidate(0));
assert_eq!(restored.get_candidates().len(), 11);
assert_eq!(
restored.get_candidate(11).expect("next_id 保真").name,
"候选0"
);
}
#[test]
fn test_snapshot_json_roundtrip() {
let mut cache = CachedCandidateData::new();
for i in 1..=5u64 {
cache.add_candidate(sample_candidate(i));
}
let json = serde_json::to_value(cache.to_data()).unwrap();
let data: CandidateCacheSnapshot = serde_json::from_value(json).unwrap();
let restored = CachedCandidateData::from_data(data);
assert_eq!(restored.get_candidates().len(), 5);
for i in 1..=5u64 {
assert_eq!(
restored.get_candidate(i).expect("id 保真").name,
format!("候选{}", i)
);
}
}
#[test]
#[should_panic(expected = "id 与位置不对应")]
fn test_from_data_detects_mismatched_id() {
let data = CandidateCacheSnapshot {
candidates: vec![sample_candidate(5)],
next_candidate_id: 2,
};
CachedCandidateData::from_data(data);
}
}