reovim_plugin_cmdline_completion/
cache.rs

1//! Lock-free completion cache using ArcSwap
2//!
3//! Follows the completion plugin pattern for non-blocking reads during render.
4
5use std::sync::Arc;
6
7use arc_swap::ArcSwap;
8
9/// Kind of completion item
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CmdlineCompletionKind {
12    /// Command name (e.g., :write, :edit)
13    Command,
14    /// File path
15    File,
16    /// Directory path
17    Directory,
18    /// Option/setting
19    Option,
20    /// Subcommand
21    Subcommand,
22}
23
24/// A single completion item
25#[derive(Debug, Clone)]
26pub struct CmdlineCompletionItem {
27    /// Display label (e.g., "write", "src/main.rs")
28    pub label: String,
29    /// Short description (e.g., "Write buffer to file")
30    pub description: String,
31    /// Icon character
32    pub icon: &'static str,
33    /// Kind for styling
34    pub kind: CmdlineCompletionKind,
35    /// Text to insert (may differ from label for abbreviations)
36    pub insert_text: String,
37}
38
39impl CmdlineCompletionItem {
40    /// Create a new command completion item
41    #[must_use]
42    pub fn command(name: impl Into<String>, description: impl Into<String>) -> Self {
43        let name = name.into();
44        Self {
45            label: name.clone(),
46            description: description.into(),
47            icon: "",
48            kind: CmdlineCompletionKind::Command,
49            insert_text: name,
50        }
51    }
52
53    /// Create a new file completion item
54    #[must_use]
55    pub fn file(name: impl Into<String>) -> Self {
56        let name = name.into();
57        Self {
58            label: name.clone(),
59            description: "File".into(),
60            icon: "",
61            kind: CmdlineCompletionKind::File,
62            insert_text: name,
63        }
64    }
65
66    /// Create a new directory completion item
67    #[must_use]
68    pub fn directory(name: impl Into<String>) -> Self {
69        let name = name.into();
70        let insert_text = format!("{name}/");
71        Self {
72            label: name,
73            description: "Directory".into(),
74            icon: "",
75            kind: CmdlineCompletionKind::Directory,
76            insert_text,
77        }
78    }
79}
80
81/// A snapshot of completion state
82#[derive(Debug, Clone, Default)]
83pub struct CmdlineCompletionSnapshot {
84    /// Available completion items
85    pub items: Vec<CmdlineCompletionItem>,
86    /// Currently selected index
87    pub selected_index: usize,
88    /// Whether completion popup is visible
89    pub active: bool,
90    /// Original prefix for fuzzy highlighting
91    pub prefix: String,
92    /// Where completion started in input
93    pub replace_start: usize,
94}
95
96impl CmdlineCompletionSnapshot {
97    /// Create an inactive/dismissed snapshot
98    #[must_use]
99    pub fn dismissed() -> Self {
100        Self {
101            active: false,
102            ..Self::default()
103        }
104    }
105
106    /// Get the currently selected item
107    #[must_use]
108    pub fn selected_item(&self) -> Option<&CmdlineCompletionItem> {
109        if self.items.is_empty() {
110            None
111        } else {
112            self.items.get(self.selected_index)
113        }
114    }
115
116    /// Check if there are any items
117    #[must_use]
118    pub fn has_items(&self) -> bool {
119        !self.items.is_empty()
120    }
121}
122
123/// Lock-free completion cache
124///
125/// Uses ArcSwap for atomic snapshot replacement.
126pub struct CmdlineCompletionCache {
127    current: ArcSwap<CmdlineCompletionSnapshot>,
128}
129
130impl CmdlineCompletionCache {
131    /// Create a new empty cache
132    #[must_use]
133    pub fn new() -> Self {
134        Self {
135            current: ArcSwap::from_pointee(CmdlineCompletionSnapshot::default()),
136        }
137    }
138
139    /// Store a new snapshot
140    pub fn store(&self, snapshot: CmdlineCompletionSnapshot) {
141        self.current.store(Arc::new(snapshot));
142    }
143
144    /// Load the current snapshot
145    #[must_use]
146    pub fn load(&self) -> Arc<CmdlineCompletionSnapshot> {
147        self.current.load_full()
148    }
149
150    /// Update selected index
151    pub fn update_selection(&self, new_index: usize) {
152        let current = self.load();
153        if current.active && new_index < current.items.len() {
154            let mut new_snapshot = (*current).clone();
155            new_snapshot.selected_index = new_index;
156            self.store(new_snapshot);
157        }
158    }
159
160    /// Select next item (wraps around)
161    pub fn select_next(&self) {
162        let current = self.load();
163        if current.active && !current.items.is_empty() {
164            let new_index = (current.selected_index + 1) % current.items.len();
165            self.update_selection(new_index);
166        }
167    }
168
169    /// Select previous item (wraps around)
170    pub fn select_prev(&self) {
171        let current = self.load();
172        if current.active && !current.items.is_empty() {
173            let new_index = if current.selected_index == 0 {
174                current.items.len() - 1
175            } else {
176                current.selected_index - 1
177            };
178            self.update_selection(new_index);
179        }
180    }
181
182    /// Dismiss completion
183    pub fn dismiss(&self) {
184        self.store(CmdlineCompletionSnapshot::dismissed());
185    }
186
187    /// Check if completion is active
188    #[must_use]
189    pub fn is_active(&self) -> bool {
190        self.load().active
191    }
192}
193
194impl Default for CmdlineCompletionCache {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200impl std::fmt::Debug for CmdlineCompletionCache {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        let snapshot = self.load();
203        f.debug_struct("CmdlineCompletionCache")
204            .field("active", &snapshot.active)
205            .field("item_count", &snapshot.items.len())
206            .field("selected_index", &snapshot.selected_index)
207            .finish()
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_cache_store_load() {
217        let cache = CmdlineCompletionCache::new();
218        assert!(!cache.is_active());
219
220        let items = vec![
221            CmdlineCompletionItem::command("write", "Write buffer"),
222            CmdlineCompletionItem::command("quit", "Quit editor"),
223        ];
224        cache.store(CmdlineCompletionSnapshot {
225            items,
226            selected_index: 0,
227            active: true,
228            prefix: "w".into(),
229            replace_start: 0,
230        });
231
232        assert!(cache.is_active());
233        let loaded = cache.load();
234        assert_eq!(loaded.items.len(), 2);
235        assert_eq!(loaded.prefix, "w");
236    }
237
238    #[test]
239    fn test_selection_navigation() {
240        let cache = CmdlineCompletionCache::new();
241
242        let items = vec![
243            CmdlineCompletionItem::command("a", "A"),
244            CmdlineCompletionItem::command("b", "B"),
245            CmdlineCompletionItem::command("c", "C"),
246        ];
247        cache.store(CmdlineCompletionSnapshot {
248            items,
249            selected_index: 0,
250            active: true,
251            prefix: String::new(),
252            replace_start: 0,
253        });
254
255        assert_eq!(cache.load().selected_index, 0);
256
257        cache.select_next();
258        assert_eq!(cache.load().selected_index, 1);
259        cache.select_next();
260        assert_eq!(cache.load().selected_index, 2);
261        cache.select_next();
262        assert_eq!(cache.load().selected_index, 0); // Wrapped
263
264        cache.select_prev();
265        assert_eq!(cache.load().selected_index, 2); // Wrapped
266    }
267
268    #[test]
269    fn test_dismiss() {
270        let cache = CmdlineCompletionCache::new();
271
272        let items = vec![CmdlineCompletionItem::command("test", "Test")];
273        cache.store(CmdlineCompletionSnapshot {
274            items,
275            selected_index: 0,
276            active: true,
277            prefix: "t".into(),
278            replace_start: 0,
279        });
280
281        assert!(cache.is_active());
282        cache.dismiss();
283        assert!(!cache.is_active());
284        assert!(cache.load().items.is_empty());
285    }
286
287    #[test]
288    fn test_completion_item_constructors() {
289        let cmd = CmdlineCompletionItem::command("write", "Write buffer");
290        assert_eq!(cmd.label, "write");
291        assert_eq!(cmd.kind, CmdlineCompletionKind::Command);
292        assert_eq!(cmd.icon, "");
293
294        let file = CmdlineCompletionItem::file("main.rs");
295        assert_eq!(file.label, "main.rs");
296        assert_eq!(file.kind, CmdlineCompletionKind::File);
297        assert_eq!(file.icon, "");
298
299        let dir = CmdlineCompletionItem::directory("src");
300        assert_eq!(dir.label, "src");
301        assert_eq!(dir.insert_text, "src/");
302        assert_eq!(dir.kind, CmdlineCompletionKind::Directory);
303        assert_eq!(dir.icon, "");
304    }
305}