lsp_server_tokio/lifecycle.rs
1//! Lifecycle state management for LSP connections.
2//!
3//! This module provides the [`LifecycleState`] enum for tracking connection state through
4//! all LSP phases, [`ProtocolError`] for lifecycle violations, and [`ExitCode`] for
5//! distinguishing clean from dirty exits.
6//!
7//! # LSP Lifecycle
8//!
9//! The LSP protocol defines a strict lifecycle:
10//!
11//! 1. **Uninitialized** - Connection established, only `initialize` request allowed
12//! 2. **Initializing** - `initialize` received, only `initialized` notification allowed
13//! 3. **Running** - Normal operation, all messages except `initialize` allowed
14//! 4. **`ShuttingDown`** - `shutdown` received, only `exit` notification allowed
15//! 5. **Exited** - `exit` received, connection should close
16//!
17//! Messages received in invalid states should be rejected (requests) or dropped (notifications).
18//!
19//! # Example
20//!
21//! ```
22//! use lsp_server_tokio::LifecycleState;
23//!
24//! let state = LifecycleState::Uninitialized;
25//!
26//! // Only initialize allowed in Uninitialized state
27//! assert!(state.is_request_allowed("initialize"));
28//! assert!(!state.is_request_allowed("shutdown"));
29//!
30//! // Exit always allowed as notification
31//! assert!(state.is_notification_allowed("exit"));
32//! ```
33
34use thiserror::Error;
35
36/// Represents the lifecycle state of an LSP connection.
37///
38/// The LSP specification mandates a strict state machine where messages are only
39/// valid in certain states. This enum tracks the current state and provides
40/// validation methods.
41///
42/// # State Transitions
43///
44/// ```text
45/// Uninitialized ──(initialize req)──> Initializing
46/// │ │
47/// │ (exit notif) │ (initialized notif)
48/// v v
49/// Exited Running
50/// │
51/// │ (shutdown req)
52/// v
53/// ShuttingDown
54/// │
55/// │ (exit notif)
56/// v
57/// Exited
58/// ```
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub enum LifecycleState {
61 /// Connection established, awaiting `initialize` request.
62 ///
63 /// Only the `initialize` request is allowed. All other requests should
64 /// receive a `ServerNotInitialized` error response. The `exit` notification
65 /// is always allowed (to handle early disconnection).
66 #[default]
67 Uninitialized,
68
69 /// `initialize` request received, awaiting `initialized` notification.
70 ///
71 /// No requests are allowed in this state. Only the `initialized` notification
72 /// can transition the connection to `Running`.
73 Initializing,
74
75 /// Normal operation - all messages allowed except `initialize`.
76 ///
77 /// The server is fully initialized and can process any request or notification
78 /// except another `initialize` request (cannot re-initialize).
79 Running,
80
81 /// `shutdown` request received, awaiting `exit` notification.
82 ///
83 /// No requests are allowed. Only the `exit` notification is valid.
84 /// Any other messages should be rejected.
85 ShuttingDown,
86
87 /// `exit` notification received, connection should close.
88 ///
89 /// No messages are allowed. The connection should be closed immediately.
90 Exited,
91}
92
93impl LifecycleState {
94 /// Returns `true` if the given request method is valid in this state.
95 ///
96 /// # Request Validity by State
97 ///
98 /// | State | Allowed Requests |
99 /// |-------|-----------------|
100 /// | Uninitialized | `initialize` only |
101 /// | Initializing | None |
102 /// | Running | All except `initialize` |
103 /// | `ShuttingDown` | None |
104 /// | Exited | None |
105 ///
106 /// # Examples
107 ///
108 /// ```
109 /// use lsp_server_tokio::LifecycleState;
110 ///
111 /// let state = LifecycleState::Running;
112 /// assert!(state.is_request_allowed("textDocument/hover"));
113 /// assert!(state.is_request_allowed("shutdown"));
114 /// assert!(!state.is_request_allowed("initialize")); // Can't re-initialize
115 /// ```
116 #[must_use]
117 pub fn is_request_allowed(&self, method: &str) -> bool {
118 match self {
119 Self::Uninitialized => method == "initialize",
120 Self::Initializing | Self::ShuttingDown | Self::Exited => false,
121 Self::Running => method != "initialize",
122 }
123 }
124
125 /// Returns `true` if the given notification method is valid in this state.
126 ///
127 /// # Notification Validity by State
128 ///
129 /// | State | Allowed Notifications |
130 /// |-------|----------------------|
131 /// | Uninitialized | `exit` only |
132 /// | Initializing | `initialized` only |
133 /// | Running | All |
134 /// | `ShuttingDown` | `exit` only |
135 /// | Exited | None |
136 ///
137 /// # Examples
138 ///
139 /// ```
140 /// use lsp_server_tokio::LifecycleState;
141 ///
142 /// let state = LifecycleState::Initializing;
143 /// assert!(state.is_notification_allowed("initialized"));
144 /// assert!(!state.is_notification_allowed("textDocument/didOpen"));
145 /// ```
146 #[must_use]
147 pub fn is_notification_allowed(&self, method: &str) -> bool {
148 match self {
149 Self::Uninitialized | Self::ShuttingDown => method == "exit",
150 Self::Initializing => method == "initialized",
151 Self::Running => true,
152 Self::Exited => false,
153 }
154 }
155}
156
157/// Exit code for the LSP server process.
158///
159/// Per the LSP specification:
160/// - Exit code 0 if proper `shutdown` -> `exit` sequence was followed
161/// - Exit code 1 if `exit` was received without prior `shutdown`
162///
163/// # Examples
164///
165/// ```
166/// use lsp_server_tokio::ExitCode;
167///
168/// let code = ExitCode::Success;
169/// assert_eq!(code as i32, 0);
170///
171/// let code = ExitCode::Error;
172/// assert_eq!(code as i32, 1);
173/// ```
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175#[repr(i32)]
176pub enum ExitCode {
177 /// Proper shutdown sequence was followed (`shutdown` then `exit`).
178 Success = 0,
179 /// Exit without prior shutdown request.
180 Error = 1,
181}
182
183/// Errors that occur during LSP protocol lifecycle management.
184///
185/// These errors represent violations of the LSP protocol's initialization
186/// and shutdown sequences.
187///
188/// # Examples
189///
190/// ```
191/// use lsp_server_tokio::ProtocolError;
192///
193/// // Creating protocol errors
194/// let err = ProtocolError::ExpectedInitialize("textDocument/hover".to_string());
195/// assert!(err.to_string().contains("textDocument/hover"));
196///
197/// let err = ProtocolError::Disconnected;
198/// assert!(err.to_string().contains("disconnected"));
199/// ```
200#[derive(Debug, Error)]
201pub enum ProtocolError {
202 /// Expected `initialize` request, but received a different message.
203 ///
204 /// This occurs when the server is in `Uninitialized` state and receives
205 /// a request other than `initialize`.
206 #[error("expected initialize request, got: {0}")]
207 ExpectedInitialize(String),
208
209 /// Expected `initialized` notification, but received a different message.
210 ///
211 /// This occurs when the server is in `Initializing` state and receives
212 /// a notification other than `initialized`.
213 #[error("expected initialized notification, got: {0}")]
214 ExpectedInitialized(String),
215
216 /// The connection was disconnected unexpectedly.
217 ///
218 /// This typically occurs when the client closes the connection without
219 /// sending an `exit` notification.
220 #[error("connection disconnected unexpectedly")]
221 Disconnected,
222
223 /// Received a request after the `shutdown` request was processed.
224 ///
225 /// After receiving `shutdown`, only the `exit` notification is valid.
226 #[error("received request after shutdown: {0}")]
227 AfterShutdown(String),
228
229 /// Timed out waiting for the `initialized` notification.
230 #[error("timed out waiting for initialized notification (60s)")]
231 InitializeTimeout,
232
233 /// Timed out waiting for a response to a server-initiated request.
234 #[error("timed out waiting for response to server request")]
235 RequestTimeout,
236
237 /// An I/O error occurred during protocol communication.
238 #[error("I/O error: {0}")]
239 Io(#[from] std::io::Error),
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 // =========================================================================
247 // LifecycleState Tests
248 // =========================================================================
249
250 #[test]
251 fn lifecycle_state_default_is_uninitialized() {
252 assert_eq!(LifecycleState::default(), LifecycleState::Uninitialized);
253 }
254
255 #[test]
256 fn lifecycle_state_is_copy() {
257 let state = LifecycleState::Running;
258 let copy = state;
259 assert_eq!(state, copy);
260 }
261
262 // -------------------------------------------------------------------------
263 // is_request_allowed tests
264 // -------------------------------------------------------------------------
265
266 #[test]
267 fn uninitialized_allows_only_initialize_request() {
268 let state = LifecycleState::Uninitialized;
269
270 assert!(state.is_request_allowed("initialize"));
271 assert!(!state.is_request_allowed("shutdown"));
272 assert!(!state.is_request_allowed("textDocument/hover"));
273 assert!(!state.is_request_allowed("workspace/symbol"));
274 }
275
276 #[test]
277 fn initializing_allows_no_requests() {
278 let state = LifecycleState::Initializing;
279
280 assert!(!state.is_request_allowed("initialize"));
281 assert!(!state.is_request_allowed("shutdown"));
282 assert!(!state.is_request_allowed("textDocument/hover"));
283 }
284
285 #[test]
286 fn running_allows_all_requests_except_initialize() {
287 let state = LifecycleState::Running;
288
289 assert!(!state.is_request_allowed("initialize"));
290 assert!(state.is_request_allowed("shutdown"));
291 assert!(state.is_request_allowed("textDocument/hover"));
292 assert!(state.is_request_allowed("textDocument/completion"));
293 assert!(state.is_request_allowed("workspace/symbol"));
294 }
295
296 #[test]
297 fn shutting_down_allows_no_requests() {
298 let state = LifecycleState::ShuttingDown;
299
300 assert!(!state.is_request_allowed("initialize"));
301 assert!(!state.is_request_allowed("shutdown"));
302 assert!(!state.is_request_allowed("textDocument/hover"));
303 }
304
305 #[test]
306 fn exited_allows_no_requests() {
307 let state = LifecycleState::Exited;
308
309 assert!(!state.is_request_allowed("initialize"));
310 assert!(!state.is_request_allowed("shutdown"));
311 assert!(!state.is_request_allowed("textDocument/hover"));
312 }
313
314 // -------------------------------------------------------------------------
315 // is_notification_allowed tests
316 // -------------------------------------------------------------------------
317
318 #[test]
319 fn uninitialized_allows_only_exit_notification() {
320 let state = LifecycleState::Uninitialized;
321
322 assert!(state.is_notification_allowed("exit"));
323 assert!(!state.is_notification_allowed("initialized"));
324 assert!(!state.is_notification_allowed("textDocument/didOpen"));
325 }
326
327 #[test]
328 fn initializing_allows_only_initialized_notification() {
329 let state = LifecycleState::Initializing;
330
331 assert!(state.is_notification_allowed("initialized"));
332 assert!(!state.is_notification_allowed("exit"));
333 assert!(!state.is_notification_allowed("textDocument/didOpen"));
334 }
335
336 #[test]
337 fn running_allows_all_notifications() {
338 let state = LifecycleState::Running;
339
340 assert!(state.is_notification_allowed("exit"));
341 assert!(state.is_notification_allowed("initialized"));
342 assert!(state.is_notification_allowed("textDocument/didOpen"));
343 assert!(state.is_notification_allowed("textDocument/didChange"));
344 assert!(state.is_notification_allowed("$/cancelRequest"));
345 }
346
347 #[test]
348 fn shutting_down_allows_only_exit_notification() {
349 let state = LifecycleState::ShuttingDown;
350
351 assert!(state.is_notification_allowed("exit"));
352 assert!(!state.is_notification_allowed("initialized"));
353 assert!(!state.is_notification_allowed("textDocument/didOpen"));
354 }
355
356 #[test]
357 fn exited_allows_no_notifications() {
358 let state = LifecycleState::Exited;
359
360 assert!(!state.is_notification_allowed("exit"));
361 assert!(!state.is_notification_allowed("initialized"));
362 assert!(!state.is_notification_allowed("textDocument/didOpen"));
363 }
364
365 // =========================================================================
366 // ExitCode Tests
367 // =========================================================================
368
369 #[test]
370 fn exit_code_success_is_zero() {
371 assert_eq!(ExitCode::Success as i32, 0);
372 }
373
374 #[test]
375 fn exit_code_error_is_one() {
376 assert_eq!(ExitCode::Error as i32, 1);
377 }
378
379 #[test]
380 fn exit_code_is_copy() {
381 let code = ExitCode::Success;
382 let copy = code;
383 assert_eq!(code, copy);
384 }
385
386 // =========================================================================
387 // ProtocolError Tests
388 // =========================================================================
389
390 #[test]
391 fn protocol_error_expected_initialize_message() {
392 let err = ProtocolError::ExpectedInitialize("textDocument/hover".to_string());
393 let msg = err.to_string();
394 assert!(msg.contains("expected initialize request"));
395 assert!(msg.contains("textDocument/hover"));
396 }
397
398 #[test]
399 fn protocol_error_expected_initialized_message() {
400 let err = ProtocolError::ExpectedInitialized("textDocument/didOpen".to_string());
401 let msg = err.to_string();
402 assert!(msg.contains("expected initialized notification"));
403 assert!(msg.contains("textDocument/didOpen"));
404 }
405
406 #[test]
407 fn protocol_error_disconnected_message() {
408 let err = ProtocolError::Disconnected;
409 let msg = err.to_string();
410 assert!(msg.contains("disconnected unexpectedly"));
411 }
412
413 #[test]
414 fn protocol_error_after_shutdown_message() {
415 let err = ProtocolError::AfterShutdown("textDocument/hover".to_string());
416 let msg = err.to_string();
417 assert!(msg.contains("after shutdown"));
418 assert!(msg.contains("textDocument/hover"));
419 }
420
421 #[test]
422 fn protocol_error_request_timeout_message() {
423 let err = ProtocolError::RequestTimeout;
424 let msg = err.to_string();
425 assert!(msg.contains("timed out waiting for response to server request"));
426 }
427
428 #[test]
429 fn protocol_error_initialize_timeout_message() {
430 let err = ProtocolError::InitializeTimeout;
431 let msg = err.to_string();
432 assert!(msg.contains("timed out waiting for initialized notification"));
433 assert!(msg.contains("60s"));
434 }
435
436 #[test]
437 fn protocol_error_io_conversion() {
438 let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "connection lost");
439 let err: ProtocolError = io_err.into();
440 let msg = err.to_string();
441 assert!(msg.contains("I/O error"));
442 assert!(msg.contains("connection lost"));
443 }
444
445 #[test]
446 fn protocol_error_is_debug() {
447 let err = ProtocolError::Disconnected;
448 let debug = format!("{err:?}");
449 assert!(debug.contains("Disconnected"));
450 }
451}