Skip to main content

mcpls_core/lsp/
client.rs

1//! LSP client implementation with async request/response handling.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicI64, Ordering};
6
7use serde::Serialize;
8use serde::de::DeserializeOwned;
9use serde_json::Value;
10use tokio::sync::{Mutex, mpsc, oneshot};
11use tokio::task::JoinHandle;
12use tokio::time::{Duration, timeout};
13use tracing::{debug, error, trace, warn};
14
15use crate::config::LspServerConfig;
16use crate::error::{Error, Result};
17use crate::lsp::transport::LspTransport;
18use crate::lsp::types::{
19    InboundMessage, JsonRpcError, JsonRpcRequest, JsonRpcResponse, LspNotification, RequestId,
20};
21
22/// JSON-RPC protocol version.
23const JSONRPC_VERSION: &str = "2.0";
24
25/// LSP error code returned when the server cancels a request and wants the client to retry.
26const SERVER_CANCELLED_CODE: i32 = -32802;
27
28/// Maximum number of retry attempts for server-cancelled requests.
29const SERVER_CANCELLED_MAX_RETRIES: u32 = 3;
30
31/// Initial backoff delay for server-cancelled retries (milliseconds).
32const SERVER_CANCELLED_INITIAL_DELAY_MS: u64 = 500;
33
34/// Type alias for pending request tracking map.
35type PendingRequests = HashMap<RequestId, oneshot::Sender<Result<Value>>>;
36
37/// LSP client with async request/response handling.
38///
39/// This client manages communication with an LSP server, handling:
40/// - Concurrent requests with unique ID tracking
41/// - Background message loop for receiving responses
42/// - Timeout support for all requests
43/// - Graceful shutdown
44#[derive(Debug)]
45pub struct LspClient {
46    /// Configuration for this LSP server.
47    config: LspServerConfig,
48
49    /// Current server state.
50    state: Arc<Mutex<super::ServerState>>,
51
52    /// Atomic counter for request IDs.
53    request_counter: Arc<AtomicI64>,
54
55    /// Command sender for outbound messages.
56    command_tx: mpsc::Sender<ClientCommand>,
57
58    /// Background receiver task handle.
59    receiver_task: Option<JoinHandle<Result<()>>>,
60}
61
62impl Clone for LspClient {
63    /// Creates a clone that shares the underlying connection.
64    ///
65    /// The clone does not own the receiver task and cannot perform shutdown.
66    /// All clones share the same command channel for sending requests.
67    fn clone(&self) -> Self {
68        Self {
69            config: self.config.clone(),
70            state: Arc::clone(&self.state),
71            request_counter: Arc::clone(&self.request_counter),
72            command_tx: self.command_tx.clone(),
73            receiver_task: None,
74        }
75    }
76}
77
78/// Commands for client control.
79enum ClientCommand {
80    /// Send a request and wait for response.
81    SendRequest {
82        request: JsonRpcRequest,
83        response_tx: oneshot::Sender<Result<Value>>,
84    },
85    /// Send a notification (no response expected).
86    SendNotification {
87        method: String,
88        params: Option<Value>,
89    },
90    /// Shutdown the client.
91    Shutdown,
92}
93
94impl LspClient {
95    /// Create a new LSP client with the given configuration.
96    ///
97    /// The client starts in an uninitialized state. Call `initialize()` to
98    /// start the server and complete the initialization handshake.
99    #[must_use]
100    pub fn new(config: LspServerConfig) -> Self {
101        // Placeholder channel - the receiver is intentionally dropped since
102        // the client starts uninitialized. A real channel is created when
103        // `from_transport` or `from_transport_with_notifications` is called.
104        let (command_tx, _command_rx) = mpsc::channel(1); // Minimal capacity for placeholder
105
106        Self {
107            config,
108            state: Arc::new(Mutex::new(super::ServerState::Uninitialized)),
109            request_counter: Arc::new(AtomicI64::new(1)),
110            command_tx,
111            receiver_task: None,
112        }
113    }
114
115    /// Create client from transport (for testing or custom spawning).
116    ///
117    /// This method initializes the background message loop with the provided transport.
118    #[cfg(test)]
119    pub(crate) fn from_transport(config: LspServerConfig, transport: LspTransport) -> Self {
120        let state = Arc::new(Mutex::new(super::ServerState::Initializing));
121        let request_counter = Arc::new(AtomicI64::new(1));
122        let pending_requests = Arc::new(Mutex::new(HashMap::new()));
123
124        let (command_tx, command_rx) = mpsc::channel(100);
125
126        let receiver_task = tokio::spawn(Self::message_loop(
127            transport,
128            command_rx,
129            pending_requests,
130            None,
131        ));
132
133        Self {
134            config,
135            state,
136            request_counter,
137            command_tx,
138            receiver_task: Some(receiver_task),
139        }
140    }
141
142    /// Create client from transport with notification forwarding.
143    ///
144    /// Notifications received from the LSP server will be parsed and sent
145    /// through the provided channel.
146    pub(crate) fn from_transport_with_notifications(
147        config: LspServerConfig,
148        transport: LspTransport,
149        notification_tx: mpsc::Sender<LspNotification>,
150    ) -> Self {
151        let state = Arc::new(Mutex::new(super::ServerState::Initializing));
152        let request_counter = Arc::new(AtomicI64::new(1));
153        let pending_requests = Arc::new(Mutex::new(HashMap::new()));
154
155        let (command_tx, command_rx) = mpsc::channel(100);
156
157        let receiver_task = tokio::spawn(Self::message_loop(
158            transport,
159            command_rx,
160            pending_requests,
161            Some(notification_tx),
162        ));
163
164        Self {
165            config,
166            state,
167            request_counter,
168            command_tx,
169            receiver_task: Some(receiver_task),
170        }
171    }
172
173    /// Get the language ID for this client.
174    #[must_use]
175    pub fn language_id(&self) -> &str {
176        &self.config.language_id
177    }
178
179    /// Get the current server state.
180    pub async fn state(&self) -> super::ServerState {
181        *self.state.lock().await
182    }
183
184    /// Send request and wait for response with timeout.
185    ///
186    /// Automatically retries up to 3 times when the server returns error code
187    /// -32802 (`ServerCancelled`) with `data.retriggerRequest == true`, using
188    /// exponential backoff starting at 500 ms.
189    ///
190    /// # Type Parameters
191    ///
192    /// * `P` - The type of the request parameters (must be serializable)
193    /// * `R` - The type of the response result (must be deserializable)
194    ///
195    /// # Errors
196    ///
197    /// Returns an error if:
198    /// - Server has shut down
199    /// - Request times out
200    /// - Response cannot be deserialized
201    /// - LSP server returns an error
202    pub async fn request<P, R>(
203        &self,
204        method: &str,
205        params: P,
206        timeout_duration: Duration,
207    ) -> Result<R>
208    where
209        P: Serialize,
210        R: DeserializeOwned,
211    {
212        let params_value = serde_json::to_value(params)?;
213        let mut delay_ms = SERVER_CANCELLED_INITIAL_DELAY_MS;
214
215        for attempt in 0..=SERVER_CANCELLED_MAX_RETRIES {
216            if attempt > 0 {
217                debug!(
218                    "Retrying {} after ServerCancelled (attempt {}/{}), backoff={}ms",
219                    method, attempt, SERVER_CANCELLED_MAX_RETRIES, delay_ms
220                );
221                tokio::time::sleep(Duration::from_millis(delay_ms)).await;
222                delay_ms *= 2;
223            }
224
225            let id = RequestId::Number(self.request_counter.fetch_add(1, Ordering::SeqCst));
226            let (response_tx, response_rx) = oneshot::channel();
227            let request = JsonRpcRequest {
228                jsonrpc: JSONRPC_VERSION.to_string(),
229                id: id.clone(),
230                method: method.to_string(),
231                params: Some(params_value.clone()),
232            };
233
234            debug!("Sending request: {} (id={:?})", method, id);
235
236            self.command_tx
237                .send(ClientCommand::SendRequest {
238                    request,
239                    response_tx,
240                })
241                .await
242                .map_err(|_| Error::ServerTerminated)?;
243
244            let outcome = timeout(timeout_duration, response_rx)
245                .await
246                .map_err(|_| Error::Timeout(timeout_duration.as_secs()))?
247                .map_err(|_| Error::ServerTerminated)?;
248
249            match outcome {
250                Ok(result_value) => {
251                    return serde_json::from_value(result_value).map_err(|e| {
252                        Error::LspProtocolError(format!("Failed to deserialize response: {e}"))
253                    });
254                }
255                Err(Error::LspServerError {
256                    code,
257                    ref message,
258                    ref data,
259                }) if code == SERVER_CANCELLED_CODE && Self::should_retrigger(data.as_ref()) => {
260                    warn!(
261                        "ServerCancelled (-32802) on '{}', will retry: {}",
262                        method, message
263                    );
264                    if attempt == SERVER_CANCELLED_MAX_RETRIES {
265                        return Err(Error::LspServerError {
266                            code,
267                            message: message.clone(),
268                            data: data.clone(),
269                        });
270                    }
271                    // continue loop for next attempt
272                }
273                Err(e) => return Err(e),
274            }
275        }
276
277        Err(Error::ServerTerminated)
278    }
279
280    /// Returns true when the error data from a `ServerCancelled` (-32802) response
281    /// indicates the server wants the client to retrigger the request.
282    ///
283    /// Per the LSP specification, `data.retriggerRequest == true` is the signal.
284    /// When `data` is absent (older servers), we default to retrying anyway because
285    /// code -32802 is exclusively used for this purpose.
286    fn should_retrigger(data: Option<&Value>) -> bool {
287        data.is_none_or(|v| {
288            v.get("retriggerRequest")
289                .and_then(Value::as_bool)
290                .unwrap_or(true)
291        })
292    }
293
294    /// Send notification (fire-and-forget, no response expected).
295    ///
296    /// # Errors
297    ///
298    /// Returns an error if the server has shut down.
299    pub async fn notify<P>(&self, method: &str, params: P) -> Result<()>
300    where
301        P: Serialize,
302    {
303        let params_value = serde_json::to_value(params)?;
304
305        debug!("Sending notification: {}", method);
306
307        self.command_tx
308            .send(ClientCommand::SendNotification {
309                method: method.to_string(),
310                params: Some(params_value),
311            })
312            .await
313            .map_err(|_| Error::ServerTerminated)?;
314
315        Ok(())
316    }
317
318    /// Shutdown client gracefully.
319    ///
320    /// This sends a shutdown command to the background task and waits for it to complete.
321    ///
322    /// # Errors
323    ///
324    /// Returns an error if the background task failed.
325    pub async fn shutdown(mut self) -> Result<()> {
326        debug!("Shutting down LSP client");
327
328        let _ = self.command_tx.send(ClientCommand::Shutdown).await;
329
330        if let Some(task) = self.receiver_task.take() {
331            task.await
332                .map_err(|e| Error::Transport(format!("Receiver task failed: {e}")))??;
333        }
334
335        *self.state.lock().await = super::ServerState::Shutdown;
336
337        Ok(())
338    }
339
340    /// Background task: handle message I/O.
341    ///
342    /// This task runs in the background, handling:
343    /// - Outbound requests and notifications
344    /// - Inbound responses and server notifications
345    /// - Matching responses to pending requests
346    async fn message_loop(
347        mut transport: LspTransport,
348        mut command_rx: mpsc::Receiver<ClientCommand>,
349        pending_requests: Arc<Mutex<PendingRequests>>,
350        notification_tx: Option<mpsc::Sender<LspNotification>>,
351    ) -> Result<()> {
352        debug!("Message loop started");
353        let result = Self::message_loop_inner(
354            &mut transport,
355            &mut command_rx,
356            &pending_requests,
357            notification_tx.as_ref(),
358        )
359        .await;
360        if let Err(ref e) = result {
361            error!("Message loop exiting with error: {}", e);
362        } else {
363            debug!("Message loop exiting normally");
364        }
365        result
366    }
367
368    async fn message_loop_inner(
369        transport: &mut LspTransport,
370        command_rx: &mut mpsc::Receiver<ClientCommand>,
371        pending_requests: &Arc<Mutex<PendingRequests>>,
372        notification_tx: Option<&mpsc::Sender<LspNotification>>,
373    ) -> Result<()> {
374        loop {
375            tokio::select! {
376                Some(command) = command_rx.recv() => {
377                    match command {
378                        ClientCommand::SendRequest { request, response_tx } => {
379                            pending_requests.lock().await.insert(
380                                request.id.clone(),
381                                response_tx,
382                            );
383
384                            let value = serde_json::to_value(&request)?;
385                            transport.send(&value).await?;
386                        }
387                        ClientCommand::SendNotification { method, params } => {
388                            let notification = serde_json::json!({
389                                "jsonrpc": "2.0",
390                                "method": method,
391                                "params": params,
392                            });
393                            transport.send(&notification).await?;
394                        }
395                        ClientCommand::Shutdown => {
396                            debug!("Client shutdown requested");
397                            break;
398                        }
399                    }
400                }
401
402                message = transport.receive() => {
403                    let message = match message {
404                        Ok(m) => m,
405                        Err(e) => {
406                            error!("Transport receive error: {}", e);
407                            return Err(e);
408                        }
409                    };
410                    match message {
411                        InboundMessage::Response(response) => {
412                            trace!("Received response: id={:?}", response.id);
413
414                            let sender = pending_requests.lock().await.remove(&response.id);
415
416                            if let Some(sender) = sender {
417                                if let Some(error) = response.error {
418                                    let message = if error.message.len() > 200 {
419                                        format!("{}... (truncated)", &error.message[..200])
420                                    } else {
421                                        error.message.clone()
422                                    };
423                                    error!("LSP error response: {} (code {})", message, error.code);
424                                    let _ = sender.send(Err(Error::LspServerError {
425                                        code: error.code,
426                                        message: error.message,
427                                        data: error.data,
428                                    }));
429                                } else if let Some(result) = response.result {
430                                    let _ = sender.send(Ok(result));
431                                } else {
432                                    // LSP spec allows null result for some requests (e.g., hover with no info).
433                                    // Treat as successful response with null value.
434                                    trace!("Response with null result: {:?}", response.id);
435                                    let _ = sender.send(Ok(Value::Null));
436                                }
437                            } else {
438                                warn!("Received response for unknown request ID: {:?}", response.id);
439                            }
440                        }
441                        InboundMessage::Request(request) => {
442                            debug!(
443                                "Received server request: {} (id={:?})",
444                                request.method, request.id
445                            );
446                            let response = Self::server_request_response(request);
447                            let value = serde_json::to_value(&response)?;
448                            transport.send(&value).await?;
449                        }
450                        InboundMessage::Notification(notification) => {
451                            debug!("Received notification: {}", notification.method);
452
453                            // Parse notification into typed variant
454                            let typed = LspNotification::parse(&notification.method, notification.params);
455
456                            // Forward to notification handler if sender is available
457                            if let Some(tx) = notification_tx {
458                                // Log diagnostics count since it's useful for debugging
459                                if let LspNotification::PublishDiagnostics(ref params) = typed {
460                                    debug!(
461                                        "Forwarding diagnostics for {}: {} items",
462                                        params.uri.as_str(),
463                                        params.diagnostics.len()
464                                    );
465                                } else {
466                                    trace!("Forwarding notification: {:?}", typed);
467                                }
468
469                                // Send the notification with backpressure handling
470                                if tx.try_send(typed).is_err() {
471                                    warn!("Notification channel full or closed, dropping notification");
472                                }
473                            }
474                        }
475                    }
476                }
477            }
478        }
479
480        Ok(())
481    }
482
483    fn server_request_response(request: JsonRpcRequest) -> JsonRpcResponse {
484        match Self::server_request_result(&request.method, request.params.as_ref()) {
485            Ok(result) => JsonRpcResponse {
486                jsonrpc: JSONRPC_VERSION.to_string(),
487                id: request.id,
488                result: Some(result),
489                error: None,
490            },
491            Err(error) => JsonRpcResponse {
492                jsonrpc: JSONRPC_VERSION.to_string(),
493                id: request.id,
494                result: None,
495                error: Some(error),
496            },
497        }
498    }
499
500    fn server_request_result(
501        method: &str,
502        params: Option<&Value>,
503    ) -> std::result::Result<Value, JsonRpcError> {
504        match method {
505            "client/registerCapability"
506            | "client/unregisterCapability"
507            | "workspace/workspaceFolders"
508            | "workspace/diagnostic/refresh"
509            | "workspace/semanticTokens/refresh"
510            | "workspace/inlayHint/refresh"
511            | "workspace/codeLens/refresh"
512            | "window/showMessageRequest" => Ok(Value::Null),
513            "workspace/configuration" => Ok(Self::workspace_configuration_result(params)),
514            "workspace/applyEdit" => Ok(serde_json::json!({ "applied": false })),
515            _ => Err(JsonRpcError {
516                code: -32601,
517                message: format!("Unhandled server request: {method}"),
518                data: None,
519            }),
520        }
521    }
522
523    fn workspace_configuration_result(params: Option<&Value>) -> Value {
524        let item_count = params
525            .and_then(|value| value.get("items"))
526            .and_then(Value::as_array)
527            .map_or(0, Vec::len);
528
529        Value::Array(vec![Value::Null; item_count])
530    }
531}
532
533#[cfg(test)]
534#[allow(clippy::unwrap_used)]
535mod tests {
536    use super::*;
537
538    #[test]
539    fn test_request_id_generation() {
540        let counter = AtomicI64::new(1);
541
542        let id1 = counter.fetch_add(1, Ordering::SeqCst);
543        let id2 = counter.fetch_add(1, Ordering::SeqCst);
544        let id3 = counter.fetch_add(1, Ordering::SeqCst);
545
546        assert_eq!(id1, 1);
547        assert_eq!(id2, 2);
548        assert_eq!(id3, 3);
549    }
550
551    #[test]
552    fn test_client_creation() {
553        let config = LspServerConfig::rust_analyzer();
554
555        let client = LspClient::new(config);
556        assert_eq!(client.language_id(), "rust");
557    }
558
559    #[test]
560    fn test_client_clone() {
561        let config = LspServerConfig::rust_analyzer();
562        let client = LspClient::new(config);
563
564        #[allow(clippy::redundant_clone)]
565        let cloned = client.clone();
566        assert_eq!(cloned.language_id(), "rust");
567
568        assert!(
569            cloned.receiver_task.is_none(),
570            "Cloned client should not own receiver task"
571        );
572    }
573
574    #[test]
575    fn test_register_capability_request_is_acknowledged() {
576        let request = JsonRpcRequest {
577            jsonrpc: JSONRPC_VERSION.to_string(),
578            id: RequestId::String("ts1".to_string()),
579            method: "client/registerCapability".to_string(),
580            params: Some(serde_json::json!({ "registrations": [] })),
581        };
582
583        let response = LspClient::server_request_response(request);
584
585        assert_eq!(response.id, RequestId::String("ts1".to_string()));
586        assert_eq!(response.result, Some(Value::Null));
587        assert!(response.error.is_none());
588    }
589
590    #[test]
591    fn test_workspace_configuration_request_returns_null_per_item() {
592        let result = LspClient::workspace_configuration_result(Some(&serde_json::json!({
593            "items": [{ "section": "typescript" }, { "section": "editor" }]
594        })));
595
596        assert_eq!(result, serde_json::json!([null, null]));
597    }
598
599    #[test]
600    fn test_unknown_server_request_returns_method_not_found() {
601        let request = JsonRpcRequest {
602            jsonrpc: JSONRPC_VERSION.to_string(),
603            id: RequestId::String("unknown-1".to_string()),
604            method: "custom/request".to_string(),
605            params: None,
606        };
607
608        let response = LspClient::server_request_response(request);
609
610        assert!(response.result.is_none());
611        match response.error {
612            Some(error) => {
613                assert_eq!(error.code, -32601);
614                assert_eq!(error.message, "Unhandled server request: custom/request");
615            }
616            None => panic!("unknown request should return error"),
617        }
618    }
619
620    #[tokio::test]
621    async fn test_null_response_handling() {
622        use crate::lsp::types::{JsonRpcResponse, RequestId};
623
624        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
625
626        let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
627
628        pending_requests
629            .lock()
630            .await
631            .insert(RequestId::Number(1), response_tx);
632
633        let null_response = JsonRpcResponse {
634            jsonrpc: "2.0".to_string(),
635            id: RequestId::Number(1),
636            result: None,
637            error: None,
638        };
639
640        let sender = pending_requests.lock().await.remove(&null_response.id);
641        if let Some(sender) = sender {
642            let _ = sender.send(Ok(Value::Null));
643        }
644
645        let timeout_result =
646            tokio::time::timeout(tokio::time::Duration::from_millis(100), response_rx).await;
647
648        assert!(timeout_result.is_ok(), "Should not timeout");
649
650        let channel_result = timeout_result.unwrap();
651        assert!(
652            channel_result.is_ok(),
653            "Channel should not be closed: {:?}",
654            channel_result.err()
655        );
656
657        let response = channel_result.unwrap();
658        assert!(
659            response.is_ok(),
660            "Should receive Ok(Value::Null), not Err: {:?}",
661            response.err()
662        );
663
664        let value = response.unwrap();
665        assert_eq!(value, Value::Null, "Should receive Value::Null");
666    }
667
668    #[tokio::test]
669    async fn test_error_response_handling() {
670        use crate::lsp::types::{JsonRpcError, JsonRpcResponse, RequestId};
671
672        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
673        let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
674
675        pending_requests
676            .lock()
677            .await
678            .insert(RequestId::Number(1), response_tx);
679
680        let error_response = JsonRpcResponse {
681            jsonrpc: "2.0".to_string(),
682            id: RequestId::Number(1),
683            result: None,
684            error: Some(JsonRpcError {
685                code: -32601,
686                message: "Method not found".to_string(),
687                data: None,
688            }),
689        };
690
691        let sender = pending_requests.lock().await.remove(&error_response.id);
692        if let Some(sender) = sender
693            && let Some(error) = error_response.error
694        {
695            let _ = sender.send(Err(Error::LspServerError {
696                code: error.code,
697                message: error.message,
698                data: error.data,
699            }));
700        }
701
702        let result = response_rx.await.unwrap();
703        assert!(result.is_err(), "Should receive error");
704
705        if let Err(Error::LspServerError { code, message, .. }) = result {
706            assert_eq!(code, -32601);
707            assert_eq!(message, "Method not found");
708        } else {
709            panic!("Expected LspServerError");
710        }
711    }
712
713    #[tokio::test]
714    async fn test_unknown_request_id() {
715        use crate::lsp::types::{JsonRpcResponse, RequestId};
716
717        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
718
719        let response = JsonRpcResponse {
720            jsonrpc: "2.0".to_string(),
721            id: RequestId::Number(999),
722            result: Some(Value::Null),
723            error: None,
724        };
725
726        let sender = pending_requests.lock().await.remove(&response.id);
727        assert!(sender.is_none(), "Should not find sender for unknown ID");
728    }
729
730    #[tokio::test]
731    async fn test_long_error_message_truncation() {
732        use crate::lsp::types::{JsonRpcError, JsonRpcResponse, RequestId};
733
734        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
735        let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
736
737        pending_requests
738            .lock()
739            .await
740            .insert(RequestId::Number(1), response_tx);
741
742        let long_message = "x".repeat(250);
743        let error_response = JsonRpcResponse {
744            jsonrpc: "2.0".to_string(),
745            id: RequestId::Number(1),
746            result: None,
747            error: Some(JsonRpcError {
748                code: -32700,
749                message: long_message.clone(),
750                data: None,
751            }),
752        };
753
754        let sender = pending_requests.lock().await.remove(&error_response.id);
755        if let Some(sender) = sender
756            && let Some(error) = error_response.error
757        {
758            let _ = sender.send(Err(Error::LspServerError {
759                code: error.code,
760                message: error.message,
761                data: error.data,
762            }));
763        }
764
765        let result = response_rx.await.unwrap();
766        assert!(result.is_err());
767
768        if let Err(Error::LspServerError { code, message, .. }) = result {
769            assert_eq!(code, -32700);
770            assert_eq!(
771                message.len(),
772                250,
773                "Full message should be preserved in Error"
774            );
775        } else {
776            panic!("Expected LspServerError");
777        }
778    }
779
780    #[tokio::test]
781    async fn test_concurrent_request_ids() {
782        let counter = Arc::new(AtomicI64::new(1));
783
784        let counter1 = Arc::clone(&counter);
785        let counter2 = Arc::clone(&counter);
786        let counter3 = Arc::clone(&counter);
787
788        let handles = vec![
789            tokio::spawn(async move { counter1.fetch_add(1, Ordering::SeqCst) }),
790            tokio::spawn(async move { counter2.fetch_add(1, Ordering::SeqCst) }),
791            tokio::spawn(async move { counter3.fetch_add(1, Ordering::SeqCst) }),
792        ];
793
794        let mut ids = Vec::new();
795        for handle in handles {
796            ids.push(handle.await.unwrap());
797        }
798
799        ids.sort_unstable();
800        assert_eq!(ids, vec![1, 2, 3], "IDs should be unique and sequential");
801    }
802
803    #[test]
804    fn test_jsonrpc_version_constant() {
805        assert_eq!(JSONRPC_VERSION, "2.0");
806    }
807}