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