1use std::collections::HashMap;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicI64, Ordering};
6
7use lsp_types::LspErrorCodes;
8use serde::Serialize;
9use serde::de::DeserializeOwned;
10use serde_json::Value;
11use tokio::sync::{Mutex, mpsc, oneshot};
12use tokio::task::JoinHandle;
13use tokio::time::{Duration, timeout};
14use tracing::{debug, error, trace, warn};
15
16use crate::config::LspServerConfig;
17use crate::error::{Error, Result};
18use crate::lsp::transport::LspTransport;
19use crate::lsp::types::{
20 InboundMessage, JsonRpcError, JsonRpcRequest, JsonRpcResponse, LspNotification, RequestId,
21};
22
23const JSONRPC_VERSION: &str = "2.0";
25
26const SERVER_CANCELLED_CODE: i32 = -32802;
28
29const SERVER_CANCELLED_MAX_RETRIES: u32 = 3;
31
32const SERVER_CANCELLED_INITIAL_DELAY_MS: u64 = 500;
34
35pub const CONTENT_MODIFIED_RETRY_METHODS: &[&str] = &[
58 "textDocument/signatureHelp",
59 "textDocument/inlayHint",
60 "textDocument/completion",
61 "textDocument/prepareCallHierarchy",
62 "callHierarchy/incomingCalls",
63 "callHierarchy/outgoingCalls",
64 "textDocument/diagnostic",
65 "textDocument/hover",
66 "textDocument/definition",
67 "textDocument/references",
68 "textDocument/implementation",
69 "textDocument/typeDefinition",
70 "textDocument/documentSymbol",
71 "workspace/symbol",
72];
73
74const MAX_ERROR_MESSAGE_LOG_BYTES: usize = 200;
81
82const MAX_ERROR_MESSAGE_CALLER_BYTES: usize = 4 * 1024;
94
95const COMPLETION_TIMEOUT_CAP: Duration = Duration::from_secs(10);
103
104type PendingRequests = HashMap<RequestId, oneshot::Sender<Result<Value>>>;
106
107#[derive(Debug)]
115pub struct LspClient {
116 config: LspServerConfig,
118
119 state: Arc<Mutex<super::ServerState>>,
121
122 request_counter: Arc<AtomicI64>,
124
125 command_tx: mpsc::Sender<ClientCommand>,
127
128 pending_requests: Arc<Mutex<PendingRequests>>,
136
137 receiver_task: Option<JoinHandle<Result<()>>>,
139}
140
141impl Clone for LspClient {
142 fn clone(&self) -> Self {
147 Self {
148 config: self.config.clone(),
149 state: Arc::clone(&self.state),
150 request_counter: Arc::clone(&self.request_counter),
151 command_tx: self.command_tx.clone(),
152 pending_requests: Arc::clone(&self.pending_requests),
153 receiver_task: None,
154 }
155 }
156}
157
158enum ClientCommand {
160 SendRequest {
162 request: JsonRpcRequest,
163 response_tx: oneshot::Sender<Result<Value>>,
164 },
165 SendNotification {
167 method: String,
168 params: Option<Value>,
169 },
170 Shutdown,
172}
173
174impl LspClient {
175 #[must_use]
180 pub fn new(config: LspServerConfig) -> Self {
181 let (command_tx, _command_rx) = mpsc::channel(1); Self {
187 config,
188 state: Arc::new(Mutex::new(super::ServerState::Uninitialized)),
189 request_counter: Arc::new(AtomicI64::new(1)),
190 command_tx,
191 pending_requests: Arc::new(Mutex::new(HashMap::new())),
192 receiver_task: None,
193 }
194 }
195
196 #[cfg(test)]
200 pub(crate) fn from_transport(config: LspServerConfig, transport: LspTransport) -> Self {
201 let state = Arc::new(Mutex::new(super::ServerState::Initializing));
202 let request_counter = Arc::new(AtomicI64::new(1));
203 let pending_requests = Arc::new(Mutex::new(HashMap::new()));
204
205 let (command_tx, command_rx) = mpsc::channel(100);
206
207 let receiver_task = tokio::spawn(Self::message_loop(
208 transport,
209 command_rx,
210 Arc::clone(&pending_requests),
211 None,
212 ));
213
214 Self {
215 config,
216 state,
217 request_counter,
218 command_tx,
219 pending_requests,
220 receiver_task: Some(receiver_task),
221 }
222 }
223
224 pub(crate) fn from_transport_with_notifications(
229 config: LspServerConfig,
230 transport: LspTransport,
231 notification_tx: mpsc::Sender<LspNotification>,
232 ) -> Self {
233 let state = Arc::new(Mutex::new(super::ServerState::Initializing));
234 let request_counter = Arc::new(AtomicI64::new(1));
235 let pending_requests = Arc::new(Mutex::new(HashMap::new()));
236
237 let (command_tx, command_rx) = mpsc::channel(100);
238
239 let receiver_task = tokio::spawn(Self::message_loop(
240 transport,
241 command_rx,
242 Arc::clone(&pending_requests),
243 Some(notification_tx),
244 ));
245
246 Self {
247 config,
248 state,
249 request_counter,
250 command_tx,
251 pending_requests,
252 receiver_task: Some(receiver_task),
253 }
254 }
255
256 #[must_use]
258 pub fn language_id(&self) -> &str {
259 &self.config.language_id
260 }
261
262 pub async fn state(&self) -> super::ServerState {
264 *self.state.lock().await
265 }
266
267 #[must_use]
312 pub fn request_timeout(&self) -> Duration {
313 Duration::from_secs(
314 self.config
315 .request_timeout_seconds
316 .clamp(1, crate::config::MAX_TIMEOUT_SECONDS),
317 )
318 }
319
320 #[must_use]
344 pub fn completion_timeout(&self) -> Duration {
345 self.request_timeout().min(COMPLETION_TIMEOUT_CAP)
346 }
347
348 pub async fn request<P, R>(
377 &self,
378 method: &str,
379 params: P,
380 timeout_duration: Duration,
381 ) -> Result<R>
382 where
383 P: Serialize,
384 R: DeserializeOwned,
385 {
386 let params_value = serde_json::to_value(params)?;
387 let mut delay_ms = SERVER_CANCELLED_INITIAL_DELAY_MS;
388
389 for attempt in 0..=SERVER_CANCELLED_MAX_RETRIES {
390 if attempt > 0 {
391 debug!(
392 "Retrying {} (attempt {}/{}), backoff={}ms",
393 method, attempt, SERVER_CANCELLED_MAX_RETRIES, delay_ms
394 );
395 tokio::time::sleep(Duration::from_millis(delay_ms)).await;
396 delay_ms *= 2;
397 }
398
399 let id = RequestId::Number(self.request_counter.fetch_add(1, Ordering::SeqCst));
400 let (response_tx, response_rx) = oneshot::channel();
401 let request = JsonRpcRequest {
402 jsonrpc: JSONRPC_VERSION.to_string(),
403 id: id.clone(),
404 method: method.to_string(),
405 params: Some(params_value.clone()),
406 };
407
408 debug!("Sending request: {} (id={:?})", method, id);
409
410 self.command_tx
411 .send(ClientCommand::SendRequest {
412 request,
413 response_tx,
414 })
415 .await
416 .map_err(|_| Error::ServerTerminated)?;
417
418 let outcome = match timeout(timeout_duration, response_rx).await {
419 Ok(received) => received.map_err(|_| Error::ServerTerminated)?,
420 Err(_elapsed) => {
421 self.pending_requests.lock().await.remove(&id);
426 return Err(Error::Timeout(timeout_duration.as_secs()));
427 }
428 };
429
430 match outcome {
431 Ok(result_value) => {
432 return serde_json::from_value(result_value).map_err(|e| {
433 Error::LspProtocolError(format!("Failed to deserialize response: {e}"))
434 });
435 }
436 Err(Error::LspServerError {
437 code,
438 message,
439 data,
440 }) if (code == SERVER_CANCELLED_CODE
441 || (LspErrorCodes::from(code) == LspErrorCodes::ContentModified
442 && CONTENT_MODIFIED_RETRY_METHODS.contains(&method)))
443 && Self::should_retrigger(data.as_ref()) =>
444 {
445 if attempt == SERVER_CANCELLED_MAX_RETRIES {
446 error!(
451 "LSP error response: {} (code {}) on '{}' (id={:?}), retries exhausted",
452 Self::truncate_error_message_for_log(&message),
453 code,
454 method,
455 id
456 );
457 return Err(Error::LspServerError {
458 code,
459 message,
460 data,
461 });
462 }
463 warn!(
464 "LSP error response: {} (code {}) on '{}' (id={:?}), will retry",
465 Self::truncate_error_message_for_log(&message),
466 code,
467 method,
468 id
469 );
470 }
472 Err(Error::LspServerError {
473 code,
474 message,
475 data,
476 }) => {
477 error!(
478 "LSP error response: {} (code {}) on '{}' (id={:?})",
479 Self::truncate_error_message_for_log(&message),
480 code,
481 method,
482 id
483 );
484 return Err(Error::LspServerError {
485 code,
486 message,
487 data,
488 });
489 }
490 Err(e) => return Err(e),
491 }
492 }
493
494 Err(Error::ServerTerminated)
495 }
496
497 pub async fn request_typed<R>(
507 &self,
508 params: R::Params,
509 timeout_duration: Duration,
510 ) -> Result<R::Result>
511 where
512 R: lsp_types::Request,
513 {
514 self.request(R::METHOD.as_str(), params, timeout_duration)
515 .await
516 }
517
518 fn should_retrigger(data: Option<&Value>) -> bool {
532 data.is_none_or(|v| {
533 v.get("retriggerRequest")
534 .and_then(Value::as_bool)
535 .unwrap_or(true)
536 })
537 }
538
539 pub(crate) async fn fail_pending_requests(&self) {
547 let mut pending = self.pending_requests.lock().await;
548 for (_, sender) in pending.drain() {
549 let _ = sender.send(Err(Error::ServerTerminated));
550 }
551 }
552
553 pub async fn notify<P>(&self, method: &str, params: P) -> Result<()>
559 where
560 P: Serialize,
561 {
562 let params_value = serde_json::to_value(params)?;
563
564 debug!("Sending notification: {}", method);
565
566 self.command_tx
567 .send(ClientCommand::SendNotification {
568 method: method.to_string(),
569 params: Some(params_value),
570 })
571 .await
572 .map_err(|_| Error::ServerTerminated)?;
573
574 Ok(())
575 }
576
577 pub async fn shutdown(mut self) -> Result<()> {
585 debug!("Shutting down LSP client");
586
587 let _ = self.command_tx.send(ClientCommand::Shutdown).await;
588
589 if let Some(task) = self.receiver_task.take() {
590 task.await
591 .map_err(|e| Error::Transport(format!("Receiver task failed: {e}")))??;
592 }
593
594 *self.state.lock().await = super::ServerState::Shutdown;
595
596 Ok(())
597 }
598
599 async fn message_loop(
606 mut transport: LspTransport,
607 mut command_rx: mpsc::Receiver<ClientCommand>,
608 pending_requests: Arc<Mutex<PendingRequests>>,
609 notification_tx: Option<mpsc::Sender<LspNotification>>,
610 ) -> Result<()> {
611 debug!("Message loop started");
612 let result = Self::message_loop_inner(
613 &mut transport,
614 &mut command_rx,
615 &pending_requests,
616 notification_tx.as_ref(),
617 )
618 .await;
619 if let Err(ref e) = result {
620 error!("Message loop exiting with error: {}", e);
621 } else {
622 debug!("Message loop exiting normally");
623 }
624 result
625 }
626
627 fn truncate_error_message_for_log(message: &str) -> String {
635 crate::util::truncate_str(message, MAX_ERROR_MESSAGE_LOG_BYTES)
636 }
637
638 async fn message_loop_inner(
639 transport: &mut LspTransport,
640 command_rx: &mut mpsc::Receiver<ClientCommand>,
641 pending_requests: &Arc<Mutex<PendingRequests>>,
642 notification_tx: Option<&mpsc::Sender<LspNotification>>,
643 ) -> Result<()> {
644 loop {
645 tokio::select! {
646 Some(command) = command_rx.recv() => {
647 match command {
648 ClientCommand::SendRequest { request, response_tx } => {
649 pending_requests.lock().await.insert(
650 request.id.clone(),
651 response_tx,
652 );
653
654 let value = serde_json::to_value(&request)?;
655 transport.send(&value).await?;
656 }
657 ClientCommand::SendNotification { method, params } => {
658 let notification = serde_json::json!({
659 "jsonrpc": "2.0",
660 "method": method,
661 "params": params,
662 });
663 transport.send(¬ification).await?;
664 }
665 ClientCommand::Shutdown => {
666 debug!("Client shutdown requested");
667 break;
668 }
669 }
670 }
671
672 message = transport.receive() => {
673 let message = match message {
674 Ok(m) => m,
675 Err(e) => {
676 error!("Transport receive error: {}", e);
677 return Err(e);
678 }
679 };
680 match message {
681 InboundMessage::Response(response) => {
682 trace!("Received response: id={:?}", response.id);
683
684 let sender = pending_requests.lock().await.remove(&response.id);
685
686 if let Some(sender) = sender {
687 if let Some(error) = response.error {
688 trace!(
699 "LSP error response: {} (code {})",
700 Self::truncate_error_message_for_log(&error.message),
701 error.code
702 );
703 let caller_message = crate::util::truncate_str(
709 &error.message,
710 MAX_ERROR_MESSAGE_CALLER_BYTES,
711 );
712 let _ = sender.send(Err(Error::LspServerError {
713 code: error.code,
714 message: caller_message,
715 data: error.data,
716 }));
717 } else if let Some(result) = response.result {
718 let _ = sender.send(Ok(result));
719 } else {
720 trace!("Response with null result: {:?}", response.id);
723 let _ = sender.send(Ok(Value::Null));
724 }
725 } else {
726 warn!("Received response for unknown request ID: {:?}", response.id);
727 }
728 }
729 InboundMessage::Request(request) => {
730 debug!(
731 "Received server request: {} (id={:?})",
732 request.method, request.id
733 );
734 let response = Self::server_request_response(request);
735 let value = serde_json::to_value(&response)?;
736 transport.send(&value).await?;
737 }
738 InboundMessage::Notification(notification) => {
739 debug!("Received notification: {}", notification.method);
740
741 let typed = LspNotification::parse(¬ification.method, notification.params);
743
744 if let Some(tx) = notification_tx {
746 if let LspNotification::PublishDiagnostics(ref params) = typed {
748 debug!(
749 "Forwarding diagnostics for {}: {} items",
750 params.uri.as_ref(),
751 params.diagnostics.len()
752 );
753 } else {
754 trace!("Forwarding notification: {:?}", typed);
755 }
756
757 if tx.try_send(typed).is_err() {
759 warn!("Notification channel full or closed, dropping notification");
760 }
761 }
762 }
763 }
764 }
765 }
766 }
767
768 Ok(())
769 }
770
771 fn server_request_response(request: JsonRpcRequest) -> JsonRpcResponse {
772 match Self::server_request_result(&request.method, request.params.as_ref()) {
773 Ok(result) => JsonRpcResponse {
774 jsonrpc: JSONRPC_VERSION.to_string(),
775 id: request.id,
776 result: Some(result),
777 error: None,
778 },
779 Err(error) => JsonRpcResponse {
780 jsonrpc: JSONRPC_VERSION.to_string(),
781 id: request.id,
782 result: None,
783 error: Some(error),
784 },
785 }
786 }
787
788 fn server_request_result(
789 method: &str,
790 params: Option<&Value>,
791 ) -> std::result::Result<Value, JsonRpcError> {
792 match method {
793 "client/registerCapability"
794 | "client/unregisterCapability"
795 | "workspace/workspaceFolders"
796 | "workspace/diagnostic/refresh"
797 | "workspace/semanticTokens/refresh"
798 | "workspace/inlayHint/refresh"
799 | "workspace/codeLens/refresh"
800 | "window/showMessageRequest" => Ok(Value::Null),
801 "workspace/configuration" => Ok(Self::workspace_configuration_result(params)),
802 "workspace/applyEdit" => Ok(serde_json::json!({ "applied": false })),
803 _ => Err(JsonRpcError {
804 code: -32601,
805 message: format!("Unhandled server request: {method}"),
806 data: None,
807 }),
808 }
809 }
810
811 fn workspace_configuration_result(params: Option<&Value>) -> Value {
812 let item_count = params
813 .and_then(|value| value.get("items"))
814 .and_then(Value::as_array)
815 .map_or(0, Vec::len);
816
817 Value::Array(vec![Value::Null; item_count])
818 }
819}
820
821#[cfg(test)]
822#[allow(clippy::unwrap_used)]
823mod tests {
824 use super::*;
825
826 #[test]
827 fn test_request_id_generation() {
828 let counter = AtomicI64::new(1);
829
830 let id1 = counter.fetch_add(1, Ordering::SeqCst);
831 let id2 = counter.fetch_add(1, Ordering::SeqCst);
832 let id3 = counter.fetch_add(1, Ordering::SeqCst);
833
834 assert_eq!(id1, 1);
835 assert_eq!(id2, 2);
836 assert_eq!(id3, 3);
837 }
838
839 #[test]
840 fn test_client_creation() {
841 let config = LspServerConfig::rust_analyzer();
842
843 let client = LspClient::new(config);
844 assert_eq!(client.language_id(), "rust");
845 }
846
847 #[test]
848 fn test_client_clone() {
849 let config = LspServerConfig::rust_analyzer();
850 let client = LspClient::new(config);
851
852 #[allow(clippy::redundant_clone)]
853 let cloned = client.clone();
854 assert_eq!(cloned.language_id(), "rust");
855
856 assert!(
857 cloned.receiver_task.is_none(),
858 "Cloned client should not own receiver task"
859 );
860 }
861
862 #[test]
863 fn test_request_timeout_and_completion_timeout_at_default() {
864 let config = LspServerConfig::rust_analyzer();
865 let client = LspClient::new(config);
866
867 assert_eq!(client.request_timeout(), Duration::from_secs(30));
868 assert_eq!(client.completion_timeout(), Duration::from_secs(10));
869 }
870
871 #[test]
872 fn test_completion_timeout_clamps_to_ten_seconds() {
873 for secs in [1, 2, 3, 30, 300] {
874 let mut config = LspServerConfig::rust_analyzer();
875 config.request_timeout_seconds = secs;
876 let client = LspClient::new(config);
877
878 assert_eq!(
879 client.completion_timeout(),
880 Duration::from_secs(secs.min(10)),
881 "request_timeout_seconds={secs}"
882 );
883 assert!(client.completion_timeout() <= client.request_timeout());
884 }
885 }
886
887 #[test]
888 fn test_request_timeout_clamps_zero_to_one_second() {
889 let mut config = LspServerConfig::rust_analyzer();
890 config.request_timeout_seconds = 0;
891 let client = LspClient::new(config);
892
893 assert_eq!(client.request_timeout(), Duration::from_secs(1));
894 assert_eq!(client.completion_timeout(), Duration::from_secs(1));
895 }
896
897 #[test]
898 fn test_request_timeout_clamps_above_max_to_max() {
899 let mut config = LspServerConfig::rust_analyzer();
900 config.request_timeout_seconds = u64::MAX;
901 let client = LspClient::new(config);
902
903 assert_eq!(
904 client.request_timeout(),
905 Duration::from_secs(crate::config::MAX_TIMEOUT_SECONDS)
906 );
907 }
908
909 #[test]
910 fn test_request_timeout_independent_per_server() {
911 let mut config_a = LspServerConfig::rust_analyzer();
912 config_a.request_timeout_seconds = 5;
913 let mut config_b = LspServerConfig::pyright();
914 config_b.request_timeout_seconds = 15;
915
916 let client_a = LspClient::new(config_a);
917 let client_b = LspClient::new(config_b);
918
919 assert_eq!(client_a.request_timeout(), Duration::from_secs(5));
920 assert_eq!(client_b.request_timeout(), Duration::from_secs(15));
921 }
922
923 #[test]
924 fn test_register_capability_request_is_acknowledged() {
925 let request = JsonRpcRequest {
926 jsonrpc: JSONRPC_VERSION.to_string(),
927 id: RequestId::String("ts1".to_string()),
928 method: "client/registerCapability".to_string(),
929 params: Some(serde_json::json!({ "registrations": [] })),
930 };
931
932 let response = LspClient::server_request_response(request);
933
934 assert_eq!(response.id, RequestId::String("ts1".to_string()));
935 assert_eq!(response.result, Some(Value::Null));
936 assert!(response.error.is_none());
937 }
938
939 #[test]
940 fn test_workspace_configuration_request_returns_null_per_item() {
941 let result = LspClient::workspace_configuration_result(Some(&serde_json::json!({
942 "items": [{ "section": "typescript" }, { "section": "editor" }]
943 })));
944
945 assert_eq!(result, serde_json::json!([null, null]));
946 }
947
948 #[test]
949 fn test_unknown_server_request_returns_method_not_found() {
950 let request = JsonRpcRequest {
951 jsonrpc: JSONRPC_VERSION.to_string(),
952 id: RequestId::String("unknown-1".to_string()),
953 method: "custom/request".to_string(),
954 params: None,
955 };
956
957 let response = LspClient::server_request_response(request);
958
959 assert!(response.result.is_none());
960 match response.error {
961 Some(error) => {
962 assert_eq!(error.code, -32601);
963 assert_eq!(error.message, "Unhandled server request: custom/request");
964 }
965 None => panic!("unknown request should return error"),
966 }
967 }
968
969 #[tokio::test]
970 async fn test_null_response_handling() {
971 use crate::lsp::types::{JsonRpcResponse, RequestId};
972
973 let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
974
975 let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
976
977 pending_requests
978 .lock()
979 .await
980 .insert(RequestId::Number(1), response_tx);
981
982 let null_response = JsonRpcResponse {
983 jsonrpc: "2.0".to_string(),
984 id: RequestId::Number(1),
985 result: None,
986 error: None,
987 };
988
989 let sender = pending_requests.lock().await.remove(&null_response.id);
990 if let Some(sender) = sender {
991 let _ = sender.send(Ok(Value::Null));
992 }
993
994 let timeout_result =
995 tokio::time::timeout(tokio::time::Duration::from_millis(100), response_rx).await;
996
997 assert!(timeout_result.is_ok(), "Should not timeout");
998
999 let channel_result = timeout_result.unwrap();
1000 assert!(
1001 channel_result.is_ok(),
1002 "Channel should not be closed: {:?}",
1003 channel_result.err()
1004 );
1005
1006 let response = channel_result.unwrap();
1007 assert!(
1008 response.is_ok(),
1009 "Should receive Ok(Value::Null), not Err: {:?}",
1010 response.err()
1011 );
1012
1013 let value = response.unwrap();
1014 assert_eq!(value, Value::Null, "Should receive Value::Null");
1015 }
1016
1017 #[tokio::test]
1018 async fn test_error_response_handling() {
1019 use crate::lsp::types::{JsonRpcError, JsonRpcResponse, RequestId};
1020
1021 let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
1022 let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
1023
1024 pending_requests
1025 .lock()
1026 .await
1027 .insert(RequestId::Number(1), response_tx);
1028
1029 let error_response = JsonRpcResponse {
1030 jsonrpc: "2.0".to_string(),
1031 id: RequestId::Number(1),
1032 result: None,
1033 error: Some(JsonRpcError {
1034 code: -32601,
1035 message: "Method not found".to_string(),
1036 data: None,
1037 }),
1038 };
1039
1040 let sender = pending_requests.lock().await.remove(&error_response.id);
1041 if let Some(sender) = sender
1042 && let Some(error) = error_response.error
1043 {
1044 let _ = sender.send(Err(Error::LspServerError {
1045 code: error.code,
1046 message: error.message,
1047 data: error.data,
1048 }));
1049 }
1050
1051 let result = response_rx.await.unwrap();
1052 assert!(result.is_err(), "Should receive error");
1053
1054 if let Err(Error::LspServerError { code, message, .. }) = result {
1055 assert_eq!(code, -32601);
1056 assert_eq!(message, "Method not found");
1057 } else {
1058 panic!("Expected LspServerError");
1059 }
1060 }
1061
1062 #[tokio::test]
1063 async fn test_unknown_request_id() {
1064 use crate::lsp::types::{JsonRpcResponse, RequestId};
1065
1066 let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
1067
1068 let response = JsonRpcResponse {
1069 jsonrpc: "2.0".to_string(),
1070 id: RequestId::Number(999),
1071 result: Some(Value::Null),
1072 error: None,
1073 };
1074
1075 let sender = pending_requests.lock().await.remove(&response.id);
1076 assert!(sender.is_none(), "Should not find sender for unknown ID");
1077 }
1078
1079 #[test]
1080 fn test_truncate_error_message_for_log_handles_multibyte_boundary() {
1081 let message = format!("{}€{}", "x".repeat(199), "y".repeat(50));
1083
1084 let truncated = LspClient::truncate_error_message_for_log(&message);
1085
1086 assert_eq!(truncated, format!("{}... (truncated)", "x".repeat(199)));
1089 }
1090
1091 #[test]
1092 fn test_truncate_error_message_for_log_no_truncation_at_or_below_limit() {
1093 let exact = "x".repeat(200);
1094 assert_eq!(LspClient::truncate_error_message_for_log(&exact), exact);
1095 assert_eq!(LspClient::truncate_error_message_for_log(""), "");
1096 }
1097
1098 #[test]
1099 fn test_truncate_error_message_for_log_truncates_just_above_limit() {
1100 let message = "x".repeat(201);
1101 assert_eq!(
1102 LspClient::truncate_error_message_for_log(&message),
1103 format!("{}... (truncated)", "x".repeat(200))
1104 );
1105 }
1106
1107 #[test]
1108 fn test_truncate_error_message_for_log_handles_wide_char_at_limit() {
1109 let message = format!("{}{}", "x".repeat(197), "🦀".repeat(10));
1111
1112 let truncated = LspClient::truncate_error_message_for_log(&message);
1113
1114 assert_eq!(truncated, format!("{}... (truncated)", "x".repeat(197)));
1115 }
1116
1117 #[tokio::test]
1118 async fn test_concurrent_request_ids() {
1119 let counter = Arc::new(AtomicI64::new(1));
1120
1121 let counter1 = Arc::clone(&counter);
1122 let counter2 = Arc::clone(&counter);
1123 let counter3 = Arc::clone(&counter);
1124
1125 let handles = vec![
1126 tokio::spawn(async move { counter1.fetch_add(1, Ordering::SeqCst) }),
1127 tokio::spawn(async move { counter2.fetch_add(1, Ordering::SeqCst) }),
1128 tokio::spawn(async move { counter3.fetch_add(1, Ordering::SeqCst) }),
1129 ];
1130
1131 let mut ids = Vec::new();
1132 for handle in handles {
1133 ids.push(handle.await.unwrap());
1134 }
1135
1136 ids.sort_unstable();
1137 assert_eq!(ids, vec![1, 2, 3], "IDs should be unique and sequential");
1138 }
1139
1140 #[test]
1141 fn test_jsonrpc_version_constant() {
1142 assert_eq!(JSONRPC_VERSION, "2.0");
1143 }
1144
1145 #[cfg(unix)]
1154 #[tokio::test]
1155 async fn test_request_timeout_removes_pending_entry() {
1156 let mut child = tokio::process::Command::new("sleep")
1157 .arg("2")
1158 .stdin(std::process::Stdio::piped())
1159 .stdout(std::process::Stdio::piped())
1160 .kill_on_drop(true)
1161 .spawn()
1162 .unwrap();
1163 let stdin = child.stdin.take().unwrap();
1164 let stdout = child.stdout.take().unwrap();
1165
1166 let transport = LspTransport::new(stdin, stdout);
1167 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
1168
1169 let result: Result<Value> = client
1170 .request(
1171 "textDocument/hover",
1172 serde_json::json!({}),
1173 Duration::from_millis(50),
1174 )
1175 .await;
1176
1177 assert!(matches!(result, Err(Error::Timeout(_))), "got {result:?}");
1178 assert!(
1179 client.pending_requests.lock().await.is_empty(),
1180 "timed-out request must not remain in pending_requests"
1181 );
1182 }
1183
1184 #[tokio::test]
1188 async fn test_fail_pending_requests_resolves_all_as_server_terminated() {
1189 let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
1190 let (command_tx, _command_rx) = mpsc::channel(1);
1191
1192 let client = LspClient {
1193 config: LspServerConfig::rust_analyzer(),
1194 state: Arc::new(Mutex::new(super::super::ServerState::Ready)),
1195 request_counter: Arc::new(AtomicI64::new(1)),
1196 command_tx,
1197 pending_requests: Arc::clone(&pending_requests),
1198 receiver_task: None,
1199 };
1200
1201 let (tx1, rx1) = oneshot::channel::<Result<Value>>();
1202 let (tx2, rx2) = oneshot::channel::<Result<Value>>();
1203 pending_requests
1204 .lock()
1205 .await
1206 .insert(RequestId::Number(1), tx1);
1207 pending_requests
1208 .lock()
1209 .await
1210 .insert(RequestId::Number(2), tx2);
1211
1212 client.fail_pending_requests().await;
1213
1214 assert!(pending_requests.lock().await.is_empty());
1215 assert!(matches!(rx1.await.unwrap(), Err(Error::ServerTerminated)));
1216 assert!(matches!(rx2.await.unwrap(), Err(Error::ServerTerminated)));
1217 }
1218
1219 #[test]
1220 fn test_should_retrigger_defaults_to_true_when_data_absent() {
1221 assert!(LspClient::should_retrigger(None));
1222 }
1223
1224 #[test]
1225 fn test_should_retrigger_false_when_flag_false() {
1226 assert!(!LspClient::should_retrigger(Some(&serde_json::json!({
1227 "retriggerRequest": false
1228 }))));
1229 }
1230
1231 #[test]
1232 fn test_should_retrigger_true_when_flag_true() {
1233 assert!(LspClient::should_retrigger(Some(&serde_json::json!({
1234 "retriggerRequest": true
1235 }))));
1236 }
1237
1238 mod retry_behavior {
1239 use tokio::io::{AsyncWriteExt, BufReader, DuplexStream};
1240
1241 use super::*;
1242 use crate::test_lsp::{
1243 CapturedLogs, fake_lsp_client, read_framed_message, write_error_response,
1244 write_response as write_success_response,
1245 };
1246
1247 async fn write_retryable_error_response(
1255 stdin: &mut DuplexStream,
1256 id: &Value,
1257 code: i32,
1258 message: &str,
1259 retrigger: bool,
1260 ) {
1261 let response = serde_json::json!({
1262 "jsonrpc": "2.0",
1263 "id": id,
1264 "error": {
1265 "code": code,
1266 "message": message,
1267 "data": { "retriggerRequest": retrigger },
1268 },
1269 });
1270 let content = serde_json::to_string(&response).unwrap();
1271 let header = format!("Content-Length: {}\r\n\r\n", content.len());
1272 stdin.write_all(header.as_bytes()).await.unwrap();
1273 stdin.write_all(content.as_bytes()).await.unwrap();
1274 stdin.flush().await.unwrap();
1275 }
1276
1277 #[tokio::test]
1287 async fn test_retry_exhaustion_returns_original_server_cancelled_error() {
1288 use tracing_subscriber::layer::SubscriberExt as _;
1289
1290 let (client, mut server) = fake_lsp_client();
1291 let captured = CapturedLogs::default();
1292 let subscriber = tracing_subscriber::registry().with(captured.clone());
1293 let guard = tracing::subscriber::set_default(subscriber);
1294
1295 let request_task = tokio::spawn(async move {
1296 client
1297 .request::<_, Value>(
1298 "textDocument/hover",
1299 serde_json::json!({}),
1300 Duration::from_secs(30),
1301 )
1302 .await
1303 });
1304
1305 let mut reader = BufReader::new(&mut server.write_stdout);
1306 for _ in 0..=SERVER_CANCELLED_MAX_RETRIES {
1310 let request = read_framed_message(&mut reader).await;
1311 let id = request["id"].clone();
1312 write_retryable_error_response(
1313 &mut server.read_half_stdin,
1314 &id,
1315 SERVER_CANCELLED_CODE,
1316 "server cancelled the request",
1317 true,
1318 )
1319 .await;
1320 }
1321
1322 let result = request_task.await.unwrap();
1323
1324 match result {
1325 Err(Error::LspServerError {
1326 code,
1327 message,
1328 data,
1329 }) => {
1330 assert_eq!(code, SERVER_CANCELLED_CODE);
1334 assert_eq!(message, "server cancelled the request");
1335 assert_eq!(data, Some(serde_json::json!({ "retriggerRequest": true })));
1336 }
1337 other => panic!("expected exhausted ServerCancelled error, got {other:?}"),
1338 }
1339
1340 drop(guard);
1341 let logs = captured.entries();
1342 assert_eq!(
1343 logs.iter()
1344 .filter(|(level, _)| *level == tracing::Level::ERROR)
1345 .count(),
1346 1,
1347 "exactly the final exhausted attempt must log at ERROR, got: {logs:?}"
1348 );
1349 assert!(
1350 logs.iter()
1351 .any(|(level, msg)| *level == tracing::Level::ERROR
1352 && msg.contains("LSP error response")
1353 && msg.contains("retries exhausted")),
1354 "expected an ERROR log sharing the 'LSP error response' prefix and naming \
1355 retry exhaustion, got: {logs:?}"
1356 );
1357 assert_eq!(
1358 logs.iter()
1359 .filter(
1360 |(level, msg)| *level == tracing::Level::WARN && msg.contains("will retry")
1361 )
1362 .count(),
1363 usize::try_from(SERVER_CANCELLED_MAX_RETRIES).unwrap(),
1364 "every attempt before the last must log a WARN 'will retry' line, got: {logs:?}"
1365 );
1366 }
1367
1368 #[tokio::test]
1369 async fn test_retrigger_false_returns_immediately_without_retry() {
1370 let (client, mut server) = fake_lsp_client();
1371
1372 let request_task = tokio::spawn(async move {
1373 client
1374 .request::<_, Value>(
1375 "textDocument/hover",
1376 serde_json::json!({}),
1377 Duration::from_secs(30),
1378 )
1379 .await
1380 });
1381
1382 let mut reader = BufReader::new(&mut server.write_stdout);
1383 let request = read_framed_message(&mut reader).await;
1384 let id = request["id"].clone();
1385 write_retryable_error_response(
1386 &mut server.read_half_stdin,
1387 &id,
1388 SERVER_CANCELLED_CODE,
1389 "server cancelled the request",
1390 false,
1391 )
1392 .await;
1393
1394 let result = tokio::time::timeout(Duration::from_millis(200), request_task)
1401 .await
1402 .unwrap()
1403 .unwrap();
1404
1405 match result {
1406 Err(Error::LspServerError { code, .. }) => {
1407 assert_eq!(code, SERVER_CANCELLED_CODE);
1408 }
1409 other => panic!("expected immediate ServerCancelled error, got {other:?}"),
1410 }
1411
1412 let second_request =
1413 tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
1414 .await;
1415 assert!(
1416 second_request.is_err(),
1417 "no retry should have been sent after retriggerRequest: false"
1418 );
1419 }
1420
1421 #[tokio::test]
1422 async fn test_retry_succeeds_after_one_server_cancelled_response() {
1423 let (client, mut server) = fake_lsp_client();
1424
1425 let request_task = tokio::spawn(async move {
1426 client
1427 .request::<_, Value>(
1428 "textDocument/hover",
1429 serde_json::json!({}),
1430 Duration::from_secs(30),
1431 )
1432 .await
1433 });
1434
1435 let mut reader = BufReader::new(&mut server.write_stdout);
1436
1437 let first = read_framed_message(&mut reader).await;
1439 write_retryable_error_response(
1440 &mut server.read_half_stdin,
1441 &first["id"].clone(),
1442 SERVER_CANCELLED_CODE,
1443 "server cancelled the request",
1444 true,
1445 )
1446 .await;
1447
1448 let second = read_framed_message(&mut reader).await;
1451 assert_ne!(
1452 first["id"], second["id"],
1453 "retry must use a fresh request id"
1454 );
1455 let expected_result = serde_json::json!({ "contents": "resolved on retry" });
1456 write_success_response(
1457 &mut server.read_half_stdin,
1458 &second["id"].clone(),
1459 expected_result.clone(),
1460 )
1461 .await;
1462
1463 let result = request_task.await.unwrap();
1464 assert_eq!(result.unwrap(), expected_result);
1465 }
1466
1467 #[tokio::test]
1468 async fn test_retry_exhaustion_returns_original_content_modified_error() {
1469 let (client, mut server) = fake_lsp_client();
1470
1471 let request_task = tokio::spawn(async move {
1472 client
1473 .request::<_, Value>(
1474 "textDocument/hover",
1475 serde_json::json!({}),
1476 Duration::from_secs(30),
1477 )
1478 .await
1479 });
1480
1481 let mut reader = BufReader::new(&mut server.write_stdout);
1482 for _ in 0..=SERVER_CANCELLED_MAX_RETRIES {
1487 let request = read_framed_message(&mut reader).await;
1488 let id = request["id"].clone();
1489 write_retryable_error_response(
1490 &mut server.read_half_stdin,
1491 &id,
1492 i32::from(LspErrorCodes::ContentModified),
1493 "content modified",
1494 true,
1495 )
1496 .await;
1497 }
1498
1499 let result = request_task.await.unwrap();
1500
1501 match result {
1502 Err(Error::LspServerError {
1503 code,
1504 message,
1505 data,
1506 }) => {
1507 assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
1511 assert_eq!(message, "content modified");
1512 assert_eq!(data, Some(serde_json::json!({ "retriggerRequest": true })));
1513 }
1514 other => panic!("expected exhausted ContentModified error, got {other:?}"),
1515 }
1516 }
1517
1518 #[tokio::test]
1519 async fn test_retrigger_false_returns_immediately_without_retry_for_content_modified() {
1520 let (client, mut server) = fake_lsp_client();
1521
1522 let request_task = tokio::spawn(async move {
1523 client
1524 .request::<_, Value>(
1525 "textDocument/hover",
1526 serde_json::json!({}),
1527 Duration::from_secs(30),
1528 )
1529 .await
1530 });
1531
1532 let mut reader = BufReader::new(&mut server.write_stdout);
1533 let request = read_framed_message(&mut reader).await;
1534 let id = request["id"].clone();
1535 write_retryable_error_response(
1536 &mut server.read_half_stdin,
1537 &id,
1538 i32::from(LspErrorCodes::ContentModified),
1539 "content modified",
1540 false,
1541 )
1542 .await;
1543
1544 let result = tokio::time::timeout(Duration::from_millis(200), request_task)
1549 .await
1550 .unwrap()
1551 .unwrap();
1552
1553 match result {
1554 Err(Error::LspServerError { code, .. }) => {
1555 assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
1556 }
1557 other => panic!("expected immediate ContentModified error, got {other:?}"),
1558 }
1559
1560 let second_request =
1561 tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
1562 .await;
1563 assert!(
1564 second_request.is_err(),
1565 "no retry should have been sent after retriggerRequest: false"
1566 );
1567 }
1568
1569 #[tokio::test]
1570 async fn test_retry_succeeds_after_one_content_modified_response() {
1571 let (client, mut server) = fake_lsp_client();
1572
1573 let request_task = tokio::spawn(async move {
1574 client
1575 .request::<_, Value>(
1576 "textDocument/hover",
1577 serde_json::json!({}),
1578 Duration::from_secs(30),
1579 )
1580 .await
1581 });
1582
1583 let mut reader = BufReader::new(&mut server.write_stdout);
1584
1585 let first = read_framed_message(&mut reader).await;
1587 write_retryable_error_response(
1588 &mut server.read_half_stdin,
1589 &first["id"].clone(),
1590 i32::from(LspErrorCodes::ContentModified),
1591 "content modified",
1592 true,
1593 )
1594 .await;
1595
1596 let second = read_framed_message(&mut reader).await;
1599 assert_ne!(
1600 first["id"], second["id"],
1601 "retry must use a fresh request id"
1602 );
1603 let expected_result = serde_json::json!({ "contents": "resolved on retry" });
1604 write_success_response(
1605 &mut server.read_half_stdin,
1606 &second["id"].clone(),
1607 expected_result.clone(),
1608 )
1609 .await;
1610
1611 let result = request_task.await.unwrap();
1612 assert_eq!(result.unwrap(), expected_result);
1613 }
1614
1615 #[tokio::test]
1616 async fn test_content_modified_on_non_allowlisted_method_does_not_retry() {
1617 let (client, mut server) = fake_lsp_client();
1618
1619 let request_task = tokio::spawn(async move {
1626 client
1627 .request::<_, Value>(
1628 "textDocument/rename",
1629 serde_json::json!({}),
1630 Duration::from_secs(30),
1631 )
1632 .await
1633 });
1634
1635 let mut reader = BufReader::new(&mut server.write_stdout);
1636 let request = read_framed_message(&mut reader).await;
1637 let id = request["id"].clone();
1638 write_retryable_error_response(
1639 &mut server.read_half_stdin,
1640 &id,
1641 i32::from(LspErrorCodes::ContentModified),
1642 "content modified",
1643 true,
1644 )
1645 .await;
1646
1647 let result = tokio::time::timeout(Duration::from_millis(200), request_task)
1648 .await
1649 .unwrap()
1650 .unwrap();
1651
1652 match result {
1653 Err(Error::LspServerError { code, .. }) => {
1654 assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
1655 }
1656 other => panic!("expected immediate ContentModified error, got {other:?}"),
1657 }
1658
1659 let second_request =
1660 tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
1661 .await;
1662 assert!(
1663 second_request.is_err(),
1664 "no retry should have been sent for a non-allowlisted method"
1665 );
1666 }
1667
1668 #[tokio::test]
1675 async fn test_oversized_error_message_truncated_for_caller() {
1676 let (client, mut server) = fake_lsp_client();
1677
1678 let request_task = tokio::spawn(async move {
1679 client
1680 .request::<_, Value>(
1681 "textDocument/hover",
1682 serde_json::json!({}),
1683 Duration::from_secs(30),
1684 )
1685 .await
1686 });
1687
1688 let mut reader = BufReader::new(&mut server.write_stdout);
1689 let request = read_framed_message(&mut reader).await;
1690 let id = request["id"].clone();
1691 let oversized_message = "x".repeat(MAX_ERROR_MESSAGE_CALLER_BYTES + 500);
1692 write_error_response(&mut server.read_half_stdin, &id, -32603, &oversized_message)
1693 .await;
1694
1695 let result = request_task.await.unwrap();
1696
1697 match result {
1698 Err(Error::LspServerError { code, message, .. }) => {
1699 assert_eq!(code, -32603);
1700 assert!(
1701 message.len() < oversized_message.len(),
1702 "caller-facing message must be truncated, got {} bytes",
1703 message.len()
1704 );
1705 assert!(message.ends_with("... (truncated)"));
1706 }
1707 other => panic!("expected truncated LspServerError, got {other:?}"),
1708 }
1709 }
1710
1711 #[tokio::test]
1717 async fn test_error_message_between_log_and_caller_caps_reaches_caller_intact() {
1718 let (client, mut server) = fake_lsp_client();
1719
1720 let request_task = tokio::spawn(async move {
1721 client
1722 .request::<_, Value>(
1723 "textDocument/hover",
1724 serde_json::json!({}),
1725 Duration::from_secs(30),
1726 )
1727 .await
1728 });
1729
1730 let mut reader = BufReader::new(&mut server.write_stdout);
1731 let request = read_framed_message(&mut reader).await;
1732 let id = request["id"].clone();
1733 let message = "x".repeat(MAX_ERROR_MESSAGE_LOG_BYTES + 50);
1734 write_error_response(&mut server.read_half_stdin, &id, -32603, &message).await;
1735
1736 let result = request_task.await.unwrap();
1737
1738 match result {
1739 Err(Error::LspServerError {
1740 message: returned, ..
1741 }) => {
1742 assert_eq!(
1743 returned, message,
1744 "message under the caller cap must not be truncated"
1745 );
1746 }
1747 other => panic!("expected untruncated LspServerError, got {other:?}"),
1748 }
1749 }
1750
1751 #[tokio::test]
1756 async fn test_retried_error_that_recovers_does_not_log_error_level() {
1757 use tracing_subscriber::layer::SubscriberExt as _;
1758
1759 let (client, mut server) = fake_lsp_client();
1760 let captured = CapturedLogs::default();
1761 let subscriber = tracing_subscriber::registry().with(captured.clone());
1762 let guard = tracing::subscriber::set_default(subscriber);
1763
1764 let request_task = tokio::spawn(async move {
1765 client
1766 .request::<_, Value>(
1767 "textDocument/hover",
1768 serde_json::json!({}),
1769 Duration::from_secs(30),
1770 )
1771 .await
1772 });
1773
1774 let mut reader = BufReader::new(&mut server.write_stdout);
1775
1776 let first = read_framed_message(&mut reader).await;
1777 write_retryable_error_response(
1778 &mut server.read_half_stdin,
1779 &first["id"].clone(),
1780 SERVER_CANCELLED_CODE,
1781 "server cancelled the request",
1782 true,
1783 )
1784 .await;
1785
1786 let second = read_framed_message(&mut reader).await;
1787 write_success_response(
1788 &mut server.read_half_stdin,
1789 &second["id"].clone(),
1790 serde_json::json!({ "contents": "resolved on retry" }),
1791 )
1792 .await;
1793
1794 let result = request_task.await.unwrap();
1795 assert!(result.is_ok(), "expected retry to recover, got {result:?}");
1796
1797 drop(guard);
1798 let logs = captured.entries();
1799 assert!(
1800 !logs
1801 .iter()
1802 .any(|(level, _)| *level == tracing::Level::ERROR),
1803 "a retried-and-recovered error must not log at ERROR, got: {logs:?}"
1804 );
1805 assert!(
1806 logs.iter().any(
1807 |(level, msg)| *level == tracing::Level::WARN && msg.contains("will retry")
1808 ),
1809 "expected a WARN 'will retry' log line, got: {logs:?}"
1810 );
1811 }
1812
1813 #[tokio::test]
1817 async fn test_non_retryable_error_logs_error_level() {
1818 use tracing_subscriber::layer::SubscriberExt as _;
1819
1820 let (client, mut server) = fake_lsp_client();
1821 let captured = CapturedLogs::default();
1822 let subscriber = tracing_subscriber::registry().with(captured.clone());
1823 let guard = tracing::subscriber::set_default(subscriber);
1824
1825 let request_task = tokio::spawn(async move {
1826 client
1827 .request::<_, Value>(
1828 "textDocument/rename",
1829 serde_json::json!({}),
1830 Duration::from_secs(30),
1831 )
1832 .await
1833 });
1834
1835 let mut reader = BufReader::new(&mut server.write_stdout);
1836 let request = read_framed_message(&mut reader).await;
1837 let id = request["id"].clone();
1838 write_retryable_error_response(
1839 &mut server.read_half_stdin,
1840 &id,
1841 i32::from(LspErrorCodes::ContentModified),
1842 "content modified",
1843 true,
1844 )
1845 .await;
1846
1847 let result = request_task.await.unwrap();
1848 assert!(result.is_err(), "expected a non-retryable error");
1849
1850 drop(guard);
1851 let logs = captured.entries();
1852 assert!(
1853 logs.iter()
1854 .any(|(level, msg)| *level == tracing::Level::ERROR
1855 && msg.contains("LSP error response")
1856 && msg.contains("content modified")),
1857 "a non-retryable error must still surface an ERROR log sharing the \
1858 'LSP error response' prefix, got: {logs:?}"
1859 );
1860 }
1861 }
1862}