ricecoder_ide/
generic_provider.rs

1//! Generic text-based provider implementation
2//!
3//! This module implements the IdeProvider trait for generic text-based IDE features
4//! that work for any language. This is the fallback provider when no language-specific
5//! provider is available.
6
7use crate::error::IdeResult;
8use crate::provider::IdeProvider;
9use crate::types::*;
10use async_trait::async_trait;
11use std::collections::HashSet;
12use tracing::debug;
13
14/// Generic text-based provider for any language
15pub struct GenericProvider;
16
17impl Default for GenericProvider {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl GenericProvider {
24    /// Create a new generic provider
25    pub fn new() -> Self {
26        GenericProvider
27    }
28
29    /// Extract words from context
30    fn extract_words(text: &str) -> Vec<String> {
31        text.split(|c: char| !c.is_alphanumeric() && c != '_')
32            .filter(|w| !w.is_empty())
33            .map(|w| w.to_string())
34            .collect()
35    }
36
37    /// Get unique words from text
38    fn get_unique_words(text: &str) -> Vec<String> {
39        let words = Self::extract_words(text);
40        let mut unique: Vec<String> = words.into_iter().collect::<HashSet<_>>().into_iter().collect();
41        unique.sort();
42        unique
43    }
44
45    /// Check if text looks like a syntax error
46    fn has_syntax_error(source: &str) -> bool {
47        let open_braces = source.matches('{').count();
48        let close_braces = source.matches('}').count();
49        let open_parens = source.matches('(').count();
50        let close_parens = source.matches(')').count();
51        let open_brackets = source.matches('[').count();
52        let close_brackets = source.matches(']').count();
53
54        open_braces != close_braces || open_parens != close_parens || open_brackets != close_brackets
55    }
56}
57
58#[async_trait]
59impl IdeProvider for GenericProvider {
60    async fn get_completions(&self, params: &CompletionParams) -> IdeResult<Vec<CompletionItem>> {
61        debug!("Getting completions from generic text-based provider");
62
63        let mut completions = Vec::new();
64
65        // Extract words from context and suggest them as completions
66        let words = Self::get_unique_words(&params.context);
67
68        for word in words.iter().take(10) {
69            // Limit to 10 suggestions
70            completions.push(CompletionItem {
71                label: word.clone(),
72                kind: CompletionItemKind::Text,
73                detail: Some("word suggestion".to_string()),
74                documentation: None,
75                insert_text: word.clone(),
76            });
77        }
78
79        Ok(completions)
80    }
81
82    async fn get_diagnostics(&self, params: &DiagnosticsParams) -> IdeResult<Vec<Diagnostic>> {
83        debug!("Getting diagnostics from generic text-based provider");
84
85        let mut diagnostics = Vec::new();
86
87        // Check for basic syntax errors
88        if Self::has_syntax_error(&params.source) {
89            diagnostics.push(Diagnostic {
90                range: Range {
91                    start: Position {
92                        line: 0,
93                        character: 0,
94                    },
95                    end: Position {
96                        line: 0,
97                        character: 10,
98                    },
99                },
100                severity: DiagnosticSeverity::Error,
101                message: "Mismatched brackets or parentheses".to_string(),
102                source: "generic".to_string(),
103            });
104        }
105
106        // Check for trailing whitespace
107        for (line_num, line) in params.source.lines().enumerate() {
108            if line.ends_with(' ') || line.ends_with('\t') {
109                diagnostics.push(Diagnostic {
110                    range: Range {
111                        start: Position {
112                            line: line_num as u32,
113                            character: (line.len() - 1) as u32,
114                        },
115                        end: Position {
116                            line: line_num as u32,
117                            character: line.len() as u32,
118                        },
119                    },
120                    severity: DiagnosticSeverity::Hint,
121                    message: "Trailing whitespace".to_string(),
122                    source: "generic".to_string(),
123                });
124            }
125        }
126
127        Ok(diagnostics)
128    }
129
130    async fn get_hover(&self, _params: &HoverParams) -> IdeResult<Option<Hover>> {
131        debug!("Getting hover from generic text-based provider");
132        // Generic provider doesn't provide hover information
133        Ok(None)
134    }
135
136    async fn get_definition(&self, _params: &DefinitionParams) -> IdeResult<Option<Location>> {
137        debug!("Getting definition from generic text-based provider");
138        // Generic provider doesn't provide definition information
139        Ok(None)
140    }
141
142    fn is_available(&self, _language: &str) -> bool {
143        // Generic provider is always available
144        true
145    }
146
147    fn name(&self) -> &str {
148        "generic"
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn test_extract_words() {
158        let text = "hello world test_var";
159        let words = GenericProvider::extract_words(text);
160        assert_eq!(words.len(), 3);
161        assert!(words.contains(&"hello".to_string()));
162        assert!(words.contains(&"world".to_string()));
163        assert!(words.contains(&"test_var".to_string()));
164    }
165
166    #[test]
167    fn test_get_unique_words() {
168        let text = "hello world hello test";
169        let words = GenericProvider::get_unique_words(text);
170        assert_eq!(words.len(), 3);
171        assert!(words.contains(&"hello".to_string()));
172        assert!(words.contains(&"world".to_string()));
173        assert!(words.contains(&"test".to_string()));
174    }
175
176    #[test]
177    fn test_has_syntax_error_balanced() {
178        let source = "{ ( [ ] ) }";
179        assert!(!GenericProvider::has_syntax_error(source));
180    }
181
182    #[test]
183    fn test_has_syntax_error_unbalanced_braces() {
184        let source = "{ ( [ ] ) ";
185        assert!(GenericProvider::has_syntax_error(source));
186    }
187
188    #[test]
189    fn test_has_syntax_error_unbalanced_parens() {
190        let source = "{ ( [ ] }";
191        assert!(GenericProvider::has_syntax_error(source));
192    }
193
194    #[tokio::test]
195    async fn test_generic_provider_completions() {
196        let provider = GenericProvider;
197        let params = CompletionParams {
198            language: "unknown".to_string(),
199            file_path: "file.txt".to_string(),
200            position: Position {
201                line: 0,
202                character: 0,
203            },
204            context: "hello world test".to_string(),
205        };
206
207        let result = provider.get_completions(&params).await;
208        assert!(result.is_ok());
209        let completions = result.unwrap();
210        assert!(!completions.is_empty());
211        assert!(completions.iter().any(|c| c.label == "hello"));
212    }
213
214    #[tokio::test]
215    async fn test_generic_provider_diagnostics_syntax_error() {
216        let provider = GenericProvider;
217        let params = DiagnosticsParams {
218            language: "unknown".to_string(),
219            file_path: "file.txt".to_string(),
220            source: "{ ( [ ] ) ".to_string(),
221        };
222
223        let result = provider.get_diagnostics(&params).await;
224        assert!(result.is_ok());
225        let diagnostics = result.unwrap();
226        assert!(!diagnostics.is_empty());
227        assert!(diagnostics[0].severity == DiagnosticSeverity::Error);
228    }
229
230    #[tokio::test]
231    async fn test_generic_provider_diagnostics_trailing_whitespace() {
232        let provider = GenericProvider;
233        let params = DiagnosticsParams {
234            language: "unknown".to_string(),
235            file_path: "file.txt".to_string(),
236            source: "hello world  \ntest".to_string(),
237        };
238
239        let result = provider.get_diagnostics(&params).await;
240        assert!(result.is_ok());
241        let diagnostics = result.unwrap();
242        assert!(!diagnostics.is_empty());
243        assert!(diagnostics.iter().any(|d| d.message.contains("Trailing whitespace")));
244    }
245
246    #[tokio::test]
247    async fn test_generic_provider_is_available() {
248        let provider = GenericProvider;
249        assert!(provider.is_available("rust"));
250        assert!(provider.is_available("typescript"));
251        assert!(provider.is_available("unknown"));
252    }
253
254    #[tokio::test]
255    async fn test_generic_provider_hover() {
256        let provider = GenericProvider;
257        let params = HoverParams {
258            language: "unknown".to_string(),
259            file_path: "file.txt".to_string(),
260            position: Position {
261                line: 0,
262                character: 0,
263            },
264        };
265
266        let result = provider.get_hover(&params).await;
267        assert!(result.is_ok());
268        assert!(result.unwrap().is_none());
269    }
270
271    #[tokio::test]
272    async fn test_generic_provider_definition() {
273        let provider = GenericProvider;
274        let params = DefinitionParams {
275            language: "unknown".to_string(),
276            file_path: "file.txt".to_string(),
277            position: Position {
278                line: 0,
279                character: 0,
280            },
281        };
282
283        let result = provider.get_definition(&params).await;
284        assert!(result.is_ok());
285        assert!(result.unwrap().is_none());
286    }
287}