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("no LSP server configured")]
94 NoServerConfigured,
95
96 #[error("configuration error: {0}")]
98 Config(String),
99
100 #[error("configuration file not found: {0}")]
102 ConfigNotFound(PathBuf),
103
104 #[error("invalid configuration: {0}")]
106 InvalidConfig(String),
107
108 #[error("I/O error: {0}")]
110 Io(#[from] std::io::Error),
111
112 #[error("JSON error: {0}")]
114 Json(#[from] serde_json::Error),
115
116 #[error("TOML parsing error: {0}")]
118 TomlDe(#[from] toml::de::Error),
119
120 #[error("TOML serialization error: {0}")]
122 TomlSer(#[from] toml::ser::Error),
123
124 #[error("transport error: {0}")]
126 Transport(String),
127
128 #[error("request timed out after {0} seconds")]
130 Timeout(u64),
131
132 #[error("server shutdown requested")]
134 Shutdown,
135
136 #[error("failed to spawn LSP server '{command}': {source}")]
138 ServerSpawnFailed {
139 command: String,
141 #[source]
143 source: std::io::Error,
144 },
145
146 #[error("LSP protocol error: {0}")]
148 LspProtocolError(String),
149
150 #[error("invalid URI: {0}")]
152 InvalidUri(String),
153
154 #[error("position encoding error: {0}")]
156 EncodingError(String),
157
158 #[error("LSP server process terminated unexpectedly")]
160 ServerTerminated,
161
162 #[error("invalid tool parameters: {0}")]
164 InvalidToolParams(String),
165
166 #[error("file I/O error for {path:?}: {source}")]
168 FileIo {
169 path: PathBuf,
171 #[source]
173 source: std::io::Error,
174 },
175
176 #[error("path outside workspace: {0}")]
178 PathOutsideWorkspace(PathBuf),
179
180 #[error("document limit exceeded: {current}/{max}")]
182 DocumentLimitExceeded {
183 current: usize,
185 max: usize,
187 },
188
189 #[error("file size limit exceeded: {size} bytes (max: {max} bytes)")]
191 FileSizeLimitExceeded {
192 size: u64,
194 max: u64,
196 },
197
198 #[error("some LSP servers failed to initialize: {failed_count}/{total_count} servers")]
200 PartialServerInit {
201 failed_count: usize,
203 total_count: usize,
205 failures: Vec<ServerSpawnFailure>,
207 },
208
209 #[error("all LSP servers failed to initialize ({count} configured)")]
211 AllServersFailedToInit {
212 count: usize,
214 failures: Vec<ServerSpawnFailure>,
216 },
217
218 #[error("{0}")]
220 NoServersAvailable(String),
221}
222
223pub type Result<T> = std::result::Result<T, Error>;
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_error_display_lsp_init_failed() {
232 let err = Error::LspInitFailed {
233 message: "server not found".to_string(),
234 };
235 assert_eq!(
236 err.to_string(),
237 "LSP server initialization failed: server not found"
238 );
239 }
240
241 #[test]
242 fn test_error_display_lsp_server_error() {
243 let err = Error::LspServerError {
244 code: -32600,
245 message: "Invalid request".to_string(),
246 data: None,
247 };
248 assert_eq!(
249 err.to_string(),
250 "LSP server error: -32600 - Invalid request"
251 );
252 }
253
254 #[test]
255 fn test_error_display_document_not_found() {
256 let err = Error::DocumentNotFound(PathBuf::from("/path/to/file.rs"));
257 assert!(err.to_string().contains("document not found"));
258 assert!(err.to_string().contains("file.rs"));
259 }
260
261 #[test]
262 fn test_error_display_no_server_for_language() {
263 let err = Error::NoServerForLanguage("rust".to_string());
264 assert_eq!(
265 err.to_string(),
266 "no LSP server configured for language: rust"
267 );
268 }
269
270 #[test]
271 fn test_error_display_timeout() {
272 let err = Error::Timeout(30);
273 assert_eq!(err.to_string(), "request timed out after 30 seconds");
274 }
275
276 #[test]
277 fn test_error_display_document_limit() {
278 let err = Error::DocumentLimitExceeded {
279 current: 150,
280 max: 100,
281 };
282 assert_eq!(err.to_string(), "document limit exceeded: 150/100");
283 }
284
285 #[test]
286 fn test_error_display_file_size_limit() {
287 let err = Error::FileSizeLimitExceeded {
288 size: 20_000_000,
289 max: 10_000_000,
290 };
291 assert!(err.to_string().contains("file size limit exceeded"));
292 }
293
294 #[test]
295 fn test_error_from_io() {
296 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
297 let err: Error = io_err.into();
298 assert!(matches!(err, Error::Io(_)));
299 }
300
301 #[test]
302 #[allow(clippy::unwrap_used)]
303 fn test_error_from_json() {
304 let json_str = "{invalid json}";
305 let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
306 let err: Error = json_err.into();
307 assert!(matches!(err, Error::Json(_)));
308 }
309
310 #[test]
311 #[allow(clippy::unwrap_used)]
312 fn test_error_from_toml_de() {
313 let toml_str = "[invalid toml";
314 let toml_err = toml::from_str::<toml::Value>(toml_str).unwrap_err();
315 let err: Error = toml_err.into();
316 assert!(matches!(err, Error::TomlDe(_)));
317 }
318
319 #[test]
320 fn test_result_type_alias() {
321 fn _returns_error() -> Result<i32> {
322 Err(Error::Config("test error".to_string()))
323 }
324
325 let result: Result<i32> = Ok(42);
326 assert!(result.is_ok());
327 if let Ok(value) = result {
328 assert_eq!(value, 42);
329 }
330 }
331
332 #[test]
333 fn test_error_source_chain() {
334 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
335 let err = Error::ServerSpawnFailed {
336 command: "rust-analyzer".to_string(),
337 source: io_err,
338 };
339
340 let source = std::error::Error::source(&err);
341 assert!(source.is_some());
342 }
343
344 #[test]
345 fn test_server_spawn_failure_display() {
346 let failure = ServerSpawnFailure {
347 server_id: ServerId::from("rust"),
348 language_id: "rust".to_string(),
349 command: "rust-analyzer".to_string(),
350 message: "No such file or directory".to_string(),
351 };
352 assert_eq!(
353 failure.to_string(),
354 "rust [rust] (rust-analyzer): No such file or directory"
355 );
356 }
357
358 #[test]
359 fn test_server_spawn_failure_debug() {
360 let failure = ServerSpawnFailure {
361 server_id: ServerId::from("python"),
362 language_id: "python".to_string(),
363 command: "pyright".to_string(),
364 message: "command not found".to_string(),
365 };
366 let debug_str = format!("{failure:?}");
367 assert!(debug_str.contains("python"));
368 assert!(debug_str.contains("pyright"));
369 assert!(debug_str.contains("command not found"));
370 }
371
372 #[test]
373 fn test_server_spawn_failure_clone() {
374 let failure = ServerSpawnFailure {
375 server_id: ServerId::from("typescript"),
376 language_id: "typescript".to_string(),
377 command: "tsserver".to_string(),
378 message: "failed to start".to_string(),
379 };
380 let cloned = failure.clone();
381 assert_eq!(failure.language_id, cloned.language_id);
382 assert_eq!(failure.command, cloned.command);
383 assert_eq!(failure.message, cloned.message);
384 }
385
386 #[test]
387 fn test_error_display_partial_server_init() {
388 let err = Error::PartialServerInit {
389 failed_count: 2,
390 total_count: 3,
391 failures: vec![],
392 };
393 assert_eq!(
394 err.to_string(),
395 "some LSP servers failed to initialize: 2/3 servers"
396 );
397 }
398
399 #[test]
400 fn test_error_display_all_servers_failed_to_init() {
401 let err = Error::AllServersFailedToInit {
402 count: 2,
403 failures: vec![],
404 };
405 assert_eq!(
406 err.to_string(),
407 "all LSP servers failed to initialize (2 configured)"
408 );
409 }
410
411 #[test]
412 fn test_error_all_servers_failed_with_failures() {
413 let failures = vec![
414 ServerSpawnFailure {
415 server_id: ServerId::from("rust"),
416 language_id: "rust".to_string(),
417 command: "rust-analyzer".to_string(),
418 message: "not found".to_string(),
419 },
420 ServerSpawnFailure {
421 server_id: ServerId::from("python"),
422 language_id: "python".to_string(),
423 command: "pyright".to_string(),
424 message: "permission denied".to_string(),
425 },
426 ];
427
428 let err = Error::AllServersFailedToInit { count: 2, failures };
429
430 assert!(err.to_string().contains("all LSP servers failed"));
431 assert!(err.to_string().contains("2 configured"));
432 }
433
434 #[test]
435 fn test_error_partial_server_init_with_failures() {
436 let failures = vec![ServerSpawnFailure {
437 server_id: ServerId::from("python"),
438 language_id: "python".to_string(),
439 command: "pyright".to_string(),
440 message: "not found".to_string(),
441 }];
442
443 let err = Error::PartialServerInit {
444 failed_count: 1,
445 total_count: 2,
446 failures,
447 };
448
449 assert!(err.to_string().contains("some LSP servers failed"));
450 assert!(err.to_string().contains("1/2"));
451 }
452
453 #[test]
454 fn test_error_display_no_servers_available() {
455 let err =
456 Error::NoServersAvailable("none configured or all failed to initialize".to_string());
457 assert_eq!(
458 err.to_string(),
459 "none configured or all failed to initialize"
460 );
461 }
462
463 #[test]
464 fn test_error_no_servers_available_with_custom_message() {
465 let custom_msg = "none configured or all failed to initialize";
466 let err = Error::NoServersAvailable(custom_msg.to_string());
467 assert_eq!(err.to_string(), custom_msg);
468 }
469}