reovim_plugin_completion/
state.rs

1//! Shared state for the completion plugin
2//!
3//! Provides thread-safe access to the completion manager.
4//! Following the treesitter pattern for cross-plugin access.
5
6use std::sync::{
7    Arc, RwLock,
8    atomic::{AtomicU64, Ordering},
9};
10
11use crate::{
12    CompletionCache, CompletionRequest, CompletionSaturatorHandle, SourceRegistry,
13    cache::CompletionSnapshot, registry::SourceSupport, source::BufferWordsSource,
14};
15
16/// Auto-popup debounce delay in milliseconds
17pub const AUTO_POPUP_DELAY_MS: u64 = 300;
18
19/// Shared completion manager state
20///
21/// Holds the source registry, cache, and saturator handle.
22/// Registered in PluginStateRegistry for cross-plugin access.
23pub struct SharedCompletionManager {
24    /// Registry of completion sources
25    pub registry: RwLock<SourceRegistry>,
26    /// Lock-free completion cache
27    pub cache: Arc<CompletionCache>,
28    /// Saturator handle (set after boot)
29    pub saturator: RwLock<Option<CompletionSaturatorHandle>>,
30    /// Debounce generation counter for auto-popup
31    /// Each keystroke increments this; the timer checks if it's still current
32    pub debounce_generation: AtomicU64,
33    /// Inner event sender for sending CommandEvent after debounce
34    pub inner_event_tx: RwLock<Option<tokio::sync::mpsc::Sender<reovim_core::event::RuntimeEvent>>>,
35}
36
37impl SharedCompletionManager {
38    /// Create a new manager with default buffer source
39    #[must_use]
40    pub fn new() -> Self {
41        let mut registry = SourceRegistry::new();
42        // Register built-in buffer words source
43        registry.register(Arc::new(BufferWordsSource::new()));
44
45        Self {
46            registry: RwLock::new(registry),
47            cache: Arc::new(CompletionCache::new()),
48            saturator: RwLock::new(None),
49            debounce_generation: AtomicU64::new(0),
50            inner_event_tx: RwLock::new(None),
51        }
52    }
53
54    /// Set the inner event sender (called during boot)
55    pub fn set_inner_event_tx(
56        &self,
57        tx: tokio::sync::mpsc::Sender<reovim_core::event::RuntimeEvent>,
58    ) {
59        *self.inner_event_tx.write().unwrap() = Some(tx);
60    }
61
62    /// Increment debounce generation and return the new value
63    ///
64    /// Called when user types; the returned generation is used to check
65    /// if the debounce timer is still valid.
66    pub fn next_debounce_generation(&self) -> u64 {
67        self.debounce_generation.fetch_add(1, Ordering::SeqCst) + 1
68    }
69
70    /// Get current debounce generation
71    pub fn current_debounce_generation(&self) -> u64 {
72        self.debounce_generation.load(Ordering::SeqCst)
73    }
74
75    /// Request a render signal (used after debounce timeout)
76    pub fn request_render(&self) {
77        if let Some(tx) = self.inner_event_tx.read().unwrap().as_ref() {
78            let _ = tx.try_send(reovim_core::event::RuntimeEvent::render_signal());
79        }
80    }
81
82    /// Send a command event to trigger completion
83    ///
84    /// Used by auto-popup to trigger CompletionTrigger command after debounce.
85    /// The command will execute with proper buffer context.
86    pub fn send_command_event(&self, event: reovim_core::event::CommandEvent) {
87        if let Some(tx) = self.inner_event_tx.read().unwrap().as_ref() {
88            let _ = tx.try_send(reovim_core::event::RuntimeEvent::command(event));
89        }
90    }
91
92    /// Register a completion source
93    pub fn register_source(&self, source: Arc<dyn SourceSupport>) {
94        self.registry.write().unwrap().register(source);
95    }
96
97    /// Get all registered sources as Arc vec for saturator
98    pub fn sources(&self) -> Arc<Vec<Arc<dyn SourceSupport>>> {
99        Arc::new(self.registry.read().unwrap().sources().to_vec())
100    }
101
102    /// Set the saturator handle (called during boot)
103    pub fn set_saturator(&self, handle: CompletionSaturatorHandle) {
104        *self.saturator.write().unwrap() = Some(handle);
105    }
106
107    /// Request completion (delegates to saturator)
108    pub fn request_completion(&self, request: CompletionRequest) {
109        if let Some(handle) = self.saturator.read().unwrap().as_ref() {
110            handle.request_completion(request);
111        }
112    }
113
114    /// Dismiss completion
115    pub fn dismiss(&self) {
116        self.cache.dismiss();
117    }
118
119    /// Select next completion item
120    pub fn select_next(&self) {
121        self.cache.select_next();
122    }
123
124    /// Select previous completion item
125    pub fn select_prev(&self) {
126        self.cache.select_prev();
127    }
128
129    /// Check if completion is active
130    #[must_use]
131    pub fn is_active(&self) -> bool {
132        self.cache.is_active()
133    }
134
135    /// Get current snapshot
136    #[must_use]
137    pub fn snapshot(&self) -> Arc<CompletionSnapshot> {
138        self.cache.load()
139    }
140}
141
142impl Default for SharedCompletionManager {
143    fn default() -> Self {
144        Self::new()
145    }
146}
147
148impl std::fmt::Debug for SharedCompletionManager {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        f.debug_struct("SharedCompletionManager")
151            .field("cache", &self.cache)
152            .finish()
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use {
159        super::*,
160        reovim_core::completion::{CompletionContext, CompletionItem},
161        std::{future::Future, pin::Pin},
162    };
163
164    /// Test source for unit tests
165    struct TestSource {
166        id: &'static str,
167        priority: u32,
168    }
169
170    impl TestSource {
171        fn new(id: &'static str) -> Self {
172            Self { id, priority: 100 }
173        }
174
175        fn with_priority(mut self, priority: u32) -> Self {
176            self.priority = priority;
177            self
178        }
179    }
180
181    impl SourceSupport for TestSource {
182        fn source_id(&self) -> &'static str {
183            self.id
184        }
185
186        fn priority(&self) -> u32 {
187            self.priority
188        }
189
190        fn complete<'a>(
191            &'a self,
192            _ctx: &'a CompletionContext,
193            _content: &'a str,
194        ) -> Pin<Box<dyn Future<Output = Vec<CompletionItem>> + Send + 'a>> {
195            Box::pin(async move { vec![CompletionItem::new("test", self.id)] })
196        }
197    }
198
199    #[test]
200    fn test_shared_manager_new() {
201        let manager = SharedCompletionManager::new();
202
203        // Should have buffer source registered by default
204        let sources = manager.sources();
205        assert!(!sources.is_empty());
206
207        // Should not be active initially
208        assert!(!manager.is_active());
209    }
210
211    #[test]
212    fn test_shared_manager_default() {
213        let manager = SharedCompletionManager::default();
214        assert!(!manager.is_active());
215    }
216
217    #[test]
218    fn test_shared_manager_register_source() {
219        let manager = SharedCompletionManager::new();
220        let initial_count = manager.sources().len();
221
222        manager.register_source(Arc::new(TestSource::new("test_source")));
223
224        let sources = manager.sources();
225        assert_eq!(sources.len(), initial_count + 1);
226    }
227
228    #[test]
229    fn test_shared_manager_dismiss() {
230        let manager = SharedCompletionManager::new();
231
232        // Store some items in cache to make it active
233        manager.cache.store(CompletionSnapshot::new(
234            vec![CompletionItem::new("test", "test")],
235            "t".to_string(),
236            0,
237            0,
238            1,
239            0,
240        ));
241        assert!(manager.is_active());
242
243        // Dismiss
244        manager.dismiss();
245        assert!(!manager.is_active());
246    }
247
248    #[test]
249    fn test_shared_manager_navigation() {
250        let manager = SharedCompletionManager::new();
251
252        // Store items
253        manager.cache.store(CompletionSnapshot::new(
254            vec![
255                CompletionItem::new("a", "test"),
256                CompletionItem::new("b", "test"),
257                CompletionItem::new("c", "test"),
258            ],
259            "".to_string(),
260            0,
261            0,
262            0,
263            0,
264        ));
265
266        // Initially at 0
267        assert_eq!(manager.snapshot().selected_index, 0);
268
269        // Select next
270        manager.select_next();
271        assert_eq!(manager.snapshot().selected_index, 1);
272
273        // Select next again
274        manager.select_next();
275        assert_eq!(manager.snapshot().selected_index, 2);
276
277        // Wrap around
278        manager.select_next();
279        assert_eq!(manager.snapshot().selected_index, 0);
280
281        // Select prev wraps to end
282        manager.select_prev();
283        assert_eq!(manager.snapshot().selected_index, 2);
284    }
285
286    #[test]
287    fn test_shared_manager_snapshot() {
288        let manager = SharedCompletionManager::new();
289
290        let items = vec![
291            CompletionItem::new("foo", "test"),
292            CompletionItem::new("bar", "test"),
293        ];
294        manager
295            .cache
296            .store(CompletionSnapshot::new(items, "prefix".to_string(), 42, 10, 15, 12));
297
298        let snapshot = manager.snapshot();
299        assert!(snapshot.active);
300        assert_eq!(snapshot.items.len(), 2);
301        assert_eq!(snapshot.prefix, "prefix");
302        assert_eq!(snapshot.buffer_id, 42);
303        assert_eq!(snapshot.cursor_row, 10);
304        assert_eq!(snapshot.cursor_col, 15);
305        assert_eq!(snapshot.word_start_col, 12);
306    }
307
308    #[test]
309    fn test_shared_manager_sources_priority_order() {
310        let manager = SharedCompletionManager::new();
311
312        // Register sources with different priorities
313        manager.register_source(Arc::new(TestSource::new("low").with_priority(200)));
314        manager.register_source(Arc::new(TestSource::new("high").with_priority(10)));
315
316        let sources = manager.sources();
317        // High priority (10) should come before medium (100, buffer) before low (200)
318        let ids: Vec<_> = sources.iter().map(|s| s.source_id()).collect();
319        let high_pos = ids.iter().position(|id| *id == "high").unwrap();
320        let low_pos = ids.iter().position(|id| *id == "low").unwrap();
321        assert!(high_pos < low_pos);
322    }
323
324    #[test]
325    fn test_shared_manager_debug_format() {
326        let manager = SharedCompletionManager::new();
327        let debug_str = format!("{:?}", manager);
328        assert!(debug_str.contains("SharedCompletionManager"));
329        assert!(debug_str.contains("cache"));
330    }
331
332    #[test]
333    fn test_shared_manager_request_without_saturator() {
334        let manager = SharedCompletionManager::new();
335
336        // Should not panic when saturator is not set
337        let request = CompletionRequest {
338            buffer_id: 1,
339            file_path: None,
340            content: "test".to_string(),
341            cursor_row: 0,
342            cursor_col: 0,
343            line: String::new(),
344            prefix: String::new(),
345            word_start_col: 0,
346            trigger_char: None,
347        };
348        manager.request_completion(request); // Should be no-op
349    }
350
351    #[test]
352    fn test_shared_manager_concurrent_source_registration() {
353        use std::thread;
354
355        let manager = Arc::new(SharedCompletionManager::new());
356
357        let handles: Vec<_> = (0..10)
358            .map(|i| {
359                let manager = Arc::clone(&manager);
360                thread::spawn(move || {
361                    // Create a unique source for each thread
362                    struct DynamicSource {
363                        id: String,
364                    }
365
366                    impl SourceSupport for DynamicSource {
367                        fn source_id(&self) -> &'static str {
368                            // Leak the string to get a 'static reference
369                            // This is acceptable in tests
370                            Box::leak(self.id.clone().into_boxed_str())
371                        }
372
373                        fn priority(&self) -> u32 {
374                            100
375                        }
376
377                        fn complete<'a>(
378                            &'a self,
379                            _ctx: &'a CompletionContext,
380                            _content: &'a str,
381                        ) -> Pin<Box<dyn Future<Output = Vec<CompletionItem>> + Send + 'a>>
382                        {
383                            Box::pin(async move { vec![] })
384                        }
385                    }
386
387                    manager.register_source(Arc::new(DynamicSource {
388                        id: format!("source_{}", i),
389                    }));
390                })
391            })
392            .collect();
393
394        for handle in handles {
395            handle.join().expect("thread panicked");
396        }
397
398        // Should have 11 sources (1 default buffer + 10 registered)
399        let sources = manager.sources();
400        assert_eq!(sources.len(), 11);
401    }
402
403    #[test]
404    fn test_shared_manager_concurrent_cache_access() {
405        use std::{
406            sync::atomic::{AtomicBool, Ordering},
407            thread,
408        };
409
410        let manager = Arc::new(SharedCompletionManager::new());
411        let running = Arc::new(AtomicBool::new(true));
412
413        // Store initial items
414        manager.cache.store(CompletionSnapshot::new(
415            vec![
416                CompletionItem::new("a", "test"),
417                CompletionItem::new("b", "test"),
418                CompletionItem::new("c", "test"),
419            ],
420            "".to_string(),
421            0,
422            0,
423            0,
424            0,
425        ));
426
427        // Spawn reader threads
428        let reader_handles: Vec<_> = (0..5)
429            .map(|_| {
430                let manager = Arc::clone(&manager);
431                let running = Arc::clone(&running);
432                thread::spawn(move || {
433                    while running.load(Ordering::SeqCst) {
434                        let _snapshot = manager.snapshot();
435                        let _active = manager.is_active();
436                    }
437                })
438            })
439            .collect();
440
441        // Spawn navigation threads
442        let nav_handles: Vec<_> = (0..3)
443            .map(|_| {
444                let manager = Arc::clone(&manager);
445                let running = Arc::clone(&running);
446                thread::spawn(move || {
447                    while running.load(Ordering::SeqCst) {
448                        manager.select_next();
449                        manager.select_prev();
450                    }
451                })
452            })
453            .collect();
454
455        // Let threads run for a bit
456        thread::sleep(std::time::Duration::from_millis(50));
457
458        // Stop threads
459        running.store(false, Ordering::SeqCst);
460
461        for handle in reader_handles {
462            handle.join().expect("reader thread panicked");
463        }
464        for handle in nav_handles {
465            handle.join().expect("nav thread panicked");
466        }
467
468        // Manager should still be in valid state
469        let snapshot = manager.snapshot();
470        assert!(snapshot.selected_index < 3);
471    }
472
473    #[test]
474    fn test_shared_manager_concurrent_dismiss() {
475        use std::thread;
476
477        let manager = Arc::new(SharedCompletionManager::new());
478
479        // Store initial items
480        manager.cache.store(CompletionSnapshot::new(
481            vec![CompletionItem::new("test", "test")],
482            "".to_string(),
483            0,
484            0,
485            0,
486            0,
487        ));
488
489        let handles: Vec<_> = (0..10)
490            .map(|_| {
491                let manager = Arc::clone(&manager);
492                thread::spawn(move || {
493                    for _ in 0..100 {
494                        if manager.is_active() {
495                            manager.dismiss();
496                        }
497                        // Re-activate
498                        manager.cache.store(CompletionSnapshot::new(
499                            vec![CompletionItem::new("test", "test")],
500                            "".to_string(),
501                            0,
502                            0,
503                            0,
504                            0,
505                        ));
506                    }
507                })
508            })
509            .collect();
510
511        for handle in handles {
512            handle.join().expect("thread panicked");
513        }
514
515        // Should complete without deadlock
516    }
517
518    #[test]
519    fn test_shared_manager_sources_thread_safe() {
520        use std::thread;
521
522        let manager = Arc::new(SharedCompletionManager::new());
523
524        let handles: Vec<_> = (0..10)
525            .map(|_| {
526                let manager = Arc::clone(&manager);
527                thread::spawn(move || {
528                    for _ in 0..100 {
529                        let _sources = manager.sources();
530                    }
531                })
532            })
533            .collect();
534
535        for handle in handles {
536            handle.join().expect("thread panicked");
537        }
538    }
539
540    #[test]
541    fn test_shared_manager_empty_after_dismiss() {
542        let manager = SharedCompletionManager::new();
543
544        // Store items
545        manager.cache.store(CompletionSnapshot::new(
546            vec![
547                CompletionItem::new("a", "test"),
548                CompletionItem::new("b", "test"),
549            ],
550            "prefix".to_string(),
551            42,
552            10,
553            15,
554            12,
555        ));
556
557        assert!(manager.is_active());
558        assert_eq!(manager.snapshot().items.len(), 2);
559
560        manager.dismiss();
561
562        assert!(!manager.is_active());
563        assert!(manager.snapshot().items.is_empty());
564    }
565
566    #[test]
567    fn test_shared_manager_navigation_wraps() {
568        let manager = SharedCompletionManager::new();
569
570        manager.cache.store(CompletionSnapshot::new(
571            vec![
572                CompletionItem::new("first", "test"),
573                CompletionItem::new("second", "test"),
574            ],
575            "".to_string(),
576            0,
577            0,
578            0,
579            0,
580        ));
581
582        // Start at 0
583        assert_eq!(manager.snapshot().selected_index, 0);
584
585        // Go to 1
586        manager.select_next();
587        assert_eq!(manager.snapshot().selected_index, 1);
588
589        // Wrap to 0
590        manager.select_next();
591        assert_eq!(manager.snapshot().selected_index, 0);
592
593        // Wrap to 1 (from 0 going backwards)
594        manager.select_prev();
595        assert_eq!(manager.snapshot().selected_index, 1);
596    }
597}