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)]
31pub enum Error {
32 #[error("LSP server initialization failed: {message}")]
34 LspInitFailed {
35 message: String,
37 },
38
39 #[error("LSP server error: {code} - {message}")]
41 LspServerError {
42 code: i32,
44 message: String,
46 },
47
48 #[error("MCP server error: {0}")]
50 McpServer(String),
51
52 #[error("document not found: {0}")]
54 DocumentNotFound(PathBuf),
55
56 #[error("no LSP server configured for language: {0}")]
58 NoServerForLanguage(String),
59
60 #[error("no LSP server configured")]
62 NoServerConfigured,
63
64 #[error("configuration error: {0}")]
66 Config(String),
67
68 #[error("configuration file not found: {0}")]
70 ConfigNotFound(PathBuf),
71
72 #[error("invalid configuration: {0}")]
74 InvalidConfig(String),
75
76 #[error("I/O error: {0}")]
78 Io(#[from] std::io::Error),
79
80 #[error("JSON error: {0}")]
82 Json(#[from] serde_json::Error),
83
84 #[error("TOML parsing error: {0}")]
86 TomlDe(#[from] toml::de::Error),
87
88 #[error("TOML serialization error: {0}")]
90 TomlSer(#[from] toml::ser::Error),
91
92 #[error("transport error: {0}")]
94 Transport(String),
95
96 #[error("request timed out after {0} seconds")]
98 Timeout(u64),
99
100 #[error("server shutdown requested")]
102 Shutdown,
103
104 #[error("failed to spawn LSP server '{command}': {source}")]
106 ServerSpawnFailed {
107 command: String,
109 #[source]
111 source: std::io::Error,
112 },
113
114 #[error("LSP protocol error: {0}")]
116 LspProtocolError(String),
117
118 #[error("invalid URI: {0}")]
120 InvalidUri(String),
121
122 #[error("position encoding error: {0}")]
124 EncodingError(String),
125
126 #[error("LSP server process terminated unexpectedly")]
128 ServerTerminated,
129
130 #[error("invalid tool parameters: {0}")]
132 InvalidToolParams(String),
133
134 #[error("file I/O error for {path:?}: {source}")]
136 FileIo {
137 path: PathBuf,
139 #[source]
141 source: std::io::Error,
142 },
143
144 #[error("path outside workspace: {0}")]
146 PathOutsideWorkspace(PathBuf),
147
148 #[error("document limit exceeded: {current}/{max}")]
150 DocumentLimitExceeded {
151 current: usize,
153 max: usize,
155 },
156
157 #[error("file size limit exceeded: {size} bytes (max: {max} bytes)")]
159 FileSizeLimitExceeded {
160 size: u64,
162 max: u64,
164 },
165
166 #[error("some LSP servers failed to initialize: {failed_count}/{total_count} servers")]
168 PartialServerInit {
169 failed_count: usize,
171 total_count: usize,
173 failures: Vec<ServerSpawnFailure>,
175 },
176
177 #[error("all LSP servers failed to initialize ({count} configured)")]
179 AllServersFailedToInit {
180 count: usize,
182 failures: Vec<ServerSpawnFailure>,
184 },
185
186 #[error("{0}")]
188 NoServersAvailable(String),
189}
190
191pub type Result<T> = std::result::Result<T, Error>;
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_error_display_lsp_init_failed() {
200 let err = Error::LspInitFailed {
201 message: "server not found".to_string(),
202 };
203 assert_eq!(
204 err.to_string(),
205 "LSP server initialization failed: server not found"
206 );
207 }
208
209 #[test]
210 fn test_error_display_lsp_server_error() {
211 let err = Error::LspServerError {
212 code: -32600,
213 message: "Invalid request".to_string(),
214 };
215 assert_eq!(
216 err.to_string(),
217 "LSP server error: -32600 - Invalid request"
218 );
219 }
220
221 #[test]
222 fn test_error_display_document_not_found() {
223 let err = Error::DocumentNotFound(PathBuf::from("/path/to/file.rs"));
224 assert!(err.to_string().contains("document not found"));
225 assert!(err.to_string().contains("file.rs"));
226 }
227
228 #[test]
229 fn test_error_display_no_server_for_language() {
230 let err = Error::NoServerForLanguage("rust".to_string());
231 assert_eq!(
232 err.to_string(),
233 "no LSP server configured for language: rust"
234 );
235 }
236
237 #[test]
238 fn test_error_display_timeout() {
239 let err = Error::Timeout(30);
240 assert_eq!(err.to_string(), "request timed out after 30 seconds");
241 }
242
243 #[test]
244 fn test_error_display_document_limit() {
245 let err = Error::DocumentLimitExceeded {
246 current: 150,
247 max: 100,
248 };
249 assert_eq!(err.to_string(), "document limit exceeded: 150/100");
250 }
251
252 #[test]
253 fn test_error_display_file_size_limit() {
254 let err = Error::FileSizeLimitExceeded {
255 size: 20_000_000,
256 max: 10_000_000,
257 };
258 assert!(err.to_string().contains("file size limit exceeded"));
259 }
260
261 #[test]
262 fn test_error_from_io() {
263 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
264 let err: Error = io_err.into();
265 assert!(matches!(err, Error::Io(_)));
266 }
267
268 #[test]
269 #[allow(clippy::unwrap_used)]
270 fn test_error_from_json() {
271 let json_str = "{invalid json}";
272 let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
273 let err: Error = json_err.into();
274 assert!(matches!(err, Error::Json(_)));
275 }
276
277 #[test]
278 #[allow(clippy::unwrap_used)]
279 fn test_error_from_toml_de() {
280 let toml_str = "[invalid toml";
281 let toml_err = toml::from_str::<toml::Value>(toml_str).unwrap_err();
282 let err: Error = toml_err.into();
283 assert!(matches!(err, Error::TomlDe(_)));
284 }
285
286 #[test]
287 fn test_result_type_alias() {
288 fn _returns_error() -> Result<i32> {
289 Err(Error::Config("test error".to_string()))
290 }
291
292 let result: Result<i32> = Ok(42);
293 assert!(result.is_ok());
294 if let Ok(value) = result {
295 assert_eq!(value, 42);
296 }
297 }
298
299 #[test]
300 fn test_error_source_chain() {
301 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
302 let err = Error::ServerSpawnFailed {
303 command: "rust-analyzer".to_string(),
304 source: io_err,
305 };
306
307 let source = std::error::Error::source(&err);
308 assert!(source.is_some());
309 }
310
311 #[test]
312 fn test_server_spawn_failure_display() {
313 let failure = ServerSpawnFailure {
314 language_id: "rust".to_string(),
315 command: "rust-analyzer".to_string(),
316 message: "No such file or directory".to_string(),
317 };
318 assert_eq!(
319 failure.to_string(),
320 "rust (rust-analyzer): No such file or directory"
321 );
322 }
323
324 #[test]
325 fn test_server_spawn_failure_debug() {
326 let failure = ServerSpawnFailure {
327 language_id: "python".to_string(),
328 command: "pyright".to_string(),
329 message: "command not found".to_string(),
330 };
331 let debug_str = format!("{failure:?}");
332 assert!(debug_str.contains("python"));
333 assert!(debug_str.contains("pyright"));
334 assert!(debug_str.contains("command not found"));
335 }
336
337 #[test]
338 fn test_server_spawn_failure_clone() {
339 let failure = ServerSpawnFailure {
340 language_id: "typescript".to_string(),
341 command: "tsserver".to_string(),
342 message: "failed to start".to_string(),
343 };
344 let cloned = failure.clone();
345 assert_eq!(failure.language_id, cloned.language_id);
346 assert_eq!(failure.command, cloned.command);
347 assert_eq!(failure.message, cloned.message);
348 }
349
350 #[test]
351 fn test_error_display_partial_server_init() {
352 let err = Error::PartialServerInit {
353 failed_count: 2,
354 total_count: 3,
355 failures: vec![],
356 };
357 assert_eq!(
358 err.to_string(),
359 "some LSP servers failed to initialize: 2/3 servers"
360 );
361 }
362
363 #[test]
364 fn test_error_display_all_servers_failed_to_init() {
365 let err = Error::AllServersFailedToInit {
366 count: 2,
367 failures: vec![],
368 };
369 assert_eq!(
370 err.to_string(),
371 "all LSP servers failed to initialize (2 configured)"
372 );
373 }
374
375 #[test]
376 fn test_error_all_servers_failed_with_failures() {
377 let failures = vec![
378 ServerSpawnFailure {
379 language_id: "rust".to_string(),
380 command: "rust-analyzer".to_string(),
381 message: "not found".to_string(),
382 },
383 ServerSpawnFailure {
384 language_id: "python".to_string(),
385 command: "pyright".to_string(),
386 message: "permission denied".to_string(),
387 },
388 ];
389
390 let err = Error::AllServersFailedToInit { count: 2, failures };
391
392 assert!(err.to_string().contains("all LSP servers failed"));
393 assert!(err.to_string().contains("2 configured"));
394 }
395
396 #[test]
397 fn test_error_partial_server_init_with_failures() {
398 let failures = vec![ServerSpawnFailure {
399 language_id: "python".to_string(),
400 command: "pyright".to_string(),
401 message: "not found".to_string(),
402 }];
403
404 let err = Error::PartialServerInit {
405 failed_count: 1,
406 total_count: 2,
407 failures,
408 };
409
410 assert!(err.to_string().contains("some LSP servers failed"));
411 assert!(err.to_string().contains("1/2"));
412 }
413
414 #[test]
415 fn test_error_display_no_servers_available() {
416 let err =
417 Error::NoServersAvailable("none configured or all failed to initialize".to_string());
418 assert_eq!(
419 err.to_string(),
420 "none configured or all failed to initialize"
421 );
422 }
423
424 #[test]
425 fn test_error_no_servers_available_with_custom_message() {
426 let custom_msg = "none configured or all failed to initialize";
427 let err = Error::NoServersAvailable(custom_msg.to_string());
428 assert_eq!(err.to_string(), custom_msg);
429 }
430}