reovim_plugin_completion/
cache.rs

1//! Lock-free completion cache using ArcSwap
2//!
3//! Follows the highlight_cache.rs pattern for non-blocking reads during render.
4
5use std::{sync::Arc, time::Instant};
6
7use arc_swap::ArcSwap;
8
9use reovim_core::completion::CompletionItem;
10
11/// A snapshot of completion state at a point in time
12#[derive(Debug, Clone)]
13pub struct CompletionSnapshot {
14    /// Completion items (already filtered and sorted)
15    pub items: Vec<CompletionItem>,
16    /// The prefix used for filtering
17    pub prefix: String,
18    /// Buffer ID this completion is for
19    pub buffer_id: usize,
20    /// Cursor position when completion was triggered
21    pub cursor_row: u32,
22    pub cursor_col: u32,
23    /// Column where the word being completed starts
24    pub word_start_col: u32,
25    /// When this snapshot was created
26    pub timestamp: Instant,
27    /// Whether completion is active
28    pub active: bool,
29    /// Currently selected index
30    pub selected_index: usize,
31}
32
33impl Default for CompletionSnapshot {
34    fn default() -> Self {
35        Self {
36            items: Vec::new(),
37            prefix: String::new(),
38            buffer_id: 0,
39            cursor_row: 0,
40            cursor_col: 0,
41            word_start_col: 0,
42            timestamp: Instant::now(),
43            active: false,
44            selected_index: 0,
45        }
46    }
47}
48
49impl CompletionSnapshot {
50    /// Create a new active snapshot with items
51    #[must_use]
52    pub fn new(
53        items: Vec<CompletionItem>,
54        prefix: String,
55        buffer_id: usize,
56        cursor_row: u32,
57        cursor_col: u32,
58        word_start_col: u32,
59    ) -> Self {
60        Self {
61            items,
62            prefix,
63            buffer_id,
64            cursor_row,
65            cursor_col,
66            word_start_col,
67            timestamp: Instant::now(),
68            active: true,
69            selected_index: 0,
70        }
71    }
72
73    /// Create an inactive/dismissed snapshot
74    #[must_use]
75    pub fn dismissed() -> Self {
76        Self {
77            active: false,
78            ..Self::default()
79        }
80    }
81
82    /// Get the currently selected item
83    #[must_use]
84    pub fn selected_item(&self) -> Option<&CompletionItem> {
85        if self.items.is_empty() {
86            None
87        } else {
88            self.items.get(self.selected_index)
89        }
90    }
91
92    /// Check if there are any items
93    #[must_use]
94    pub fn has_items(&self) -> bool {
95        !self.items.is_empty()
96    }
97
98    /// Get the number of items
99    #[must_use]
100    pub fn item_count(&self) -> usize {
101        self.items.len()
102    }
103}
104
105/// Lock-free completion cache
106///
107/// Uses ArcSwap for atomic snapshot replacement, allowing the render
108/// thread to read without blocking the saturator thread.
109pub struct CompletionCache {
110    current: ArcSwap<CompletionSnapshot>,
111}
112
113impl CompletionCache {
114    /// Create a new empty cache
115    #[must_use]
116    pub fn new() -> Self {
117        Self {
118            current: ArcSwap::from_pointee(CompletionSnapshot::default()),
119        }
120    }
121
122    /// Store a new snapshot (called by saturator)
123    ///
124    /// This is lock-free and will immediately be visible to readers.
125    pub fn store(&self, snapshot: CompletionSnapshot) {
126        self.current.store(Arc::new(snapshot));
127    }
128
129    /// Load the current snapshot (called by render)
130    ///
131    /// This is lock-free and will never block.
132    #[must_use]
133    pub fn load(&self) -> Arc<CompletionSnapshot> {
134        self.current.load_full()
135    }
136
137    /// Update selected index without replacing the entire snapshot
138    ///
139    /// Creates a new snapshot with updated selection.
140    pub fn update_selection(&self, new_index: usize) {
141        let current = self.load();
142        if current.active && new_index < current.items.len() {
143            let mut new_snapshot = (*current).clone();
144            new_snapshot.selected_index = new_index;
145            self.store(new_snapshot);
146        }
147    }
148
149    /// Select next item (wraps around)
150    pub fn select_next(&self) {
151        let current = self.load();
152        if current.active && !current.items.is_empty() {
153            let new_index = (current.selected_index + 1) % current.items.len();
154            self.update_selection(new_index);
155        }
156    }
157
158    /// Select previous item (wraps around)
159    pub fn select_prev(&self) {
160        let current = self.load();
161        if current.active && !current.items.is_empty() {
162            let new_index = if current.selected_index == 0 {
163                current.items.len() - 1
164            } else {
165                current.selected_index - 1
166            };
167            self.update_selection(new_index);
168        }
169    }
170
171    /// Dismiss completion
172    pub fn dismiss(&self) {
173        self.store(CompletionSnapshot::dismissed());
174    }
175
176    /// Check if completion is currently active
177    #[must_use]
178    pub fn is_active(&self) -> bool {
179        self.load().active
180    }
181}
182
183impl Default for CompletionCache {
184    fn default() -> Self {
185        Self::new()
186    }
187}
188
189impl std::fmt::Debug for CompletionCache {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        let snapshot = self.load();
192        f.debug_struct("CompletionCache")
193            .field("active", &snapshot.active)
194            .field("item_count", &snapshot.items.len())
195            .field("selected_index", &snapshot.selected_index)
196            .finish()
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_cache_store_load() {
206        let cache = CompletionCache::new();
207
208        // Initially inactive
209        assert!(!cache.is_active());
210
211        // Store a snapshot
212        let items = vec![
213            CompletionItem::new("foo", "test"),
214            CompletionItem::new("bar", "test"),
215        ];
216        let snapshot = CompletionSnapshot::new(items, "f".to_string(), 1, 0, 1, 0);
217        cache.store(snapshot);
218
219        // Now active
220        assert!(cache.is_active());
221        let loaded = cache.load();
222        assert_eq!(loaded.items.len(), 2);
223        assert_eq!(loaded.prefix, "f");
224    }
225
226    #[test]
227    fn test_selection_navigation() {
228        let cache = CompletionCache::new();
229
230        let items = vec![
231            CompletionItem::new("a", "test"),
232            CompletionItem::new("b", "test"),
233            CompletionItem::new("c", "test"),
234        ];
235        let snapshot = CompletionSnapshot::new(items, "".to_string(), 1, 0, 0, 0);
236        cache.store(snapshot);
237
238        // Initially at 0
239        assert_eq!(cache.load().selected_index, 0);
240
241        // Next wraps around
242        cache.select_next();
243        assert_eq!(cache.load().selected_index, 1);
244        cache.select_next();
245        assert_eq!(cache.load().selected_index, 2);
246        cache.select_next();
247        assert_eq!(cache.load().selected_index, 0); // Wrapped
248
249        // Prev wraps around
250        cache.select_prev();
251        assert_eq!(cache.load().selected_index, 2); // Wrapped
252    }
253
254    #[test]
255    fn test_dismiss() {
256        let cache = CompletionCache::new();
257
258        let items = vec![CompletionItem::new("foo", "test")];
259        let snapshot = CompletionSnapshot::new(items, "f".to_string(), 1, 0, 1, 0);
260        cache.store(snapshot);
261
262        assert!(cache.is_active());
263
264        cache.dismiss();
265
266        assert!(!cache.is_active());
267        assert!(cache.load().items.is_empty());
268    }
269
270    #[test]
271    fn test_snapshot_default() {
272        let snapshot = CompletionSnapshot::default();
273
274        assert!(!snapshot.active);
275        assert!(snapshot.items.is_empty());
276        assert_eq!(snapshot.selected_index, 0);
277        assert_eq!(snapshot.buffer_id, 0);
278        assert!(snapshot.prefix.is_empty());
279    }
280
281    #[test]
282    fn test_snapshot_new_is_active() {
283        let items = vec![CompletionItem::new("test", "source")];
284        let snapshot = CompletionSnapshot::new(items, "te".to_string(), 1, 5, 7, 5);
285
286        assert!(snapshot.active);
287        assert_eq!(snapshot.items.len(), 1);
288        assert_eq!(snapshot.prefix, "te");
289        assert_eq!(snapshot.buffer_id, 1);
290        assert_eq!(snapshot.cursor_row, 5);
291        assert_eq!(snapshot.cursor_col, 7);
292        assert_eq!(snapshot.word_start_col, 5);
293        assert_eq!(snapshot.selected_index, 0);
294    }
295
296    #[test]
297    fn test_snapshot_dismissed() {
298        let dismissed = CompletionSnapshot::dismissed();
299
300        assert!(!dismissed.active);
301        assert!(dismissed.items.is_empty());
302    }
303
304    #[test]
305    fn test_snapshot_selected_item() {
306        let items = vec![
307            CompletionItem::new("first", "test"),
308            CompletionItem::new("second", "test"),
309            CompletionItem::new("third", "test"),
310        ];
311        let mut snapshot = CompletionSnapshot::new(items, "".to_string(), 0, 0, 0, 0);
312
313        // Initially selects first item
314        let selected = snapshot.selected_item();
315        assert!(selected.is_some());
316        assert_eq!(selected.unwrap().label, "first");
317
318        // Change selection
319        snapshot.selected_index = 2;
320        let selected = snapshot.selected_item();
321        assert_eq!(selected.unwrap().label, "third");
322    }
323
324    #[test]
325    fn test_snapshot_selected_item_empty() {
326        let snapshot = CompletionSnapshot::default();
327        assert!(snapshot.selected_item().is_none());
328    }
329
330    #[test]
331    fn test_snapshot_has_items() {
332        let empty = CompletionSnapshot::default();
333        assert!(!empty.has_items());
334
335        let with_items = CompletionSnapshot::new(
336            vec![CompletionItem::new("a", "test")],
337            "".to_string(),
338            0,
339            0,
340            0,
341            0,
342        );
343        assert!(with_items.has_items());
344    }
345
346    #[test]
347    fn test_snapshot_item_count() {
348        let snapshot = CompletionSnapshot::new(
349            vec![
350                CompletionItem::new("a", "test"),
351                CompletionItem::new("b", "test"),
352            ],
353            "".to_string(),
354            0,
355            0,
356            0,
357            0,
358        );
359        assert_eq!(snapshot.item_count(), 2);
360    }
361
362    #[test]
363    fn test_update_selection() {
364        let cache = CompletionCache::new();
365        let items = vec![
366            CompletionItem::new("a", "test"),
367            CompletionItem::new("b", "test"),
368            CompletionItem::new("c", "test"),
369        ];
370        cache.store(CompletionSnapshot::new(items, "".to_string(), 0, 0, 0, 0));
371
372        cache.update_selection(2);
373        assert_eq!(cache.load().selected_index, 2);
374
375        // Out of bounds should not update
376        cache.update_selection(10);
377        assert_eq!(cache.load().selected_index, 2); // Still 2
378    }
379
380    #[test]
381    fn test_select_on_empty_cache() {
382        let cache = CompletionCache::new();
383
384        // Should not panic on empty cache
385        cache.select_next();
386        cache.select_prev();
387
388        assert_eq!(cache.load().selected_index, 0);
389    }
390
391    #[test]
392    fn test_select_on_single_item() {
393        let cache = CompletionCache::new();
394        cache.store(CompletionSnapshot::new(
395            vec![CompletionItem::new("only", "test")],
396            "".to_string(),
397            0,
398            0,
399            0,
400            0,
401        ));
402
403        // Should wrap around to same item
404        cache.select_next();
405        assert_eq!(cache.load().selected_index, 0);
406
407        cache.select_prev();
408        assert_eq!(cache.load().selected_index, 0);
409    }
410
411    #[test]
412    fn test_cache_debug_format() {
413        let cache = CompletionCache::new();
414        cache.store(CompletionSnapshot::new(
415            vec![
416                CompletionItem::new("a", "test"),
417                CompletionItem::new("b", "test"),
418            ],
419            "".to_string(),
420            0,
421            0,
422            0,
423            0,
424        ));
425
426        let debug_str = format!("{:?}", cache);
427        assert!(debug_str.contains("CompletionCache"));
428        assert!(debug_str.contains("active"));
429        assert!(debug_str.contains("item_count"));
430    }
431
432    #[test]
433    fn test_cache_default() {
434        let cache = CompletionCache::default();
435        assert!(!cache.is_active());
436    }
437
438    #[test]
439    fn test_concurrent_reads() {
440        use std::{sync::Arc, thread};
441
442        let cache = Arc::new(CompletionCache::new());
443        let items = vec![
444            CompletionItem::new("a", "test"),
445            CompletionItem::new("b", "test"),
446            CompletionItem::new("c", "test"),
447        ];
448        cache.store(CompletionSnapshot::new(items, "".to_string(), 0, 0, 0, 0));
449
450        let handles: Vec<_> = (0..10)
451            .map(|_| {
452                let cache = Arc::clone(&cache);
453                thread::spawn(move || {
454                    for _ in 0..100 {
455                        let snapshot = cache.load();
456                        assert!(snapshot.active);
457                        assert_eq!(snapshot.items.len(), 3);
458                    }
459                })
460            })
461            .collect();
462
463        for handle in handles {
464            handle.join().expect("thread panicked");
465        }
466    }
467
468    #[test]
469    fn test_concurrent_read_write() {
470        use std::{
471            sync::{
472                Arc,
473                atomic::{AtomicBool, Ordering},
474            },
475            thread,
476        };
477
478        let cache = Arc::new(CompletionCache::new());
479        let running = Arc::new(AtomicBool::new(true));
480
481        // Spawn reader threads
482        let reader_handles: Vec<_> = (0..5)
483            .map(|_| {
484                let cache = Arc::clone(&cache);
485                let running = Arc::clone(&running);
486                thread::spawn(move || {
487                    while running.load(Ordering::SeqCst) {
488                        let snapshot = cache.load();
489                        // Just read - shouldn't panic
490                        let _ = snapshot.items.len();
491                        let _ = snapshot.active;
492                    }
493                })
494            })
495            .collect();
496
497        // Spawn writer threads
498        let writer_handles: Vec<_> = (0..3)
499            .map(|i| {
500                let cache = Arc::clone(&cache);
501                thread::spawn(move || {
502                    for j in 0..50 {
503                        let items = vec![CompletionItem::new(format!("item_{i}_{j}"), "test")];
504                        cache.store(CompletionSnapshot::new(
505                            items,
506                            format!("prefix_{j}"),
507                            i,
508                            0,
509                            0,
510                            0,
511                        ));
512                    }
513                })
514            })
515            .collect();
516
517        // Wait for writers to finish
518        for handle in writer_handles {
519            handle.join().expect("writer thread panicked");
520        }
521
522        // Stop readers
523        running.store(false, Ordering::SeqCst);
524        for handle in reader_handles {
525            handle.join().expect("reader thread panicked");
526        }
527
528        // Cache should have some value
529        let snapshot = cache.load();
530        assert!(!snapshot.items.is_empty() || !snapshot.active);
531    }
532
533    #[test]
534    fn test_concurrent_selection_update() {
535        use std::{sync::Arc, thread};
536
537        let cache = Arc::new(CompletionCache::new());
538        let items: Vec<_> = (0..100)
539            .map(|i| CompletionItem::new(format!("item_{i}"), "test"))
540            .collect();
541        cache.store(CompletionSnapshot::new(items, "".to_string(), 0, 0, 0, 0));
542
543        let handles: Vec<_> = (0..10)
544            .map(|_| {
545                let cache = Arc::clone(&cache);
546                thread::spawn(move || {
547                    for _ in 0..100 {
548                        cache.select_next();
549                        cache.select_prev();
550                    }
551                })
552            })
553            .collect();
554
555        for handle in handles {
556            handle.join().expect("thread panicked");
557        }
558
559        // Should still be valid (selection index should be within bounds)
560        let snapshot = cache.load();
561        assert!(snapshot.selected_index < 100);
562    }
563
564    #[test]
565    fn test_rapid_store_load_cycle() {
566        let cache = CompletionCache::new();
567
568        for i in 0..1000 {
569            let items = vec![CompletionItem::new(format!("item_{i}"), "test")];
570            cache.store(CompletionSnapshot::new(items, format!("{i}"), i, 0, 0, 0));
571
572            let snapshot = cache.load();
573            assert!(snapshot.active);
574            assert_eq!(snapshot.buffer_id, i);
575        }
576    }
577
578    #[test]
579    fn test_selection_bounds_after_new_store() {
580        let cache = CompletionCache::new();
581
582        // Store 10 items and select index 5
583        let items: Vec<_> = (0..10)
584            .map(|i| CompletionItem::new(format!("item_{i}"), "test"))
585            .collect();
586        cache.store(CompletionSnapshot::new(items, "".to_string(), 0, 0, 0, 0));
587        cache.update_selection(5);
588        assert_eq!(cache.load().selected_index, 5);
589
590        // Store new items (only 3) - selection should be reset to 0
591        let new_items: Vec<_> = (0..3)
592            .map(|i| CompletionItem::new(format!("new_{i}"), "test"))
593            .collect();
594        cache.store(CompletionSnapshot::new(new_items, "".to_string(), 0, 0, 0, 0));
595
596        // New snapshot starts at 0
597        assert_eq!(cache.load().selected_index, 0);
598    }
599
600    #[test]
601    fn test_dismiss_clears_selection() {
602        let cache = CompletionCache::new();
603        let items = vec![
604            CompletionItem::new("a", "test"),
605            CompletionItem::new("b", "test"),
606        ];
607        cache.store(CompletionSnapshot::new(items, "".to_string(), 0, 0, 0, 0));
608        cache.update_selection(1);
609
610        cache.dismiss();
611
612        let snapshot = cache.load();
613        assert!(!snapshot.active);
614        assert_eq!(snapshot.selected_index, 0);
615    }
616
617    #[test]
618    fn test_multiple_store_overwrites() {
619        let cache = CompletionCache::new();
620
621        // Store first snapshot
622        cache.store(CompletionSnapshot::new(
623            vec![CompletionItem::new("first", "test")],
624            "f".to_string(),
625            0,
626            0,
627            0,
628            0,
629        ));
630        assert_eq!(cache.load().items[0].label, "first");
631
632        // Store second snapshot - should overwrite
633        cache.store(CompletionSnapshot::new(
634            vec![CompletionItem::new("second", "test")],
635            "s".to_string(),
636            0,
637            0,
638            0,
639            0,
640        ));
641        assert_eq!(cache.load().items[0].label, "second");
642        assert_eq!(cache.load().prefix, "s");
643    }
644}