1use std::path::PathBuf;
7
8use crate::config::{ServerId, ToolKind};
9
10#[derive(Debug, Clone)]
12pub struct ServerSpawnFailure {
13 pub server_id: ServerId,
15 pub language_id: String,
17 pub command: String,
19 pub message: String,
21}
22
23impl std::fmt::Display for ServerSpawnFailure {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 write!(
26 f,
27 "{} [{}] ({}): {}",
28 self.server_id, self.language_id, self.command, self.message
29 )
30 }
31}
32
33#[derive(Debug, thiserror::Error)]
39#[non_exhaustive]
40pub enum Error {
41 #[error("LSP server initialization failed: {message}")]
43 LspInitFailed {
44 message: String,
46 },
47
48 #[error("LSP server error: {code} - {message}")]
50 LspServerError {
51 code: i32,
53 message: String,
55 data: Option<serde_json::Value>,
57 },
58
59 #[error("MCP server error: {0}")]
61 McpServer(String),
62
63 #[error("document not found: {0}")]
65 DocumentNotFound(PathBuf),
66
67 #[error("no LSP server configured for language: {0}")]
69 NoServerForLanguage(String),
70
71 #[error("no server handles tool '{tool}' for language '{language_id}'")]
76 NoServerForTool {
77 language_id: String,
79 tool: ToolKind,
81 },
82
83 #[error(
85 "LSP server '{server_id}' is still initializing (large project load in progress); wait and retry the request (this may take a few minutes on large projects)"
86 )]
87 ServerInitializing {
88 server_id: ServerId,
90 },
91
92 #[error(
98 "LSP servers are still initializing (large project load in progress); wait and retry the request (this may take a few minutes on large projects)"
99 )]
100 WorkspaceServersInitializing,
101
102 #[error("no LSP server configured")]
104 NoServerConfigured,
105
106 #[error("no server handles tool '{tool}' (no server's `handles` list or catch-all claims it)")]
111 NoServerForWorkspaceTool {
112 tool: ToolKind,
114 },
115
116 #[error("configuration error: {0}")]
118 Config(String),
119
120 #[error("configuration file not found: {0}")]
122 ConfigNotFound(PathBuf),
123
124 #[error("invalid configuration: {0}")]
126 InvalidConfig(String),
127
128 #[error("I/O error: {0}")]
130 Io(#[from] std::io::Error),
131
132 #[error("JSON error: {0}")]
134 Json(#[from] serde_json::Error),
135
136 #[error("TOML parsing error: {0}")]
138 TomlDe(#[from] toml::de::Error),
139
140 #[error("TOML serialization error: {0}")]
142 TomlSer(#[from] toml::ser::Error),
143
144 #[error("transport error: {0}")]
146 Transport(String),
147
148 #[error("request timed out after {0} seconds")]
150 Timeout(u64),
151
152 #[error("server shutdown requested")]
154 Shutdown,
155
156 #[error("failed to spawn LSP server '{command}': {source}")]
158 ServerSpawnFailed {
159 command: String,
161 #[source]
163 source: std::io::Error,
164 },
165
166 #[error("LSP protocol error: {0}")]
168 LspProtocolError(String),
169
170 #[error("invalid URI: {0}")]
172 InvalidUri(String),
173
174 #[error("position encoding error: {0}")]
176 EncodingError(String),
177
178 #[error("LSP server process terminated unexpectedly")]
180 ServerTerminated,
181
182 #[error("LSP server '{server_id}' is unavailable: {reason}")]
189 ServerUnavailable {
190 server_id: ServerId,
192 reason: String,
194 },
195
196 #[error("invalid tool parameters: {0}")]
198 InvalidToolParams(String),
199
200 #[error("file I/O error for {path:?}: {source}")]
202 FileIo {
203 path: PathBuf,
205 #[source]
207 source: std::io::Error,
208 },
209
210 #[error("path outside workspace: {0}")]
212 PathOutsideWorkspace(PathBuf),
213
214 #[error(
216 "document limit exceeded: {current}/{max} (raise workspace.max_documents in config to increase this)"
217 )]
218 DocumentLimitExceeded {
219 current: usize,
221 max: usize,
223 },
224
225 #[error(
227 "file size limit exceeded: {size} bytes, max {max} bytes (raise workspace.max_file_size in config to increase this)"
228 )]
229 FileSizeLimitExceeded {
230 size: u64,
232 max: u64,
234 },
235
236 #[error("some LSP servers failed to initialize: {failed_count}/{total_count} servers")]
238 PartialServerInit {
239 failed_count: usize,
241 total_count: usize,
243 failures: Vec<ServerSpawnFailure>,
245 },
246
247 #[error("all LSP servers failed to initialize ({count} configured)")]
249 AllServersFailedToInit {
250 count: usize,
252 failures: Vec<ServerSpawnFailure>,
254 },
255
256 #[error("{0}")]
258 NoServersAvailable(String),
259
260 #[error("server '{server_id}' does not support capability '{capability}'")]
264 CapabilityNotSupported {
265 server_id: ServerId,
267 capability: &'static str,
269 },
270}
271
272pub type Result<T> = std::result::Result<T, Error>;
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn test_error_display_lsp_init_failed() {
281 let err = Error::LspInitFailed {
282 message: "server not found".to_string(),
283 };
284 assert_eq!(
285 err.to_string(),
286 "LSP server initialization failed: server not found"
287 );
288 }
289
290 #[test]
291 fn test_error_display_lsp_server_error() {
292 let err = Error::LspServerError {
293 code: -32600,
294 message: "Invalid request".to_string(),
295 data: None,
296 };
297 assert_eq!(
298 err.to_string(),
299 "LSP server error: -32600 - Invalid request"
300 );
301 }
302
303 #[test]
304 fn test_error_display_document_not_found() {
305 let err = Error::DocumentNotFound(PathBuf::from("/path/to/file.rs"));
306 assert!(err.to_string().contains("document not found"));
307 assert!(err.to_string().contains("file.rs"));
308 }
309
310 #[test]
311 fn test_error_display_no_server_for_language() {
312 let err = Error::NoServerForLanguage("rust".to_string());
313 assert_eq!(
314 err.to_string(),
315 "no LSP server configured for language: rust"
316 );
317 }
318
319 #[test]
320 fn test_error_display_workspace_servers_initializing() {
321 let err = Error::WorkspaceServersInitializing;
322 assert!(err.to_string().contains("still initializing"));
323 }
324
325 #[test]
326 fn test_error_display_no_server_for_workspace_tool() {
327 let err = Error::NoServerForWorkspaceTool {
328 tool: crate::config::ToolKind::WorkspaceSymbols,
329 };
330 assert!(err.to_string().contains("workspace_symbols"));
331 assert!(err.to_string().contains("no server's `handles` list"));
332 }
333
334 #[test]
335 fn test_error_display_timeout() {
336 let err = Error::Timeout(30);
337 assert_eq!(err.to_string(), "request timed out after 30 seconds");
338 }
339
340 #[test]
341 fn test_error_display_document_limit() {
342 let err = Error::DocumentLimitExceeded {
343 current: 150,
344 max: 100,
345 };
346 assert_eq!(
347 err.to_string(),
348 "document limit exceeded: 150/100 (raise workspace.max_documents in config to increase this)"
349 );
350 }
351
352 #[test]
353 fn test_error_display_file_size_limit() {
354 let err = Error::FileSizeLimitExceeded {
355 size: 20_000_000,
356 max: 10_000_000,
357 };
358 assert_eq!(
359 err.to_string(),
360 "file size limit exceeded: 20000000 bytes, max 10000000 bytes (raise workspace.max_file_size in config to increase this)"
361 );
362 }
363
364 #[test]
365 fn test_error_from_io() {
366 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
367 let err: Error = io_err.into();
368 assert!(matches!(err, Error::Io(_)));
369 }
370
371 #[test]
372 #[allow(clippy::unwrap_used)]
373 fn test_error_from_json() {
374 let json_str = "{invalid json}";
375 let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
376 let err: Error = json_err.into();
377 assert!(matches!(err, Error::Json(_)));
378 }
379
380 #[test]
381 #[allow(clippy::unwrap_used)]
382 fn test_error_from_toml_de() {
383 let toml_str = "[invalid toml";
384 let toml_err = toml::from_str::<toml::Value>(toml_str).unwrap_err();
385 let err: Error = toml_err.into();
386 assert!(matches!(err, Error::TomlDe(_)));
387 }
388
389 #[test]
390 fn test_result_type_alias() {
391 fn _returns_error() -> Result<i32> {
392 Err(Error::Config("test error".to_string()))
393 }
394
395 let result: Result<i32> = Ok(42);
396 assert!(result.is_ok());
397 if let Ok(value) = result {
398 assert_eq!(value, 42);
399 }
400 }
401
402 #[test]
403 fn test_error_source_chain() {
404 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
405 let err = Error::ServerSpawnFailed {
406 command: "rust-analyzer".to_string(),
407 source: io_err,
408 };
409
410 let source = std::error::Error::source(&err);
411 assert!(source.is_some());
412 }
413
414 #[test]
415 fn test_server_spawn_failure_display() {
416 let failure = ServerSpawnFailure {
417 server_id: ServerId::from("rust"),
418 language_id: "rust".to_string(),
419 command: "rust-analyzer".to_string(),
420 message: "No such file or directory".to_string(),
421 };
422 assert_eq!(
423 failure.to_string(),
424 "rust [rust] (rust-analyzer): No such file or directory"
425 );
426 }
427
428 #[test]
429 fn test_server_spawn_failure_debug() {
430 let failure = ServerSpawnFailure {
431 server_id: ServerId::from("python"),
432 language_id: "python".to_string(),
433 command: "pyright".to_string(),
434 message: "command not found".to_string(),
435 };
436 let debug_str = format!("{failure:?}");
437 assert!(debug_str.contains("python"));
438 assert!(debug_str.contains("pyright"));
439 assert!(debug_str.contains("command not found"));
440 }
441
442 #[test]
443 fn test_server_spawn_failure_clone() {
444 let failure = ServerSpawnFailure {
445 server_id: ServerId::from("typescript"),
446 language_id: "typescript".to_string(),
447 command: "tsserver".to_string(),
448 message: "failed to start".to_string(),
449 };
450 let cloned = failure.clone();
451 assert_eq!(failure.language_id, cloned.language_id);
452 assert_eq!(failure.command, cloned.command);
453 assert_eq!(failure.message, cloned.message);
454 }
455
456 #[test]
457 fn test_error_display_partial_server_init() {
458 let err = Error::PartialServerInit {
459 failed_count: 2,
460 total_count: 3,
461 failures: vec![],
462 };
463 assert_eq!(
464 err.to_string(),
465 "some LSP servers failed to initialize: 2/3 servers"
466 );
467 }
468
469 #[test]
470 fn test_error_display_all_servers_failed_to_init() {
471 let err = Error::AllServersFailedToInit {
472 count: 2,
473 failures: vec![],
474 };
475 assert_eq!(
476 err.to_string(),
477 "all LSP servers failed to initialize (2 configured)"
478 );
479 }
480
481 #[test]
482 fn test_error_all_servers_failed_with_failures() {
483 let failures = vec![
484 ServerSpawnFailure {
485 server_id: ServerId::from("rust"),
486 language_id: "rust".to_string(),
487 command: "rust-analyzer".to_string(),
488 message: "not found".to_string(),
489 },
490 ServerSpawnFailure {
491 server_id: ServerId::from("python"),
492 language_id: "python".to_string(),
493 command: "pyright".to_string(),
494 message: "permission denied".to_string(),
495 },
496 ];
497
498 let err = Error::AllServersFailedToInit { count: 2, failures };
499
500 assert!(err.to_string().contains("all LSP servers failed"));
501 assert!(err.to_string().contains("2 configured"));
502 }
503
504 #[test]
505 fn test_error_partial_server_init_with_failures() {
506 let failures = vec![ServerSpawnFailure {
507 server_id: ServerId::from("python"),
508 language_id: "python".to_string(),
509 command: "pyright".to_string(),
510 message: "not found".to_string(),
511 }];
512
513 let err = Error::PartialServerInit {
514 failed_count: 1,
515 total_count: 2,
516 failures,
517 };
518
519 assert!(err.to_string().contains("some LSP servers failed"));
520 assert!(err.to_string().contains("1/2"));
521 }
522
523 #[test]
524 fn test_error_display_no_servers_available() {
525 let err =
526 Error::NoServersAvailable("none configured or all failed to initialize".to_string());
527 assert_eq!(
528 err.to_string(),
529 "none configured or all failed to initialize"
530 );
531 }
532
533 #[test]
534 fn test_error_no_servers_available_with_custom_message() {
535 let custom_msg = "none configured or all failed to initialize";
536 let err = Error::NoServersAvailable(custom_msg.to_string());
537 assert_eq!(err.to_string(), custom_msg);
538 }
539
540 #[test]
541 fn test_error_display_capability_not_supported() {
542 let err = Error::CapabilityNotSupported {
543 server_id: ServerId::from("rust"),
544 capability: "renameProvider",
545 };
546 assert_eq!(
547 err.to_string(),
548 "server 'rust' does not support capability 'renameProvider'"
549 );
550 }
551}