1use std::path::PathBuf;
7
8#[derive(Debug, Clone)]
10pub struct ServerSpawnFailure {
11 pub language_id: String,
13 pub command: String,
15 pub message: String,
17}
18
19impl std::fmt::Display for ServerSpawnFailure {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 write!(
22 f,
23 "{} ({}): {}",
24 self.language_id, self.command, self.message
25 )
26 }
27}
28
29#[derive(Debug, thiserror::Error)]
35#[non_exhaustive]
36pub enum Error {
37 #[error("LSP server initialization failed: {message}")]
39 LspInitFailed {
40 message: String,
42 },
43
44 #[error("LSP server error: {code} - {message}")]
46 LspServerError {
47 code: i32,
49 message: String,
51 data: Option<serde_json::Value>,
53 },
54
55 #[error("MCP server error: {0}")]
57 McpServer(String),
58
59 #[error("document not found: {0}")]
61 DocumentNotFound(PathBuf),
62
63 #[error("no LSP server configured for language: {0}")]
65 NoServerForLanguage(String),
66
67 #[error(
69 "LSP server for language '{0}' is still initializing (large project load in progress); wait and retry the request (this may take a few minutes on large projects)"
70 )]
71 ServerInitializing(String),
72
73 #[error("no LSP server configured")]
75 NoServerConfigured,
76
77 #[error("configuration error: {0}")]
79 Config(String),
80
81 #[error("configuration file not found: {0}")]
83 ConfigNotFound(PathBuf),
84
85 #[error("invalid configuration: {0}")]
87 InvalidConfig(String),
88
89 #[error("I/O error: {0}")]
91 Io(#[from] std::io::Error),
92
93 #[error("JSON error: {0}")]
95 Json(#[from] serde_json::Error),
96
97 #[error("TOML parsing error: {0}")]
99 TomlDe(#[from] toml::de::Error),
100
101 #[error("TOML serialization error: {0}")]
103 TomlSer(#[from] toml::ser::Error),
104
105 #[error("transport error: {0}")]
107 Transport(String),
108
109 #[error("request timed out after {0} seconds")]
111 Timeout(u64),
112
113 #[error("server shutdown requested")]
115 Shutdown,
116
117 #[error("failed to spawn LSP server '{command}': {source}")]
119 ServerSpawnFailed {
120 command: String,
122 #[source]
124 source: std::io::Error,
125 },
126
127 #[error("LSP protocol error: {0}")]
129 LspProtocolError(String),
130
131 #[error("invalid URI: {0}")]
133 InvalidUri(String),
134
135 #[error("position encoding error: {0}")]
137 EncodingError(String),
138
139 #[error("LSP server process terminated unexpectedly")]
141 ServerTerminated,
142
143 #[error("invalid tool parameters: {0}")]
145 InvalidToolParams(String),
146
147 #[error("file I/O error for {path:?}: {source}")]
149 FileIo {
150 path: PathBuf,
152 #[source]
154 source: std::io::Error,
155 },
156
157 #[error("path outside workspace: {0}")]
159 PathOutsideWorkspace(PathBuf),
160
161 #[error("document limit exceeded: {current}/{max}")]
163 DocumentLimitExceeded {
164 current: usize,
166 max: usize,
168 },
169
170 #[error("file size limit exceeded: {size} bytes (max: {max} bytes)")]
172 FileSizeLimitExceeded {
173 size: u64,
175 max: u64,
177 },
178
179 #[error("some LSP servers failed to initialize: {failed_count}/{total_count} servers")]
181 PartialServerInit {
182 failed_count: usize,
184 total_count: usize,
186 failures: Vec<ServerSpawnFailure>,
188 },
189
190 #[error("all LSP servers failed to initialize ({count} configured)")]
192 AllServersFailedToInit {
193 count: usize,
195 failures: Vec<ServerSpawnFailure>,
197 },
198
199 #[error("{0}")]
201 NoServersAvailable(String),
202}
203
204pub type Result<T> = std::result::Result<T, Error>;
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_error_display_lsp_init_failed() {
213 let err = Error::LspInitFailed {
214 message: "server not found".to_string(),
215 };
216 assert_eq!(
217 err.to_string(),
218 "LSP server initialization failed: server not found"
219 );
220 }
221
222 #[test]
223 fn test_error_display_lsp_server_error() {
224 let err = Error::LspServerError {
225 code: -32600,
226 message: "Invalid request".to_string(),
227 data: None,
228 };
229 assert_eq!(
230 err.to_string(),
231 "LSP server error: -32600 - Invalid request"
232 );
233 }
234
235 #[test]
236 fn test_error_display_document_not_found() {
237 let err = Error::DocumentNotFound(PathBuf::from("/path/to/file.rs"));
238 assert!(err.to_string().contains("document not found"));
239 assert!(err.to_string().contains("file.rs"));
240 }
241
242 #[test]
243 fn test_error_display_no_server_for_language() {
244 let err = Error::NoServerForLanguage("rust".to_string());
245 assert_eq!(
246 err.to_string(),
247 "no LSP server configured for language: rust"
248 );
249 }
250
251 #[test]
252 fn test_error_display_timeout() {
253 let err = Error::Timeout(30);
254 assert_eq!(err.to_string(), "request timed out after 30 seconds");
255 }
256
257 #[test]
258 fn test_error_display_document_limit() {
259 let err = Error::DocumentLimitExceeded {
260 current: 150,
261 max: 100,
262 };
263 assert_eq!(err.to_string(), "document limit exceeded: 150/100");
264 }
265
266 #[test]
267 fn test_error_display_file_size_limit() {
268 let err = Error::FileSizeLimitExceeded {
269 size: 20_000_000,
270 max: 10_000_000,
271 };
272 assert!(err.to_string().contains("file size limit exceeded"));
273 }
274
275 #[test]
276 fn test_error_from_io() {
277 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
278 let err: Error = io_err.into();
279 assert!(matches!(err, Error::Io(_)));
280 }
281
282 #[test]
283 #[allow(clippy::unwrap_used)]
284 fn test_error_from_json() {
285 let json_str = "{invalid json}";
286 let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
287 let err: Error = json_err.into();
288 assert!(matches!(err, Error::Json(_)));
289 }
290
291 #[test]
292 #[allow(clippy::unwrap_used)]
293 fn test_error_from_toml_de() {
294 let toml_str = "[invalid toml";
295 let toml_err = toml::from_str::<toml::Value>(toml_str).unwrap_err();
296 let err: Error = toml_err.into();
297 assert!(matches!(err, Error::TomlDe(_)));
298 }
299
300 #[test]
301 fn test_result_type_alias() {
302 fn _returns_error() -> Result<i32> {
303 Err(Error::Config("test error".to_string()))
304 }
305
306 let result: Result<i32> = Ok(42);
307 assert!(result.is_ok());
308 if let Ok(value) = result {
309 assert_eq!(value, 42);
310 }
311 }
312
313 #[test]
314 fn test_error_source_chain() {
315 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
316 let err = Error::ServerSpawnFailed {
317 command: "rust-analyzer".to_string(),
318 source: io_err,
319 };
320
321 let source = std::error::Error::source(&err);
322 assert!(source.is_some());
323 }
324
325 #[test]
326 fn test_server_spawn_failure_display() {
327 let failure = ServerSpawnFailure {
328 language_id: "rust".to_string(),
329 command: "rust-analyzer".to_string(),
330 message: "No such file or directory".to_string(),
331 };
332 assert_eq!(
333 failure.to_string(),
334 "rust (rust-analyzer): No such file or directory"
335 );
336 }
337
338 #[test]
339 fn test_server_spawn_failure_debug() {
340 let failure = ServerSpawnFailure {
341 language_id: "python".to_string(),
342 command: "pyright".to_string(),
343 message: "command not found".to_string(),
344 };
345 let debug_str = format!("{failure:?}");
346 assert!(debug_str.contains("python"));
347 assert!(debug_str.contains("pyright"));
348 assert!(debug_str.contains("command not found"));
349 }
350
351 #[test]
352 fn test_server_spawn_failure_clone() {
353 let failure = ServerSpawnFailure {
354 language_id: "typescript".to_string(),
355 command: "tsserver".to_string(),
356 message: "failed to start".to_string(),
357 };
358 let cloned = failure.clone();
359 assert_eq!(failure.language_id, cloned.language_id);
360 assert_eq!(failure.command, cloned.command);
361 assert_eq!(failure.message, cloned.message);
362 }
363
364 #[test]
365 fn test_error_display_partial_server_init() {
366 let err = Error::PartialServerInit {
367 failed_count: 2,
368 total_count: 3,
369 failures: vec![],
370 };
371 assert_eq!(
372 err.to_string(),
373 "some LSP servers failed to initialize: 2/3 servers"
374 );
375 }
376
377 #[test]
378 fn test_error_display_all_servers_failed_to_init() {
379 let err = Error::AllServersFailedToInit {
380 count: 2,
381 failures: vec![],
382 };
383 assert_eq!(
384 err.to_string(),
385 "all LSP servers failed to initialize (2 configured)"
386 );
387 }
388
389 #[test]
390 fn test_error_all_servers_failed_with_failures() {
391 let failures = vec![
392 ServerSpawnFailure {
393 language_id: "rust".to_string(),
394 command: "rust-analyzer".to_string(),
395 message: "not found".to_string(),
396 },
397 ServerSpawnFailure {
398 language_id: "python".to_string(),
399 command: "pyright".to_string(),
400 message: "permission denied".to_string(),
401 },
402 ];
403
404 let err = Error::AllServersFailedToInit { count: 2, failures };
405
406 assert!(err.to_string().contains("all LSP servers failed"));
407 assert!(err.to_string().contains("2 configured"));
408 }
409
410 #[test]
411 fn test_error_partial_server_init_with_failures() {
412 let failures = vec![ServerSpawnFailure {
413 language_id: "python".to_string(),
414 command: "pyright".to_string(),
415 message: "not found".to_string(),
416 }];
417
418 let err = Error::PartialServerInit {
419 failed_count: 1,
420 total_count: 2,
421 failures,
422 };
423
424 assert!(err.to_string().contains("some LSP servers failed"));
425 assert!(err.to_string().contains("1/2"));
426 }
427
428 #[test]
429 fn test_error_display_no_servers_available() {
430 let err =
431 Error::NoServersAvailable("none configured or all failed to initialize".to_string());
432 assert_eq!(
433 err.to_string(),
434 "none configured or all failed to initialize"
435 );
436 }
437
438 #[test]
439 fn test_error_no_servers_available_with_custom_message() {
440 let custom_msg = "none configured or all failed to initialize";
441 let err = Error::NoServersAvailable(custom_msg.to_string());
442 assert_eq!(err.to_string(), custom_msg);
443 }
444}