ricecoder_ide/
provider_error_handling.rs

1//! Error handling for provider chain
2//!
3//! This module provides comprehensive error handling for the provider chain,
4//! including graceful fallback, error recovery, and clear error messages.
5
6use crate::error::{IdeError, IdeResult};
7use std::fmt;
8use tracing::{debug, warn, error};
9
10/// Provider chain error context
11#[derive(Debug, Clone)]
12pub struct ProviderErrorContext {
13    /// The language being processed
14    pub language: String,
15    /// The operation being performed (e.g., "completion", "diagnostics")
16    pub operation: String,
17    /// The provider that failed
18    pub provider_name: String,
19    /// The underlying error message
20    pub error_message: String,
21    /// Whether this is a recoverable error
22    pub is_recoverable: bool,
23}
24
25impl fmt::Display for ProviderErrorContext {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        write!(
28            f,
29            "Provider '{}' failed for {} operation on language '{}': {}",
30            self.provider_name, self.operation, self.language, self.error_message
31        )
32    }
33}
34
35/// Provider chain error handler
36pub struct ProviderErrorHandler;
37
38impl ProviderErrorHandler {
39    /// Handle LSP server failure with graceful fallback
40    pub fn handle_lsp_failure(
41        language: &str,
42        operation: &str,
43        error: &IdeError,
44    ) -> IdeResult<ProviderErrorContext> {
45        debug!(
46            "LSP server failed for {} operation on language '{}': {}",
47            operation, language, error
48        );
49
50        let context = ProviderErrorContext {
51            language: language.to_string(),
52            operation: operation.to_string(),
53            provider_name: "external_lsp".to_string(),
54            error_message: error.to_string(),
55            is_recoverable: true,
56        };
57
58        Ok(context)
59    }
60
61    /// Handle configuration error with remediation
62    pub fn handle_config_error(error: &IdeError) -> String {
63        warn!("Configuration error: {}", error);
64
65        // Configuration errors are already detailed in the error message
66        error.to_string()
67    }
68
69    /// Handle IDE communication error with retry logic
70    pub fn handle_communication_error(
71        ide_type: &str,
72        error: &IdeError,
73        retry_count: u32,
74        max_retries: u32,
75    ) -> IdeResult<()> {
76        if retry_count < max_retries {
77            debug!(
78                "IDE communication error for '{}' (retry {}/{}): {}",
79                ide_type, retry_count, max_retries, error
80            );
81            Ok(())
82        } else {
83            error!(
84                "IDE communication error for '{}' after {} retries: {}",
85                ide_type, max_retries, error
86            );
87            Err(IdeError::communication_error(format!(
88                "Failed to communicate with {} IDE after {} retries: {}",
89                ide_type, max_retries, error
90            )))
91        }
92    }
93
94    /// Handle timeout error with suggestions
95    pub fn handle_timeout_error(
96        language: &str,
97        operation: &str,
98        timeout_ms: u64,
99    ) -> IdeError {
100        warn!(
101            "Timeout for {} operation on language '{}' after {}ms",
102            operation, language, timeout_ms
103        );
104
105        IdeError::timeout(timeout_ms)
106    }
107
108    /// Create a fallback suggestion based on error context
109    pub fn create_fallback_suggestion(context: &ProviderErrorContext) -> String {
110        format!(
111            "Provider '{}' failed for {} operation on language '{}'. \
112             Falling back to next available provider in the chain. \
113             If all providers fail, generic text-based features will be used.",
114            context.provider_name, context.operation, context.language
115        )
116    }
117
118    /// Create a recovery suggestion based on error type
119    pub fn create_recovery_suggestion(error: &IdeError) -> String {
120        match error {
121            IdeError::LspError(msg) => {
122                format!(
123                    "LSP server error: {}. \
124                     Recovery steps:\n\
125                     1. Check if the LSP server is installed and running\n\
126                     2. Verify the LSP server command in your configuration\n\
127                     3. Check the LSP server logs for more details\n\
128                     4. Try restarting the LSP server",
129                    msg
130                )
131            }
132            IdeError::ConfigError(msg) => {
133                format!(
134                    "Configuration error: {}. \
135                     Recovery steps:\n\
136                     1. Check your configuration file for syntax errors\n\
137                     2. Verify all required fields are present\n\
138                     3. Check the configuration documentation\n\
139                     4. Try using the default configuration",
140                    msg
141                )
142            }
143            IdeError::ConfigValidationError(msg) => {
144                format!(
145                    "Configuration validation error: {}. \
146                     Recovery steps:\n\
147                     1. Review the validation error message\n\
148                     2. Follow the remediation steps provided\n\
149                     3. Verify your configuration against the schema\n\
150                     4. Check the configuration documentation",
151                    msg
152                )
153            }
154            IdeError::PathResolutionError(msg) => {
155                format!(
156                    "Path resolution error: {}. \
157                     Recovery steps:\n\
158                     1. Check that the path exists\n\
159                     2. Verify the path is readable\n\
160                     3. Use absolute paths instead of relative paths\n\
161                     4. Check for permission issues",
162                    msg
163                )
164            }
165            IdeError::CommunicationError(msg) => {
166                format!(
167                    "IDE communication error: {}. \
168                     Recovery steps:\n\
169                     1. Check that the IDE is running\n\
170                     2. Verify the IDE is connected to ricecoder\n\
171                     3. Check the IDE logs for errors\n\
172                     4. Try restarting the IDE",
173                    msg
174                )
175            }
176            IdeError::Timeout(ms) => {
177                format!(
178                    "Operation timeout after {}ms. \
179                     Recovery steps:\n\
180                     1. Increase the timeout value in your configuration\n\
181                     2. Check system resources (CPU, memory)\n\
182                     3. Check network connectivity\n\
183                     4. Try the operation again",
184                    ms
185                )
186            }
187            _ => {
188                format!(
189                    "Error: {}. \
190                     Recovery steps:\n\
191                     1. Check the error message for details\n\
192                     2. Review the logs for more information\n\
193                     3. Try the operation again\n\
194                     4. Contact support if the issue persists",
195                    error
196                )
197            }
198        }
199    }
200
201    /// Log error with context
202    pub fn log_error_with_context(context: &ProviderErrorContext) {
203        if context.is_recoverable {
204            debug!("Recoverable error: {}", context);
205        } else {
206            error!("Non-recoverable error: {}", context);
207        }
208    }
209}
210
211/// Provider chain error recovery strategy
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum RecoveryStrategy {
214    /// Retry the operation with the same provider
215    Retry,
216    /// Fall back to the next provider in the chain
217    Fallback,
218    /// Use generic fallback provider
219    GenericFallback,
220    /// Fail and return error to caller
221    Fail,
222}
223
224impl RecoveryStrategy {
225    /// Determine recovery strategy based on error type
226    pub fn from_error(error: &IdeError) -> Self {
227        match error {
228            IdeError::Timeout(_) => RecoveryStrategy::Retry,
229            IdeError::LspError(_) => RecoveryStrategy::Fallback,
230            IdeError::ProviderError(_) => RecoveryStrategy::Fallback,
231            IdeError::CommunicationError(_) => RecoveryStrategy::Retry,
232            IdeError::ConfigError(_) => RecoveryStrategy::Fail,
233            IdeError::ConfigValidationError(_) => RecoveryStrategy::Fail,
234            IdeError::PathResolutionError(_) => RecoveryStrategy::Fail,
235            _ => RecoveryStrategy::Fallback,
236        }
237    }
238
239    /// Get description of recovery strategy
240    pub fn description(&self) -> &'static str {
241        match self {
242            RecoveryStrategy::Retry => "Retrying operation with same provider",
243            RecoveryStrategy::Fallback => "Falling back to next provider in chain",
244            RecoveryStrategy::GenericFallback => "Using generic fallback provider",
245            RecoveryStrategy::Fail => "Failing and returning error to caller",
246        }
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_provider_error_context_display() {
256        let context = ProviderErrorContext {
257            language: "rust".to_string(),
258            operation: "completion".to_string(),
259            provider_name: "external_lsp".to_string(),
260            error_message: "LSP server not found".to_string(),
261            is_recoverable: true,
262        };
263
264        let display = context.to_string();
265        assert!(display.contains("external_lsp"));
266        assert!(display.contains("completion"));
267        assert!(display.contains("rust"));
268        assert!(display.contains("LSP server not found"));
269    }
270
271    #[test]
272    fn test_recovery_strategy_from_timeout_error() {
273        let error = IdeError::timeout(5000);
274        let strategy = RecoveryStrategy::from_error(&error);
275        assert_eq!(strategy, RecoveryStrategy::Retry);
276    }
277
278    #[test]
279    fn test_recovery_strategy_from_lsp_error() {
280        let error = IdeError::lsp_error("Server not found");
281        let strategy = RecoveryStrategy::from_error(&error);
282        assert_eq!(strategy, RecoveryStrategy::Fallback);
283    }
284
285    #[test]
286    fn test_recovery_strategy_from_config_error() {
287        let error = IdeError::config_error("Invalid configuration");
288        let strategy = RecoveryStrategy::from_error(&error);
289        assert_eq!(strategy, RecoveryStrategy::Fail);
290    }
291
292    #[test]
293    fn test_recovery_strategy_from_communication_error() {
294        let error = IdeError::communication_error("Connection lost");
295        let strategy = RecoveryStrategy::from_error(&error);
296        assert_eq!(strategy, RecoveryStrategy::Retry);
297    }
298
299    #[test]
300    fn test_recovery_strategy_description() {
301        assert_eq!(
302            RecoveryStrategy::Retry.description(),
303            "Retrying operation with same provider"
304        );
305        assert_eq!(
306            RecoveryStrategy::Fallback.description(),
307            "Falling back to next provider in chain"
308        );
309        assert_eq!(
310            RecoveryStrategy::GenericFallback.description(),
311            "Using generic fallback provider"
312        );
313        assert_eq!(
314            RecoveryStrategy::Fail.description(),
315            "Failing and returning error to caller"
316        );
317    }
318
319    #[test]
320    fn test_fallback_suggestion() {
321        let context = ProviderErrorContext {
322            language: "rust".to_string(),
323            operation: "completion".to_string(),
324            provider_name: "external_lsp".to_string(),
325            error_message: "Server error".to_string(),
326            is_recoverable: true,
327        };
328
329        let suggestion = ProviderErrorHandler::create_fallback_suggestion(&context);
330        assert!(suggestion.contains("external_lsp"));
331        assert!(suggestion.contains("completion"));
332        assert!(suggestion.contains("Falling back"));
333    }
334
335    #[test]
336    fn test_recovery_suggestion_for_lsp_error() {
337        let error = IdeError::lsp_error("Server not found");
338        let suggestion = ProviderErrorHandler::create_recovery_suggestion(&error);
339        assert!(suggestion.contains("LSP server error"));
340        assert!(suggestion.contains("Recovery steps"));
341        assert!(suggestion.contains("installed"));
342    }
343
344    #[test]
345    fn test_recovery_suggestion_for_config_error() {
346        let error = IdeError::config_error("Invalid YAML");
347        let suggestion = ProviderErrorHandler::create_recovery_suggestion(&error);
348        assert!(suggestion.contains("Configuration error"));
349        assert!(suggestion.contains("Recovery steps"));
350        assert!(suggestion.contains("syntax errors"));
351    }
352
353    #[test]
354    fn test_recovery_suggestion_for_timeout() {
355        let error = IdeError::timeout(5000);
356        let suggestion = ProviderErrorHandler::create_recovery_suggestion(&error);
357        assert!(suggestion.contains("timeout"));
358        assert!(suggestion.contains("5000"));
359        assert!(suggestion.contains("Recovery steps"));
360    }
361}