1use std::collections::HashSet;
8use std::path::PathBuf;
9use std::time::{Duration, Instant};
10
11use ryo_analysis::SymbolId;
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub struct GoalId(pub u64);
17
18impl GoalId {
19 pub fn new(id: u64) -> Self {
20 Self(id)
21 }
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub struct WaveId(pub u64);
27
28impl WaveId {
29 pub fn new(id: u64) -> Self {
30 Self(id)
31 }
32}
33
34#[derive(Debug, Clone, Default)]
36pub struct AcChanges {
37 pub added: Vec<SymbolId>,
39
40 pub modified: Vec<SymbolId>,
42
43 pub removed: Vec<SymbolId>,
45}
46
47impl AcChanges {
48 pub fn new() -> Self {
50 Self::default()
51 }
52
53 pub fn is_empty(&self) -> bool {
55 self.added.is_empty() && self.modified.is_empty() && self.removed.is_empty()
56 }
57
58 pub fn affected_symbols(&self) -> impl Iterator<Item = &SymbolId> {
60 self.added.iter().chain(self.modified.iter())
61 }
62
63 pub fn len(&self) -> usize {
65 self.added.len() + self.modified.len() + self.removed.len()
66 }
67
68 pub fn merge(&mut self, other: AcChanges) {
70 self.added.extend(other.added);
71 self.modified.extend(other.modified);
72 self.removed.extend(other.removed);
73
74 self.added.sort();
76 self.added.dedup();
77 self.modified.sort();
78 self.modified.dedup();
79 self.removed.sort();
80 self.removed.dedup();
81 }
82}
83
84#[derive(Debug, Clone)]
86pub enum SuggestTrigger {
87 GoalCompleted { goal_id: GoalId, changes: AcChanges },
89
90 WaveCompleted {
93 wave_id: WaveId,
94 goal_count: usize,
95 changes: AcChanges,
96 },
97
98 Manual,
100
101 Periodic { elapsed: Duration },
103
104 FileChanged { paths: Vec<PathBuf> },
106}
107
108impl SuggestTrigger {
109 pub fn kind(&self) -> TriggerKind {
111 match self {
112 Self::GoalCompleted { .. } => TriggerKind::GoalCompleted,
113 Self::WaveCompleted { .. } => TriggerKind::WaveCompleted,
114 Self::Manual => TriggerKind::Manual,
115 Self::Periodic { .. } => TriggerKind::Periodic,
116 Self::FileChanged { .. } => TriggerKind::FileChanged,
117 }
118 }
119
120 pub fn changes(&self) -> Option<&AcChanges> {
122 match self {
123 Self::GoalCompleted { changes, .. } => Some(changes),
124 Self::WaveCompleted { changes, .. } => Some(changes),
125 _ => None,
126 }
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
132pub enum TriggerKind {
133 GoalCompleted,
134 WaveCompleted,
135 Manual,
136 Periodic,
137 FileChanged,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
142pub enum EvalGranularity {
143 #[default]
145 PerGoal,
146
147 PerWave,
149
150 EveryNGoals(usize),
152
153 ManualOnly,
155}
156
157#[derive(Debug, Clone)]
159pub struct SuggestStrategy {
160 pub granularity: EvalGranularity,
162
163 pub min_interval: Duration,
165
166 pub max_pending_changes: usize,
168
169 pub enabled_triggers: HashSet<TriggerKind>,
171}
172
173impl Default for SuggestStrategy {
174 fn default() -> Self {
175 Self {
176 granularity: EvalGranularity::PerGoal,
177 min_interval: Duration::from_millis(100),
178 max_pending_changes: 50,
179 enabled_triggers: [
180 TriggerKind::GoalCompleted,
181 TriggerKind::WaveCompleted,
182 TriggerKind::Manual,
183 ]
184 .into_iter()
185 .collect(),
186 }
187 }
188}
189
190impl SuggestStrategy {
191 pub fn interactive() -> Self {
193 Self::default()
194 }
195
196 pub fn high_perf() -> Self {
198 Self {
199 granularity: EvalGranularity::PerWave,
200 min_interval: Duration::from_millis(500),
201 max_pending_changes: 200,
202 enabled_triggers: [TriggerKind::WaveCompleted, TriggerKind::Manual]
203 .into_iter()
204 .collect(),
205 }
206 }
207
208 pub fn batch() -> Self {
210 Self {
211 granularity: EvalGranularity::ManualOnly,
212 min_interval: Duration::from_secs(1),
213 max_pending_changes: 1000,
214 enabled_triggers: [TriggerKind::Manual].into_iter().collect(),
215 }
216 }
217
218 pub fn should_evaluate(&self, trigger: &SuggestTrigger, pending: &PendingChanges) -> bool {
220 let kind = trigger.kind();
221
222 if !self.enabled_triggers.contains(&kind) {
224 return false;
225 }
226
227 if pending.last_eval.elapsed() < self.min_interval {
229 if pending.changes.len() < self.max_pending_changes {
231 return false;
232 }
233 }
234
235 match self.granularity {
237 EvalGranularity::PerGoal => {
238 matches!(trigger, SuggestTrigger::GoalCompleted { .. })
239 || matches!(trigger, SuggestTrigger::Manual)
240 }
241 EvalGranularity::PerWave => {
242 matches!(trigger, SuggestTrigger::WaveCompleted { .. })
243 || matches!(trigger, SuggestTrigger::Manual)
244 }
245 EvalGranularity::EveryNGoals(n) => {
246 pending.goal_count >= n || matches!(trigger, SuggestTrigger::Manual)
247 }
248 EvalGranularity::ManualOnly => matches!(trigger, SuggestTrigger::Manual),
249 }
250 }
251
252 pub fn with_granularity(mut self, granularity: EvalGranularity) -> Self {
254 self.granularity = granularity;
255 self
256 }
257
258 pub fn with_min_interval(mut self, interval: Duration) -> Self {
260 self.min_interval = interval;
261 self
262 }
263
264 pub fn enable_trigger(mut self, kind: TriggerKind) -> Self {
266 self.enabled_triggers.insert(kind);
267 self
268 }
269
270 pub fn disable_trigger(mut self, kind: TriggerKind) -> Self {
272 self.enabled_triggers.remove(&kind);
273 self
274 }
275}
276
277#[derive(Debug)]
279pub struct PendingChanges {
280 pub goal_count: usize,
282
283 pub changes: AcChanges,
285
286 pub last_eval: Instant,
288}
289
290impl Default for PendingChanges {
291 fn default() -> Self {
292 Self::new()
293 }
294}
295
296impl PendingChanges {
297 pub fn new() -> Self {
299 Self {
300 goal_count: 0,
301 changes: AcChanges::default(),
302 last_eval: Instant::now(),
303 }
304 }
305
306 pub fn record_goal(&mut self, changes: AcChanges) {
308 self.goal_count += 1;
309 self.changes.merge(changes);
310 }
311
312 pub fn take(&mut self) -> (usize, AcChanges) {
314 let goal_count = self.goal_count;
315 let changes = std::mem::take(&mut self.changes);
316 self.goal_count = 0;
317 self.last_eval = Instant::now();
318 (goal_count, changes)
319 }
320
321 pub fn reset(&mut self) {
323 self.goal_count = 0;
324 self.changes = AcChanges::default();
325 self.last_eval = Instant::now();
326 }
327
328 pub fn has_pending(&self) -> bool {
330 self.goal_count > 0 || !self.changes.is_empty()
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_ac_changes_merge() {
340 let mut c1 = AcChanges {
341 added: vec![
342 SymbolId::parse("1v1").unwrap(),
343 SymbolId::parse("2v1").unwrap(),
344 ],
345 modified: vec![SymbolId::parse("3v1").unwrap()],
346 removed: vec![],
347 };
348
349 let c2 = AcChanges {
350 added: vec![
351 SymbolId::parse("2v1").unwrap(),
352 SymbolId::parse("4v1").unwrap(),
353 ],
354 modified: vec![
355 SymbolId::parse("3v1").unwrap(),
356 SymbolId::parse("5v1").unwrap(),
357 ],
358 removed: vec![SymbolId::parse("6v1").unwrap()],
359 };
360
361 c1.merge(c2);
362
363 assert_eq!(c1.added.len(), 3); assert_eq!(c1.modified.len(), 2); assert_eq!(c1.removed.len(), 1); }
367
368 #[test]
369 fn test_strategy_per_goal() {
370 let strategy = SuggestStrategy::interactive().with_min_interval(Duration::ZERO);
372 let pending = PendingChanges::new();
373
374 let trigger = SuggestTrigger::GoalCompleted {
375 goal_id: GoalId::new(1),
376 changes: AcChanges::default(),
377 };
378
379 assert!(strategy.should_evaluate(&trigger, &pending));
380 }
381
382 #[test]
383 fn test_strategy_per_wave() {
384 let strategy = SuggestStrategy::high_perf().with_min_interval(Duration::ZERO);
386 let pending = PendingChanges::new();
387
388 let goal_trigger = SuggestTrigger::GoalCompleted {
390 goal_id: GoalId::new(1),
391 changes: AcChanges::default(),
392 };
393 assert!(!strategy.should_evaluate(&goal_trigger, &pending));
394
395 let wave_trigger = SuggestTrigger::WaveCompleted {
397 wave_id: WaveId::new(1),
398 goal_count: 5,
399 changes: AcChanges::default(),
400 };
401 assert!(strategy.should_evaluate(&wave_trigger, &pending));
402 }
403
404 #[test]
405 fn test_strategy_manual_only() {
406 let strategy = SuggestStrategy::batch().with_min_interval(Duration::ZERO);
408 let pending = PendingChanges::new();
409
410 let goal_trigger = SuggestTrigger::GoalCompleted {
412 goal_id: GoalId::new(1),
413 changes: AcChanges::default(),
414 };
415 assert!(!strategy.should_evaluate(&goal_trigger, &pending));
416
417 assert!(strategy.should_evaluate(&SuggestTrigger::Manual, &pending));
419 }
420
421 #[test]
422 fn test_pending_changes() {
423 let mut pending = PendingChanges::new();
424
425 assert!(!pending.has_pending());
426
427 pending.record_goal(AcChanges {
428 added: vec![SymbolId::parse("1v1").unwrap()],
429 ..Default::default()
430 });
431
432 assert!(pending.has_pending());
433 assert_eq!(pending.goal_count, 1);
434
435 let (count, changes) = pending.take();
436 assert_eq!(count, 1);
437 assert_eq!(changes.added.len(), 1);
438 assert!(!pending.has_pending());
439 }
440}