reovim_plugin_cmdline_completion/
cache.rs1use std::sync::Arc;
6
7use arc_swap::ArcSwap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CmdlineCompletionKind {
12 Command,
14 File,
16 Directory,
18 Option,
20 Subcommand,
22}
23
24#[derive(Debug, Clone)]
26pub struct CmdlineCompletionItem {
27 pub label: String,
29 pub description: String,
31 pub icon: &'static str,
33 pub kind: CmdlineCompletionKind,
35 pub insert_text: String,
37}
38
39impl CmdlineCompletionItem {
40 #[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 #[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 #[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#[derive(Debug, Clone, Default)]
83pub struct CmdlineCompletionSnapshot {
84 pub items: Vec<CmdlineCompletionItem>,
86 pub selected_index: usize,
88 pub active: bool,
90 pub prefix: String,
92 pub replace_start: usize,
94}
95
96impl CmdlineCompletionSnapshot {
97 #[must_use]
99 pub fn dismissed() -> Self {
100 Self {
101 active: false,
102 ..Self::default()
103 }
104 }
105
106 #[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 #[must_use]
118 pub fn has_items(&self) -> bool {
119 !self.items.is_empty()
120 }
121}
122
123pub struct CmdlineCompletionCache {
127 current: ArcSwap<CmdlineCompletionSnapshot>,
128}
129
130impl CmdlineCompletionCache {
131 #[must_use]
133 pub fn new() -> Self {
134 Self {
135 current: ArcSwap::from_pointee(CmdlineCompletionSnapshot::default()),
136 }
137 }
138
139 pub fn store(&self, snapshot: CmdlineCompletionSnapshot) {
141 self.current.store(Arc::new(snapshot));
142 }
143
144 #[must_use]
146 pub fn load(&self) -> Arc<CmdlineCompletionSnapshot> {
147 self.current.load_full()
148 }
149
150 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 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 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 pub fn dismiss(&self) {
184 self.store(CmdlineCompletionSnapshot::dismissed());
185 }
186
187 #[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); cache.select_prev();
265 assert_eq!(cache.load().selected_index, 2); }
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}