reovim_plugin_completion/
lib.rs

1//! Code completion plugin for reovim
2//!
3//! This plugin provides auto-completion functionality:
4//! - Trigger completion popup
5//! - Navigate suggestions with next/prev
6//! - Confirm selection or dismiss
7//!
8//! # Architecture
9//!
10//! This plugin follows the treesitter decoupling pattern:
11//! - Defines `SourceSupport` trait for external sources to implement
12//! - Uses background saturator for non-blocking completion
13//! - Uses ArcSwap cache for lock-free render access
14//! - Communicates via `EventBus` events
15
16mod cache;
17mod commands;
18mod events;
19mod registry;
20mod saturator;
21mod source;
22mod state;
23mod window;
24
25use std::{any::TypeId, sync::Arc};
26
27// Re-export public API
28pub use {
29    cache::{CompletionCache, CompletionSnapshot},
30    commands::{
31        CompletionConfirm, CompletionDismiss, CompletionSelectNext, CompletionSelectPrev,
32        CompletionTrigger, CompletionTriggered,
33    },
34    events::{CompletionDismissed, CompletionReady, RegisterSource},
35    registry::{SourceRegistry, SourceSupport},
36    saturator::{CompletionRequest, CompletionSaturatorHandle, spawn_completion_saturator},
37    source::BufferWordsSource,
38    state::SharedCompletionManager,
39    window::CompletionPluginWindow,
40};
41
42// Re-export from core for convenience
43pub use reovim_core::completion::{CompletionContext, CompletionItem, CompletionKind};
44
45use reovim_core::{
46    bind::CommandRef,
47    command::{CommandContext, id::CommandId},
48    event::CommandEvent,
49    event_bus::{
50        EventBus, EventResult,
51        core_events::{BufferModification, BufferModified, RequestInsertText},
52    },
53    keys,
54    plugin::{Plugin, PluginContext, PluginId, PluginStateRegistry},
55};
56
57/// Plugin-local command IDs
58pub mod command_id {
59    use super::CommandId;
60
61    pub const COMPLETION_TRIGGER: CommandId = CommandId::new("completion_trigger");
62    pub const COMPLETION_NEXT: CommandId = CommandId::new("completion_next");
63    pub const COMPLETION_PREV: CommandId = CommandId::new("completion_prev");
64    pub const COMPLETION_CONFIRM: CommandId = CommandId::new("completion_confirm");
65    pub const COMPLETION_DISMISS: CommandId = CommandId::new("completion_dismiss");
66}
67
68/// Code completion plugin
69///
70/// Provides auto-completion with:
71/// - Background saturator for non-blocking computation
72/// - Lock-free cache for responsive UI
73/// - Extensible source system via `SourceSupport` trait
74pub struct CompletionPlugin {
75    manager: Arc<SharedCompletionManager>,
76}
77
78impl CompletionPlugin {
79    /// Create a new completion plugin
80    #[must_use]
81    pub fn new() -> Self {
82        Self {
83            manager: Arc::new(SharedCompletionManager::new()),
84        }
85    }
86}
87
88impl Default for CompletionPlugin {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94impl Plugin for CompletionPlugin {
95    fn id(&self) -> PluginId {
96        PluginId::new("reovim:completion")
97    }
98
99    fn name(&self) -> &'static str {
100        "Completion"
101    }
102
103    fn description(&self) -> &'static str {
104        "Auto-completion with background processing"
105    }
106
107    fn dependencies(&self) -> Vec<TypeId> {
108        vec![]
109    }
110
111    fn build(&self, ctx: &mut PluginContext) {
112        // Register commands (unified types)
113        let _ = ctx.register_command(CompletionTrigger::new(0));
114        let _ = ctx.register_command(CompletionSelectNext);
115        let _ = ctx.register_command(CompletionSelectPrev);
116        let _ = ctx.register_command(CompletionConfirm);
117        let _ = ctx.register_command(CompletionDismiss);
118
119        // Register keybindings
120        use reovim_core::bind::KeymapScope;
121        let insert_mode = KeymapScope::editor_insert();
122
123        // Alt-Space to trigger completion in insert mode
124        ctx.bind_key_scoped(
125            insert_mode.clone(),
126            keys![(Alt Space)],
127            CommandRef::Registered(command_id::COMPLETION_TRIGGER),
128        );
129
130        // Navigation keybindings when completion is active
131        ctx.bind_key_scoped(
132            insert_mode.clone(),
133            keys![(Ctrl 'n')],
134            CommandRef::Registered(command_id::COMPLETION_NEXT),
135        );
136
137        ctx.bind_key_scoped(
138            insert_mode.clone(),
139            keys![(Ctrl 'p')],
140            CommandRef::Registered(command_id::COMPLETION_PREV),
141        );
142
143        // Ctrl+y to confirm completion selection (vim convention)
144        ctx.bind_key_scoped(
145            insert_mode.clone(),
146            keys![(Ctrl 'y')],
147            CommandRef::Registered(command_id::COMPLETION_CONFIRM),
148        );
149
150        // Note: Tab for confirm is NOT supported due to architectural limitations.
151        // When a keybinding is found, the Tab fallback (insert '\t') is skipped.
152        // The keybinding system doesn't support "fallback" when a command returns NotHandled.
153        // See Issue #4 in docs/cmp.md for details.
154        // Users should use Ctrl+y to confirm completion.
155    }
156
157    fn init_state(&self, registry: &PluginStateRegistry) {
158        // Register the shared manager for cross-plugin access
159        registry.register(Arc::clone(&self.manager));
160
161        // Register the plugin window
162        registry.register_plugin_window(Arc::new(CompletionPluginWindow::new(Arc::clone(
163            &self.manager,
164        ))));
165    }
166
167    fn subscribe(&self, bus: &EventBus, state: Arc<PluginStateRegistry>) {
168        // Capture tokio runtime handle for use in EventBus handlers
169        // (EventBus handlers run on std::thread, not tokio runtime)
170        let rt_handle = tokio::runtime::Handle::current();
171
172        // Subscribe to CompletionTriggered events (from CompletionTrigger command)
173        let manager = Arc::clone(&self.manager);
174        bus.subscribe::<CompletionTriggered, _>(100, move |event, ctx| {
175            // If completion is already active, check if cursor moved outside valid range
176            if manager.is_active() {
177                let snapshot = manager.snapshot();
178                let req = &event.request;
179
180                // Dismiss if cursor moved to different line
181                if req.cursor_row != snapshot.cursor_row {
182                    tracing::debug!("Completion dismissed: line changed");
183                    manager.dismiss();
184                    ctx.request_render();
185                    return EventResult::Handled;
186                }
187
188                // Dismiss if cursor moved before word start
189                if req.cursor_col < snapshot.word_start_col {
190                    tracing::debug!("Completion dismissed: cursor before word start");
191                    manager.dismiss();
192                    ctx.request_render();
193                    return EventResult::Handled;
194                }
195
196                // Dismiss if prefix is empty (deleted all characters)
197                if req.prefix.is_empty() {
198                    tracing::debug!("Completion dismissed: empty prefix");
199                    manager.dismiss();
200                    ctx.request_render();
201                    return EventResult::Handled;
202                }
203            }
204
205            tracing::info!("CompletionTriggered event received, prefix={}", event.request.prefix);
206            manager.request_completion(event.request.clone());
207            ctx.request_render();
208            EventResult::Handled
209        });
210
211        // Subscribe to RegisterSource events from external plugins
212        let manager = Arc::clone(&self.manager);
213        bus.subscribe::<RegisterSource, _>(100, move |event, _ctx| {
214            manager.register_source(Arc::clone(&event.source));
215            EventResult::Handled
216        });
217
218        // Subscribe to CompletionSelectNext command
219        let manager = Arc::clone(&self.manager);
220        bus.subscribe::<CompletionSelectNext, _>(100, move |_event, ctx| {
221            if manager.is_active() {
222                manager.select_next();
223                ctx.request_render();
224                EventResult::Handled
225            } else {
226                EventResult::NotHandled
227            }
228        });
229
230        // Subscribe to CompletionSelectPrev command
231        let manager = Arc::clone(&self.manager);
232        bus.subscribe::<CompletionSelectPrev, _>(100, move |_event, ctx| {
233            if manager.is_active() {
234                manager.select_prev();
235                ctx.request_render();
236                EventResult::Handled
237            } else {
238                EventResult::NotHandled
239            }
240        });
241
242        // Subscribe to CompletionDismiss command
243        let manager = Arc::clone(&self.manager);
244        bus.subscribe::<CompletionDismiss, _>(100, move |_event, ctx| {
245            if manager.is_active() {
246                manager.dismiss();
247                ctx.request_render();
248                EventResult::Handled
249            } else {
250                EventResult::NotHandled
251            }
252        });
253
254        // Subscribe to CompletionConfirm command
255        let manager = Arc::clone(&self.manager);
256        bus.subscribe::<CompletionConfirm, _>(100, move |_event, ctx| {
257            if !manager.is_active() {
258                return EventResult::NotHandled;
259            }
260
261            let snapshot = manager.snapshot();
262            let Some(item) = snapshot.selected_item() else {
263                return EventResult::NotHandled;
264            };
265
266            // Delete the typed prefix and insert the full completion text
267            // This replaces "pkg" with "CARGO_PKG" instead of appending
268            let prefix_len = snapshot.prefix.len();
269            let insert_text = item.insert_text.clone();
270
271            if !insert_text.is_empty() || prefix_len > 0 {
272                ctx.emit(RequestInsertText {
273                    text: insert_text,
274                    move_cursor_left: false,
275                    delete_prefix_len: prefix_len,
276                });
277            }
278
279            manager.dismiss();
280            ctx.request_render();
281            EventResult::Handled
282        });
283
284        // Subscribe to ModeChanged to dismiss completion when leaving insert
285        let manager = Arc::clone(&self.manager);
286        bus.subscribe::<reovim_core::event_bus::core_events::ModeChanged, _>(
287            100,
288            move |event, ctx| {
289                // Dismiss if leaving insert mode
290                if !event.to.contains("Insert") && manager.is_active() {
291                    manager.dismiss();
292                    ctx.request_render();
293                }
294                EventResult::Handled
295            },
296        );
297
298        // Cursor position tracking is handled via CursorMoved events (emitted by runtime).
299        // Additional checks are in CompletionTriggered handler for completion re-triggers.
300
301        // Subscribe to BufferModified for auto-popup and live update
302        let manager = Arc::clone(&self.manager);
303        let rt_handle = rt_handle.clone();
304        bus.subscribe::<BufferModified, _>(100, move |event, _ctx| {
305            let is_active = manager.is_active();
306
307            match &event.modification {
308                BufferModification::Insert { text, .. } => {
309                    // Ignore whitespace-only insertions for auto-popup
310                    if text.chars().all(|c| c.is_whitespace()) {
311                        if is_active {
312                            // Dismiss on whitespace (space/enter ends word)
313                            manager.dismiss();
314                        }
315                        return EventResult::NotHandled;
316                    }
317                }
318                BufferModification::Delete { .. } => {
319                    // On delete (backspace), re-trigger if active to update filter
320                    if !is_active {
321                        return EventResult::NotHandled;
322                    }
323                }
324                BufferModification::Replace { .. } | BufferModification::FullReplace => {
325                    // On replace operations, dismiss completion
326                    if is_active {
327                        manager.dismiss();
328                    }
329                    return EventResult::NotHandled;
330                }
331            }
332
333            let generation = manager.next_debounce_generation();
334            let manager = Arc::clone(&manager);
335            let buffer_id = event.buffer_id;
336
337            // Spawn task to trigger/update completion
338            // Use captured rt_handle since EventBus handlers run on std::thread (#120)
339            rt_handle.spawn(async move {
340                if !is_active {
341                    // Completion not active: use debounce delay for auto-popup
342                    tokio::time::sleep(std::time::Duration::from_millis(
343                        crate::state::AUTO_POPUP_DELAY_MS,
344                    ))
345                    .await;
346
347                    // Check if this generation is still current
348                    if manager.current_debounce_generation() != generation {
349                        return;
350                    }
351                }
352                // When active: trigger immediately (no delay) for live filtering
353
354                // Send CommandEvent to trigger completion with proper buffer context
355                manager.send_command_event(CommandEvent {
356                    command: CommandRef::Registered(command_id::COMPLETION_TRIGGER),
357                    context: CommandContext {
358                        buffer_id,
359                        window_id: 0,
360                        count: None,
361                    },
362                });
363            });
364
365            EventResult::NotHandled
366        });
367
368        let _ = state; // Suppress unused warning
369    }
370
371    fn boot(
372        &self,
373        _bus: &EventBus,
374        state: Arc<PluginStateRegistry>,
375        event_tx: Option<tokio::sync::mpsc::Sender<reovim_core::event::RuntimeEvent>>,
376    ) {
377        // Get event_tx from parameter or fall back to state registry
378        let Some(event_tx) = event_tx.or_else(|| state.inner_event_tx()) else {
379            tracing::warn!("Completion plugin boot: event_tx not available");
380            return;
381        };
382
383        // Store event_tx for auto-popup debounce (to send CommandEvent)
384        self.manager.set_inner_event_tx(event_tx.clone());
385
386        // Spawn the completion saturator
387        let sources = self.manager.sources();
388        let cache = Arc::clone(&self.manager.cache);
389
390        let handle = spawn_completion_saturator(sources, cache, event_tx, 50);
391
392        self.manager.set_saturator(handle);
393
394        tracing::info!("Completion plugin booted with saturator");
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    #[test]
403    fn test_completion_plugin_new() {
404        let plugin = CompletionPlugin::new();
405        assert_eq!(plugin.id().as_str(), "reovim:completion");
406        assert_eq!(plugin.name(), "Completion");
407    }
408
409    #[test]
410    fn test_completion_plugin_default() {
411        let plugin = CompletionPlugin::default();
412        assert_eq!(plugin.name(), "Completion");
413    }
414
415    #[test]
416    fn test_completion_plugin_dependencies() {
417        let plugin = CompletionPlugin::new();
418        let deps = plugin.dependencies();
419        assert!(deps.is_empty());
420    }
421}