reovim_plugin_cmdline_completion/
lib.rs

1//! Command-line auto-completion plugin for reovim
2//!
3//! Provides TAB completion for command mode (`:` commands):
4//! - Command name completion with descriptions
5//! - File path completion for :e, :w commands
6//!
7//! # Architecture
8//!
9//! - Synchronous completion (fast enough without background processing)
10//! - Lock-free cache for responsive rendering (ArcSwap pattern)
11//! - Wildmenu-style popup with icons and descriptions
12
13mod cache;
14mod command;
15mod source;
16mod window;
17
18use std::{any::TypeId, sync::Arc};
19
20pub use {
21    cache::{
22        CmdlineCompletionCache, CmdlineCompletionItem, CmdlineCompletionKind,
23        CmdlineCompletionSnapshot,
24    },
25    command::{
26        CmdlineComplete, CmdlineCompleteConfirm, CmdlineCompleteDismiss, CmdlineCompleteNext,
27        CmdlineCompletePrev,
28    },
29    source::{complete_commands, complete_paths},
30    window::CmdlineCompletionWindow,
31};
32
33use reovim_core::{
34    command_line::CmdlineCompletionContext,
35    event_bus::{EventBus, EventResult, core_events},
36    plugin::{Plugin, PluginContext, PluginId, PluginStateRegistry},
37};
38
39/// Shared state for command-line completion
40pub struct CmdlineCompletionState {
41    /// Lock-free cache for completion items
42    pub cache: Arc<CmdlineCompletionCache>,
43}
44
45impl CmdlineCompletionState {
46    /// Create a new state
47    #[must_use]
48    pub fn new() -> Self {
49        Self {
50            cache: Arc::new(CmdlineCompletionCache::new()),
51        }
52    }
53}
54
55impl Default for CmdlineCompletionState {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61/// Command-line completion plugin
62pub struct CmdlineCompletionPlugin {
63    state: Arc<CmdlineCompletionState>,
64}
65
66impl CmdlineCompletionPlugin {
67    /// Create a new plugin instance
68    #[must_use]
69    pub fn new() -> Self {
70        Self {
71            state: Arc::new(CmdlineCompletionState::new()),
72        }
73    }
74}
75
76impl Default for CmdlineCompletionPlugin {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl Plugin for CmdlineCompletionPlugin {
83    fn id(&self) -> PluginId {
84        PluginId::new("reovim:cmdline-completion")
85    }
86
87    fn name(&self) -> &'static str {
88        "Command-line Completion"
89    }
90
91    fn description(&self) -> &'static str {
92        "TAB completion for command-line mode"
93    }
94
95    fn dependencies(&self) -> Vec<TypeId> {
96        vec![]
97    }
98
99    fn build(&self, ctx: &mut PluginContext) {
100        // Register commands
101        let _ = ctx.register_command(CmdlineComplete);
102        let _ = ctx.register_command(CmdlineCompleteNext);
103        let _ = ctx.register_command(CmdlineCompletePrev);
104        let _ = ctx.register_command(CmdlineCompleteConfirm);
105        let _ = ctx.register_command(CmdlineCompleteDismiss);
106
107        // Key bindings are registered in core bind/mod.rs for Command mode
108    }
109
110    fn init_state(&self, registry: &PluginStateRegistry) {
111        // Register the shared state for cross-plugin access
112        registry.register(Arc::clone(&self.state));
113
114        // Register the plugin window
115        registry.register_plugin_window(Arc::new(CmdlineCompletionWindow::new(Arc::clone(
116            &self.state.cache,
117        ))));
118    }
119
120    fn subscribe(&self, bus: &EventBus, state: Arc<PluginStateRegistry>) {
121        // Subscribe to completion trigger event (Tab key in command mode)
122        let cache = Arc::clone(&self.state.cache);
123        let bus_sender = bus.sender();
124        bus.subscribe::<core_events::CmdlineCompletionTriggered, _>(100, move |event, ctx| {
125            // Check if completion is already active - if so, go to next item
126            if cache.is_active() {
127                cache.select_next();
128                ctx.request_render();
129                return EventResult::Handled;
130            }
131
132            // Generate completions based on context
133            let items = match &event.context {
134                CmdlineCompletionContext::Command { prefix } => {
135                    complete_commands(prefix, &event.registry_commands)
136                }
137                CmdlineCompletionContext::Argument {
138                    command,
139                    prefix,
140                    arg_start,
141                } => {
142                    // Check if the command accepts file paths
143                    let path_commands = [
144                        "e", "edit", "w", "write", "sp", "split", "vs", "vsplit", "tabnew",
145                    ];
146                    if path_commands.contains(&command.as_str()) {
147                        complete_paths(prefix, *arg_start)
148                    } else {
149                        Vec::new()
150                    }
151                }
152            };
153
154            if items.is_empty() {
155                return EventResult::Handled; // Nothing to complete
156            }
157
158            // Get the replace_start position
159            let replace_start = match &event.context {
160                CmdlineCompletionContext::Command { .. } => 0,
161                CmdlineCompletionContext::Argument { arg_start, .. } => *arg_start,
162            };
163
164            // Get the prefix for display
165            let prefix = match &event.context {
166                CmdlineCompletionContext::Command { prefix } => prefix.clone(),
167                CmdlineCompletionContext::Argument { prefix, .. } => prefix.clone(),
168            };
169
170            // Store in cache
171            cache.store(CmdlineCompletionSnapshot {
172                items,
173                selected_index: 0,
174                active: true,
175                prefix,
176                replace_start,
177            });
178
179            ctx.request_render();
180            EventResult::Handled
181        });
182
183        // Subscribe to CmdlineCompleteNext command
184        let cache = Arc::clone(&self.state.cache);
185        bus.subscribe::<CmdlineCompleteNext, _>(100, move |_event, ctx| {
186            if cache.is_active() {
187                cache.select_next();
188                ctx.request_render();
189                EventResult::Handled
190            } else {
191                EventResult::NotHandled
192            }
193        });
194
195        // Subscribe to completion prev event (Shift-Tab)
196        let cache = Arc::clone(&self.state.cache);
197        bus.subscribe::<core_events::CmdlineCompletionPrevRequested, _>(100, move |_event, ctx| {
198            if cache.is_active() {
199                cache.select_prev();
200                ctx.request_render();
201                EventResult::Handled
202            } else {
203                EventResult::NotHandled
204            }
205        });
206
207        // Subscribe to CmdlineCompletePrev command (alternative route)
208        let cache = Arc::clone(&self.state.cache);
209        bus.subscribe::<CmdlineCompletePrev, _>(100, move |_event, ctx| {
210            if cache.is_active() {
211                cache.select_prev();
212                ctx.request_render();
213                EventResult::Handled
214            } else {
215                EventResult::NotHandled
216            }
217        });
218
219        // Subscribe to CmdlineCompleteConfirm command
220        let cache = Arc::clone(&self.state.cache);
221        let bus_sender_confirm = bus.sender();
222        bus.subscribe::<CmdlineCompleteConfirm, _>(100, move |_event, ctx| {
223            if !cache.is_active() {
224                return EventResult::NotHandled;
225            }
226
227            let snapshot = cache.load();
228            let Some(item) = snapshot.items.get(snapshot.selected_index) else {
229                cache.dismiss();
230                return EventResult::NotHandled;
231            };
232
233            // Emit event to apply the completion
234            bus_sender_confirm.try_send(core_events::RequestApplyCmdlineCompletion {
235                text: item.insert_text.clone(),
236                replace_start: snapshot.replace_start,
237            });
238
239            cache.dismiss();
240            ctx.request_render();
241            EventResult::Handled
242        });
243
244        // Subscribe to CmdlineCompleteDismiss command
245        let cache = Arc::clone(&self.state.cache);
246        bus.subscribe::<CmdlineCompleteDismiss, _>(100, move |_event, ctx| {
247            if cache.is_active() {
248                cache.dismiss();
249                ctx.request_render();
250                EventResult::Handled
251            } else {
252                EventResult::NotHandled
253            }
254        });
255
256        // Subscribe to ModeChanged to dismiss completion when leaving command mode
257        let cache = Arc::clone(&self.state.cache);
258        bus.subscribe::<core_events::ModeChanged, _>(100, move |event, ctx| {
259            // Dismiss if leaving command mode
260            if !event.to.contains("Command") && cache.is_active() {
261                cache.dismiss();
262                ctx.request_render();
263            }
264            EventResult::NotHandled // Don't consume the event
265        });
266
267        // Note: Completion is dismissed via ModeChanged handler when leaving command mode
268        // (which happens after execute/cancel)
269
270        let _ = state; // Suppress unused warning
271        let _ = bus_sender; // Suppress unused warning (used in closure above)
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_plugin_new() {
281        let plugin = CmdlineCompletionPlugin::new();
282        assert_eq!(plugin.id().as_str(), "reovim:cmdline-completion");
283        assert_eq!(plugin.name(), "Command-line Completion");
284    }
285
286    #[test]
287    fn test_plugin_default() {
288        let plugin = CmdlineCompletionPlugin::default();
289        assert_eq!(plugin.name(), "Command-line Completion");
290    }
291
292    #[test]
293    fn test_plugin_dependencies() {
294        let plugin = CmdlineCompletionPlugin::new();
295        assert!(plugin.dependencies().is_empty());
296    }
297}