Skip to main content

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}