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)]
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}