1use thiserror::Error;
4
5#[cfg(test)]
6mod tests {
7 use super::*;
8 use std::io;
9
10 #[test]
11 #[allow(clippy::unwrap_used)]
12 fn test_lsp_error_no_lsp_available_recovery_hint() {
13 let err = LspError::NoLspAvailable;
14 let hint = err.recovery_hint();
15
16 assert!(hint.is_some());
17 let hint_str = hint.unwrap();
18 assert!(hint_str.contains("No LSP available"));
19 assert!(hint_str.contains("lsp_health"));
20 }
21
22 #[test]
23 #[allow(clippy::unwrap_used)]
24 fn test_lsp_error_timeout_recovery_hint() {
25 let err = LspError::Timeout {
26 operation: "textDocument/definition".to_string(),
27 timeout_ms: 10000,
28 };
29 let hint = err.recovery_hint();
30
31 assert!(hint.is_some());
32 let hint_str = hint.unwrap();
33 assert!(hint_str.contains("textDocument/definition"));
34 assert!(hint_str.contains("10000ms"));
35 assert!(hint_str.contains("lsp_health"));
36 assert!(hint_str.contains("retry in 30s"));
37 }
38
39 #[test]
40 #[allow(clippy::unwrap_used)]
41 fn test_lsp_error_protocol_recovery_hint() {
42 let err = LspError::Protocol("malformed JSON-RPC".to_string());
43 let hint = err.recovery_hint();
44
45 assert!(hint.is_some());
46 let hint_str = hint.unwrap();
47 assert!(hint_str.contains("malformed JSON-RPC"));
48 assert!(hint_str.contains("lsp_health"));
49 assert!(hint_str.contains("search_codebase"));
50 }
51
52 #[test]
53 #[allow(clippy::unwrap_used)]
54 fn test_lsp_error_connection_lost_recovery_hint() {
55 let err = LspError::ConnectionLost;
56 let hint = err.recovery_hint();
57
58 assert!(hint.is_some());
59 let hint_str = hint.unwrap();
60 assert!(hint_str.contains("connection lost"));
61 assert!(hint_str.contains("lsp_health"));
62 assert!(hint_str.contains("restart"));
63 assert!(hint_str.contains("read_source_file"));
64 }
65
66 #[test]
67 #[allow(clippy::unwrap_used)]
68 fn test_lsp_error_unsupported_capability_recovery_hint() {
69 let err = LspError::UnsupportedCapability {
70 capability: "diagnosticProvider".to_string(),
71 };
72 let hint = err.recovery_hint();
73
74 assert!(hint.is_some());
75 let hint_str = hint.unwrap();
76 assert!(hint_str.contains("diagnosticProvider"));
77 assert!(hint_str.contains("does not support"));
78 assert!(hint_str.contains("search_codebase"));
79 }
80
81 #[test]
82 #[allow(clippy::unwrap_used)]
83 fn test_lsp_error_io_not_found_recovery_hint() {
84 let io_err = io::Error::new(io::ErrorKind::NotFound, "binary not found");
85 let err = LspError::Io(io_err);
86 let hint = err.recovery_hint();
87
88 assert!(hint.is_some());
89 let hint_str = hint.unwrap();
90 assert!(hint_str.contains("binary not found"));
91 assert!(hint_str.contains("installed"));
92 assert!(hint_str.contains("PATH"));
93 assert!(hint_str.contains("lsp_health"));
94 }
95
96 #[test]
97 #[allow(clippy::unwrap_used)]
98 fn test_lsp_error_io_permission_denied_recovery_hint() {
99 let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
100 let err = LspError::Io(io_err);
101 let hint = err.recovery_hint();
102
103 assert!(hint.is_some());
104 let hint_str = hint.unwrap();
105 assert!(hint_str.contains("I/O error"));
106 assert!(hint_str.contains("access denied"));
107 assert!(hint_str.contains("lsp_health"));
108 }
109
110 #[test]
111 #[allow(clippy::unwrap_used)]
112 fn test_lsp_error_io_broken_pipe_recovery_hint() {
113 let io_err = io::Error::new(io::ErrorKind::BrokenPipe, "pipe broken");
114 let err = LspError::Io(io_err);
115 let hint = err.recovery_hint();
116
117 assert!(hint.is_some());
118 let hint_str = hint.unwrap();
119 assert!(hint_str.contains("I/O error"));
120 assert!(hint_str.contains("pipe broken"));
121 assert!(hint_str.contains("lsp_health"));
122 }
123
124 #[test]
125 fn test_lsp_error_display_no_lsp_available() {
126 let err = LspError::NoLspAvailable;
127 let display = format!("{err}");
128 assert_eq!(display, "no LSP available for this file type");
129 }
130
131 #[test]
132 fn test_lsp_error_display_timeout() {
133 let err = LspError::Timeout {
134 operation: "initialize".to_string(),
135 timeout_ms: 30000,
136 };
137 let display = format!("{err}");
138 assert!(display.contains("initialize"));
139 assert!(display.contains("30000ms"));
140 }
141
142 #[test]
143 fn test_lsp_error_display_protocol() {
144 let err = LspError::Protocol("invalid response".to_string());
145 let display = format!("{err}");
146 assert_eq!(display, "LSP protocol error: invalid response");
147 }
148
149 #[test]
150 fn test_lsp_error_display_connection_lost() {
151 let err = LspError::ConnectionLost;
152 let display = format!("{err}");
153 assert_eq!(display, "LSP connection lost");
154 }
155
156 #[test]
157 fn test_lsp_error_display_unsupported_capability() {
158 let err = LspError::UnsupportedCapability {
159 capability: "callHierarchy".to_string(),
160 };
161 let display = format!("{err}");
162 assert!(display.contains("callHierarchy"));
163 }
164
165 #[test]
166 fn test_lsp_error_display_io() {
167 let io_err = io::Error::new(io::ErrorKind::NotFound, "test error");
168 let err = LspError::Io(io_err);
169 let display = format!("{err}");
170 assert!(display.contains("I/O error"));
171 assert!(display.contains("test error"));
172 }
173
174 #[test]
175 fn test_all_error_variants_have_recovery_hints() {
176 assert!(LspError::NoLspAvailable.recovery_hint().is_some());
178
179 assert!(LspError::Timeout {
181 operation: "test".to_string(),
182 timeout_ms: 5000,
183 }
184 .recovery_hint()
185 .is_some());
186
187 assert!(LspError::Protocol("test".to_string())
189 .recovery_hint()
190 .is_some());
191
192 assert!(LspError::ConnectionLost.recovery_hint().is_some());
194
195 assert!(LspError::UnsupportedCapability {
197 capability: "test".to_string(),
198 }
199 .recovery_hint()
200 .is_some());
201
202 assert!(
204 LspError::Io(io::Error::new(io::ErrorKind::NotFound, "test"))
205 .recovery_hint()
206 .is_some()
207 );
208
209 assert!(
211 LspError::Io(io::Error::new(io::ErrorKind::PermissionDenied, "test"))
212 .recovery_hint()
213 .is_some()
214 );
215
216 assert!(LspError::Io(std::io::Error::other("test"))
218 .recovery_hint()
219 .is_some());
220 }
221}
222
223#[derive(Debug, Error)]
225pub enum LspError {
226 #[error("no LSP available for this file type")]
232 NoLspAvailable,
233
234 #[error("LSP timed out on '{operation}' after {timeout_ms}ms")]
240 Timeout {
241 operation: String,
243 timeout_ms: u64,
245 },
246
247 #[error("LSP protocol error: {0}")]
252 Protocol(String),
253
254 #[error("LSP connection lost")]
258 ConnectionLost,
259
260 #[error("LSP does not support capability: {capability}")]
266 UnsupportedCapability {
267 capability: String,
269 },
270
271 #[error("LSP I/O error: {0}")]
273 Io(#[source] std::io::Error),
274}
275
276impl LspError {
277 #[must_use]
285 pub fn recovery_hint(&self) -> Option<String> {
286 match self {
287 Self::NoLspAvailable => Some(
288 "No LSP available for this file type. \
289 Call lsp_health to see install instructions and check which languages are configured."
290 .to_owned(),
291 ),
292 Self::Timeout { operation, timeout_ms } => Some(format!(
293 "LSP request '{operation}' timed out after {timeout_ms}ms. \
294 The language server may still be indexing or under memory pressure. \
295 (1) Call lsp_health to check server status and indexing progress. \
296 (2) If status is 'warming_up', retry in 30s. \
297 (3) Use search_codebase + read_symbol_scope as tree-sitter fallbacks."
298 )),
299 Self::ConnectionLost => Some(
300 "LSP connection lost — the language server may have crashed. \
301 Call lsp_health(action='restart') to recover the server. \
302 Use read_source_file and search_codebase as fallbacks while it restarts."
303 .to_owned(),
304 ),
305 Self::Protocol(msg) => Some(format!(
306 "LSP protocol error: {msg}. \
307 Call lsp_health to check server health. \
308 Use search_codebase for text-based navigation in the meantime."
309 )),
310 Self::UnsupportedCapability { capability } => Some(format!(
311 "The LSP server does not support '{capability}'. \
312 This is a server limitation — validation is skipped for this file type. \
313 Use search_codebase + read_symbol_scope for navigation."
314 )),
315 Self::Io(io_err) if io_err.kind() == std::io::ErrorKind::NotFound => Some(
316 "LSP binary not found. \
317 Ensure the language server is installed and available in PATH. \
318 Call lsp_health for install instructions."
319 .to_owned(),
320 ),
321 Self::Io(io_err) => Some(format!(
322 "LSP I/O error: {io_err}. \
323 The language server process failed to start. \
324 Call lsp_health to check server status."
325 )),
326 }
327 }
328}