1use std::path::PathBuf;
7
8use crate::config::{ServerId, ToolKind};
9
10fn sanitize_lsp_server_message(message: &str) -> String {
33 if message.contains("Invalid offset LineCol") {
34 "position out of range for this document".to_string()
35 } else {
36 message.to_string()
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct ServerSpawnFailure {
43 pub server_id: ServerId,
45 pub language_id: String,
47 pub command: String,
49 pub message: String,
51}
52
53impl std::fmt::Display for ServerSpawnFailure {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 write!(
56 f,
57 "{} [{}] ({}): {}",
58 self.server_id, self.language_id, self.command, self.message
59 )
60 }
61}
62
63#[derive(Debug, thiserror::Error)]
69#[non_exhaustive]
70pub enum Error {
71 #[error("LSP server initialization failed: {message}")]
73 LspInitFailed {
74 message: String,
76 },
77
78 #[error("LSP server error: {code} - {}", sanitize_lsp_server_message(message))]
80 LspServerError {
81 code: i32,
83 message: String,
88 data: Option<serde_json::Value>,
90 },
91
92 #[error("MCP server error: {0}")]
94 McpServer(String),
95
96 #[error("document not found: {0}")]
98 DocumentNotFound(PathBuf),
99
100 #[error("no LSP server configured for language: {0}")]
102 NoServerForLanguage(String),
103
104 #[error("no server handles tool '{tool}' for language '{language_id}'")]
109 NoServerForTool {
110 language_id: String,
112 tool: ToolKind,
114 },
115
116 #[error(
118 "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)"
119 )]
120 ServerInitializing {
121 server_id: ServerId,
123 },
124
125 #[error(
131 "LSP servers are still initializing (large project load in progress); wait and retry the request (this may take a few minutes on large projects)"
132 )]
133 WorkspaceServersInitializing,
134
135 #[error("no LSP server configured")]
137 NoServerConfigured,
138
139 #[error("no server handles tool '{tool}' (no server's `handles` list or catch-all claims it)")]
144 NoServerForWorkspaceTool {
145 tool: ToolKind,
147 },
148
149 #[error("configuration file not found: {0}")]
151 ConfigNotFound(PathBuf),
152
153 #[error("invalid configuration: {0}")]
155 InvalidConfig(String),
156
157 #[error("I/O error: {0}")]
159 Io(#[from] std::io::Error),
160
161 #[error("JSON error: {0}")]
163 Json(#[from] serde_json::Error),
164
165 #[error("TOML parsing error: {0}")]
167 TomlDe(#[from] toml::de::Error),
168
169 #[error("TOML serialization error: {0}")]
171 TomlSer(#[from] toml::ser::Error),
172
173 #[error("transport error: {0}")]
175 Transport(String),
176
177 #[error("request timed out after {0} seconds")]
179 Timeout(u64),
180
181 #[error("failed to spawn LSP server '{command}': {source}")]
183 ServerSpawnFailed {
184 command: String,
186 #[source]
188 source: std::io::Error,
189 },
190
191 #[error("LSP protocol error: {0}")]
193 LspProtocolError(String),
194
195 #[error("invalid URI: {0}")]
197 InvalidUri(String),
198
199 #[error("LSP server process terminated unexpectedly")]
201 ServerTerminated,
202
203 #[error("LSP server '{server_id}' is unavailable: {reason}")]
210 ServerUnavailable {
211 server_id: ServerId,
213 reason: String,
215 },
216
217 #[error("invalid tool parameters: {0}")]
219 InvalidToolParams(String),
220
221 #[error("file I/O error for {path:?}: {source}")]
223 FileIo {
224 path: PathBuf,
226 #[source]
228 source: std::io::Error,
229 },
230
231 #[error("path outside workspace: {0}")]
233 PathOutsideWorkspace(PathBuf),
234
235 #[error(
237 "document limit exceeded: {current}/{max} (raise workspace.max_documents in config to increase this)"
238 )]
239 DocumentLimitExceeded {
240 current: usize,
242 max: usize,
244 },
245
246 #[error(
248 "file size limit exceeded: {size} bytes, max {max} bytes (raise workspace.max_file_size in config to increase this)"
249 )]
250 FileSizeLimitExceeded {
251 size: u64,
253 max: u64,
255 },
256
257 #[error("all LSP servers failed to initialize ({count} configured)")]
259 AllServersFailedToInit {
260 count: usize,
262 failures: Vec<ServerSpawnFailure>,
264 },
265
266 #[error("{0}")]
268 NoServersAvailable(String),
269
270 #[error("server '{server_id}' does not support capability '{capability}'")]
274 CapabilityNotSupported {
275 server_id: ServerId,
277 capability: &'static str,
279 },
280}
281
282pub type Result<T> = std::result::Result<T, Error>;
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_error_display_lsp_init_failed() {
291 let err = Error::LspInitFailed {
292 message: "server not found".to_string(),
293 };
294 assert_eq!(
295 err.to_string(),
296 "LSP server initialization failed: server not found"
297 );
298 }
299
300 #[test]
301 fn test_error_display_lsp_server_error() {
302 let err = Error::LspServerError {
303 code: -32600,
304 message: "Invalid request".to_string(),
305 data: None,
306 };
307 assert_eq!(
308 err.to_string(),
309 "LSP server error: -32600 - Invalid request"
310 );
311 }
312
313 #[test]
314 fn test_error_display_lsp_server_error_sanitizes_invalid_offset() {
315 let err = Error::LspServerError {
316 code: -32603,
317 message: "Invalid offset LineCol { line: 2291, col: 0 } (line index length: 100417)"
318 .to_string(),
319 data: None,
320 };
321 assert_eq!(
322 err.to_string(),
323 "LSP server error: -32603 - position out of range for this document"
324 );
325 }
326
327 #[test]
328 fn test_error_display_lsp_server_error_sanitizes_wrapped_invalid_offset() {
329 let err = Error::LspServerError {
333 code: -32803,
334 message: "request handler panicked: Invalid offset LineCol { line: 5, col: 0 } \
335 (line index length: 3)"
336 .to_string(),
337 data: None,
338 };
339 assert_eq!(
340 err.to_string(),
341 "LSP server error: -32803 - position out of range for this document"
342 );
343 }
344
345 #[test]
346 fn test_error_display_lsp_server_error_passes_through_unrelated_message() {
347 let err = Error::LspServerError {
348 code: -32602,
349 message: "Invalid params: expected object".to_string(),
350 data: None,
351 };
352 assert_eq!(
353 err.to_string(),
354 "LSP server error: -32602 - Invalid params: expected object"
355 );
356 }
357
358 #[test]
359 fn test_error_display_document_not_found() {
360 let err = Error::DocumentNotFound(PathBuf::from("/path/to/file.rs"));
361 assert!(err.to_string().contains("document not found"));
362 assert!(err.to_string().contains("file.rs"));
363 }
364
365 #[test]
366 fn test_error_display_no_server_for_language() {
367 let err = Error::NoServerForLanguage("rust".to_string());
368 assert_eq!(
369 err.to_string(),
370 "no LSP server configured for language: rust"
371 );
372 }
373
374 #[test]
375 fn test_error_display_workspace_servers_initializing() {
376 let err = Error::WorkspaceServersInitializing;
377 assert!(err.to_string().contains("still initializing"));
378 }
379
380 #[test]
381 fn test_error_display_no_server_for_workspace_tool() {
382 let err = Error::NoServerForWorkspaceTool {
383 tool: crate::config::ToolKind::WorkspaceSymbols,
384 };
385 assert!(err.to_string().contains("workspace_symbols"));
386 assert!(err.to_string().contains("no server's `handles` list"));
387 }
388
389 #[test]
390 fn test_error_display_timeout() {
391 let err = Error::Timeout(30);
392 assert_eq!(err.to_string(), "request timed out after 30 seconds");
393 }
394
395 #[test]
396 fn test_error_display_document_limit() {
397 let err = Error::DocumentLimitExceeded {
398 current: 150,
399 max: 100,
400 };
401 assert_eq!(
402 err.to_string(),
403 "document limit exceeded: 150/100 (raise workspace.max_documents in config to increase this)"
404 );
405 }
406
407 #[test]
408 fn test_error_display_file_size_limit() {
409 let err = Error::FileSizeLimitExceeded {
410 size: 20_000_000,
411 max: 10_000_000,
412 };
413 assert_eq!(
414 err.to_string(),
415 "file size limit exceeded: 20000000 bytes, max 10000000 bytes (raise workspace.max_file_size in config to increase this)"
416 );
417 }
418
419 #[test]
420 fn test_error_from_io() {
421 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
422 let err: Error = io_err.into();
423 assert!(matches!(err, Error::Io(_)));
424 }
425
426 #[test]
427 #[allow(clippy::unwrap_used)]
428 fn test_error_from_json() {
429 let json_str = "{invalid json}";
430 let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
431 let err: Error = json_err.into();
432 assert!(matches!(err, Error::Json(_)));
433 }
434
435 #[test]
436 #[allow(clippy::unwrap_used)]
437 fn test_error_from_toml_de() {
438 let toml_str = "[invalid toml";
439 let toml_err = toml::from_str::<toml::Value>(toml_str).unwrap_err();
440 let err: Error = toml_err.into();
441 assert!(matches!(err, Error::TomlDe(_)));
442 }
443
444 #[test]
445 fn test_result_type_alias() {
446 fn _returns_error() -> Result<i32> {
447 Err(Error::InvalidConfig("test error".to_string()))
448 }
449
450 let result: Result<i32> = Ok(42);
451 assert!(result.is_ok());
452 if let Ok(value) = result {
453 assert_eq!(value, 42);
454 }
455 }
456
457 #[test]
458 fn test_error_source_chain() {
459 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
460 let err = Error::ServerSpawnFailed {
461 command: "rust-analyzer".to_string(),
462 source: io_err,
463 };
464
465 let source = std::error::Error::source(&err);
466 assert!(source.is_some());
467 }
468
469 #[test]
470 fn test_server_spawn_failure_display() {
471 let failure = ServerSpawnFailure {
472 server_id: ServerId::from("rust"),
473 language_id: "rust".to_string(),
474 command: "rust-analyzer".to_string(),
475 message: "No such file or directory".to_string(),
476 };
477 assert_eq!(
478 failure.to_string(),
479 "rust [rust] (rust-analyzer): No such file or directory"
480 );
481 }
482
483 #[test]
484 fn test_server_spawn_failure_debug() {
485 let failure = ServerSpawnFailure {
486 server_id: ServerId::from("python"),
487 language_id: "python".to_string(),
488 command: "pyright".to_string(),
489 message: "command not found".to_string(),
490 };
491 let debug_str = format!("{failure:?}");
492 assert!(debug_str.contains("python"));
493 assert!(debug_str.contains("pyright"));
494 assert!(debug_str.contains("command not found"));
495 }
496
497 #[test]
498 fn test_server_spawn_failure_clone() {
499 let failure = ServerSpawnFailure {
500 server_id: ServerId::from("typescript"),
501 language_id: "typescript".to_string(),
502 command: "tsserver".to_string(),
503 message: "failed to start".to_string(),
504 };
505 let cloned = failure.clone();
506 assert_eq!(failure.language_id, cloned.language_id);
507 assert_eq!(failure.command, cloned.command);
508 assert_eq!(failure.message, cloned.message);
509 }
510
511 #[test]
512 fn test_error_display_all_servers_failed_to_init() {
513 let err = Error::AllServersFailedToInit {
514 count: 2,
515 failures: vec![],
516 };
517 assert_eq!(
518 err.to_string(),
519 "all LSP servers failed to initialize (2 configured)"
520 );
521 }
522
523 #[test]
524 fn test_error_all_servers_failed_with_failures() {
525 let failures = vec![
526 ServerSpawnFailure {
527 server_id: ServerId::from("rust"),
528 language_id: "rust".to_string(),
529 command: "rust-analyzer".to_string(),
530 message: "not found".to_string(),
531 },
532 ServerSpawnFailure {
533 server_id: ServerId::from("python"),
534 language_id: "python".to_string(),
535 command: "pyright".to_string(),
536 message: "permission denied".to_string(),
537 },
538 ];
539
540 let err = Error::AllServersFailedToInit { count: 2, failures };
541
542 assert!(err.to_string().contains("all LSP servers failed"));
543 assert!(err.to_string().contains("2 configured"));
544 }
545
546 #[test]
547 fn test_error_display_no_servers_available() {
548 let err =
549 Error::NoServersAvailable("none configured or all failed to initialize".to_string());
550 assert_eq!(
551 err.to_string(),
552 "none configured or all failed to initialize"
553 );
554 }
555
556 #[test]
557 fn test_error_no_servers_available_with_custom_message() {
558 let custom_msg = "none configured or all failed to initialize";
559 let err = Error::NoServersAvailable(custom_msg.to_string());
560 assert_eq!(err.to_string(), custom_msg);
561 }
562
563 #[test]
564 fn test_error_display_capability_not_supported() {
565 let err = Error::CapabilityNotSupported {
566 server_id: ServerId::from("rust"),
567 capability: "renameProvider",
568 };
569 assert_eq!(
570 err.to_string(),
571 "server 'rust' does not support capability 'renameProvider'"
572 );
573 }
574}