reovim_core/completion/
mod.rs

1//! Abstract completion provider trait for auto-completion
2//!
3//! Core defines the traits, plugins provide implementations.
4//! This allows different completion backends (buffer words, LSP, snippets, AI, etc.)
5
6use std::sync::Arc;
7
8/// Completion item kind (subset of LSP `CompletionItemKind`)
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum CompletionKind {
11    #[default]
12    Text,
13    Method,
14    Function,
15    Constructor,
16    Field,
17    Variable,
18    Class,
19    Interface,
20    Module,
21    Property,
22    Unit,
23    Value,
24    Enum,
25    Keyword,
26    Snippet,
27    Color,
28    File,
29    Reference,
30    Folder,
31    EnumMember,
32    Constant,
33    Struct,
34    Event,
35    Operator,
36    TypeParameter,
37}
38
39/// A completion item returned by a completion provider
40#[derive(Debug, Clone)]
41pub struct CompletionItem {
42    /// Text to insert when this item is selected
43    pub insert_text: String,
44    /// Display label shown in the completion menu
45    pub label: String,
46    /// Optional detail text (e.g., type info, signature)
47    pub detail: Option<String>,
48    /// Optional documentation for preview
49    pub documentation: Option<String>,
50    /// Source identifier (e.g., "buffer", "lsp", "path")
51    pub source: &'static str,
52    /// Kind of completion for icon/styling
53    pub kind: CompletionKind,
54    /// Sort priority (lower = higher priority)
55    pub sort_priority: u32,
56    /// Custom filter text (defaults to label if None)
57    pub filter_text: Option<String>,
58    /// Score from fuzzy matching (set by completion engine)
59    pub score: u32,
60    /// Indices of matched characters in `filter_text` (for highlighting)
61    pub match_indices: Vec<u32>,
62}
63
64impl CompletionItem {
65    /// Create a new completion item with minimal data
66    #[must_use]
67    pub fn new(insert_text: impl Into<String>, source: &'static str) -> Self {
68        let text = insert_text.into();
69        Self {
70            label: text.clone(),
71            insert_text: text,
72            detail: None,
73            documentation: None,
74            source,
75            kind: CompletionKind::Text,
76            sort_priority: 100,
77            filter_text: None,
78            score: 0,
79            match_indices: Vec::new(),
80        }
81    }
82
83    /// Set custom label
84    #[must_use]
85    pub fn with_label(mut self, label: impl Into<String>) -> Self {
86        self.label = label.into();
87        self
88    }
89
90    /// Set detail text
91    #[must_use]
92    pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
93        self.detail = Some(detail.into());
94        self
95    }
96
97    /// Set documentation
98    #[must_use]
99    pub fn with_documentation(mut self, doc: impl Into<String>) -> Self {
100        self.documentation = Some(doc.into());
101        self
102    }
103
104    /// Set completion kind
105    #[must_use]
106    pub const fn with_kind(mut self, kind: CompletionKind) -> Self {
107        self.kind = kind;
108        self
109    }
110
111    /// Set sort priority
112    #[must_use]
113    pub const fn with_priority(mut self, priority: u32) -> Self {
114        self.sort_priority = priority;
115        self
116    }
117
118    /// Set filter text
119    #[must_use]
120    pub fn with_filter_text(mut self, text: impl Into<String>) -> Self {
121        self.filter_text = Some(text.into());
122        self
123    }
124
125    /// Get the text to use for filtering
126    #[must_use]
127    pub fn filter_text(&self) -> &str {
128        self.filter_text.as_deref().unwrap_or(&self.label)
129    }
130}
131
132/// Context for completion requests
133#[derive(Debug, Clone)]
134pub struct CompletionContext {
135    /// Buffer ID being completed
136    pub buffer_id: usize,
137    /// File path (for language detection)
138    pub file_path: Option<String>,
139    /// Current cursor row (0-indexed)
140    pub cursor_row: u32,
141    /// Current cursor column (0-indexed)
142    pub cursor_col: u32,
143    /// Current line content
144    pub line: String,
145    /// Prefix text being completed
146    pub prefix: String,
147    /// Column where the word being completed starts
148    pub word_start_col: u32,
149    /// Character that triggered completion (if any)
150    pub trigger_char: Option<char>,
151}
152
153impl CompletionContext {
154    /// Create a new completion context
155    #[must_use]
156    #[allow(clippy::missing_const_for_fn)] // String parameters aren't const constructible
157    pub fn new(
158        buffer_id: usize,
159        cursor_row: u32,
160        cursor_col: u32,
161        line: String,
162        prefix: String,
163        word_start_col: u32,
164    ) -> Self {
165        Self {
166            buffer_id,
167            file_path: None,
168            cursor_row,
169            cursor_col,
170            line,
171            prefix,
172            word_start_col,
173            trigger_char: None,
174        }
175    }
176
177    /// Set file path
178    #[must_use]
179    pub fn with_file_path(mut self, path: impl Into<String>) -> Self {
180        self.file_path = Some(path.into());
181        self
182    }
183
184    /// Set trigger character
185    #[must_use]
186    pub const fn with_trigger_char(mut self, ch: char) -> Self {
187        self.trigger_char = Some(ch);
188        self
189    }
190}
191
192/// Abstract completion provider
193///
194/// Plugins implement this trait to provide completion functionality.
195/// The runtime uses this provider to fetch completions for buffers.
196pub trait CompletionProvider: Send + Sync {
197    /// Check if this provider is available for the given context
198    fn is_available(&self, ctx: &CompletionContext) -> bool;
199
200    /// Request completion to start
201    ///
202    /// This triggers an async completion request. Results are delivered
203    /// via the completion cache and render signal.
204    fn request_completion(&self, ctx: CompletionContext);
205
206    /// Dismiss active completion
207    fn dismiss(&self);
208
209    /// Check if completion is currently active
210    fn is_active(&self) -> bool;
211}
212
213/// Factory for creating completion providers
214///
215/// Plugins implement this trait to provide completion for files.
216/// The runtime uses this factory to obtain the completion provider.
217pub trait CompletionFactory: Send + Sync {
218    /// Get the completion provider
219    ///
220    /// Returns the shared completion provider instance.
221    /// Unlike syntax which is per-buffer, completion is typically global.
222    fn get_provider(&self) -> Arc<dyn CompletionProvider>;
223}
224
225/// Shared reference to a completion factory
226pub type SharedCompletionFactory = Arc<dyn CompletionFactory>;
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_completion_kind_default() {
234        let kind = CompletionKind::default();
235        assert_eq!(kind, CompletionKind::Text);
236    }
237
238    #[test]
239    fn test_completion_kind_variants() {
240        // Just ensure all variants exist and are distinct
241        let kinds = [
242            CompletionKind::Text,
243            CompletionKind::Method,
244            CompletionKind::Function,
245            CompletionKind::Constructor,
246            CompletionKind::Field,
247            CompletionKind::Variable,
248            CompletionKind::Class,
249            CompletionKind::Interface,
250            CompletionKind::Module,
251            CompletionKind::Property,
252            CompletionKind::Unit,
253            CompletionKind::Value,
254            CompletionKind::Enum,
255            CompletionKind::Keyword,
256            CompletionKind::Snippet,
257            CompletionKind::Color,
258            CompletionKind::File,
259            CompletionKind::Reference,
260            CompletionKind::Folder,
261            CompletionKind::EnumMember,
262            CompletionKind::Constant,
263            CompletionKind::Struct,
264            CompletionKind::Event,
265            CompletionKind::Operator,
266            CompletionKind::TypeParameter,
267        ];
268        assert_eq!(kinds.len(), 25);
269    }
270
271    #[test]
272    fn test_completion_item_new() {
273        let item = CompletionItem::new("test_text", "test_source");
274
275        assert_eq!(item.insert_text, "test_text");
276        assert_eq!(item.label, "test_text");
277        assert_eq!(item.source, "test_source");
278        assert_eq!(item.kind, CompletionKind::Text);
279        assert_eq!(item.sort_priority, 100);
280        assert!(item.detail.is_none());
281        assert!(item.documentation.is_none());
282        assert!(item.filter_text.is_none());
283        assert_eq!(item.score, 0);
284    }
285
286    #[test]
287    fn test_completion_item_with_label() {
288        let item = CompletionItem::new("insert", "source").with_label("Display Label");
289
290        assert_eq!(item.insert_text, "insert");
291        assert_eq!(item.label, "Display Label");
292    }
293
294    #[test]
295    fn test_completion_item_with_detail() {
296        let item = CompletionItem::new("func", "source").with_detail("fn() -> i32");
297
298        assert_eq!(item.detail, Some("fn() -> i32".to_string()));
299    }
300
301    #[test]
302    fn test_completion_item_with_documentation() {
303        let item = CompletionItem::new("func", "source")
304            .with_documentation("This function does something useful.");
305
306        assert_eq!(item.documentation, Some("This function does something useful.".to_string()));
307    }
308
309    #[test]
310    fn test_completion_item_with_kind() {
311        let item = CompletionItem::new("func", "source").with_kind(CompletionKind::Function);
312
313        assert_eq!(item.kind, CompletionKind::Function);
314    }
315
316    #[test]
317    fn test_completion_item_with_priority() {
318        let item = CompletionItem::new("item", "source").with_priority(50);
319
320        assert_eq!(item.sort_priority, 50);
321    }
322
323    #[test]
324    fn test_completion_item_with_filter_text() {
325        let item = CompletionItem::new("item", "source").with_filter_text("custom_filter");
326
327        assert_eq!(item.filter_text, Some("custom_filter".to_string()));
328    }
329
330    #[test]
331    fn test_completion_item_filter_text_accessor() {
332        // Without custom filter text, should return label
333        let item1 = CompletionItem::new("label_text", "source");
334        assert_eq!(item1.filter_text(), "label_text");
335
336        // With custom filter text, should return it
337        let item2 = CompletionItem::new("label", "source").with_filter_text("filter");
338        assert_eq!(item2.filter_text(), "filter");
339    }
340
341    #[test]
342    fn test_completion_item_builder_chain() {
343        let item = CompletionItem::new("complete", "lsp")
344            .with_label("complete()")
345            .with_detail("fn complete() -> Result<()>")
346            .with_documentation("Completes the operation")
347            .with_kind(CompletionKind::Function)
348            .with_priority(10)
349            .with_filter_text("comp");
350
351        assert_eq!(item.insert_text, "complete");
352        assert_eq!(item.label, "complete()");
353        assert_eq!(item.detail, Some("fn complete() -> Result<()>".to_string()));
354        assert_eq!(item.documentation, Some("Completes the operation".to_string()));
355        assert_eq!(item.kind, CompletionKind::Function);
356        assert_eq!(item.sort_priority, 10);
357        assert_eq!(item.filter_text, Some("comp".to_string()));
358        assert_eq!(item.source, "lsp");
359    }
360
361    #[test]
362    fn test_completion_context_new() {
363        let ctx =
364            CompletionContext::new(42, 10, 15, "let x = some".to_string(), "some".to_string(), 8);
365
366        assert_eq!(ctx.buffer_id, 42);
367        assert_eq!(ctx.cursor_row, 10);
368        assert_eq!(ctx.cursor_col, 15);
369        assert_eq!(ctx.line, "let x = some");
370        assert_eq!(ctx.prefix, "some");
371        assert_eq!(ctx.word_start_col, 8);
372        assert!(ctx.file_path.is_none());
373        assert!(ctx.trigger_char.is_none());
374    }
375
376    #[test]
377    fn test_completion_context_with_file_path() {
378        let ctx = CompletionContext::new(0, 0, 0, String::new(), String::new(), 0)
379            .with_file_path("/path/to/file.rs");
380
381        assert_eq!(ctx.file_path, Some("/path/to/file.rs".to_string()));
382    }
383
384    #[test]
385    fn test_completion_context_with_trigger_char() {
386        let ctx =
387            CompletionContext::new(0, 0, 0, String::new(), String::new(), 0).with_trigger_char('.');
388
389        assert_eq!(ctx.trigger_char, Some('.'));
390    }
391
392    #[test]
393    fn test_completion_context_full_builder() {
394        let ctx = CompletionContext::new(1, 5, 10, "text".to_string(), "pre".to_string(), 7)
395            .with_file_path("main.rs")
396            .with_trigger_char(':');
397
398        assert_eq!(ctx.buffer_id, 1);
399        assert_eq!(ctx.cursor_row, 5);
400        assert_eq!(ctx.cursor_col, 10);
401        assert_eq!(ctx.line, "text");
402        assert_eq!(ctx.prefix, "pre");
403        assert_eq!(ctx.word_start_col, 7);
404        assert_eq!(ctx.file_path, Some("main.rs".to_string()));
405        assert_eq!(ctx.trigger_char, Some(':'));
406    }
407
408    #[test]
409    fn test_completion_item_clone() {
410        let item = CompletionItem::new("test", "source")
411            .with_label("label")
412            .with_detail("detail");
413
414        let cloned = item.clone();
415
416        assert_eq!(cloned.insert_text, item.insert_text);
417        assert_eq!(cloned.label, item.label);
418        assert_eq!(cloned.detail, item.detail);
419    }
420
421    #[test]
422    fn test_completion_context_clone() {
423        let ctx = CompletionContext::new(1, 2, 3, "line".to_string(), "p".to_string(), 0)
424            .with_file_path("/file");
425
426        let cloned = ctx.clone();
427
428        assert_eq!(cloned.buffer_id, ctx.buffer_id);
429        assert_eq!(cloned.file_path, ctx.file_path);
430    }
431
432    #[test]
433    fn test_completion_item_debug() {
434        let item = CompletionItem::new("test", "source");
435        let debug_str = format!("{item:?}");
436
437        assert!(debug_str.contains("CompletionItem"));
438        assert!(debug_str.contains("test"));
439    }
440
441    #[test]
442    fn test_completion_context_debug() {
443        let ctx = CompletionContext::new(0, 0, 0, String::new(), String::new(), 0);
444        let debug_str = format!("{ctx:?}");
445
446        assert!(debug_str.contains("CompletionContext"));
447    }
448
449    #[test]
450    fn test_completion_kind_debug() {
451        let kind = CompletionKind::Function;
452        let debug_str = format!("{kind:?}");
453
454        assert!(debug_str.contains("Function"));
455    }
456
457    #[test]
458    fn test_completion_kind_copy() {
459        let kind1 = CompletionKind::Method;
460        let kind2 = kind1; // Copy
461
462        assert_eq!(kind1, kind2);
463    }
464
465    #[test]
466    fn test_completion_kind_eq() {
467        assert_eq!(CompletionKind::Text, CompletionKind::Text);
468        assert_ne!(CompletionKind::Text, CompletionKind::Method);
469    }
470}