1use crate::error::{IdeError, IdeResult};
7use std::fmt;
8use tracing::{debug, warn, error};
9
10#[derive(Debug, Clone)]
12pub struct ProviderErrorContext {
13 pub language: String,
15 pub operation: String,
17 pub provider_name: String,
19 pub error_message: String,
21 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
35pub struct ProviderErrorHandler;
37
38impl ProviderErrorHandler {
39 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 pub fn handle_config_error(error: &IdeError) -> String {
63 warn!("Configuration error: {}", error);
64
65 error.to_string()
67 }
68
69 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum RecoveryStrategy {
214 Retry,
216 Fallback,
218 GenericFallback,
220 Fail,
222}
223
224impl RecoveryStrategy {
225 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 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}