reovim_plugin_completion/
registry.rs

1//! Source registry and SourceSupport trait
2//!
3//! Following the treesitter LanguageRegistry pattern, this module provides
4//! the trait that completion sources implement and the registry that manages them.
5
6use std::{future::Future, pin::Pin, sync::Arc};
7
8use reovim_core::completion::{CompletionContext, CompletionItem};
9
10/// Trait for completion source implementations
11///
12/// External plugins implement this trait to provide completions.
13/// Similar to treesitter's `LanguageSupport` trait.
14///
15/// # Example
16///
17/// ```ignore
18/// struct LspCompletionSource { client: LspClient }
19///
20/// impl SourceSupport for LspCompletionSource {
21///     fn source_id(&self) -> &'static str { "lsp" }
22///     fn priority(&self) -> u32 { 50 }  // Higher priority than buffer
23///
24///     fn complete(&self, ctx: &CompletionContext, content: &str)
25///         -> Pin<Box<dyn Future<Output = Vec<CompletionItem>> + Send + '_>>
26///     {
27///         Box::pin(async move {
28///             self.client.request_completion(ctx).await
29///         })
30///     }
31/// }
32/// ```
33pub trait SourceSupport: Send + Sync + 'static {
34    /// Unique identifier for this source (e.g., "buffer", "lsp", "path")
35    fn source_id(&self) -> &'static str;
36
37    /// Priority for sorting results (lower = higher priority)
38    ///
39    /// Default is 100. LSP sources typically use 50, buffer sources use 100.
40    fn priority(&self) -> u32 {
41        100
42    }
43
44    /// Check if this source is available for the given context
45    ///
46    /// Called before `complete()` to filter out unavailable sources.
47    fn is_available(&self, _ctx: &CompletionContext) -> bool {
48        true
49    }
50
51    /// Fetch completions asynchronously
52    ///
53    /// Called by the saturator in a background task.
54    fn complete<'a>(
55        &'a self,
56        ctx: &'a CompletionContext,
57        content: &'a str,
58    ) -> Pin<Box<dyn Future<Output = Vec<CompletionItem>> + Send + 'a>>;
59
60    /// Optional: Resolve additional details for a completion item
61    ///
62    /// Called when an item is selected to fetch documentation, etc.
63    fn resolve<'a>(
64        &'a self,
65        item: &'a CompletionItem,
66    ) -> Pin<Box<dyn Future<Output = CompletionItem> + Send + 'a>> {
67        let item = item.clone();
68        Box::pin(async move { item })
69    }
70
71    /// Optional: Execute post-selection action
72    ///
73    /// Called after a completion is confirmed (e.g., to add imports).
74    fn execute(&self, _item: &CompletionItem) {}
75
76    /// Optional: Characters that trigger completion immediately
77    ///
78    /// For example, LSP might return ['.', ':', '<'] for Rust.
79    fn trigger_characters(&self) -> Option<&[char]> {
80        None
81    }
82}
83
84/// Registry of completion sources
85///
86/// Manages registered sources and provides access by ID.
87#[derive(Default)]
88pub struct SourceRegistry {
89    sources: Vec<Arc<dyn SourceSupport>>,
90}
91
92impl SourceRegistry {
93    /// Create a new empty registry
94    #[must_use]
95    pub fn new() -> Self {
96        Self {
97            sources: Vec::new(),
98        }
99    }
100
101    /// Register a completion source
102    ///
103    /// Sources are stored in priority order (lower priority value = earlier).
104    pub fn register(&mut self, source: Arc<dyn SourceSupport>) {
105        tracing::info!(
106            source_id = source.source_id(),
107            priority = source.priority(),
108            "Registered completion source"
109        );
110
111        // Insert in priority order
112        let priority = source.priority();
113        let pos = self
114            .sources
115            .iter()
116            .position(|s| s.priority() > priority)
117            .unwrap_or(self.sources.len());
118        self.sources.insert(pos, source);
119    }
120
121    /// Get all registered sources
122    #[must_use]
123    pub fn sources(&self) -> &[Arc<dyn SourceSupport>] {
124        &self.sources
125    }
126
127    /// Get available sources for a context
128    pub fn available_sources(&self, ctx: &CompletionContext) -> Vec<Arc<dyn SourceSupport>> {
129        self.sources
130            .iter()
131            .filter(|s| s.is_available(ctx))
132            .cloned()
133            .collect()
134    }
135
136    /// Get a source by ID
137    #[must_use]
138    pub fn get(&self, source_id: &str) -> Option<Arc<dyn SourceSupport>> {
139        self.sources
140            .iter()
141            .find(|s| s.source_id() == source_id)
142            .cloned()
143    }
144
145    /// Get all trigger characters from all sources
146    #[must_use]
147    pub fn all_trigger_characters(&self) -> Vec<char> {
148        self.sources
149            .iter()
150            .filter_map(|s| s.trigger_characters())
151            .flatten()
152            .copied()
153            .collect()
154    }
155
156    /// Number of registered sources
157    #[must_use]
158    pub fn len(&self) -> usize {
159        self.sources.len()
160    }
161
162    /// Check if registry is empty
163    #[must_use]
164    pub fn is_empty(&self) -> bool {
165        self.sources.is_empty()
166    }
167}
168
169impl std::fmt::Debug for SourceRegistry {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        f.debug_struct("SourceRegistry")
172            .field(
173                "sources",
174                &self
175                    .sources
176                    .iter()
177                    .map(|s| s.source_id())
178                    .collect::<Vec<_>>(),
179            )
180            .finish()
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    /// Test source with configurable priority and trigger characters
189    struct TestSource {
190        id: &'static str,
191        priority: u32,
192        trigger_chars: Option<&'static [char]>,
193        available: bool,
194    }
195
196    impl TestSource {
197        fn new(id: &'static str) -> Self {
198            Self {
199                id,
200                priority: 100,
201                trigger_chars: None,
202                available: true,
203            }
204        }
205
206        fn with_priority(mut self, priority: u32) -> Self {
207            self.priority = priority;
208            self
209        }
210
211        fn with_trigger_chars(mut self, chars: &'static [char]) -> Self {
212            self.trigger_chars = Some(chars);
213            self
214        }
215
216        fn unavailable(mut self) -> Self {
217            self.available = false;
218            self
219        }
220    }
221
222    impl SourceSupport for TestSource {
223        fn source_id(&self) -> &'static str {
224            self.id
225        }
226
227        fn priority(&self) -> u32 {
228            self.priority
229        }
230
231        fn is_available(&self, _ctx: &CompletionContext) -> bool {
232            self.available
233        }
234
235        fn trigger_characters(&self) -> Option<&[char]> {
236            self.trigger_chars
237        }
238
239        fn complete<'a>(
240            &'a self,
241            _ctx: &'a CompletionContext,
242            _content: &'a str,
243        ) -> Pin<Box<dyn Future<Output = Vec<CompletionItem>> + Send + 'a>> {
244            Box::pin(async move { vec![CompletionItem::new("test", self.id)] })
245        }
246    }
247
248    fn make_context() -> CompletionContext {
249        CompletionContext::new(0, 0, 0, String::new(), String::new(), 0)
250    }
251
252    #[test]
253    fn test_registry_new_is_empty() {
254        let registry = SourceRegistry::new();
255        assert!(registry.is_empty());
256        assert_eq!(registry.len(), 0);
257    }
258
259    #[test]
260    fn test_registry_register_source() {
261        let mut registry = SourceRegistry::new();
262        registry.register(Arc::new(TestSource::new("test1")));
263
264        assert!(!registry.is_empty());
265        assert_eq!(registry.len(), 1);
266    }
267
268    #[test]
269    fn test_registry_get_by_id() {
270        let mut registry = SourceRegistry::new();
271        registry.register(Arc::new(TestSource::new("source_a")));
272        registry.register(Arc::new(TestSource::new("source_b")));
273
274        let source = registry.get("source_a");
275        assert!(source.is_some());
276        assert_eq!(source.unwrap().source_id(), "source_a");
277
278        let missing = registry.get("nonexistent");
279        assert!(missing.is_none());
280    }
281
282    #[test]
283    fn test_registry_priority_ordering() {
284        let mut registry = SourceRegistry::new();
285
286        // Register in random order
287        registry.register(Arc::new(TestSource::new("low").with_priority(200)));
288        registry.register(Arc::new(TestSource::new("high").with_priority(10)));
289        registry.register(Arc::new(TestSource::new("medium").with_priority(100)));
290
291        // Should be sorted by priority (ascending)
292        let sources = registry.sources();
293        assert_eq!(sources.len(), 3);
294        assert_eq!(sources[0].source_id(), "high");
295        assert_eq!(sources[1].source_id(), "medium");
296        assert_eq!(sources[2].source_id(), "low");
297    }
298
299    #[test]
300    fn test_registry_available_sources() {
301        let mut registry = SourceRegistry::new();
302        registry.register(Arc::new(TestSource::new("available1")));
303        registry.register(Arc::new(TestSource::new("unavailable").unavailable()));
304        registry.register(Arc::new(TestSource::new("available2")));
305
306        let ctx = make_context();
307        let available = registry.available_sources(&ctx);
308
309        assert_eq!(available.len(), 2);
310        let ids: Vec<_> = available.iter().map(|s| s.source_id()).collect();
311        assert!(ids.contains(&"available1"));
312        assert!(ids.contains(&"available2"));
313        assert!(!ids.contains(&"unavailable"));
314    }
315
316    #[test]
317    fn test_registry_all_trigger_characters() {
318        let mut registry = SourceRegistry::new();
319        registry.register(Arc::new(TestSource::new("source1").with_trigger_chars(&['.', ':'])));
320        registry.register(Arc::new(TestSource::new("source2"))); // No triggers
321        registry.register(Arc::new(TestSource::new("source3").with_trigger_chars(&['/', '<'])));
322
323        let triggers = registry.all_trigger_characters();
324
325        assert_eq!(triggers.len(), 4);
326        assert!(triggers.contains(&'.'));
327        assert!(triggers.contains(&':'));
328        assert!(triggers.contains(&'/'));
329        assert!(triggers.contains(&'<'));
330    }
331
332    #[test]
333    fn test_registry_sources_returns_all() {
334        let mut registry = SourceRegistry::new();
335        registry.register(Arc::new(TestSource::new("a")));
336        registry.register(Arc::new(TestSource::new("b")));
337        registry.register(Arc::new(TestSource::new("c")));
338
339        let sources = registry.sources();
340        assert_eq!(sources.len(), 3);
341    }
342
343    #[test]
344    fn test_registry_debug_format() {
345        let mut registry = SourceRegistry::new();
346        registry.register(Arc::new(TestSource::new("test_source")));
347
348        let debug_str = format!("{:?}", registry);
349        assert!(debug_str.contains("SourceRegistry"));
350        assert!(debug_str.contains("test_source"));
351    }
352
353    #[test]
354    fn test_source_default_implementations() {
355        struct MinimalSource;
356
357        impl SourceSupport for MinimalSource {
358            fn source_id(&self) -> &'static str {
359                "minimal"
360            }
361
362            fn complete<'a>(
363                &'a self,
364                _ctx: &'a CompletionContext,
365                _content: &'a str,
366            ) -> Pin<Box<dyn Future<Output = Vec<CompletionItem>> + Send + 'a>> {
367                Box::pin(async move { vec![] })
368            }
369        }
370
371        let source = MinimalSource;
372        let ctx = make_context();
373
374        // Test default implementations
375        assert_eq!(source.priority(), 100);
376        assert!(source.is_available(&ctx));
377        assert!(source.trigger_characters().is_none());
378    }
379
380    #[tokio::test]
381    async fn test_source_resolve_default() {
382        struct SimpleSource;
383
384        impl SourceSupport for SimpleSource {
385            fn source_id(&self) -> &'static str {
386                "simple"
387            }
388
389            fn complete<'a>(
390                &'a self,
391                _ctx: &'a CompletionContext,
392                _content: &'a str,
393            ) -> Pin<Box<dyn Future<Output = Vec<CompletionItem>> + Send + 'a>> {
394                Box::pin(async move { vec![] })
395            }
396        }
397
398        let source = SimpleSource;
399        let item = CompletionItem::new("test", "simple");
400
401        // Default resolve returns the same item
402        let resolved = source.resolve(&item).await;
403        assert_eq!(resolved.label, item.label);
404        assert_eq!(resolved.source, item.source);
405    }
406}