reovim_plugin_context/
manager.rs

1//! Context manager for caching and computing context
2//!
3//! Manages context computation and caching to avoid redundant calculations.
4
5use std::sync::Arc;
6
7use {arc_swap::ArcSwap, reovim_core::context_provider::ContextHierarchy};
8
9/// Cached context state
10#[derive(Debug, Clone)]
11pub struct CachedContext {
12    /// Buffer ID this context is for
13    pub buffer_id: usize,
14    /// Line where context was computed
15    pub line: u32,
16    /// Column where context was computed
17    pub col: u32,
18    /// The context hierarchy
19    pub context: Option<ContextHierarchy>,
20}
21
22/// Last known cursor position for change detection
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct LastCursorPosition {
25    pub buffer_id: usize,
26    pub line: u32,
27    pub col: u32,
28}
29
30/// Last known viewport position for change detection
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct LastViewportPosition {
33    pub buffer_id: usize,
34    pub top_line: u32,
35}
36
37/// Context manager that caches computed contexts
38///
39/// Uses `ArcSwap` for lock-free reads during rendering.
40pub struct ContextManager {
41    /// Cached cursor context (per active buffer)
42    cursor_context: ArcSwap<Option<CachedContext>>,
43    /// Cached viewport context (per window)
44    viewport_context: ArcSwap<Option<CachedContext>>,
45    /// Last known cursor position to detect changes (None = never processed)
46    last_cursor: std::sync::RwLock<Option<LastCursorPosition>>,
47    /// Last known viewport top line to detect changes (None = never processed)
48    last_viewport: std::sync::RwLock<Option<LastViewportPosition>>,
49    /// Buffer modification version to invalidate cache
50    buffer_versions: std::sync::RwLock<std::collections::HashMap<usize, u64>>,
51}
52
53impl Default for ContextManager {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl ContextManager {
60    /// Create a new context manager
61    #[must_use]
62    pub fn new() -> Self {
63        Self {
64            cursor_context: ArcSwap::new(Arc::new(None)),
65            viewport_context: ArcSwap::new(Arc::new(None)),
66            last_cursor: std::sync::RwLock::new(None),
67            last_viewport: std::sync::RwLock::new(None),
68            buffer_versions: std::sync::RwLock::new(std::collections::HashMap::new()),
69        }
70    }
71
72    /// Check if cursor position changed
73    ///
74    /// Returns true if position differs from last known position, or if never processed.
75    ///
76    /// # Panics
77    ///
78    /// Panics if the internal lock is poisoned.
79    #[must_use]
80    pub fn cursor_changed(&self, buffer_id: usize, line: u32, col: u32) -> bool {
81        let last = self.last_cursor.read().unwrap();
82        last.is_none_or(|pos| {
83            pos != LastCursorPosition {
84                buffer_id,
85                line,
86                col,
87            }
88        })
89    }
90
91    /// Update last cursor position
92    ///
93    /// # Panics
94    ///
95    /// Panics if the internal lock is poisoned.
96    pub fn update_cursor(&self, buffer_id: usize, line: u32, col: u32) {
97        let mut last = self.last_cursor.write().unwrap();
98        *last = Some(LastCursorPosition {
99            buffer_id,
100            line,
101            col,
102        });
103    }
104
105    /// Check if viewport top line changed
106    ///
107    /// Returns true if position differs from last known position, or if never processed.
108    ///
109    /// # Panics
110    ///
111    /// Panics if the internal lock is poisoned.
112    #[must_use]
113    pub fn viewport_changed(&self, buffer_id: usize, top_line: u32) -> bool {
114        let last = self.last_viewport.read().unwrap();
115        last.is_none_or(|pos| {
116            pos != LastViewportPosition {
117                buffer_id,
118                top_line,
119            }
120        })
121    }
122
123    /// Update last viewport position
124    ///
125    /// # Panics
126    ///
127    /// Panics if the internal lock is poisoned.
128    pub fn update_viewport(&self, buffer_id: usize, top_line: u32) {
129        let mut last = self.last_viewport.write().unwrap();
130        *last = Some(LastViewportPosition {
131            buffer_id,
132            top_line,
133        });
134    }
135
136    /// Store cursor context
137    pub fn set_cursor_context(&self, context: CachedContext) {
138        self.cursor_context.store(Arc::new(Some(context)));
139    }
140
141    /// Get cursor context (lock-free read)
142    #[must_use]
143    pub fn get_cursor_context(&self) -> Arc<Option<CachedContext>> {
144        self.cursor_context.load_full()
145    }
146
147    /// Store viewport context
148    pub fn set_viewport_context(&self, context: CachedContext) {
149        self.viewport_context.store(Arc::new(Some(context)));
150    }
151
152    /// Get viewport context (lock-free read)
153    #[must_use]
154    pub fn get_viewport_context(&self) -> Arc<Option<CachedContext>> {
155        self.viewport_context.load_full()
156    }
157
158    /// Invalidate cache for a buffer (called on `BufferModified`)
159    ///
160    /// # Panics
161    ///
162    /// Panics if the internal lock is poisoned.
163    pub fn invalidate_buffer(&self, buffer_id: usize) {
164        // Increment buffer version
165        *self
166            .buffer_versions
167            .write()
168            .unwrap()
169            .entry(buffer_id)
170            .or_insert(0) += 1;
171
172        // Clear cached contexts if they're for this buffer
173        if let Some(ref ctx) = **self.cursor_context.load()
174            && ctx.buffer_id == buffer_id
175        {
176            self.cursor_context.store(Arc::new(None));
177        }
178        if let Some(ref ctx) = **self.viewport_context.load()
179            && ctx.buffer_id == buffer_id
180        {
181            self.viewport_context.store(Arc::new(None));
182        }
183    }
184
185    /// Get current buffer version (for cache validation)
186    ///
187    /// # Panics
188    ///
189    /// Panics if the internal lock is poisoned.
190    #[must_use]
191    pub fn buffer_version(&self, buffer_id: usize) -> u64 {
192        self.buffer_versions
193            .read()
194            .unwrap()
195            .get(&buffer_id)
196            .copied()
197            .unwrap_or(0)
198    }
199}
200
201/// Shared context manager for cross-plugin access
202pub type SharedContextManager = Arc<ContextManager>;
203
204#[cfg(test)]
205mod tests {
206    use {super::*, reovim_core::context_provider::ContextItem};
207
208    fn create_test_hierarchy(buffer_id: usize, line: u32, col: u32) -> ContextHierarchy {
209        ContextHierarchy::with_items(
210            buffer_id,
211            line,
212            col,
213            vec![ContextItem {
214                text: "test_function".to_string(),
215                start_line: 0,
216                end_line: 100,
217                kind: "function".to_string(),
218                level: 0,
219            }],
220        )
221    }
222
223    #[test]
224    fn test_cursor_change_detection() {
225        let manager = ContextManager::new();
226
227        // Initial state is None, so any position should be detected as changed
228        assert!(manager.cursor_changed(0, 0, 0));
229        assert!(manager.cursor_changed(1, 0, 0));
230
231        // After update, same position should not be changed
232        manager.update_cursor(0, 0, 0);
233        assert!(!manager.cursor_changed(0, 0, 0));
234        assert!(manager.cursor_changed(1, 0, 0)); // Different buffer
235        assert!(manager.cursor_changed(0, 1, 0)); // Different line
236        assert!(manager.cursor_changed(0, 0, 1)); // Different column
237    }
238
239    #[test]
240    fn test_cursor_update() {
241        let manager = ContextManager::new();
242
243        // Update cursor position
244        manager.update_cursor(5, 10, 20);
245
246        // Now the new position should not be detected as changed
247        assert!(!manager.cursor_changed(5, 10, 20));
248        assert!(manager.cursor_changed(5, 11, 20)); // Different line
249    }
250
251    #[test]
252    fn test_viewport_change_detection() {
253        let manager = ContextManager::new();
254
255        // Initial state is None, so any position should be detected as changed
256        assert!(manager.viewport_changed(0, 0));
257        assert!(manager.viewport_changed(1, 0));
258
259        // After update, same position should not be changed
260        manager.update_viewport(0, 0);
261        assert!(!manager.viewport_changed(0, 0));
262        assert!(manager.viewport_changed(1, 0)); // Different buffer
263        assert!(manager.viewport_changed(0, 1)); // Different top line
264    }
265
266    #[test]
267    fn test_viewport_update() {
268        let manager = ContextManager::new();
269
270        // Update viewport position
271        manager.update_viewport(3, 50);
272
273        // Now the new position should not be detected as changed
274        assert!(!manager.viewport_changed(3, 50));
275        assert!(manager.viewport_changed(3, 51)); // Different top line
276    }
277
278    #[test]
279    fn test_cursor_context_caching() {
280        let manager = ContextManager::new();
281
282        // Initially no context
283        assert!((*manager.get_cursor_context()).is_none());
284
285        // Set cursor context
286        let hierarchy = create_test_hierarchy(1, 5, 0);
287        manager.set_cursor_context(CachedContext {
288            buffer_id: 1,
289            line: 5,
290            col: 0,
291            context: Some(hierarchy),
292        });
293
294        // Now context should be available
295        let cached = manager.get_cursor_context();
296        assert!((*cached).is_some());
297        let ctx = cached.as_ref().as_ref().unwrap();
298        assert_eq!(ctx.buffer_id, 1);
299        assert_eq!(ctx.line, 5);
300    }
301
302    #[test]
303    fn test_viewport_context_caching() {
304        let manager = ContextManager::new();
305
306        // Initially no context
307        assert!((*manager.get_viewport_context()).is_none());
308
309        // Set viewport context
310        let hierarchy = create_test_hierarchy(2, 100, 0);
311        manager.set_viewport_context(CachedContext {
312            buffer_id: 2,
313            line: 100,
314            col: 0,
315            context: Some(hierarchy),
316        });
317
318        // Now context should be available
319        let cached = manager.get_viewport_context();
320        assert!((*cached).is_some());
321        let ctx = cached.as_ref().as_ref().unwrap();
322        assert_eq!(ctx.buffer_id, 2);
323        assert_eq!(ctx.line, 100);
324    }
325
326    #[test]
327    fn test_buffer_invalidation() {
328        let manager = ContextManager::new();
329
330        // Set contexts for buffer 1
331        manager.set_cursor_context(CachedContext {
332            buffer_id: 1,
333            line: 5,
334            col: 0,
335            context: Some(create_test_hierarchy(1, 5, 0)),
336        });
337        manager.set_viewport_context(CachedContext {
338            buffer_id: 1,
339            line: 0,
340            col: 0,
341            context: Some(create_test_hierarchy(1, 0, 0)),
342        });
343
344        // Both should be cached
345        assert!((*manager.get_cursor_context()).is_some());
346        assert!((*manager.get_viewport_context()).is_some());
347
348        // Invalidate buffer 1
349        manager.invalidate_buffer(1);
350
351        // Both should be cleared
352        assert!((*manager.get_cursor_context()).is_none());
353        assert!((*manager.get_viewport_context()).is_none());
354    }
355
356    #[test]
357    fn test_buffer_invalidation_different_buffer() {
358        let manager = ContextManager::new();
359
360        // Set contexts for buffer 1
361        manager.set_cursor_context(CachedContext {
362            buffer_id: 1,
363            line: 5,
364            col: 0,
365            context: Some(create_test_hierarchy(1, 5, 0)),
366        });
367
368        // Invalidate buffer 2 (different buffer)
369        manager.invalidate_buffer(2);
370
371        // Buffer 1's context should still be cached
372        assert!((*manager.get_cursor_context()).is_some());
373    }
374
375    #[test]
376    fn test_buffer_version() {
377        let manager = ContextManager::new();
378
379        // Initial version is 0
380        assert_eq!(manager.buffer_version(1), 0);
381
382        // Invalidate increments version
383        manager.invalidate_buffer(1);
384        assert_eq!(manager.buffer_version(1), 1);
385
386        manager.invalidate_buffer(1);
387        assert_eq!(manager.buffer_version(1), 2);
388
389        // Different buffer still at 0
390        assert_eq!(manager.buffer_version(2), 0);
391    }
392}