1use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::process::Stdio;
13
14use lsp_types::{
15 ClientCapabilities, ClientInfo, ExitNotification, GeneralClientCapabilities, InitializeParams,
16 InitializeRequest, InitializeResult, InitializedNotification, InitializedParams,
17 PositionEncodingKind, Request, ServerCapabilities, ShutdownRequest, StaleRequestSupportOptions,
18 SymbolKind, WorkspaceFolder,
19};
20use tokio::process::Command;
21use tokio::sync::mpsc;
22use tokio::time::Duration;
23use tracing::{debug, info, warn};
24
25use crate::bridge::try_path_to_uri;
26use crate::config::{LspServerConfig, ServerId};
27use crate::error::{Error, Result, ServerSpawnFailure};
28use crate::lsp::CONTENT_MODIFIED_RETRY_METHODS;
29use crate::lsp::client::LspClient;
30use crate::lsp::transport::LspTransport;
31use crate::lsp::types::LspNotification;
32
33const ENV_PASSTHROUGH: &[&str] = &["PATH", "HOME", "USERPROFILE", "TMPDIR", "TEMP", "TMP"];
46
47const CHILD_EXIT_GRACE: Duration = Duration::from_secs(3);
51
52const NOTIFICATION_CHANNEL_CAPACITY: usize = 256;
60
61const LIFECYCLE_CHANNEL_CAPACITY: usize = 128;
71
72pub const SUPPORTED_SYMBOL_KINDS: [SymbolKind; 26] = [
79 SymbolKind::File,
80 SymbolKind::Module,
81 SymbolKind::Namespace,
82 SymbolKind::Package,
83 SymbolKind::Class,
84 SymbolKind::Method,
85 SymbolKind::Property,
86 SymbolKind::Field,
87 SymbolKind::Constructor,
88 SymbolKind::Enum,
89 SymbolKind::Interface,
90 SymbolKind::Function,
91 SymbolKind::Variable,
92 SymbolKind::Constant,
93 SymbolKind::String,
94 SymbolKind::Number,
95 SymbolKind::Boolean,
96 SymbolKind::Array,
97 SymbolKind::Object,
98 SymbolKind::Key,
99 SymbolKind::Null,
100 SymbolKind::EnumMember,
101 SymbolKind::Struct,
102 SymbolKind::Event,
103 SymbolKind::Operator,
104 SymbolKind::TypeParameter,
105];
106
107#[cfg(windows)]
114const ENV_PASSTHROUGH_WINDOWS: &[&str] = &[
115 "SystemRoot",
116 "SystemDrive",
117 "windir",
118 "APPDATA",
119 "LOCALAPPDATA",
120 "ProgramData",
121 "ProgramFiles",
122 "COMSPEC",
123 "PATHEXT",
124 "NUMBER_OF_PROCESSORS",
125 "USERNAME",
126];
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum ServerState {
131 Uninitialized,
133 Initializing,
135 Ready,
137 ShuttingDown,
139 Shutdown,
141}
142
143impl ServerState {
144 #[must_use]
146 pub const fn is_ready(&self) -> bool {
147 matches!(self, Self::Ready)
148 }
149
150 #[must_use]
152 pub const fn can_accept_requests(&self) -> bool {
153 matches!(self, Self::Ready)
154 }
155}
156
157#[derive(Debug, Clone)]
159pub struct ServerInitConfig {
160 pub server_config: LspServerConfig,
162 pub workspace_roots: Vec<PathBuf>,
164 pub initialization_options: Option<serde_json::Value>,
166 pub position_encodings: Vec<String>,
180 pub notification_tx: Option<mpsc::Sender<LspNotification>>,
187}
188
189#[derive(Debug)]
213pub struct ServerInitResult {
214 pub servers: HashMap<ServerId, LspServer>,
216 pub failures: Vec<ServerSpawnFailure>,
218}
219
220impl ServerInitResult {
221 #[must_use]
223 pub fn new() -> Self {
224 Self {
225 servers: HashMap::new(),
226 failures: Vec::new(),
227 }
228 }
229
230 #[must_use]
234 pub fn has_servers(&self) -> bool {
235 !self.servers.is_empty()
236 }
237
238 #[must_use]
243 pub fn all_failed(&self) -> bool {
244 self.servers.is_empty() && !self.failures.is_empty()
245 }
246
247 #[must_use]
251 pub fn partial_success(&self) -> bool {
252 !self.servers.is_empty() && !self.failures.is_empty()
253 }
254
255 #[must_use]
257 pub fn server_count(&self) -> usize {
258 self.servers.len()
259 }
260
261 #[must_use]
263 pub const fn failure_count(&self) -> usize {
264 self.failures.len()
265 }
266
267 pub fn add_server(&mut self, id: impl Into<ServerId>, server: LspServer) {
271 self.servers.insert(id.into(), server);
272 }
273
274 pub fn add_failure(&mut self, failure: ServerSpawnFailure) {
276 self.failures.push(failure);
277 }
278}
279
280impl Default for ServerInitResult {
281 fn default() -> Self {
282 Self::new()
283 }
284}
285
286pub struct LspServer {
288 client: LspClient,
289 capabilities: ServerCapabilities,
290 position_encoding: PositionEncodingKind,
291 pub notification_rx: mpsc::Receiver<LspNotification>,
297 pub lifecycle_rx: mpsc::Receiver<LspNotification>,
306 child: Option<tokio::process::Child>,
316}
317
318impl std::fmt::Debug for LspServer {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 f.debug_struct("LspServer")
321 .field("client", &self.client)
322 .field("capabilities", &self.capabilities)
323 .field("position_encoding", &self.position_encoding)
324 .field("notification_rx", &"<channel>")
325 .field("lifecycle_rx", &"<channel>")
326 .field("child", &"<process>")
327 .finish()
328 }
329}
330
331impl LspServer {
332 pub fn take_notification_rx(&mut self) -> tokio::sync::mpsc::Receiver<LspNotification> {
338 let (_, dummy) = tokio::sync::mpsc::channel(1);
339 std::mem::replace(&mut self.notification_rx, dummy)
340 }
341
342 pub fn take_lifecycle_rx(&mut self) -> tokio::sync::mpsc::Receiver<LspNotification> {
348 let (_, dummy) = tokio::sync::mpsc::channel(1);
349 std::mem::replace(&mut self.lifecycle_rx, dummy)
350 }
351
352 pub async fn spawn(config: ServerInitConfig) -> Result<Self> {
367 info!(
368 "Spawning LSP server: {} {:?}",
369 config.server_config.command, config.server_config.args
370 );
371
372 let mut command = Self::build_command(&config.server_config, |key| std::env::var_os(key));
373
374 let passthrough_present = {
379 let base = ENV_PASSTHROUGH
380 .iter()
381 .filter(|key| std::env::var_os(key).is_some())
382 .count();
383 #[cfg(windows)]
384 let windows = ENV_PASSTHROUGH_WINDOWS
385 .iter()
386 .filter(|key| std::env::var_os(key).is_some())
387 .count();
388 #[cfg(not(windows))]
389 let windows = 0;
390 base + windows
391 };
392 debug!(
393 "Effective LSP server env: {passthrough_present} allowlisted key(s) present, \
394 {} configured override(s) applied",
395 config.server_config.env.len()
396 );
397
398 let mut child = command.spawn().map_err(|e| Error::ServerSpawnFailed {
399 command: config.server_config.command.clone(),
400 source: e,
401 })?;
402
403 let stdin = child
404 .stdin
405 .take()
406 .ok_or_else(|| Error::Transport("Failed to capture stdin".to_string()))?;
407 let stdout = child
408 .stdout
409 .take()
410 .ok_or_else(|| Error::Transport("Failed to capture stdout".to_string()))?;
411
412 let transport = LspTransport::new(stdin, stdout);
413 let (notification_tx, notification_rx) = mpsc::channel(NOTIFICATION_CHANNEL_CAPACITY);
414 let (lifecycle_tx, lifecycle_rx) = mpsc::channel(LIFECYCLE_CHANNEL_CAPACITY);
415 let client = LspClient::from_transport_with_notifications(
416 config.server_config.clone(),
417 transport,
418 notification_tx,
419 lifecycle_tx,
420 );
421
422 let (capabilities, position_encoding) = Self::initialize(&client, &config).await?;
423
424 info!("LSP server initialized successfully");
425
426 Ok(Self {
427 client,
428 capabilities,
429 position_encoding,
430 notification_rx,
431 lifecycle_rx,
432 child: Some(child),
433 })
434 }
435
436 fn build_command(
446 config: &LspServerConfig,
447 parent_env: impl Fn(&str) -> Option<std::ffi::OsString>,
448 ) -> Command {
449 let mut command = Command::new(&config.command);
450 command.args(&config.args).env_clear();
451
452 for key in ENV_PASSTHROUGH {
453 if let Some(value) = parent_env(key) {
454 command.env(key, value);
455 }
456 }
457 #[cfg(windows)]
458 for key in ENV_PASSTHROUGH_WINDOWS {
459 if let Some(value) = parent_env(key) {
460 command.env(key, value);
461 }
462 }
463
464 command
465 .envs(&config.env)
466 .stdin(Stdio::piped())
467 .stdout(Stdio::piped())
468 .stderr(Stdio::null())
469 .kill_on_drop(true);
470
471 command
472 }
473
474 #[allow(clippy::too_many_lines)]
486 fn client_capabilities(position_encodings: &[String]) -> ClientCapabilities {
487 ClientCapabilities {
488 general: Some(GeneralClientCapabilities {
489 position_encodings: Some(resolve_position_encodings(position_encodings)),
490 stale_request_support: Some(StaleRequestSupportOptions {
491 cancel: false,
494 retry_on_content_modified: CONTENT_MODIFIED_RETRY_METHODS
495 .iter()
496 .map(ToString::to_string)
497 .collect(),
498 }),
499 ..Default::default()
500 }),
501 text_document: Some(lsp_types::TextDocumentClientCapabilities {
502 document_symbol: Some(lsp_types::DocumentSymbolClientCapabilities {
503 dynamic_registration: Some(false),
504 symbol_kind: Some(lsp_types::ClientSymbolKindOptions {
505 value_set: Some(SUPPORTED_SYMBOL_KINDS.to_vec()),
506 }),
507 hierarchical_document_symbol_support: Some(true),
508 ..Default::default()
509 }),
510 hover: Some(lsp_types::HoverClientCapabilities {
511 dynamic_registration: Some(false),
512 content_format: Some(vec![
513 lsp_types::MarkupKind::Markdown,
514 lsp_types::MarkupKind::PlainText,
515 ]),
516 }),
517 definition: Some(lsp_types::DefinitionClientCapabilities {
518 dynamic_registration: Some(false),
519 link_support: Some(true),
520 }),
521 references: Some(lsp_types::ReferenceClientCapabilities {
522 dynamic_registration: Some(false),
523 }),
524 code_action: Some(lsp_types::CodeActionClientCapabilities {
525 dynamic_registration: Some(false),
526 data_support: Some(true),
527 resolve_support: Some(lsp_types::ClientCodeActionResolveOptions {
528 properties: vec!["edit".to_string()],
529 }),
530 code_action_literal_support: Some(lsp_types::ClientCodeActionLiteralOptions {
533 code_action_kind: lsp_types::ClientCodeActionKindOptions {
534 value_set: vec![
535 lsp_types::CodeActionKind::Empty,
536 lsp_types::CodeActionKind::QuickFix,
537 lsp_types::CodeActionKind::Refactor,
538 lsp_types::CodeActionKind::RefactorExtract,
539 lsp_types::CodeActionKind::RefactorInline,
540 lsp_types::CodeActionKind::RefactorRewrite,
541 lsp_types::CodeActionKind::Source,
542 lsp_types::CodeActionKind::SourceOrganizeImports,
543 ],
544 },
545 }),
546 ..Default::default()
547 }),
548 ..Default::default()
549 }),
550 workspace: Some(lsp_types::WorkspaceClientCapabilities {
551 workspace_folders: Some(true),
552 ..Default::default()
553 }),
554 window: Some(lsp_types::WindowClientCapabilities {
556 work_done_progress: Some(true),
557 ..Default::default()
558 }),
559 experimental: Some(serde_json::json!({ "serverStatusNotification": true })),
561 ..Default::default()
562 }
563 }
564
565 async fn initialize(
569 client: &LspClient,
570 config: &ServerInitConfig,
571 ) -> Result<(ServerCapabilities, PositionEncodingKind)> {
572 debug!("Sending initialize request");
573
574 let workspace_folders: Vec<WorkspaceFolder> = config
575 .workspace_roots
576 .iter()
577 .map(|root| workspace_folder(root))
578 .collect::<Result<Vec<_>>>()?;
579
580 let params = InitializeParams {
581 process_id: Some(i32::try_from(std::process::id()).unwrap_or(i32::MAX)),
582 #[allow(deprecated)]
583 root_uri: None,
584 initialization_options: config.initialization_options.clone(),
585 capabilities: Self::client_capabilities(&config.position_encodings),
586 client_info: Some(ClientInfo {
587 name: "mcpls".to_string(),
588 version: Some(env!("CARGO_PKG_VERSION").to_string()),
589 }),
590 workspace_folders_initialize_params: lsp_types::WorkspaceFoldersInitializeParams {
591 workspace_folders: Some(lsp_types::WorkspaceFolders::WorkspaceFolderList(
592 workspace_folders,
593 )),
594 },
595 ..Default::default()
596 };
597
598 let result: InitializeResult = client
602 .request_typed::<InitializeRequest>(
603 params,
604 Duration::from_secs(
614 config
615 .server_config
616 .timeout_seconds
617 .clamp(1, crate::config::MAX_TIMEOUT_SECONDS),
618 ),
619 )
620 .await
621 .map_err(|e| Error::LspInitFailed {
622 message: format!("Initialize request failed: {e}"),
623 })?;
624
625 let position_encoding = result
626 .capabilities
627 .position_encoding
628 .clone()
629 .unwrap_or(PositionEncodingKind::UTF16);
630
631 debug!(
632 "Server capabilities received, encoding: {:?}",
633 position_encoding
634 );
635
636 client
637 .notify_typed::<InitializedNotification>(InitializedParams {})
638 .await
639 .map_err(|e| Error::LspInitFailed {
640 message: format!("Initialized notification failed: {e}"),
641 })?;
642
643 Ok((result.capabilities, position_encoding))
644 }
645
646 #[must_use]
648 pub const fn capabilities(&self) -> &ServerCapabilities {
649 &self.capabilities
650 }
651
652 #[must_use]
654 pub fn position_encoding(&self) -> PositionEncodingKind {
655 self.position_encoding.clone()
656 }
657
658 #[must_use]
660 pub const fn client(&self) -> &LspClient {
661 &self.client
662 }
663
664 pub fn has_exited(&mut self) -> Result<bool> {
682 match &mut self.child {
683 Some(child) => Ok(child.try_wait()?.is_some()),
684 None => Ok(false),
685 }
686 }
687
688 pub async fn shutdown(self) -> Result<()> {
705 debug!("Shutting down LSP server");
706
707 let handshake: Result<()> = async move {
708 let _: serde_json::Value = self
709 .client
710 .request(ShutdownRequest::METHOD.as_str(), (), Duration::from_secs(5))
711 .await?;
712 self.client.notify_typed::<ExitNotification>(()).await?;
713 self.client.shutdown().await
714 }
715 .await;
716
717 if let Some(mut child) = self.child {
718 match tokio::time::timeout(CHILD_EXIT_GRACE, child.wait()).await {
719 Ok(Ok(status)) => {
720 debug!(
721 ?status,
722 "LSP server process exited after `exit` notification"
723 );
724 }
725 Ok(Err(e)) => warn!(error = %e, "failed to wait for LSP server process exit"),
726 Err(_) => warn!(
727 timeout = ?CHILD_EXIT_GRACE,
728 "LSP server process did not exit within grace period after `exit` \
729 notification, killing it"
730 ),
731 }
732 }
735
736 handshake?;
737 info!("LSP server shut down successfully");
738 Ok(())
739 }
740
741 pub async fn spawn_batch(configs: &[ServerInitConfig]) -> ServerInitResult {
792 let mut result = ServerInitResult::new();
793
794 for config in configs {
795 let server_id = config.server_config.id();
796 let language_id = config.server_config.language_id.clone();
797 let command = config.server_config.command.clone();
798
799 match Self::spawn(config.clone()).await {
800 Ok(server) => {
801 info!(
802 "Successfully spawned LSP server: {} ({})",
803 server_id, command
804 );
805 result.add_server(server_id, server);
806 }
807 Err(e) => {
808 tracing::error!(
809 "Failed to spawn LSP server: {} ({}): {}",
810 server_id,
811 command,
812 e
813 );
814 result.add_failure(ServerSpawnFailure {
815 server_id,
816 language_id,
817 command,
818 message: e.to_string(),
819 });
820 }
821 }
822 }
823
824 result
825 }
826}
827
828fn resolve_position_encodings(configured: &[String]) -> Vec<PositionEncodingKind> {
837 let encodings: Vec<PositionEncodingKind> = configured
838 .iter()
839 .filter_map(|value| {
840 let kind = crate::config::parse_position_encoding(value);
841 if kind.is_none() {
842 warn!(value = %value, "ignoring invalid configured position encoding");
843 }
844 kind
845 })
846 .collect();
847
848 if encodings.is_empty() {
849 crate::config::default_position_encodings()
850 .iter()
851 .filter_map(|value| crate::config::parse_position_encoding(value))
852 .collect()
853 } else {
854 encodings
855 }
856}
857
858fn workspace_folder(root: &Path) -> Result<WorkspaceFolder> {
864 let uri = try_path_to_uri(root).ok_or_else(|| {
865 let root_display = root.display();
866 Error::InvalidUri(format!("Invalid workspace root: {root_display}"))
867 })?;
868 Ok(WorkspaceFolder {
869 uri,
870 name: root
871 .file_name()
872 .and_then(|n| n.to_str())
873 .unwrap_or("workspace")
874 .to_string(),
875 })
876}
877
878#[cfg(test)]
891pub fn fake_lsp_server() -> LspServer {
892 let transport = crate::test_lsp::inert_transport();
893 let client = LspClient::from_transport(LspServerConfig::pyright(), transport);
894 let (_, mock_notification_rx) = mpsc::channel(1);
895 let (_, mock_lifecycle_rx) = mpsc::channel(1);
896 LspServer {
897 client,
898 capabilities: lsp_types::ServerCapabilities::default(),
899 position_encoding: PositionEncodingKind::UTF8,
900 notification_rx: mock_notification_rx,
901 lifecycle_rx: mock_lifecycle_rx,
902 child: None,
903 }
904}
905
906#[cfg(test)]
907impl LspServer {
908 pub(crate) fn new_for_test(capabilities: ServerCapabilities) -> Self {
920 Self::new_for_test_with_encoding(capabilities, PositionEncodingKind::UTF16)
921 }
922
923 pub(crate) fn new_for_test_with_encoding(
928 capabilities: ServerCapabilities,
929 position_encoding: PositionEncodingKind,
930 ) -> Self {
931 let client = LspClient::new(LspServerConfig::rust_analyzer());
932 let (_, notification_rx) = mpsc::channel(1);
933 let (_, lifecycle_rx) = mpsc::channel(1);
934
935 Self {
936 client,
937 capabilities,
938 position_encoding,
939 notification_rx,
940 lifecycle_rx,
941 child: None,
942 }
943 }
944}
945
946#[cfg(test)]
947#[allow(clippy::unwrap_used)]
948mod tests {
949 use super::*;
950
951 #[test]
952 fn test_resolve_position_encodings_preserves_configured_order() {
953 let result = resolve_position_encodings(&["utf-32".to_string(), "utf-8".to_string()]);
954 assert_eq!(
955 result,
956 vec![PositionEncodingKind::UTF32, PositionEncodingKind::UTF8]
957 );
958 }
959
960 #[test]
961 fn test_resolve_position_encodings_skips_invalid_and_keeps_valid() {
962 let result = resolve_position_encodings(&["utf-7".to_string(), "utf-16".to_string()]);
963 assert_eq!(result, vec![PositionEncodingKind::UTF16]);
964 }
965
966 #[test]
967 fn test_resolve_position_encodings_falls_back_when_all_invalid() {
968 let result = resolve_position_encodings(&["utf-7".to_string(), "bogus".to_string()]);
969 assert_eq!(
970 result,
971 vec![PositionEncodingKind::UTF8, PositionEncodingKind::UTF16]
972 );
973 }
974
975 #[test]
976 fn test_resolve_position_encodings_falls_back_when_empty() {
977 let result = resolve_position_encodings(&[]);
978 assert_eq!(
979 result,
980 vec![PositionEncodingKind::UTF8, PositionEncodingKind::UTF16]
981 );
982 }
983
984 #[test]
990 fn test_client_capabilities_advertises_work_done_progress() {
991 let capabilities =
992 LspServer::client_capabilities(&["utf-8".to_string(), "utf-16".to_string()]);
993
994 assert_eq!(
995 capabilities.window.and_then(|w| w.work_done_progress),
996 Some(true)
997 );
998 }
999
1000 #[test]
1001 fn test_server_state_ready() {
1002 assert!(ServerState::Ready.is_ready());
1003 assert!(ServerState::Ready.can_accept_requests());
1004 }
1005
1006 #[test]
1007 fn test_server_state_uninitialized() {
1008 assert!(!ServerState::Uninitialized.is_ready());
1009 assert!(!ServerState::Uninitialized.can_accept_requests());
1010 }
1011
1012 #[test]
1013 fn test_server_state_initializing() {
1014 assert!(!ServerState::Initializing.is_ready());
1015 assert!(!ServerState::Initializing.can_accept_requests());
1016 }
1017
1018 #[test]
1019 fn test_workspace_folder_encodes_fragment_char() {
1020 #[cfg(windows)]
1023 let (root, expected) = (
1024 Path::new(r"C:\home\me\dev\#work"),
1025 "file:///C:/home/me/dev/%23work",
1026 );
1027 #[cfg(not(windows))]
1028 let (root, expected) = (
1029 Path::new("/home/me/dev/#work"),
1030 "file:///home/me/dev/%23work",
1031 );
1032
1033 let folder = workspace_folder(root).unwrap();
1034
1035 assert_eq!(folder.uri.as_ref(), expected);
1036 assert_eq!(folder.name, "#work");
1037 }
1038
1039 #[test]
1040 fn test_workspace_folder_encodes_bracket_chars() {
1041 #[cfg(windows)]
1042 let (root, expected) = (
1043 Path::new(r"C:\home\me\dev\[env]"),
1044 "file:///C:/home/me/dev/%5Benv%5D",
1045 );
1046 #[cfg(not(windows))]
1047 let (root, expected) = (
1048 Path::new("/home/me/dev/[env]"),
1049 "file:///home/me/dev/%5Benv%5D",
1050 );
1051
1052 let folder = workspace_folder(root).unwrap();
1053
1054 assert_eq!(folder.uri.as_ref(), expected);
1055 assert_eq!(folder.name, "[env]");
1056 }
1057
1058 #[test]
1059 fn test_workspace_folder_rejects_relative_root() {
1060 let err = workspace_folder(Path::new("relative/root")).unwrap_err();
1061 assert!(matches!(err, Error::InvalidUri(_)), "got {err:?}");
1062 }
1063
1064 #[test]
1065 fn test_server_state_shutting_down() {
1066 assert!(!ServerState::ShuttingDown.is_ready());
1067 assert!(!ServerState::ShuttingDown.can_accept_requests());
1068 }
1069
1070 #[test]
1071 fn test_server_state_shutdown() {
1072 assert!(!ServerState::Shutdown.is_ready());
1073 assert!(!ServerState::Shutdown.can_accept_requests());
1074 }
1075
1076 #[test]
1077 fn test_server_state_equality() {
1078 assert_eq!(ServerState::Ready, ServerState::Ready);
1079 assert_ne!(ServerState::Ready, ServerState::Uninitialized);
1080 assert_eq!(ServerState::Shutdown, ServerState::Shutdown);
1081 }
1082
1083 #[test]
1084 fn test_server_state_clone() {
1085 let state = ServerState::Ready;
1086 let cloned = state;
1087 assert_eq!(state, cloned);
1088 }
1089
1090 #[test]
1091 fn test_server_state_debug() {
1092 let state = ServerState::Ready;
1093 let debug_str = format!("{state:?}");
1094 assert!(debug_str.contains("Ready"));
1095 }
1096
1097 #[test]
1098 fn test_server_init_config_clone() {
1099 let config = ServerInitConfig {
1100 server_config: LspServerConfig::rust_analyzer(),
1101 workspace_roots: vec![PathBuf::from("/tmp/workspace")],
1102 initialization_options: Some(serde_json::json!({"key": "value"})),
1103 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1104 notification_tx: None,
1105 };
1106
1107 #[allow(clippy::redundant_clone)]
1108 let cloned = config.clone();
1109 assert_eq!(cloned.server_config.language_id, "rust");
1110 assert_eq!(cloned.workspace_roots.len(), 1);
1111 }
1112
1113 #[test]
1114 fn test_server_init_config_debug() {
1115 let config = ServerInitConfig {
1116 server_config: LspServerConfig::pyright(),
1117 workspace_roots: vec![],
1118 initialization_options: None,
1119 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1120 notification_tx: None,
1121 };
1122
1123 let debug_str = format!("{config:?}");
1124 assert!(debug_str.contains("python"));
1125 assert!(debug_str.contains("pyright"));
1126 }
1127
1128 #[test]
1129 fn test_server_init_config_with_options() {
1130 use std::collections::HashMap;
1131
1132 let init_opts = serde_json::json!({
1133 "settings": {
1134 "python": {
1135 "analysis": {
1136 "typeCheckingMode": "strict"
1137 }
1138 }
1139 }
1140 });
1141
1142 let mut env = HashMap::new();
1143 env.insert("PYTHONPATH".to_string(), "/usr/lib".to_string());
1144
1145 let config = ServerInitConfig {
1146 server_config: LspServerConfig {
1147 language_id: "python".to_string(),
1148 command: "pyright-langserver".to_string(),
1149 args: vec!["--stdio".to_string()],
1150 env,
1151 file_patterns: vec!["**/*.py".to_string()],
1152 initialization_options: Some(init_opts.clone()),
1153 timeout_seconds: 10,
1154 request_timeout_seconds: 10,
1155 heuristics: None,
1156 name: None,
1157 handles: None,
1158 indexing: crate::bridge::IndexingPolicy::Auto,
1159 },
1160 workspace_roots: vec![PathBuf::from("/workspace")],
1161 initialization_options: Some(init_opts),
1162 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1163 notification_tx: None,
1164 };
1165
1166 assert!(config.initialization_options.is_some());
1167 assert_eq!(config.workspace_roots.len(), 1);
1168 }
1169
1170 #[test]
1171 fn test_server_init_config_empty_workspace() {
1172 let config = ServerInitConfig {
1173 server_config: LspServerConfig::typescript(),
1174 workspace_roots: vec![],
1175 initialization_options: None,
1176 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1177 notification_tx: None,
1178 };
1179
1180 assert!(config.workspace_roots.is_empty());
1181 }
1182
1183 #[test]
1184 fn test_server_init_config_multiple_workspaces() {
1185 let config = ServerInitConfig {
1186 server_config: LspServerConfig::rust_analyzer(),
1187 workspace_roots: vec![
1188 PathBuf::from("/workspace1"),
1189 PathBuf::from("/workspace2"),
1190 PathBuf::from("/workspace3"),
1191 ],
1192 initialization_options: None,
1193 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1194 notification_tx: None,
1195 };
1196
1197 assert_eq!(config.workspace_roots.len(), 3);
1198 }
1199
1200 #[cfg(unix)]
1207 #[tokio::test]
1208 async fn test_has_exited_reflects_child_process_state() {
1209 use lsp_types::ServerCapabilities;
1210
1211 let mut mock_child = tokio::process::Command::new("sleep")
1212 .arg("2")
1213 .stdin(Stdio::piped())
1214 .stdout(Stdio::piped())
1215 .kill_on_drop(true)
1216 .spawn()
1217 .unwrap();
1218
1219 let mock_stdin = mock_child.stdin.take().unwrap();
1220 let mock_stdout = mock_child.stdout.take().unwrap();
1221
1222 let transport = LspTransport::new(mock_stdin, mock_stdout);
1223 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
1224 let (_, mock_notification_rx) = mpsc::channel(1);
1225 let (_, mock_lifecycle_rx) = mpsc::channel(1);
1226
1227 let mut server = LspServer {
1228 client,
1229 capabilities: ServerCapabilities::default(),
1230 position_encoding: PositionEncodingKind::UTF8,
1231 notification_rx: mock_notification_rx,
1232 lifecycle_rx: mock_lifecycle_rx,
1233 child: Some(mock_child),
1234 };
1235
1236 assert!(
1237 !server.has_exited().unwrap(),
1238 "freshly spawned `sleep 2` should still be running"
1239 );
1240
1241 server.child.as_mut().unwrap().kill().await.unwrap();
1242 assert!(
1245 server.has_exited().unwrap(),
1246 "killed child must report as exited"
1247 );
1248 }
1249
1250 #[tokio::test]
1251 async fn test_lsp_server_getters() {
1252 use lsp_types::ServerCapabilities;
1253
1254 let transport = crate::test_lsp::inert_transport();
1255 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
1256 let (_, mock_notification_rx) = mpsc::channel(1);
1257 let (_, mock_lifecycle_rx) = mpsc::channel(1);
1258
1259 let server = LspServer {
1260 client,
1261 capabilities: ServerCapabilities::default(),
1262 position_encoding: PositionEncodingKind::UTF8,
1263 notification_rx: mock_notification_rx,
1264 lifecycle_rx: mock_lifecycle_rx,
1265 child: None,
1266 };
1267
1268 assert_eq!(server.position_encoding(), PositionEncodingKind::UTF8);
1269 assert!(server.capabilities().text_document_sync.is_none());
1270
1271 let debug_str = format!("{server:?}");
1272 assert!(debug_str.contains("LspServer"));
1273 assert!(debug_str.contains("<process>"));
1274 }
1275
1276 #[test]
1277 fn test_server_init_result_new_empty() {
1278 let result = ServerInitResult::new();
1279 assert!(!result.has_servers());
1280 assert!(!result.all_failed());
1281 assert!(!result.partial_success());
1282 assert_eq!(result.server_count(), 0);
1283 assert_eq!(result.failure_count(), 0);
1284 }
1285
1286 #[test]
1287 fn test_server_init_result_default() {
1288 let result = ServerInitResult::default();
1289 assert!(!result.has_servers());
1290 assert_eq!(result.server_count(), 0);
1291 assert_eq!(result.failure_count(), 0);
1292 }
1293
1294 #[test]
1295 fn test_server_init_result_all_failures() {
1296 let mut result = ServerInitResult::new();
1297
1298 result.add_failure(ServerSpawnFailure {
1299 server_id: ServerId::from("rust"),
1300 language_id: "rust".to_string(),
1301 command: "rust-analyzer".to_string(),
1302 message: "not found".to_string(),
1303 });
1304
1305 result.add_failure(ServerSpawnFailure {
1306 server_id: ServerId::from("python"),
1307 language_id: "python".to_string(),
1308 command: "pyright".to_string(),
1309 message: "permission denied".to_string(),
1310 });
1311
1312 assert!(!result.has_servers());
1313 assert!(result.all_failed());
1314 assert!(!result.partial_success());
1315 assert_eq!(result.server_count(), 0);
1316 assert_eq!(result.failure_count(), 2);
1317 }
1318
1319 #[tokio::test]
1320 async fn test_server_init_result_all_success() {
1321 let mut result = ServerInitResult::new();
1322
1323 let transport1 = crate::test_lsp::inert_transport();
1324 let client1 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport1);
1325 let (_, mock_notification_rx1) = mpsc::channel(1);
1326 let (_, mock_lifecycle_rx1) = mpsc::channel(1);
1327
1328 let server1 = LspServer {
1329 client: client1,
1330 capabilities: lsp_types::ServerCapabilities::default(),
1331 position_encoding: PositionEncodingKind::UTF8,
1332 notification_rx: mock_notification_rx1,
1333 lifecycle_rx: mock_lifecycle_rx1,
1334 child: None,
1335 };
1336
1337 result.add_server("rust".to_string(), server1);
1338
1339 assert!(result.has_servers());
1340 assert!(!result.all_failed());
1341 assert!(!result.partial_success());
1342 assert_eq!(result.server_count(), 1);
1343 assert_eq!(result.failure_count(), 0);
1344 }
1345
1346 #[tokio::test]
1347 async fn test_server_init_result_partial_success() {
1348 let mut result = ServerInitResult::new();
1349
1350 let transport = crate::test_lsp::inert_transport();
1351 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
1352 let (_, mock_notification_rx) = mpsc::channel(1);
1353 let (_, mock_lifecycle_rx) = mpsc::channel(1);
1354
1355 let server = LspServer {
1356 client,
1357 capabilities: lsp_types::ServerCapabilities::default(),
1358 position_encoding: PositionEncodingKind::UTF8,
1359 notification_rx: mock_notification_rx,
1360 lifecycle_rx: mock_lifecycle_rx,
1361 child: None,
1362 };
1363
1364 result.add_server("rust".to_string(), server);
1365
1366 result.add_failure(ServerSpawnFailure {
1367 server_id: ServerId::from("python"),
1368 language_id: "python".to_string(),
1369 command: "pyright".to_string(),
1370 message: "not found".to_string(),
1371 });
1372
1373 assert!(result.has_servers());
1374 assert!(!result.all_failed());
1375 assert!(result.partial_success());
1376 assert_eq!(result.server_count(), 1);
1377 assert_eq!(result.failure_count(), 1);
1378 }
1379
1380 #[tokio::test]
1381 async fn test_server_init_result_multiple_servers() {
1382 let mut result = ServerInitResult::new();
1383
1384 for i in 0..3 {
1385 let transport = crate::test_lsp::inert_transport();
1386 let config = if i == 0 {
1387 LspServerConfig::rust_analyzer()
1388 } else if i == 1 {
1389 LspServerConfig::pyright()
1390 } else {
1391 LspServerConfig::typescript()
1392 };
1393 let client = LspClient::from_transport(config.clone(), transport);
1394 let (_, mock_notification_rx) = mpsc::channel(1);
1395 let (_, mock_lifecycle_rx) = mpsc::channel(1);
1396
1397 let server = LspServer {
1398 client,
1399 capabilities: lsp_types::ServerCapabilities::default(),
1400 position_encoding: PositionEncodingKind::UTF8,
1401 notification_rx: mock_notification_rx,
1402 lifecycle_rx: mock_lifecycle_rx,
1403 child: None,
1404 };
1405
1406 result.add_server(config.language_id, server);
1407 }
1408
1409 assert!(result.has_servers());
1410 assert!(!result.all_failed());
1411 assert!(!result.partial_success());
1412 assert_eq!(result.server_count(), 3);
1413 assert_eq!(result.failure_count(), 0);
1414 }
1415
1416 #[tokio::test]
1417 async fn test_server_init_result_replace_server() {
1418 let mut result = ServerInitResult::new();
1419
1420 let transport1 = crate::test_lsp::inert_transport();
1421 let client1 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport1);
1422 let (_, mock_notification_rx1) = mpsc::channel(1);
1423 let (_, mock_lifecycle_rx1) = mpsc::channel(1);
1424
1425 let server1 = LspServer {
1426 client: client1,
1427 capabilities: lsp_types::ServerCapabilities::default(),
1428 position_encoding: PositionEncodingKind::UTF8,
1429 notification_rx: mock_notification_rx1,
1430 lifecycle_rx: mock_lifecycle_rx1,
1431 child: None,
1432 };
1433
1434 result.add_server("rust".to_string(), server1);
1435 assert_eq!(result.server_count(), 1);
1436
1437 let transport2 = crate::test_lsp::inert_transport();
1438 let client2 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport2);
1439 let (_, mock_notification_rx2) = mpsc::channel(1);
1440 let (_, mock_lifecycle_rx2) = mpsc::channel(1);
1441
1442 let server2 = LspServer {
1443 client: client2,
1444 capabilities: lsp_types::ServerCapabilities::default(),
1445 position_encoding: PositionEncodingKind::UTF16,
1446 notification_rx: mock_notification_rx2,
1447 lifecycle_rx: mock_lifecycle_rx2,
1448 child: None,
1449 };
1450
1451 result.add_server("rust".to_string(), server2);
1452 assert_eq!(result.server_count(), 1);
1453 }
1454
1455 #[test]
1456 fn test_server_init_result_debug() {
1457 let mut result = ServerInitResult::new();
1458
1459 result.add_failure(ServerSpawnFailure {
1460 server_id: ServerId::from("rust"),
1461 language_id: "rust".to_string(),
1462 command: "rust-analyzer".to_string(),
1463 message: "not found".to_string(),
1464 });
1465
1466 let debug_str = format!("{result:?}");
1467 assert!(debug_str.contains("ServerInitResult"));
1468 }
1469
1470 #[test]
1471 fn test_server_init_result_multiple_failures() {
1472 let mut result = ServerInitResult::new();
1473
1474 result.add_failure(ServerSpawnFailure {
1475 server_id: ServerId::from("python"),
1476 language_id: "python".to_string(),
1477 command: "pyright".to_string(),
1478 message: "not found".to_string(),
1479 });
1480
1481 result.add_failure(ServerSpawnFailure {
1482 server_id: ServerId::from("typescript"),
1483 language_id: "typescript".to_string(),
1484 command: "tsserver".to_string(),
1485 message: "command not found".to_string(),
1486 });
1487
1488 assert_eq!(result.failure_count(), 2);
1489 assert_eq!(result.server_count(), 0);
1490 assert!(result.all_failed());
1491 assert!(!result.partial_success());
1492 }
1493
1494 #[tokio::test]
1495 async fn test_spawn_batch_empty_configs() {
1496 let configs: &[ServerInitConfig] = &[];
1497 let result = LspServer::spawn_batch(configs).await;
1498
1499 assert!(!result.has_servers());
1500 assert!(!result.all_failed());
1501 assert!(!result.partial_success());
1502 assert_eq!(result.server_count(), 0);
1503 assert_eq!(result.failure_count(), 0);
1504 }
1505
1506 #[tokio::test]
1507 async fn test_spawn_batch_single_invalid_config() {
1508 let configs = vec![ServerInitConfig {
1509 server_config: LspServerConfig {
1510 language_id: "rust".to_string(),
1511 command: "nonexistent-command-12345".to_string(),
1512 args: vec![],
1513 env: std::collections::HashMap::new(),
1514 file_patterns: vec!["**/*.rs".to_string()],
1515 initialization_options: None,
1516 timeout_seconds: 10,
1517 request_timeout_seconds: 10,
1518 heuristics: None,
1519 name: None,
1520 handles: None,
1521 indexing: crate::bridge::IndexingPolicy::Auto,
1522 },
1523 workspace_roots: vec![],
1524 initialization_options: None,
1525 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1526 notification_tx: None,
1527 }];
1528
1529 let result = LspServer::spawn_batch(&configs).await;
1530
1531 assert!(!result.has_servers());
1532 assert!(result.all_failed());
1533 assert!(!result.partial_success());
1534 assert_eq!(result.server_count(), 0);
1535 assert_eq!(result.failure_count(), 1);
1536
1537 let failure = &result.failures[0];
1538 assert_eq!(failure.language_id, "rust");
1539 assert_eq!(failure.command, "nonexistent-command-12345");
1540 assert!(failure.message.contains("spawn"));
1541 }
1542
1543 #[tokio::test]
1544 async fn test_spawn_batch_all_invalid_configs() {
1545 let configs = vec![
1546 ServerInitConfig {
1547 server_config: LspServerConfig {
1548 language_id: "rust".to_string(),
1549 command: "nonexistent-rust-analyzer".to_string(),
1550 args: vec![],
1551 env: std::collections::HashMap::new(),
1552 file_patterns: vec!["**/*.rs".to_string()],
1553 initialization_options: None,
1554 timeout_seconds: 10,
1555 request_timeout_seconds: 10,
1556 heuristics: None,
1557 name: None,
1558 handles: None,
1559 indexing: crate::bridge::IndexingPolicy::Auto,
1560 },
1561 workspace_roots: vec![],
1562 initialization_options: None,
1563 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1564 notification_tx: None,
1565 },
1566 ServerInitConfig {
1567 server_config: LspServerConfig {
1568 language_id: "python".to_string(),
1569 command: "nonexistent-pyright".to_string(),
1570 args: vec![],
1571 env: std::collections::HashMap::new(),
1572 file_patterns: vec!["**/*.py".to_string()],
1573 initialization_options: None,
1574 timeout_seconds: 10,
1575 request_timeout_seconds: 10,
1576 heuristics: None,
1577 name: None,
1578 handles: None,
1579 indexing: crate::bridge::IndexingPolicy::Auto,
1580 },
1581 workspace_roots: vec![],
1582 initialization_options: None,
1583 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1584 notification_tx: None,
1585 },
1586 ServerInitConfig {
1587 server_config: LspServerConfig {
1588 language_id: "typescript".to_string(),
1589 command: "nonexistent-tsserver".to_string(),
1590 args: vec![],
1591 env: std::collections::HashMap::new(),
1592 file_patterns: vec!["**/*.ts".to_string()],
1593 initialization_options: None,
1594 timeout_seconds: 10,
1595 request_timeout_seconds: 10,
1596 heuristics: None,
1597 name: None,
1598 handles: None,
1599 indexing: crate::bridge::IndexingPolicy::Auto,
1600 },
1601 workspace_roots: vec![],
1602 initialization_options: None,
1603 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1604 notification_tx: None,
1605 },
1606 ];
1607
1608 let result = LspServer::spawn_batch(&configs).await;
1609
1610 assert!(!result.has_servers());
1611 assert!(result.all_failed());
1612 assert!(!result.partial_success());
1613 assert_eq!(result.server_count(), 0);
1614 assert_eq!(result.failure_count(), 3);
1615
1616 let failure_languages: Vec<_> = result
1617 .failures
1618 .iter()
1619 .map(|f| f.language_id.as_str())
1620 .collect();
1621 assert!(failure_languages.contains(&"rust"));
1622 assert!(failure_languages.contains(&"python"));
1623 assert!(failure_languages.contains(&"typescript"));
1624 }
1625
1626 #[tokio::test]
1627 async fn test_spawn_batch_multiple_invalid_configs_ordering() {
1628 let configs = vec![
1629 ServerInitConfig {
1630 server_config: LspServerConfig {
1631 language_id: "lang1".to_string(),
1632 command: "cmd1-nonexistent".to_string(),
1633 args: vec![],
1634 env: std::collections::HashMap::new(),
1635 file_patterns: vec![],
1636 initialization_options: None,
1637 timeout_seconds: 10,
1638 request_timeout_seconds: 10,
1639 heuristics: None,
1640 name: None,
1641 handles: None,
1642 indexing: crate::bridge::IndexingPolicy::Auto,
1643 },
1644 workspace_roots: vec![],
1645 initialization_options: None,
1646 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1647 notification_tx: None,
1648 },
1649 ServerInitConfig {
1650 server_config: LspServerConfig {
1651 language_id: "lang2".to_string(),
1652 command: "cmd2-nonexistent".to_string(),
1653 args: vec![],
1654 env: std::collections::HashMap::new(),
1655 file_patterns: vec![],
1656 initialization_options: None,
1657 timeout_seconds: 10,
1658 request_timeout_seconds: 10,
1659 heuristics: None,
1660 name: None,
1661 handles: None,
1662 indexing: crate::bridge::IndexingPolicy::Auto,
1663 },
1664 workspace_roots: vec![],
1665 initialization_options: None,
1666 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1667 notification_tx: None,
1668 },
1669 ];
1670
1671 let result = LspServer::spawn_batch(&configs).await;
1672
1673 assert_eq!(result.failure_count(), 2);
1674
1675 assert_eq!(result.failures[0].language_id, "lang1");
1676 assert_eq!(result.failures[0].command, "cmd1-nonexistent");
1677
1678 assert_eq!(result.failures[1].language_id, "lang2");
1679 assert_eq!(result.failures[1].command, "cmd2-nonexistent");
1680 }
1681
1682 mod initialize_wire {
1688 use tempfile::TempDir;
1689 use tokio::io::BufReader;
1690
1691 use super::*;
1692 use crate::test_lsp::{
1693 fake_lsp_client, read_framed_message, write_response as write_success_response,
1694 };
1695
1696 #[tokio::test]
1697 async fn test_initialize_sends_configured_position_encodings() {
1698 let (client, mut server) = fake_lsp_client();
1699
1700 let config = ServerInitConfig {
1701 server_config: LspServerConfig::rust_analyzer(),
1702 workspace_roots: vec![],
1703 initialization_options: None,
1704 position_encodings: vec!["utf-32".to_string(), "utf-8".to_string()],
1705 notification_tx: None,
1706 };
1707
1708 let init_task =
1709 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
1710
1711 let mut reader = BufReader::new(&mut server.write_stdout);
1712 let request = read_framed_message(&mut reader).await;
1713
1714 assert_eq!(request["method"], "initialize");
1715 assert_eq!(
1716 request["params"]["capabilities"]["general"]["positionEncodings"],
1717 serde_json::json!(["utf-32", "utf-8"]),
1718 "initialize request must carry the configured encoding order, not the \
1719 hardcoded [UTF8, UTF16] default"
1720 );
1721
1722 write_success_response(
1723 &mut server.read_half_stdin,
1724 &request["id"].clone(),
1725 serde_json::json!({ "capabilities": {} }),
1726 )
1727 .await;
1728
1729 init_task.await.unwrap().unwrap();
1731 }
1732
1733 #[tokio::test]
1734 async fn test_initialize_advertises_stale_request_support() {
1735 let (client, mut server) = fake_lsp_client();
1736
1737 let config = ServerInitConfig {
1738 server_config: LspServerConfig::rust_analyzer(),
1739 workspace_roots: vec![],
1740 initialization_options: None,
1741 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1742 notification_tx: None,
1743 };
1744
1745 let init_task =
1746 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
1747
1748 let mut reader = BufReader::new(&mut server.write_stdout);
1749 let request = read_framed_message(&mut reader).await;
1750 let params: InitializeParams =
1751 serde_json::from_value(request["params"].clone()).unwrap();
1752 let stale_request_support = params
1753 .capabilities
1754 .general
1755 .unwrap()
1756 .stale_request_support
1757 .unwrap();
1758
1759 assert_eq!(request["method"], "initialize");
1760 assert!(
1761 !stale_request_support.cancel,
1762 "mcpls does not implement active in-flight request cancellation"
1763 );
1764 assert_eq!(
1765 stale_request_support.retry_on_content_modified,
1766 CONTENT_MODIFIED_RETRY_METHODS
1767 .iter()
1768 .map(ToString::to_string)
1769 .collect::<Vec<_>>(),
1770 "the wire-advertised capability must match the methods LspClient::request \
1771 actually retries -32801 for, not drift from it"
1772 );
1773
1774 write_success_response(
1775 &mut server.read_half_stdin,
1776 &request["id"].clone(),
1777 serde_json::json!({ "capabilities": {} }),
1778 )
1779 .await;
1780
1781 init_task.await.unwrap().unwrap();
1782 }
1783
1784 #[tokio::test]
1785 async fn test_initialize_advertises_server_status_notification_support() {
1786 let (client, mut server) = fake_lsp_client();
1787
1788 let config = ServerInitConfig {
1789 server_config: LspServerConfig::rust_analyzer(),
1790 workspace_roots: vec![],
1791 initialization_options: None,
1792 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1793 notification_tx: None,
1794 };
1795
1796 let init_task =
1797 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
1798
1799 let mut reader = BufReader::new(&mut server.write_stdout);
1800 let request = read_framed_message(&mut reader).await;
1801
1802 assert_eq!(request["method"], "initialize");
1803 assert_eq!(
1804 request["params"]["capabilities"]["experimental"]["serverStatusNotification"],
1805 serde_json::json!(true),
1806 "without this, rust-analyzer never emits experimental/serverStatus and the \
1807 indexing-readiness gate in Translator::wait_for_indexing_ready is a \
1808 permanent no-op"
1809 );
1810
1811 write_success_response(
1812 &mut server.read_half_stdin,
1813 &request["id"].clone(),
1814 serde_json::json!({ "capabilities": {} }),
1815 )
1816 .await;
1817
1818 init_task.await.unwrap().unwrap();
1819 }
1820
1821 #[tokio::test]
1822 async fn test_initialize_advertises_hierarchical_document_symbols() {
1823 let (client, mut server) = fake_lsp_client();
1824
1825 let config = ServerInitConfig {
1826 server_config: LspServerConfig::rust_analyzer(),
1827 workspace_roots: vec![],
1828 initialization_options: None,
1829 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1830 notification_tx: None,
1831 };
1832
1833 let init_task =
1834 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
1835
1836 let mut reader = BufReader::new(&mut server.write_stdout);
1837 let request = read_framed_message(&mut reader).await;
1838 let params: InitializeParams =
1839 serde_json::from_value(request["params"].clone()).unwrap();
1840 let document_symbol = params
1841 .capabilities
1842 .text_document
1843 .unwrap()
1844 .document_symbol
1845 .unwrap();
1846
1847 assert_eq!(request["method"], "initialize");
1848 assert_eq!(document_symbol.dynamic_registration, Some(false));
1849 assert_eq!(
1850 document_symbol.hierarchical_document_symbol_support,
1851 Some(true)
1852 );
1853 assert_eq!(
1854 document_symbol.symbol_kind.unwrap().value_set,
1855 Some(SUPPORTED_SYMBOL_KINDS.to_vec())
1856 );
1857
1858 write_success_response(
1859 &mut server.read_half_stdin,
1860 &request["id"].clone(),
1861 serde_json::json!({ "capabilities": {} }),
1862 )
1863 .await;
1864
1865 init_task.await.unwrap().unwrap();
1866 }
1867
1868 #[tokio::test]
1869 async fn test_initialize_accepts_resolved_dot_workspace_root() {
1870 let temp_dir = TempDir::new().unwrap();
1871 let base = dunce::canonicalize(temp_dir.path()).unwrap();
1872 let workspace_roots =
1873 crate::resolve_workspace_roots(&[PathBuf::from(".")], &base).unwrap();
1874 assert_eq!(workspace_roots, vec![base.clone()]);
1875
1876 let (client, mut server) = fake_lsp_client();
1877 let config = ServerInitConfig {
1878 server_config: LspServerConfig::rust_analyzer(),
1879 workspace_roots,
1880 initialization_options: None,
1881 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1882 notification_tx: None,
1883 };
1884
1885 let init_task =
1886 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
1887
1888 let mut reader = BufReader::new(&mut server.write_stdout);
1889 let request = read_framed_message(&mut reader).await;
1890 let expected_uri = try_path_to_uri(&base).unwrap();
1891 assert_eq!(
1892 request["params"]["workspaceFolders"][0]["uri"],
1893 expected_uri.as_ref()
1894 );
1895
1896 write_success_response(
1897 &mut server.read_half_stdin,
1898 &request["id"].clone(),
1899 serde_json::json!({ "capabilities": {} }),
1900 )
1901 .await;
1902
1903 init_task.await.unwrap().unwrap();
1904 }
1905 }
1906
1907 #[tokio::test]
1908 async fn test_spawn_batch_logs_each_failure() {
1909 let configs = vec![
1910 ServerInitConfig {
1911 server_config: LspServerConfig {
1912 language_id: "test1".to_string(),
1913 command: "nonexistent-test1".to_string(),
1914 args: vec![],
1915 env: std::collections::HashMap::new(),
1916 file_patterns: vec![],
1917 initialization_options: None,
1918 timeout_seconds: 10,
1919 request_timeout_seconds: 10,
1920 heuristics: None,
1921 name: None,
1922 handles: None,
1923 indexing: crate::bridge::IndexingPolicy::Auto,
1924 },
1925 workspace_roots: vec![],
1926 initialization_options: None,
1927 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1928 notification_tx: None,
1929 },
1930 ServerInitConfig {
1931 server_config: LspServerConfig {
1932 language_id: "test2".to_string(),
1933 command: "nonexistent-test2".to_string(),
1934 args: vec![],
1935 env: std::collections::HashMap::new(),
1936 file_patterns: vec![],
1937 initialization_options: None,
1938 timeout_seconds: 10,
1939 request_timeout_seconds: 10,
1940 heuristics: None,
1941 name: None,
1942 handles: None,
1943 indexing: crate::bridge::IndexingPolicy::Auto,
1944 },
1945 workspace_roots: vec![],
1946 initialization_options: None,
1947 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
1948 notification_tx: None,
1949 },
1950 ];
1951
1952 let result = LspServer::spawn_batch(&configs).await;
1953
1954 assert_eq!(result.failure_count(), 2);
1955 assert_eq!(result.failures[0].language_id, "test1");
1956 assert_eq!(result.failures[1].language_id, "test2");
1957 }
1958
1959 fn bare_server_config(env: HashMap<String, String>) -> LspServerConfig {
1962 LspServerConfig {
1963 language_id: "test".to_string(),
1964 command: "irrelevant-for-build-command".to_string(),
1965 args: vec![],
1966 env,
1967 file_patterns: vec![],
1968 initialization_options: None,
1969 timeout_seconds: 5,
1970 request_timeout_seconds: 5,
1971 heuristics: None,
1972 name: None,
1973 handles: None,
1974 indexing: crate::bridge::IndexingPolicy::Auto,
1975 }
1976 }
1977
1978 fn effective_envs(command: &Command) -> HashMap<String, String> {
1982 command
1983 .as_std()
1984 .get_envs()
1985 .filter_map(|(k, v)| {
1986 v.map(|v| {
1987 (
1988 k.to_string_lossy().into_owned(),
1989 v.to_string_lossy().into_owned(),
1990 )
1991 })
1992 })
1993 .collect()
1994 }
1995
1996 #[test]
2000 fn test_build_command_excludes_non_allowlisted_parent_env_vars() {
2001 let config = bare_server_config(HashMap::new());
2002 let command = LspServer::build_command(&config, |key| match key {
2003 "PATH" => Some("/parent/bin".into()),
2004 "MCPLS_TEST_LEAK_CANARY" => Some("should-not-reach-child".into()),
2005 _ => None,
2006 });
2007
2008 let envs = effective_envs(&command);
2009
2010 assert!(
2011 !envs.contains_key("MCPLS_TEST_LEAK_CANARY"),
2012 "non-allowlisted parent env var leaked into child command: {envs:?}"
2013 );
2014
2015 #[cfg(unix)]
2026 assert!(
2027 format!("{:?}", command.as_std()).starts_with("env -i "),
2028 "build_command must call .env_clear() so the child doesn't inherit the full parent environment"
2029 );
2030 }
2031
2032 #[test]
2035 fn test_build_command_passes_through_allowlisted_env_vars() {
2036 let config = bare_server_config(HashMap::new());
2037 let command =
2038 LspServer::build_command(&config, |key| (key == "PATH").then(|| "/parent/bin".into()));
2039
2040 let envs = effective_envs(&command);
2041
2042 assert_eq!(envs.get("PATH"), Some(&"/parent/bin".to_string()));
2043 }
2044
2045 #[test]
2048 fn test_build_command_includes_configured_env_vars() {
2049 let mut env = HashMap::new();
2050 env.insert(
2051 "MCPLS_TEST_CONFIGURED".to_string(),
2052 "from-server-config".to_string(),
2053 );
2054 let config = bare_server_config(env);
2055 let command = LspServer::build_command(&config, |_| None);
2056
2057 let envs = effective_envs(&command);
2058
2059 assert_eq!(
2060 envs.get("MCPLS_TEST_CONFIGURED"),
2061 Some(&"from-server-config".to_string())
2062 );
2063 }
2064
2065 #[test]
2069 fn test_build_command_configured_env_overrides_allowlisted_var() {
2070 let mut env = HashMap::new();
2071 env.insert("PATH".to_string(), "/configured/override/path".to_string());
2072 let config = bare_server_config(env);
2073 let command =
2074 LspServer::build_command(&config, |key| (key == "PATH").then(|| "/parent/bin".into()));
2075
2076 let envs = effective_envs(&command);
2077
2078 assert_eq!(
2079 envs.get("PATH"),
2080 Some(&"/configured/override/path".to_string())
2081 );
2082 }
2083
2084 #[tokio::test]
2094 async fn test_register_servers_computes_diagnostics_flags_from_rebound_router() {
2095 use crate::bridge::Translator;
2096 use crate::config::{ServerId, ToolKind, ToolRouter};
2097
2098 let pylsp_id = ServerId::from("pylsp");
2099 let configs = vec![
2100 LspServerConfig {
2101 language_id: "python".to_string(),
2102 command: "pyright-langserver".to_string(),
2103 args: vec![],
2104 env: std::collections::HashMap::new(),
2105 file_patterns: vec![],
2106 initialization_options: None,
2107 timeout_seconds: 30,
2108 request_timeout_seconds: 30,
2109 heuristics: None,
2110 name: Some("pyright-diag".to_string()),
2111 handles: Some(vec![ToolKind::Diagnostics]),
2112 indexing: crate::bridge::IndexingPolicy::Auto,
2113 },
2114 LspServerConfig {
2115 language_id: "python".to_string(),
2116 command: "pylsp".to_string(),
2117 args: vec![],
2118 env: std::collections::HashMap::new(),
2119 file_patterns: vec![],
2120 initialization_options: None,
2121 timeout_seconds: 30,
2122 request_timeout_seconds: 30,
2123 heuristics: None,
2124 name: Some("pylsp".to_string()),
2125 handles: None,
2126 indexing: crate::bridge::IndexingPolicy::Auto,
2127 },
2128 ];
2129 let router = ToolRouter::from_configs(&configs).unwrap();
2130 let translator = Translator::new().with_router(router);
2131
2132 let mut result = ServerInitResult::new();
2134 result.add_server(pylsp_id.clone(), fake_lsp_server());
2135
2136 let registered = crate::register_servers(result, &translator, &HashMap::new());
2137
2138 assert_eq!(
2139 registered.diagnostics_flags.get(&pylsp_id),
2140 Some(&true),
2141 "pylsp must inherit the diagnostics route once pyright-diag is \
2142 known dead, and the flag must reflect that post-rebind state"
2143 );
2144 }
2145}