1use std::path::{Path, PathBuf};
11use std::process::Stdio;
12use std::sync::Arc;
13
14use lsp_types::{
15 ClientCapabilities, ClientInfo, DidChangeConfigurationNotification,
16 DidChangeConfigurationParams, ExitNotification, GeneralClientCapabilities, InitializeParams,
17 InitializeRequest, InitializeResult, InitializedNotification, InitializedParams,
18 PositionEncodingKind, Request, ServerCapabilities, ShutdownRequest, StaleRequestSupportOptions,
19 SymbolKind, WorkspaceFolder,
20};
21use tokio::process::Command;
22use tokio::sync::mpsc;
23use tokio::time::{Duration, Instant, timeout, timeout_at};
24use tracing::{debug, info, warn};
25
26use crate::bridge::{WorkspaceRoots, try_path_to_uri};
27use crate::config::{LspServerConfig, LspSettings, PositionEncodings, ServerCommand};
28use crate::error::{
29 BackgroundTask, Error, InitFailureHint, InitPhase, Result, ServerSpawnFailure, StartupFailure,
30 StdioStream, UntrustedRefusal,
31};
32use crate::lsp::client::{LspClient, SHUTDOWN_TIMEOUT};
33#[cfg(unix)]
34use crate::lsp::process::Binding;
35use crate::lsp::process::{MarkOutcome, ServerProcess};
36use crate::lsp::stderr::{EofWait, StderrCapture};
37use crate::lsp::transport::LspTransport;
38use crate::lsp::types::LspNotification;
39use crate::lsp::vetting::UntrustedVetting;
40use crate::lsp::{
41 CONTENT_MODIFIED_RETRY_METHODS, ManagedEnvVar, NotificationInbox, NotificationSink, ParentEnv,
42 process_env, tsserver_pin,
43};
44use crate::redaction::Redactions;
45
46const ENV_PASSTHROUGH: &[&str] = &[
59 ManagedEnvVar::Path.name(),
60 ManagedEnvVar::Home.name(),
61 ManagedEnvVar::UserProfile.name(),
62 "TMPDIR",
63 "TEMP",
64 "TMP",
65];
66
67const CHILD_EXIT_GRACE: Duration = Duration::from_secs(3);
71
72const MIN_SWEEP_BUDGET: Duration = Duration::from_secs(3);
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum ExitGrace {
81 WithinBudget,
84 IfAnswered(Duration),
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92enum ExitNotice {
93 Sent,
95 NotSent,
98}
99
100impl ExitNotice {
101 const fn of(handshake: &Result<MarkOutcome>) -> Self {
104 match handshake {
105 Ok(MarkOutcome::Confirmed) => Self::Sent,
106 _ => Self::NotSent,
107 }
108 }
109}
110
111fn child_exit_wait(
115 grace: ExitGrace,
116 mark: MarkOutcome,
117 answered: &Result<()>,
118 remaining: Duration,
119) -> Duration {
120 match grace {
121 _ if mark != MarkOutcome::Confirmed => Duration::ZERO,
122 ExitGrace::WithinBudget => remaining.min(CHILD_EXIT_GRACE),
123 ExitGrace::IfAnswered(grace) if answered.is_ok() => grace,
124 ExitGrace::IfAnswered(_) => Duration::ZERO,
125 }
126}
127
128const EARLY_EXIT_PROBE: Duration = Duration::from_millis(500);
131
132const NOTIFICATION_CHANNEL_CAPACITY: usize = 256;
140
141const LIFECYCLE_CHANNEL_CAPACITY: usize = 128;
151
152pub const SUPPORTED_SYMBOL_KINDS: [(SymbolKind, &str); 26] = [
158 (SymbolKind::File, "file"),
159 (SymbolKind::Module, "module"),
160 (SymbolKind::Namespace, "namespace"),
161 (SymbolKind::Package, "package"),
162 (SymbolKind::Class, "class"),
163 (SymbolKind::Method, "method"),
164 (SymbolKind::Property, "property"),
165 (SymbolKind::Field, "field"),
166 (SymbolKind::Constructor, "constructor"),
167 (SymbolKind::Enum, "enum"),
168 (SymbolKind::Interface, "interface"),
169 (SymbolKind::Function, "function"),
170 (SymbolKind::Variable, "variable"),
171 (SymbolKind::Constant, "constant"),
172 (SymbolKind::String, "string"),
173 (SymbolKind::Number, "number"),
174 (SymbolKind::Boolean, "boolean"),
175 (SymbolKind::Array, "array"),
176 (SymbolKind::Object, "object"),
177 (SymbolKind::Key, "key"),
178 (SymbolKind::Null, "null"),
179 (SymbolKind::EnumMember, "enummember"),
180 (SymbolKind::Struct, "struct"),
181 (SymbolKind::Event, "event"),
182 (SymbolKind::Operator, "operator"),
183 (SymbolKind::TypeParameter, "typeparameter"),
184];
185
186fn advertised_symbol_kinds() -> Vec<SymbolKind> {
188 SUPPORTED_SYMBOL_KINDS
189 .iter()
190 .map(|&(kind, _)| kind)
191 .collect()
192}
193
194#[cfg(windows)]
204const ENV_PASSTHROUGH_WINDOWS: &[&str] = &[
205 ManagedEnvVar::NoDefaultCurrentDirectoryInExePath.name(),
206 "SystemRoot",
207 "SystemDrive",
208 "windir",
209 "APPDATA",
210 "LOCALAPPDATA",
211 "ProgramData",
212 "ProgramFiles",
213 "COMSPEC",
214 "PATHEXT",
215 "NUMBER_OF_PROCESSORS",
216 "USERNAME",
217];
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub enum ServerState {
222 Uninitialized,
224 Initializing,
226 Ready,
228 ShuttingDown,
230 Shutdown,
232}
233
234impl ServerState {
235 #[must_use]
237 pub const fn is_ready(&self) -> bool {
238 matches!(self, Self::Ready)
239 }
240}
241
242#[derive(Debug, Clone, PartialEq, Eq, Default)]
259pub enum ChildWorkingDir {
260 #[default]
262 Inherit,
263 Fixed(PathBuf),
265}
266
267#[derive(Debug, Clone)]
270pub enum SpawnTrust {
271 Trusted,
273 Untrusted(UntrustedVetting),
276}
277
278impl SpawnTrust {
279 pub const fn vetting(&self) -> Option<&UntrustedVetting> {
281 match self {
282 Self::Trusted => None,
283 Self::Untrusted(vetting) => Some(vetting),
284 }
285 }
286}
287
288#[derive(Debug, Clone)]
293pub struct VettedInitConfig(ServerInitConfig);
294
295impl VettedInitConfig {
296 pub(crate) fn into_inner(self) -> ServerInitConfig {
298 self.0
299 }
300}
301
302impl std::ops::Deref for VettedInitConfig {
303 type Target = ServerInitConfig;
304
305 fn deref(&self) -> &ServerInitConfig {
306 &self.0
307 }
308}
309
310#[derive(Debug, Clone)]
316pub struct ServerInitConfig {
317 server_config: LspServerConfig,
318 workspace_roots: WorkspaceRoots,
319 position_encodings: PositionEncodings,
320 redactions: Arc<Redactions>,
321 child_working_dir: ChildWorkingDir,
322 auto_pin: Option<PathBuf>,
326 trust: SpawnTrust,
327}
328
329impl ServerInitConfig {
330 #[must_use]
364 pub const fn new(
365 server_config: LspServerConfig,
366 workspace_roots: WorkspaceRoots,
367 position_encodings: PositionEncodings,
368 redactions: Arc<Redactions>,
369 ) -> Self {
370 Self {
371 server_config,
372 workspace_roots,
373 position_encodings,
374 redactions,
375 child_working_dir: ChildWorkingDir::Inherit,
376 auto_pin: None,
377 trust: SpawnTrust::Trusted,
378 }
379 }
380
381 #[must_use]
384 pub(crate) fn with_trust(mut self, trust: SpawnTrust) -> Self {
385 self.trust = trust;
386 self
387 }
388
389 pub(crate) fn vet_arguments(&self) -> Result<()> {
402 let Some(vetting) = self.trust.vetting() else {
403 return Ok(());
404 };
405 vetting
406 .boundary()
407 .refuse_arguments(
408 &self.server_config.args,
409 &self.child_working_dir,
410 self.server_config.env.host(),
411 )
412 .map_err(|refusal| self.refusal(refusal))
413 }
414
415 pub(crate) async fn vet_arguments_off_runtime(&self) -> Result<()> {
418 if self.trust.vetting().is_none() {
419 return Ok(());
420 }
421 self.on_planning_pool(Self::vet_arguments).await
422 }
423
424 async fn on_planning_pool<T: Send + 'static>(
426 &self,
427 plan: impl FnOnce(&Self) -> Result<T> + Send + 'static,
428 ) -> Result<T> {
429 let config = self.clone();
430 crate::on_blocking_pool(move || plan(&config))
431 .await
432 .map_err(|source| Error::TaskFailed {
433 task: crate::error::BackgroundTask::ServerPlanning,
434 source,
435 })?
436 }
437
438 #[must_use]
440 pub(crate) fn with_auto_pin(mut self, tsserver: PathBuf) -> Self {
441 self.auto_pin = Some(tsserver);
442 self
443 }
444
445 #[must_use]
468 pub fn with_child_working_dir(mut self, dir: ChildWorkingDir) -> Self {
469 self.child_working_dir = dir;
470 self
471 }
472
473 #[must_use]
475 pub const fn server_config(&self) -> &LspServerConfig {
476 &self.server_config
477 }
478
479 #[must_use]
481 pub const fn workspace_roots(&self) -> &WorkspaceRoots {
482 &self.workspace_roots
483 }
484
485 #[must_use]
487 pub const fn position_encodings(&self) -> &PositionEncodings {
488 &self.position_encodings
489 }
490
491 #[must_use]
493 pub const fn redactions(&self) -> &Arc<Redactions> {
494 &self.redactions
495 }
496
497 #[must_use]
499 pub const fn child_working_dir(&self) -> &ChildWorkingDir {
500 &self.child_working_dir
501 }
502
503 #[cfg(test)]
505 pub(crate) const fn server_config_mut(&mut self) -> &mut LspServerConfig {
506 &mut self.server_config
507 }
508
509 #[must_use]
511 pub fn pinned_tsserver(&self) -> Option<PathBuf> {
512 tsserver_pin::configured_tsserver_path(self.server_config.initialization_options.as_ref())
513 }
514
515 pub(crate) async fn for_respawn(&self) -> Result<VettedInitConfig> {
538 if self.auto_pin.is_none() && self.trust.vetting().is_none() {
539 return Ok(VettedInitConfig(self.clone()));
540 }
541 self.on_planning_pool(Self::plan_respawn).await
542 }
543
544 pub(crate) fn plan_respawn(&self) -> Result<VettedInitConfig> {
547 self.vet_arguments()?;
548 let pin_is_current = self
549 .auto_pin
550 .as_ref()
551 .is_none_or(|pin| dunce::canonicalize(pin).ok().as_ref() == Some(pin));
552 if pin_is_current {
553 return Ok(VettedInitConfig(self.clone()));
554 }
555 let mut unpinned = self.server_config.clone();
556 unpinned.initialization_options = None;
557 let plan = tsserver_pin::plan_typescript(unpinned, process_env);
558 if let Some(vetting) = self.trust.vetting() {
559 plan.vet_untrusted(vetting)
560 .map_err(|refusal| self.refusal(refusal))?;
561 }
562 let (server_config, auto_pin) = plan.apply(&self.workspace_roots);
563 Ok(VettedInitConfig(Self {
564 server_config,
565 auto_pin,
566 ..self.clone()
567 }))
568 }
569
570 fn refusal(&self, refusal: UntrustedRefusal) -> Error {
571 Error::ServerFailedToStart(Box::new(ServerSpawnFailure {
572 server_id: self.server_config.id(),
573 language_id: self.server_config.language_id.clone(),
574 command: self.server_config.command.server_command().clone(),
575 reason: StartupFailure::RefusedUntrustedWorkspace(refusal),
576 }))
577 }
578}
579
580#[derive(Debug)]
585pub enum ServerStartOutcome {
586 Started(Box<SpawnedServer>),
588 Failed(ServerSpawnFailure),
590}
591
592#[derive(Debug)]
597pub struct ServerLanes {
598 pub notifications: NotificationInbox,
603 pub lifecycle: mpsc::Receiver<LspNotification>,
609}
610
611#[derive(Debug)]
613pub struct SpawnedServer {
614 pub server: LspServer,
616 pub lanes: ServerLanes,
618}
619
620pub struct LspServer {
622 client: LspClient,
623 capabilities: ServerCapabilities,
624 position_encoding: PositionEncodingKind,
625 child: Option<ServerProcess>,
636 init_config: ServerInitConfig,
639}
640
641impl std::fmt::Debug for LspServer {
642 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
643 f.debug_struct("LspServer")
644 .field("client", &self.client)
645 .field("capabilities", &self.capabilities)
646 .field("position_encoding", &self.position_encoding)
647 .field("child", &"<process>")
648 .field("id", &self.init_config.server_config.id())
649 .finish()
650 }
651}
652
653pub fn child_env_var(
659 config: &LspServerConfig,
660 key: &str,
661 parent_env: impl ParentEnv,
662) -> Option<std::ffi::OsString> {
663 config
664 .env
665 .get(key)
666 .map(std::ffi::OsString::from)
667 .or_else(|| parent_env(key))
668}
669
670impl LspServer {
671 pub(crate) const fn init_config(&self) -> &ServerInitConfig {
673 &self.init_config
674 }
675
676 #[cfg(test)]
678 pub(crate) fn set_init_config(&mut self, config: ServerInitConfig) {
679 self.init_config = config;
680 }
681
682 pub async fn spawn(config: ServerInitConfig) -> Result<SpawnedServer> {
703 config.vet_arguments_off_runtime().await?;
705 let own = Redactions::for_server(config.server_config(), current_environment());
706 let redactions = Arc::new(Redactions::union([config.redactions().as_ref(), &own]));
707 Self::log_spawn(config.server_config(), &redactions);
708
709 let command = Self::build_command(
710 config.server_config(),
711 config.child_working_dir(),
712 process_env,
713 );
714
715 let passthrough_present = {
720 let base = ENV_PASSTHROUGH
721 .iter()
722 .filter(|key| std::env::var_os(key).is_some())
723 .count();
724 #[cfg(windows)]
725 let windows = ENV_PASSTHROUGH_WINDOWS
726 .iter()
727 .filter(|key| std::env::var_os(key).is_some())
728 .count();
729 #[cfg(not(windows))]
730 let windows = 0;
731 base.saturating_add(windows)
732 };
733 debug!(
734 "Effective LSP server env: {passthrough_present} allowlisted key(s) present, \
735 {} configured override(s) applied",
736 config.server_config.env.len()
737 );
738
739 let mut child = ServerProcess::spawn(command)
740 .map_err(|e| spawn_error(config.server_config.command.server_command().clone(), e))?;
741
742 let stdin = child
743 .take_stdin()
744 .ok_or(Error::StdioCapture(StdioStream::Stdin))?;
745 let stdout = child
746 .take_stdout()
747 .ok_or(Error::StdioCapture(StdioStream::Stdout))?;
748
749 let stderr = child
750 .take_stderr()
751 .ok_or(Error::StdioCapture(StdioStream::Stderr))?;
752 let stderr_capture = StderrCapture::start(stderr);
753
754 let transport = LspTransport::with_redactions(stdin, stdout, Arc::clone(&redactions));
755 let (notification_tx, notification_rx) = mpsc::channel(NOTIFICATION_CHANNEL_CAPACITY);
756 let (notification_sink, publish_rx) = NotificationSink::new(notification_tx);
757 let (lifecycle_tx, lifecycle_rx) = mpsc::channel(LIFECYCLE_CHANNEL_CAPACITY);
758 let client = LspClient::from_transport_with_notifications(
759 config.server_config.clone(),
760 transport,
761 notification_sink,
762 lifecycle_tx,
763 Arc::clone(&redactions),
764 );
765 #[cfg(unix)]
766 let process_id = initialize_process_id(child.binding());
767 #[cfg(windows)]
768 let process_id = Some(mcpls_process_id());
769 let (capabilities, position_encoding) =
770 match Self::initialize_as(&client, &config, process_id).await {
771 Ok(negotiated) => negotiated,
772 Err(init_error) if is_connection_loss(&init_error) => {
773 return Err(Self::connection_loss_error(
774 &config,
775 &mut child,
776 stderr_capture,
777 &redactions,
778 init_error,
779 )
780 .await);
781 }
782 Err(Error::LspInitFailed { phase, cause, .. }) => {
783 let stderr = stderr_capture.finish(EofWait::Grace, &redactions).await;
787 let hint = Self::init_failure_hint(&config).await;
788 return Err(Error::LspInitFailed {
789 phase,
790 cause: Box::new(redactions.sanitize_error(*cause)),
791 hint,
792 stderr,
793 });
794 }
795 Err(init_error) => return Err(redactions.sanitize_error(init_error)),
796 };
797
798 info!("LSP server initialized successfully");
799
800 Ok(SpawnedServer {
801 server: Self {
802 client,
803 capabilities,
804 position_encoding,
805 child: Some(child),
806 init_config: config,
807 },
808 lanes: ServerLanes {
809 notifications: NotificationInbox::new(notification_rx, publish_rx),
810 lifecycle: lifecycle_rx,
811 },
812 })
813 }
814
815 async fn connection_loss_error(
818 config: &ServerInitConfig,
819 child: &mut ServerProcess,
820 stderr_capture: StderrCapture,
821 redactions: &Redactions,
822 init_error: Error,
823 ) -> Error {
824 let exit_status = early_exit_status(child).await;
825 let eof_wait = if exit_status.is_some() {
826 EofWait::Grace
827 } else {
828 EofWait::Skip
829 };
830 let stderr = stderr_capture.finish(eof_wait, redactions).await;
831 let hint = Self::init_failure_hint(config).await;
832 match exit_status {
833 Some(status) => Error::ServerExitedDuringInit {
834 command: config.server_config.command.server_command().clone(),
835 exit_code: status.code(),
836 hint,
837 stderr,
838 },
839 None => Error::LspInitFailed {
840 phase: InitPhase::Initialize,
841 cause: Box::new(redactions.sanitize_error(init_error)),
842 hint,
843 stderr,
844 },
845 }
846 }
847
848 async fn init_failure_hint(config: &ServerInitConfig) -> Option<InitFailureHint> {
854 let config = config.clone();
855 let hint = crate::on_blocking_pool(move || {
856 tsserver_pin::init_failure_hint(
857 config.server_config(),
858 config.workspace_roots(),
859 process_env,
860 )
861 })
862 .await;
863 hint.unwrap_or_else(|error| {
864 debug!("Dropped the init failure hint: {error}");
865 None
866 })
867 }
868
869 fn log_spawn(server_config: &LspServerConfig, redactions: &Redactions) {
872 info!(
873 "Spawning LSP server: {} ({} arg(s))",
874 server_config.command,
875 server_config.args.len()
876 );
877 debug!(
878 "LSP server args: {:?}",
879 server_config
880 .args
881 .iter()
882 .map(|arg| redactions.apply(arg))
883 .collect::<Vec<_>>()
884 );
885 }
886
887 fn build_command(
897 config: &LspServerConfig,
898 working_dir: &ChildWorkingDir,
899 parent_env: impl ParentEnv,
900 ) -> Command {
901 let mut command = Command::new(&config.command);
902 command.args(&config.args).env_clear();
903 if let ChildWorkingDir::Fixed(dir) = working_dir {
904 command.current_dir(dir);
905 }
906
907 for key in ENV_PASSTHROUGH {
908 if let Some(value) = child_env_var(config, key, &parent_env) {
909 command.env(key, value);
910 }
911 }
912 #[cfg(windows)]
913 for key in ENV_PASSTHROUGH_WINDOWS {
914 if let Some(value) = child_env_var(config, key, &parent_env) {
915 command.env(key, value);
916 }
917 }
918
919 command
920 .envs(config.env.iter())
921 .stdin(Stdio::piped())
922 .stdout(Stdio::piped())
923 .stderr(Stdio::piped());
924
925 command
926 }
927
928 fn folding_range_capabilities() -> lsp_types::FoldingRangeClientCapabilities {
931 lsp_types::FoldingRangeClientCapabilities {
932 dynamic_registration: Some(false),
933 line_folding_only: Some(false),
934 folding_range_kind: Some(lsp_types::ClientFoldingRangeKindOptions {
935 value_set: Some(vec![
936 lsp_types::FoldingRangeKind::Comment,
937 lsp_types::FoldingRangeKind::Imports,
938 lsp_types::FoldingRangeKind::Region,
939 ]),
940 }),
941 folding_range: Some(lsp_types::ClientFoldingRangeOptions {
942 collapsed_text: Some(true),
943 }),
944 ..Default::default()
945 }
946 }
947
948 fn client_capabilities(
963 position_encodings: &PositionEncodings,
964 settings: Option<&LspSettings>,
965 ) -> ClientCapabilities {
966 ClientCapabilities {
967 general: Some(GeneralClientCapabilities {
968 position_encodings: Some(
969 position_encodings
970 .as_slice()
971 .iter()
972 .map(|encoding| encoding.to_kind())
973 .collect(),
974 ),
975 stale_request_support: Some(StaleRequestSupportOptions {
976 cancel: false,
979 retry_on_content_modified: CONTENT_MODIFIED_RETRY_METHODS
980 .iter()
981 .map(ToString::to_string)
982 .collect(),
983 }),
984 ..Default::default()
985 }),
986 text_document: Some(lsp_types::TextDocumentClientCapabilities {
987 synchronization: Some(lsp_types::TextDocumentSyncClientCapabilities {
988 dynamic_registration: Some(false),
989 did_save: Some(true),
990 ..Default::default()
991 }),
992 document_symbol: Some(lsp_types::DocumentSymbolClientCapabilities {
993 dynamic_registration: Some(false),
994 symbol_kind: Some(lsp_types::ClientSymbolKindOptions {
995 value_set: Some(advertised_symbol_kinds()),
996 }),
997 hierarchical_document_symbol_support: Some(true),
998 ..Default::default()
999 }),
1000 hover: Some(lsp_types::HoverClientCapabilities {
1001 dynamic_registration: Some(false),
1002 content_format: Some(vec![
1003 lsp_types::MarkupKind::Markdown,
1004 lsp_types::MarkupKind::PlainText,
1005 ]),
1006 }),
1007 definition: Some(lsp_types::DefinitionClientCapabilities {
1008 dynamic_registration: Some(false),
1009 link_support: Some(true),
1010 }),
1011 declaration: Some(lsp_types::DeclarationClientCapabilities {
1012 dynamic_registration: Some(false),
1013 link_support: Some(true),
1014 }),
1015 references: Some(lsp_types::ReferenceClientCapabilities {
1016 dynamic_registration: Some(false),
1017 }),
1018 rename: Some(lsp_types::RenameClientCapabilities {
1021 dynamic_registration: Some(false),
1022 prepare_support: Some(true),
1023 prepare_support_default_behavior: Some(
1024 lsp_types::PrepareSupportDefaultBehavior::Identifier,
1025 ),
1026 ..Default::default()
1027 }),
1028 folding_range: Some(Self::folding_range_capabilities()),
1029 publish_diagnostics: Some(lsp_types::PublishDiagnosticsClientCapabilities {
1032 version_support: Some(true),
1033 ..Default::default()
1034 }),
1035 selection_range: Some(lsp_types::SelectionRangeClientCapabilities {
1036 dynamic_registration: Some(false),
1037 }),
1038 code_action: Some(lsp_types::CodeActionClientCapabilities {
1039 dynamic_registration: Some(false),
1040 data_support: Some(true),
1041 resolve_support: Some(lsp_types::ClientCodeActionResolveOptions {
1042 properties: vec!["edit".to_string()],
1043 }),
1044 code_action_literal_support: Some(lsp_types::ClientCodeActionLiteralOptions {
1047 code_action_kind: lsp_types::ClientCodeActionKindOptions {
1048 value_set: std::iter::once(lsp_types::CodeActionKind::Empty)
1049 .chain(
1050 crate::bridge::CodeActionKindFilter::ALL
1051 .map(lsp_types::CodeActionKind::from),
1052 )
1053 .collect(),
1054 },
1055 }),
1056 ..Default::default()
1057 }),
1058 ..Default::default()
1059 }),
1060 workspace: Some(lsp_types::WorkspaceClientCapabilities {
1061 workspace_folders: Some(true),
1062 configuration: settings.map(|_| true),
1063 ..Default::default()
1064 }),
1065 window: Some(lsp_types::WindowClientCapabilities {
1067 work_done_progress: Some(true),
1068 ..Default::default()
1069 }),
1070 experimental: Some(serde_json::json!({ "serverStatusNotification": true })),
1072 ..Default::default()
1073 }
1074 }
1075
1076 #[cfg(test)]
1089 async fn initialize(
1090 client: &LspClient,
1091 config: &ServerInitConfig,
1092 ) -> Result<(ServerCapabilities, PositionEncodingKind)> {
1093 Self::initialize_as(client, config, Some(mcpls_process_id())).await
1094 }
1095
1096 async fn initialize_as(
1101 client: &LspClient,
1102 config: &ServerInitConfig,
1103 process_id: Option<i32>,
1104 ) -> Result<(ServerCapabilities, PositionEncodingKind)> {
1105 debug!("Sending initialize request");
1106 if config.server_config().initialization_options.is_some()
1107 && config.server_config().settings.is_some()
1108 {
1109 warn!(
1110 "server `{}` has both initialization_options and settings: servers that pull \
1111 workspace/configuration (rust-analyzer, jdtls) may replace the options with \
1112 the settings",
1113 config.server_config.command
1114 );
1115 }
1116
1117 let workspace_folders: Vec<WorkspaceFolder> = config
1118 .workspace_roots()
1119 .canonical()
1120 .iter()
1121 .map(|root| workspace_folder(root))
1122 .collect::<Result<Vec<_>>>()?;
1123
1124 let params = InitializeParams {
1125 process_id,
1126 #[allow(
1127 deprecated,
1128 reason = "`root_uri` is deprecated but the struct literal must still set it"
1129 )]
1130 root_uri: None,
1131 initialization_options: config.server_config().initialization_options.clone(),
1132 capabilities: Self::client_capabilities(
1133 &config.position_encodings,
1134 config.server_config.settings.as_ref(),
1135 ),
1136 client_info: Some(ClientInfo {
1137 name: "mcpls".to_string(),
1138 version: Some(env!("CARGO_PKG_VERSION").to_string()),
1139 }),
1140 workspace_folders_initialize_params: lsp_types::WorkspaceFoldersInitializeParams {
1141 workspace_folders: Some(lsp_types::WorkspaceFolders::WorkspaceFolderList(
1142 workspace_folders,
1143 )),
1144 },
1145 ..Default::default()
1146 };
1147
1148 let result: InitializeResult = client
1152 .request_typed::<InitializeRequest>(
1153 params,
1154 config.server_config.timeout_seconds.as_duration(),
1155 )
1156 .await
1157 .map_err(|e| {
1158 if is_connection_loss(&e) {
1159 e
1160 } else {
1161 Error::LspInitFailed {
1162 phase: InitPhase::Initialize,
1163 cause: Box::new(e),
1164 hint: None,
1165 stderr: None,
1166 }
1167 }
1168 })?;
1169
1170 let position_encoding = result
1171 .capabilities
1172 .position_encoding
1173 .clone()
1174 .unwrap_or(PositionEncodingKind::UTF16);
1175
1176 debug!(
1177 "Server capabilities received, encoding: {:?}",
1178 position_encoding
1179 );
1180
1181 notify_handshake::<InitializedNotification>(
1182 client,
1183 InitPhase::Initialized,
1184 InitializedParams {},
1185 )
1186 .await?;
1187 notify_handshake::<DidChangeConfigurationNotification>(
1188 client,
1189 InitPhase::DidChangeConfiguration,
1190 DidChangeConfigurationParams {
1191 settings: config
1192 .server_config
1193 .settings
1194 .as_ref()
1195 .map_or(serde_json::Value::Null, LspSettings::to_value),
1196 },
1197 )
1198 .await?;
1199
1200 Ok((result.capabilities, position_encoding))
1201 }
1202
1203 #[must_use]
1205 pub const fn capabilities(&self) -> &ServerCapabilities {
1206 &self.capabilities
1207 }
1208
1209 #[must_use]
1211 pub fn position_encoding(&self) -> PositionEncodingKind {
1212 self.position_encoding.clone()
1213 }
1214
1215 #[must_use]
1217 pub const fn client(&self) -> &LspClient {
1218 &self.client
1219 }
1220
1221 pub fn has_exited(&mut self) -> Result<bool> {
1239 match &mut self.child {
1240 Some(child) => Ok(child.try_wait()?.is_some()),
1241 None => Ok(false),
1242 }
1243 }
1244
1245 pub(crate) fn is_dead(&mut self) -> Result<bool> {
1257 if self.has_exited()? {
1258 return Ok(true);
1259 }
1260 Ok(self.child.is_some() && self.client.is_message_loop_finished())
1261 }
1262
1263 pub async fn shutdown(mut self) -> Result<()> {
1285 self.terminate(SHUTDOWN_TIMEOUT, ExitGrace::WithinBudget)
1286 .await
1287 }
1288
1289 #[allow(
1308 clippy::arithmetic_side_effects,
1309 reason = "SHUTDOWN_TIMEOUT and CHILD_EXIT_GRACE are small constants"
1310 )]
1311 pub(crate) async fn terminate(&mut self, budget: Duration, grace: ExitGrace) -> Result<()> {
1312 debug!("Shutting down LSP server");
1313
1314 let deadline = Instant::now() + budget;
1315 let client = &mut self.client;
1316 let shutdown_request_timeout = Duration::from_secs(5).min(budget);
1317
1318 let handshake: Result<MarkOutcome> = timeout_at(deadline, async {
1319 client.suppress_cancels();
1320 let _: serde_json::Value = client
1321 .request(
1322 ShutdownRequest::METHOD.as_str(),
1323 (),
1324 shutdown_request_timeout,
1325 )
1326 .await?;
1327 #[cfg(unix)]
1328 let mark = match self.child.as_mut() {
1329 Some(child) => child.mark_escapees().await,
1330 None => MarkOutcome::Confirmed,
1331 };
1332 #[cfg(windows)]
1333 let mark = MarkOutcome::Confirmed;
1334 if mark == MarkOutcome::Confirmed {
1335 client.notify_typed::<ExitNotification>(()).await?;
1336 }
1337 Ok(mark)
1338 })
1339 .await
1340 .unwrap_or(Err(Error::ShutdownTimeout));
1341 let mark = handshake
1342 .as_ref()
1343 .map_or(MarkOutcome::Confirmed, |mark| *mark);
1344 let notice = ExitNotice::of(&handshake);
1345 let handshake = handshake.map(drop);
1346 let handshake = match client.shutdown_until(deadline).await {
1347 Ok(()) => handshake,
1348 Err(e) => handshake.and(Err(e)),
1349 };
1350
1351 if let Some(child) = &mut self.child {
1352 let wait = child_exit_wait(
1353 grace,
1354 mark,
1355 &handshake,
1356 deadline.saturating_duration_since(Instant::now()),
1357 );
1358 match timeout(wait, child.wait()).await {
1359 Ok(Ok(status)) => match notice {
1360 ExitNotice::Sent => {
1361 debug!(
1362 ?status,
1363 "LSP server process exited after `exit` notification"
1364 );
1365 }
1366 ExitNotice::NotSent => {
1367 debug!(
1368 ?status,
1369 "LSP server process exited without an `exit` notification"
1370 );
1371 }
1372 },
1373 Ok(Err(e)) => warn!(error = %e, "failed to wait for LSP server process exit"),
1374 Err(_) => match notice {
1375 ExitNotice::Sent => warn!(
1376 waited = ?wait,
1377 exit = ?notice,
1378 "LSP server process did not exit within the wait after `exit` \
1379 notification, killing it"
1380 ),
1381 ExitNotice::NotSent => warn!(
1382 waited = ?wait,
1383 exit = ?notice,
1384 "LSP server process killed without an `exit` notification"
1385 ),
1386 },
1387 }
1388 let sweep_budget = deadline
1389 .saturating_duration_since(Instant::now())
1390 .clamp(MIN_SWEEP_BUDGET, crate::lsp::LIFELINE_SWEEP_BUDGET);
1391 child.terminate_tree(sweep_budget).await;
1392 }
1393
1394 handshake?;
1395 info!("LSP server shut down successfully");
1396 Ok(())
1397 }
1398
1399 pub(crate) async fn start_contained(config: &ServerInitConfig) -> ServerStartOutcome {
1406 Box::pin(contain(config, Self::spawn(config.clone()))).await
1407 }
1408}
1409
1410async fn contain(
1413 config: &ServerInitConfig,
1414 start: impl std::future::Future<Output = Result<SpawnedServer>>,
1415) -> ServerStartOutcome {
1416 let server_id = config.server_config.id();
1417 let language_id = config.server_config.language_id.clone();
1418 let command = config.server_config.command.server_command().clone();
1419 let started = Instant::now();
1420
1421 let reason = match crate::util::catch_panic(start).await {
1422 Ok(Ok(spawned)) => {
1423 info!(
1424 "Successfully spawned LSP server: {} ({}) in {:?}",
1425 server_id,
1426 command,
1427 started.elapsed()
1428 );
1429 return ServerStartOutcome::Started(Box::new(spawned));
1430 }
1431 Ok(Err(e)) => {
1432 tracing::error!(
1433 "Failed to spawn LSP server: {} ({}) after {:?}: {}",
1434 server_id,
1435 command,
1436 started.elapsed(),
1437 e
1438 );
1439 StartupFailure::Spawn(Arc::new(e))
1440 }
1441 Err(panicked) => {
1442 tracing::error!(
1443 "Starting LSP server {} ({}) panicked after {:?}: {}",
1444 server_id,
1445 command,
1446 started.elapsed(),
1447 panicked.message()
1448 );
1449 StartupFailure::InitTaskPanicked
1450 }
1451 };
1452 ServerStartOutcome::Failed(ServerSpawnFailure {
1453 server_id,
1454 language_id,
1455 command,
1456 reason,
1457 })
1458}
1459
1460fn mcpls_process_id() -> i32 {
1461 i32::try_from(std::process::id()).unwrap_or(i32::MAX)
1462}
1463
1464#[cfg(unix)]
1467fn initialize_process_id(binding: Binding) -> Option<i32> {
1468 match binding {
1469 Binding::Bound => None,
1470 Binding::Unbound => Some(mcpls_process_id()),
1471 }
1472}
1473
1474async fn notify_handshake<N>(client: &LspClient, phase: InitPhase, params: N::Params) -> Result<()>
1477where
1478 N: lsp_types::Notification,
1479{
1480 client
1481 .notify_typed::<N>(params)
1482 .await
1483 .map_err(|e| Error::LspInitFailed {
1484 phase,
1485 cause: Box::new(e),
1486 hint: None,
1487 stderr: None,
1488 })
1489}
1490
1491const fn is_connection_loss(error: &Error) -> bool {
1494 matches!(
1495 error,
1496 Error::ServerTerminated
1497 | Error::TaskFailed {
1498 task: BackgroundTask::LspReceiver,
1499 ..
1500 }
1501 )
1502}
1503
1504async fn early_exit_status(child: &mut ServerProcess) -> Option<std::process::ExitStatus> {
1507 timeout(EARLY_EXIT_PROBE, child.wait()).await.ok()?.ok()
1508}
1509
1510fn spawn_error(command: ServerCommand, source: std::io::Error) -> Error {
1513 if source.kind() == std::io::ErrorKind::NotFound {
1514 Error::ServerNotFound {
1515 command,
1516 io: source,
1517 }
1518 } else {
1519 Error::ServerSpawnFailed { command, source }
1520 }
1521}
1522
1523pub fn current_environment() -> Vec<(String, String)> {
1525 std::env::vars_os()
1526 .filter_map(|(name, value)| Some((name.into_string().ok()?, value.into_string().ok()?)))
1527 .collect()
1528}
1529
1530fn workspace_folder(root: &Path) -> Result<WorkspaceFolder> {
1536 let uri = try_path_to_uri(root).ok_or_else(|| Error::PathToUri(root.to_path_buf()))?;
1537 Ok(WorkspaceFolder {
1538 uri,
1539 name: root
1540 .file_name()
1541 .and_then(|n| n.to_str())
1542 .unwrap_or("workspace")
1543 .to_string(),
1544 })
1545}
1546
1547#[cfg(test)]
1560pub fn fake_lsp_server() -> LspServer {
1561 fake_lsp_server_with_config(LspServerConfig::pyright())
1562}
1563
1564#[cfg(test)]
1567pub fn fake_lsp_server_with_config(server_config: LspServerConfig) -> LspServer {
1568 let transport = crate::test_lsp::inert_transport();
1569 let client = LspClient::from_transport(server_config.clone(), transport);
1570 LspServer {
1571 client,
1572 capabilities: lsp_types::ServerCapabilities::default(),
1573 position_encoding: PositionEncodingKind::UTF8,
1574 child: None,
1575 init_config: crate::test_lsp::init_config_for(server_config),
1576 }
1577}
1578
1579#[cfg(test)]
1582pub fn fake_spawned_server_with_config(server_config: LspServerConfig) -> SpawnedServer {
1583 let (_, messages) = mpsc::channel(1);
1584 let (_, lifecycle) = mpsc::channel(1);
1585 SpawnedServer {
1586 server: fake_lsp_server_with_config(server_config),
1587 lanes: ServerLanes {
1588 notifications: NotificationInbox::new(messages, crate::lsp::PublishReader::closed()),
1589 lifecycle,
1590 },
1591 }
1592}
1593
1594#[cfg(test)]
1597#[cfg(unix)]
1598pub fn fake_lsp_server_with_dead_loop_and_live_child() -> LspServer {
1599 let child = tokio::process::Command::new("sleep")
1600 .arg("30")
1601 .kill_on_drop(true)
1602 .spawn()
1603 .unwrap_or_else(|e| panic!("failed to spawn sleep: {e}"));
1604 let mut server = fake_lsp_server();
1605 server.child = Some(ServerProcess::from_unbound(child));
1606 server
1607}
1608
1609#[cfg(test)]
1610impl LspServer {
1611 pub(crate) fn new_for_test(capabilities: ServerCapabilities) -> Self {
1623 Self::new_for_test_with_encoding(capabilities, PositionEncodingKind::UTF16)
1624 }
1625
1626 pub(crate) fn new_for_test_with_encoding(
1631 capabilities: ServerCapabilities,
1632 position_encoding: PositionEncodingKind,
1633 ) -> Self {
1634 Self::new_for_test_with_client(
1635 capabilities,
1636 position_encoding,
1637 LspClient::new(LspServerConfig::rust_analyzer()),
1638 )
1639 }
1640
1641 pub(crate) fn new_for_test_with_client(
1645 capabilities: ServerCapabilities,
1646 position_encoding: PositionEncodingKind,
1647 client: LspClient,
1648 ) -> Self {
1649 Self {
1650 client,
1651 capabilities,
1652 position_encoding,
1653 child: None,
1654 init_config: crate::test_lsp::init_config_for(LspServerConfig::rust_analyzer()),
1655 }
1656 }
1657}
1658
1659#[cfg(test)]
1660mod tests {
1661 use std::assert_matches;
1662 use std::collections::HashMap;
1663
1664 use super::*;
1665 use crate::bridge::PositionEncoding;
1666 use crate::config::{FilePattern, HostOs, LanguageId, TimeoutSecs, ToolSet};
1667 use crate::lsp::JsonRpcError;
1668
1669 #[test]
1670 fn test_child_exit_wait_follows_the_grace_policy() {
1671 let remaining = Duration::from_secs(10);
1672 let grace = Duration::from_secs(7);
1673 let answered = Ok(());
1674 let unanswered = Err(Error::ShutdownTimeout);
1675 let wait = |policy, mark, answered: &Result<()>| {
1676 child_exit_wait(policy, mark, answered, remaining)
1677 };
1678
1679 assert_eq!(
1680 wait(ExitGrace::WithinBudget, MarkOutcome::Confirmed, &answered),
1681 CHILD_EXIT_GRACE
1682 );
1683 assert_eq!(
1684 child_exit_wait(
1685 ExitGrace::WithinBudget,
1686 MarkOutcome::Confirmed,
1687 &answered,
1688 Duration::from_secs(1)
1689 ),
1690 Duration::from_secs(1)
1691 );
1692 assert_eq!(
1693 wait(
1694 ExitGrace::IfAnswered(grace),
1695 MarkOutcome::Confirmed,
1696 &answered
1697 ),
1698 grace
1699 );
1700 assert_eq!(
1701 wait(
1702 ExitGrace::IfAnswered(grace),
1703 MarkOutcome::Confirmed,
1704 &unanswered
1705 ),
1706 Duration::ZERO
1707 );
1708 }
1709
1710 #[test]
1711 fn test_exit_notice_is_sent_only_after_a_confirmed_handshake() {
1712 assert_eq!(
1713 ExitNotice::of(&Ok(MarkOutcome::Confirmed)),
1714 ExitNotice::Sent
1715 );
1716 assert_eq!(
1717 ExitNotice::of(&Err(Error::ShutdownTimeout)),
1718 ExitNotice::NotSent
1719 );
1720 #[cfg(unix)]
1721 assert_eq!(
1722 ExitNotice::of(&Ok(MarkOutcome::Failed(
1723 crate::lsp::process::MarkFailure::NoConfirmation
1724 ))),
1725 ExitNotice::NotSent,
1726 "a withheld `exit` is not a sent one"
1727 );
1728 }
1729
1730 #[test]
1731 fn test_client_capabilities_offer_configured_encodings_in_order() {
1732 let encodings =
1733 PositionEncodings::new(vec![PositionEncoding::Utf32, PositionEncoding::Utf8]).unwrap();
1734 let general = LspServer::client_capabilities(&encodings, None)
1735 .general
1736 .unwrap();
1737 assert_eq!(
1738 general.position_encodings,
1739 Some(vec![
1740 PositionEncodingKind::UTF32,
1741 PositionEncodingKind::UTF8
1742 ])
1743 );
1744 }
1745
1746 #[test]
1752 fn test_client_capabilities_advertises_work_done_progress() {
1753 let capabilities = LspServer::client_capabilities(&PositionEncodings::DEFAULT, None);
1754
1755 assert_eq!(
1756 capabilities.window.and_then(|w| w.work_done_progress),
1757 Some(true)
1758 );
1759 }
1760
1761 #[test]
1765 fn test_client_capabilities_advertises_prepare_rename_support() {
1766 let rename = LspServer::client_capabilities(&PositionEncodings::DEFAULT, None)
1767 .text_document
1768 .and_then(|t| t.rename)
1769 .unwrap();
1770
1771 assert_eq!(rename.prepare_support, Some(true));
1772 assert_eq!(
1773 rename.prepare_support_default_behavior,
1774 Some(lsp_types::PrepareSupportDefaultBehavior::Identifier)
1775 );
1776 }
1777
1778 #[test]
1779 fn test_client_capabilities_advertises_folding_range_with_columns_and_collapsed_text() {
1780 let folding = LspServer::client_capabilities(&PositionEncodings::DEFAULT, None)
1781 .text_document
1782 .and_then(|t| t.folding_range)
1783 .unwrap();
1784
1785 assert_eq!(folding.line_folding_only, Some(false));
1786 assert_eq!(
1787 folding
1788 .folding_range
1789 .and_then(|options| options.collapsed_text),
1790 Some(true)
1791 );
1792 }
1793
1794 #[test]
1795 fn test_client_capabilities_advertises_selection_range() {
1796 let capabilities = LspServer::client_capabilities(&PositionEncodings::DEFAULT, None)
1797 .text_document
1798 .and_then(|t| t.selection_range)
1799 .unwrap();
1800
1801 assert_eq!(capabilities.dynamic_registration, Some(false));
1802 }
1803
1804 #[test]
1805 fn test_client_capabilities_advertise_did_save_without_dynamic_registration() {
1806 let synchronization = LspServer::client_capabilities(&PositionEncodings::DEFAULT, None)
1807 .text_document
1808 .and_then(|t| t.synchronization)
1809 .unwrap();
1810
1811 assert_eq!(
1812 synchronization,
1813 lsp_types::TextDocumentSyncClientCapabilities {
1814 dynamic_registration: Some(false),
1815 did_save: Some(true),
1816 ..Default::default()
1817 }
1818 );
1819 }
1820
1821 #[test]
1822 fn test_client_capabilities_advertises_publish_diagnostics_with_version_support_only() {
1823 let capabilities = LspServer::client_capabilities(&PositionEncodings::DEFAULT, None)
1824 .text_document
1825 .and_then(|t| t.publish_diagnostics)
1826 .unwrap();
1827
1828 assert_eq!(
1829 capabilities,
1830 lsp_types::PublishDiagnosticsClientCapabilities {
1831 version_support: Some(true),
1832 ..Default::default()
1833 }
1834 );
1835 }
1836
1837 #[test]
1838 fn test_client_capabilities_do_not_advertise_pull_diagnostics() {
1839 let text_document = LspServer::client_capabilities(&PositionEncodings::DEFAULT, None)
1840 .text_document
1841 .unwrap();
1842
1843 assert!(text_document.diagnostic.is_none());
1844 }
1845
1846 #[test]
1847 fn test_server_state_ready() {
1848 assert!(ServerState::Ready.is_ready());
1849 }
1850
1851 #[test]
1852 fn test_server_state_uninitialized() {
1853 assert!(!ServerState::Uninitialized.is_ready());
1854 }
1855
1856 #[test]
1857 fn test_server_state_initializing() {
1858 assert!(!ServerState::Initializing.is_ready());
1859 }
1860
1861 #[test]
1862 fn test_workspace_folder_encodes_fragment_char() {
1863 #[cfg(windows)]
1866 let (root, expected) = (
1867 Path::new(r"C:\home\me\dev\#work"),
1868 "file:///C:/home/me/dev/%23work",
1869 );
1870 #[cfg(not(windows))]
1871 let (root, expected) = (
1872 Path::new("/home/me/dev/#work"),
1873 "file:///home/me/dev/%23work",
1874 );
1875
1876 let folder = workspace_folder(root).unwrap();
1877
1878 assert_eq!(folder.uri.as_ref(), expected);
1879 assert_eq!(folder.name, "#work");
1880 }
1881
1882 #[test]
1883 fn test_workspace_folder_encodes_bracket_chars() {
1884 #[cfg(windows)]
1885 let (root, expected) = (
1886 Path::new(r"C:\home\me\dev\[env]"),
1887 "file:///C:/home/me/dev/%5Benv%5D",
1888 );
1889 #[cfg(not(windows))]
1890 let (root, expected) = (
1891 Path::new("/home/me/dev/[env]"),
1892 "file:///home/me/dev/%5Benv%5D",
1893 );
1894
1895 let folder = workspace_folder(root).unwrap();
1896
1897 assert_eq!(folder.uri.as_ref(), expected);
1898 assert_eq!(folder.name, "[env]");
1899 }
1900
1901 #[test]
1902 fn test_workspace_folder_rejects_relative_root() {
1903 let err = workspace_folder(Path::new("relative/root")).unwrap_err();
1904 assert_matches!(err, Error::PathToUri(_), "got {err:?}");
1905 }
1906
1907 #[test]
1908 fn test_server_state_shutting_down() {
1909 assert!(!ServerState::ShuttingDown.is_ready());
1910 }
1911
1912 #[test]
1913 fn test_server_state_shutdown() {
1914 assert!(!ServerState::Shutdown.is_ready());
1915 }
1916
1917 #[test]
1918 fn test_server_state_equality() {
1919 assert_eq!(ServerState::Ready, ServerState::Ready);
1920 assert_ne!(ServerState::Ready, ServerState::Uninitialized);
1921 assert_eq!(ServerState::Shutdown, ServerState::Shutdown);
1922 }
1923
1924 #[test]
1925 fn test_server_state_clone() {
1926 let state = ServerState::Ready;
1927 let cloned = state;
1928 assert_eq!(state, cloned);
1929 }
1930
1931 #[test]
1932 fn test_server_state_debug() {
1933 let state = ServerState::Ready;
1934 let debug_str = format!("{state:?}");
1935 assert!(debug_str.contains("Ready"));
1936 }
1937
1938 #[test]
1939 fn test_server_init_config_clone() {
1940 let config = ServerInitConfig::new(
1941 {
1942 let mut c = LspServerConfig::rust_analyzer();
1943 c.initialization_options = Some(serde_json::json!({"key": "value"}));
1944 c
1945 },
1946 WorkspaceRoots::for_test(vec![PathBuf::from("/tmp/workspace")], vec![]),
1947 PositionEncodings::DEFAULT,
1948 std::sync::Arc::default(),
1949 );
1950
1951 #[allow(clippy::redundant_clone, reason = "the test exercises `Clone` itself")]
1952 let cloned = config.clone();
1953 assert_eq!(cloned.server_config().language_id, "rust");
1954 assert_eq!(cloned.workspace_roots().canonical().len(), 1);
1955 }
1956
1957 #[test]
1958 fn test_server_init_config_debug() {
1959 let config = ServerInitConfig::new(
1960 LspServerConfig::pyright(),
1961 WorkspaceRoots::default(),
1962 PositionEncodings::DEFAULT,
1963 std::sync::Arc::default(),
1964 );
1965
1966 let debug_str = format!("{config:?}");
1967 assert!(debug_str.contains("python"));
1968 assert!(debug_str.contains("pyright"));
1969 }
1970
1971 #[test]
1972 fn test_server_init_config_with_options() {
1973 let init_opts = serde_json::json!({
1974 "settings": {
1975 "python": {
1976 "analysis": {
1977 "typeCheckingMode": "strict"
1978 }
1979 }
1980 }
1981 });
1982
1983 let mut env = crate::config::ServerEnv::default();
1984 env.insert(
1985 crate::config::EnvKey::from_static("PYTHONPATH"),
1986 crate::config::EnvValue::from_static("/usr/lib"),
1987 )
1988 .unwrap();
1989
1990 let config = ServerInitConfig::new(
1991 LspServerConfig {
1992 language_id: LanguageId::from_static("python"),
1993 command: ServerCommand::from_static("pyright-langserver").into(),
1994 args: crate::config::ServerArgs::try_from(vec!["--stdio".to_string()]).unwrap(),
1995 env,
1996 file_patterns: vec![FilePattern::from_static("**/*.py")],
1997 initialization_options: Some(init_opts),
1998 settings: None,
1999 timeout_seconds: TimeoutSecs::new(10).unwrap(),
2000 request_timeout_seconds: TimeoutSecs::new(10).unwrap(),
2001 heuristics: None,
2002 name: None,
2003 handles: None,
2004 indexing: crate::bridge::IndexingPolicy::Auto,
2005 },
2006 WorkspaceRoots::for_test(vec![PathBuf::from("/workspace")], vec![]),
2007 PositionEncodings::DEFAULT,
2008 std::sync::Arc::default(),
2009 );
2010
2011 assert!(config.server_config().initialization_options.is_some());
2012 assert_eq!(config.workspace_roots().canonical().len(), 1);
2013 }
2014
2015 #[test]
2016 fn test_server_init_config_empty_workspace() {
2017 let config = ServerInitConfig::new(
2018 LspServerConfig::typescript(),
2019 WorkspaceRoots::default(),
2020 PositionEncodings::DEFAULT,
2021 std::sync::Arc::default(),
2022 );
2023
2024 assert_eq!(config.workspace_roots().canonical().len(), 0);
2025 }
2026
2027 #[test]
2028 fn test_server_init_config_multiple_workspaces() {
2029 let config = ServerInitConfig::new(
2030 LspServerConfig::rust_analyzer(),
2031 WorkspaceRoots::for_test(
2032 vec![
2033 PathBuf::from("/workspace1"),
2034 PathBuf::from("/workspace2"),
2035 PathBuf::from("/workspace3"),
2036 ],
2037 vec![],
2038 ),
2039 PositionEncodings::DEFAULT,
2040 std::sync::Arc::default(),
2041 );
2042
2043 assert_eq!(config.workspace_roots().canonical().len(), 3);
2044 }
2045
2046 #[cfg(unix)]
2053 #[tokio::test]
2054 async fn test_has_exited_reflects_child_process_state() {
2055 use lsp_types::ServerCapabilities;
2056
2057 let mut mock_child = tokio::process::Command::new("sleep")
2058 .arg("2")
2059 .stdin(Stdio::piped())
2060 .stdout(Stdio::piped())
2061 .kill_on_drop(true)
2062 .spawn()
2063 .unwrap();
2064
2065 let mock_stdin = mock_child.stdin.take().unwrap();
2066 let mock_stdout = mock_child.stdout.take().unwrap();
2067
2068 let transport = LspTransport::new(mock_stdin, mock_stdout);
2069 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
2070
2071 let mut server = LspServer {
2072 client,
2073 capabilities: ServerCapabilities::default(),
2074 position_encoding: PositionEncodingKind::UTF8,
2075 child: Some(ServerProcess::from_unbound(mock_child)),
2076 init_config: crate::test_lsp::init_config_for(LspServerConfig::rust_analyzer()),
2077 };
2078
2079 assert!(
2080 !server.has_exited().unwrap(),
2081 "freshly spawned `sleep 2` should still be running"
2082 );
2083
2084 server.child.as_mut().unwrap().kill().await.unwrap();
2085 assert!(
2088 server.has_exited().unwrap(),
2089 "killed child must report as exited"
2090 );
2091 }
2092
2093 #[cfg(unix)]
2095 #[tokio::test]
2096 async fn test_is_dead_when_message_loop_finished_but_child_alive() {
2097 let mut server = fake_lsp_server_with_dead_loop_and_live_child();
2098
2099 tokio::time::timeout(Duration::from_secs(2), async {
2100 while !server.client.is_message_loop_finished() {
2101 tokio::time::sleep(Duration::from_millis(10)).await;
2102 }
2103 })
2104 .await
2105 .unwrap();
2106
2107 assert!(!server.has_exited().unwrap());
2108 assert!(server.is_dead().unwrap());
2109 }
2110
2111 #[tokio::test(start_paused = true)]
2115 async fn test_shutdown_reports_handshake_timeout_within_deadline() {
2116 let (client, _fake_server) = crate::test_lsp::fake_lsp_client();
2117 let probe = client.clone();
2118 let server = LspServer {
2119 client,
2120 capabilities: lsp_types::ServerCapabilities::default(),
2121 position_encoding: PositionEncodingKind::UTF8,
2122 child: None,
2123 init_config: crate::test_lsp::init_config_for(LspServerConfig::rust_analyzer()),
2124 };
2125
2126 let started = Instant::now();
2127 let result = server.shutdown().await;
2128
2129 assert_matches!(result, Err(Error::Timeout(_)), "got {result:?}");
2130 assert!(started.elapsed() <= SHUTDOWN_TIMEOUT);
2131 assert_matches!(probe.state().await, ServerState::Shutdown);
2132 }
2133
2134 #[tokio::test]
2135 async fn test_is_dead_false_for_childless_fixture() {
2136 let mut server = fake_lsp_server();
2137 tokio::time::sleep(Duration::from_millis(50)).await;
2138 assert!(!server.is_dead().unwrap());
2139 }
2140
2141 #[tokio::test]
2142 async fn test_lsp_server_getters() {
2143 use lsp_types::ServerCapabilities;
2144
2145 let transport = crate::test_lsp::inert_transport();
2146 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
2147
2148 let server = LspServer {
2149 client,
2150 capabilities: ServerCapabilities::default(),
2151 position_encoding: PositionEncodingKind::UTF8,
2152 child: None,
2153 init_config: crate::test_lsp::init_config_for(LspServerConfig::rust_analyzer()),
2154 };
2155
2156 assert_eq!(server.position_encoding(), PositionEncodingKind::UTF8);
2157 assert!(server.capabilities().text_document_sync.is_none());
2158
2159 let debug_str = format!("{server:?}");
2160 assert!(debug_str.contains("LspServer"));
2161 assert!(debug_str.contains("<process>"));
2162 }
2163
2164 #[test]
2165 fn test_spawn_error_classifies_not_found() {
2166 use std::io::{Error as IoError, ErrorKind};
2167
2168 let missing = spawn_error(
2169 ServerCommand::from_static("x"),
2170 IoError::from(ErrorKind::NotFound),
2171 );
2172 assert_matches!(missing, Error::ServerNotFound { .. });
2173 let denied = spawn_error(
2174 ServerCommand::from_static("x"),
2175 IoError::from(ErrorKind::PermissionDenied),
2176 );
2177 assert_matches!(denied, Error::ServerSpawnFailed { .. });
2178 }
2179
2180 #[tokio::test]
2181 async fn test_spawn_nonexistent_command_is_server_not_found() {
2182 let mut server_config = LspServerConfig::rust_analyzer();
2183 server_config.command = ServerCommand::from_static("nonexistent-lsp-cmd-xyz").into();
2184 let config = ServerInitConfig::new(
2185 server_config,
2186 WorkspaceRoots::default(),
2187 PositionEncodings::DEFAULT,
2188 std::sync::Arc::default(),
2189 );
2190 let err = LspServer::spawn(config).await.unwrap_err();
2191 assert_matches!(err, Error::ServerNotFound { .. }, "got {err:?}");
2192 }
2193
2194 #[tokio::test]
2195 async fn test_is_connection_loss_excludes_server_replies() {
2196 let join_error = tokio::spawn(async { panic!("receiver died") })
2197 .await
2198 .unwrap_err();
2199 assert!(is_connection_loss(&Error::ServerTerminated));
2200 assert!(is_connection_loss(&Error::TaskFailed {
2201 task: BackgroundTask::LspReceiver,
2202 source: join_error,
2203 }));
2204 assert!(!is_connection_loss(&Error::StdioCapture(
2205 StdioStream::Stdin
2206 )));
2207 assert!(!is_connection_loss(&Error::LspServerError(JsonRpcError {
2208 code: -32603,
2209 message: "bad".to_string(),
2210 data: None,
2211 })));
2212 }
2213
2214 #[cfg(unix)]
2217 #[tokio::test]
2218 async fn test_spawn_keeps_servers_own_initialize_error_when_it_exits_afterwards() {
2219 let dir = tempfile::TempDir::new().unwrap();
2220 let config = crate::test_lsp::sh_script_init_config(
2221 dir.path(),
2222 &crate::test_lsp::with_read_preamble(
2223 r#"body='{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"rejected by server"}}'
2224printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
2225"#,
2226 ),
2227 );
2228
2229 let err = LspServer::spawn(config).await.unwrap_err();
2230
2231 assert_matches!(
2232 &err,
2233 Error::LspInitFailed { phase: InitPhase::Initialize, cause, .. }
2234 if matches!(**cause, Error::LspServerError(JsonRpcError { code: -32603, .. })),
2235 "got {err:?}"
2236 );
2237 assert!(err.to_string().contains("rejected by server"), "{err}");
2238 }
2239
2240 #[cfg(unix)]
2243 #[tokio::test]
2244 async fn test_spawn_connection_loss_with_live_child_is_lsp_init_failed() {
2245 let dir = tempfile::TempDir::new().unwrap();
2246 let config = crate::test_lsp::sh_script_init_config(dir.path(), "exec 1>&-\nsleep 5\n");
2247
2248 let err = LspServer::spawn(config).await.unwrap_err();
2249
2250 assert_matches!(
2251 &err,
2252 Error::LspInitFailed {
2253 phase: InitPhase::Initialize,
2254 ..
2255 },
2256 "got {err:?}"
2257 );
2258 assert!(
2259 err.to_string().contains("Initialize request failed"),
2260 "{err}"
2261 );
2262 }
2263
2264 #[cfg(unix)]
2265 #[tokio::test]
2266 async fn test_spawn_exiting_before_initialize_reply_is_server_exited_during_init() {
2267 let dir = tempfile::TempDir::new().unwrap();
2268 let config = crate::test_lsp::sh_script_init_config(dir.path(), "exit 1\n");
2269
2270 let err = LspServer::spawn(config).await.unwrap_err();
2271
2272 assert_matches!(
2273 err,
2274 Error::ServerExitedDuringInit {
2275 exit_code: Some(1),
2276 ..
2277 },
2278 "got {err:?}"
2279 );
2280 }
2281
2282 #[cfg(unix)]
2283 #[test]
2284 fn test_unbound_server_gets_the_real_process_id() {
2285 assert_eq!(
2286 initialize_process_id(Binding::Unbound),
2287 Some(i32::try_from(std::process::id()).unwrap())
2288 );
2289 }
2290
2291 #[cfg(unix)]
2292 #[test]
2293 fn test_bound_server_gets_no_process_id() {
2294 assert_eq!(initialize_process_id(Binding::Bound), None);
2295 }
2296
2297 #[cfg(unix)]
2300 #[tokio::test]
2301 async fn test_spawn_sends_null_process_id_while_a_lifeline_is_bound() {
2302 let dir = tempfile::TempDir::new().unwrap();
2303 let dump = dir.path().join("request");
2304 let script = format!(
2305 "read -r header\nread -r _\nhead -c \"$(printf %s \"$header\" | tr -dc 0-9)\" > '{}'\nexit 1\n",
2306 dump.display()
2307 );
2308 let config = crate::test_lsp::sh_script_init_config(dir.path(), &script);
2309
2310 LspServer::spawn(config).await.unwrap_err();
2311
2312 let request = std::fs::read_to_string(&dump).unwrap();
2313 assert!(request.contains(r#""processId":null"#), "got {request}");
2314 }
2315
2316 #[cfg(unix)]
2318 #[tokio::test]
2319 async fn test_spawn_exit_during_init_carries_stderr() {
2320 let dir = tempfile::TempDir::new().unwrap();
2321 let config = crate::test_lsp::sh_script_init_config(
2322 dir.path(),
2323 "echo 'fatal: bad toolchain' >&2\nexit 1\n",
2324 );
2325
2326 let err = LspServer::spawn(config).await.unwrap_err();
2327
2328 let Error::ServerExitedDuringInit {
2329 stderr: Some(stderr),
2330 ..
2331 } = &err
2332 else {
2333 panic!("got {err:?}");
2334 };
2335 assert_eq!(stderr.head(), "fatal: bad toolchain");
2336 assert!(err.to_string().contains("stderr: fatal: bad toolchain"));
2337 }
2338
2339 #[cfg(unix)]
2342 #[tokio::test]
2343 async fn test_spawn_rejection_keeps_stderr_written_just_before_exit() {
2344 let dir = tempfile::TempDir::new().unwrap();
2345 let config = crate::test_lsp::sh_script_init_config(
2346 dir.path(),
2347 &crate::test_lsp::with_read_preamble(
2348 r#"body='{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"rejected by server"}}'
2349printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
2350# Shell-only pause: an external `sleep` can take longer than the 100 ms EOF grace just to start on a loaded macOS runner.
2351i=0; while [ $i -lt 300 ]; do i=$((i+1)); done
2352echo 'fatal: bad toolchain' >&2
2353"#,
2354 ),
2355 );
2356
2357 let err = LspServer::spawn(config).await.unwrap_err();
2358
2359 let Error::LspInitFailed {
2360 stderr: Some(stderr),
2361 ..
2362 } = &err
2363 else {
2364 panic!("got {err:?}");
2365 };
2366 assert_eq!(stderr.head(), "fatal: bad toolchain");
2367 }
2368
2369 #[cfg(unix)]
2372 #[tokio::test]
2373 async fn test_spawn_initialize_timeout_carries_stderr() {
2374 let dir = tempfile::TempDir::new().unwrap();
2375 let mut config = crate::test_lsp::sh_script_init_config(
2376 dir.path(),
2377 "echo 'indexing forever' >&2\nsleep 5\n",
2378 );
2379 config.server_config.timeout_seconds = TimeoutSecs::new(1).unwrap();
2380
2381 let err = LspServer::spawn(config).await.unwrap_err();
2382
2383 let Error::LspInitFailed {
2384 stderr: Some(stderr),
2385 ..
2386 } = &err
2387 else {
2388 panic!("got {err:?}");
2389 };
2390 assert_eq!(stderr.head(), "indexing forever");
2391 }
2392
2393 #[cfg(unix)]
2397 #[tokio::test]
2398 async fn test_spawn_hides_another_servers_secret_from_stderr() {
2399 let dir = tempfile::TempDir::new().unwrap();
2400 let mut config = crate::test_lsp::sh_script_init_config(
2401 dir.path(),
2402 "echo \"seen=$OTHER_VALUE own=$API_TOKEN\" >&2\nexit 1\n",
2403 );
2404 config
2405 .server_config
2406 .env
2407 .insert(
2408 crate::config::EnvKey::from_static("OTHER_VALUE"),
2409 crate::config::EnvValue::from_static("bravo-secret-222"),
2410 )
2411 .unwrap();
2412 config
2413 .server_config
2414 .env
2415 .insert(
2416 crate::config::EnvKey::from_static("API_TOKEN"),
2417 crate::config::EnvValue::from_static("s3cr3t-value"),
2418 )
2419 .unwrap();
2420 config.redactions = std::sync::Arc::new(Redactions::new([(
2421 "B_TOKEN".to_owned(),
2422 "bravo-secret-222".to_owned(),
2423 )]));
2424
2425 let err = LspServer::spawn(config).await.unwrap_err();
2426
2427 let text = err.to_string();
2428 assert!(text.contains("seen=[redacted:B_TOKEN]"), "{text}");
2429 assert!(text.contains("own=[redacted:API_TOKEN]"), "{text}");
2430 }
2431
2432 #[cfg(unix)]
2434 #[tokio::test]
2435 async fn test_spawn_stderr_redacts_configured_env_values() {
2436 let dir = tempfile::TempDir::new().unwrap();
2437 let mut config = crate::test_lsp::sh_script_init_config(
2438 dir.path(),
2439 "echo \"token=$API_TOKEN toolchain=$RUSTUP_TOOLCHAIN\" >&2\nexit 1\n",
2440 );
2441 config
2442 .server_config
2443 .env
2444 .insert(
2445 crate::config::EnvKey::from_static("RUSTUP_TOOLCHAIN"),
2446 crate::config::EnvValue::from_static("nightly-2024-01-01"),
2447 )
2448 .unwrap();
2449 config
2450 .server_config
2451 .env
2452 .insert(
2453 crate::config::EnvKey::from_static("API_TOKEN"),
2454 crate::config::EnvValue::from_static("s3cr3t-value"),
2455 )
2456 .unwrap();
2457
2458 let err = LspServer::spawn(config).await.unwrap_err();
2459
2460 let text = err.to_string();
2461 assert!(text.contains("token=[redacted:API_TOKEN]"), "{text}");
2462 assert!(text.contains("toolchain=nightly-2024-01-01"), "{text}");
2463 assert!(!text.contains("s3cr3t-value"), "{text}");
2464 }
2465
2466 #[test]
2467 fn test_log_spawn_hides_argument_values() {
2468 use tracing_subscriber::prelude::*;
2469
2470 use crate::test_lsp::CapturedLogs;
2471
2472 let mut config = LspServerConfig::rust_analyzer();
2473 config.args = crate::config::ServerArgs::new(["--api-key=SuperSecretArg456"]).unwrap();
2474 let redactions = Redactions::for_server(&config, std::iter::empty());
2475 let logs = CapturedLogs::default();
2476 let info_only = tracing_subscriber::registry()
2477 .with(tracing_subscriber::filter::LevelFilter::INFO)
2478 .with(logs.clone());
2479
2480 tracing::subscriber::with_default(info_only, || {
2481 LspServer::log_spawn(&config, &redactions);
2482 });
2483
2484 let output = logs.messages().join("\n");
2485 assert!(output.contains("(1 arg(s))"), "{output}");
2486 assert!(!output.contains("SuperSecretArg456"), "{output}");
2487 }
2488
2489 #[tokio::test]
2491 async fn contain_attributes_a_panic_to_its_own_server() {
2492 let config = crate::test_lsp::init_config_for(LspServerConfig::rust_analyzer());
2493
2494 let outcome = contain(&config, async { panic!("start boom") }).await;
2495
2496 assert_matches!(
2497 outcome,
2498 ServerStartOutcome::Failed(failure)
2499 if failure.server_id == config.server_config.id()
2500 && matches!(failure.reason, StartupFailure::InitTaskPanicked)
2501 );
2502 }
2503
2504 #[tokio::test]
2505 async fn contain_reports_a_spawn_error_as_that_servers_failure() {
2506 let config = crate::test_lsp::init_config_for(LspServerConfig::rust_analyzer());
2507
2508 let outcome = contain(&config, async { Err(Error::ServerTerminated) }).await;
2509
2510 assert_matches!(
2511 outcome,
2512 ServerStartOutcome::Failed(failure) if matches!(failure.reason, StartupFailure::Spawn(_))
2513 );
2514 }
2515
2516 #[cfg(unix)]
2519 #[tokio::test]
2520 async fn test_spawn_redacts_secrets_in_server_log_and_show_messages() {
2521 let dir = tempfile::TempDir::new().unwrap();
2522 let script = r#"body='{"jsonrpc":"2.0","id":1,"result":{"capabilities":{}}}'
2523printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
2524body="{\"jsonrpc\":\"2.0\",\"method\":\"window/logMessage\",\"params\":{\"type\":3,\"message\":\"env=$API_TOKEN arg=$1\"}}"
2525printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
2526body="{\"jsonrpc\":\"2.0\",\"method\":\"window/showMessage\",\"params\":{\"type\":3,\"message\":\"env=$API_TOKEN\"}}"
2527printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
2528sleep 5
2529"#;
2530 let mut config = crate::test_lsp::sh_script_init_config(
2531 dir.path(),
2532 &crate::test_lsp::with_read_preamble(script),
2533 );
2534 config.server_config.args = crate::config::ServerArgs::new(
2535 config
2536 .server_config
2537 .args
2538 .iter()
2539 .cloned()
2540 .chain(["--api-key=SuperSecretArg456".to_string()]),
2541 )
2542 .unwrap();
2543 config
2544 .server_config
2545 .env
2546 .insert(
2547 crate::config::EnvKey::from_static("API_TOKEN"),
2548 crate::config::EnvValue::from_static("SuperSecretValue123"),
2549 )
2550 .unwrap();
2551
2552 let SpawnedServer {
2553 server: _server,
2554 lanes,
2555 } = LspServer::spawn(config).await.unwrap();
2556 let mut rx = lanes.notifications.messages;
2557 let mut texts = Vec::new();
2558 for _ in 0..2 {
2559 let notification = tokio::time::timeout(Duration::from_secs(5), rx.recv())
2560 .await
2561 .unwrap()
2562 .unwrap();
2563 texts.push(format!("{notification:?}"));
2564 }
2565
2566 let joined = texts.join("\n");
2567 assert!(!joined.contains("SuperSecretValue123"), "{joined}");
2568 assert!(!joined.contains("SuperSecretArg456"), "{joined}");
2569 assert!(joined.contains("[redacted:API_TOKEN]"), "{joined}");
2570 assert!(joined.contains("[redacted:api-key]"), "{joined}");
2571 }
2572
2573 #[cfg(unix)]
2576 #[tokio::test]
2577 async fn test_spawn_redacts_secrets_in_initialize_decode_error() {
2578 let dir = tempfile::TempDir::new().unwrap();
2579 let script = r#"body='{"jsonrpc":"2.0","id":1,"result":{"capabilities":"SuperSecretValue123"}}'
2580printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
2581sleep 5
2582"#;
2583 let mut config = crate::test_lsp::sh_script_init_config(
2584 dir.path(),
2585 &crate::test_lsp::with_read_preamble(script),
2586 );
2587 config
2588 .server_config
2589 .env
2590 .insert(
2591 crate::config::EnvKey::from_static("API_TOKEN"),
2592 crate::config::EnvValue::from_static("SuperSecretValue123"),
2593 )
2594 .unwrap();
2595
2596 let err = LspServer::spawn(config).await.unwrap_err();
2597
2598 let text = err.to_string();
2599 assert!(!text.contains("SuperSecretValue123"), "{text}");
2600 }
2601
2602 #[cfg(unix)]
2604 #[tokio::test]
2605 async fn test_spawn_redacts_secrets_in_initialize_error_message() {
2606 let dir = tempfile::TempDir::new().unwrap();
2607 let script = r#"body="{\"jsonrpc\":\"2.0\",\"id\":1,\"error\":{\"code\":-32603,\"message\":\"bad token $API_TOKEN\"}}"
2608printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
2609sleep 5
2610"#;
2611 let mut config = crate::test_lsp::sh_script_init_config(
2612 dir.path(),
2613 &crate::test_lsp::with_read_preamble(script),
2614 );
2615 config
2616 .server_config
2617 .env
2618 .insert(
2619 crate::config::EnvKey::from_static("API_TOKEN"),
2620 crate::config::EnvValue::from_static("SuperSecretValue123"),
2621 )
2622 .unwrap();
2623
2624 let err = LspServer::spawn(config).await.unwrap_err();
2625
2626 let text = err.to_string();
2627 assert!(text.contains("[redacted:API_TOKEN]"), "{text}");
2628 assert!(!text.contains("SuperSecretValue123"), "{text}");
2629 }
2630
2631 #[cfg(unix)]
2635 #[tokio::test]
2636 async fn test_spawn_keeps_draining_stderr_after_initialize() {
2637 let dir = tempfile::TempDir::new().unwrap();
2638 let marker = dir.path().join("flooded");
2639 let script = format!(
2640 r#"head -c 200000 /dev/zero | tr '\0' 'e' >&2
2641body='{{"jsonrpc":"2.0","id":1,"result":{{"capabilities":{{}}}}}}'
2642printf 'Content-Length: %d\r\n\r\n%s' ${{#body}} "$body"
2643sleep 0.3
2644head -c 200000 /dev/zero | tr '\0' 'e' >&2 && echo done > '{}'
2645sleep 5
2646"#,
2647 marker.display()
2648 );
2649 let config = crate::test_lsp::sh_script_init_config(
2650 dir.path(),
2651 &crate::test_lsp::with_read_preamble(&script),
2652 );
2653
2654 let mut server = LspServer::spawn(config).await.unwrap().server;
2655
2656 let flooded = tokio::time::timeout(Duration::from_secs(5), async {
2657 while !marker.exists() {
2658 tokio::time::sleep(Duration::from_millis(20)).await;
2659 }
2660 })
2661 .await;
2662 assert!(
2663 flooded.is_ok(),
2664 "server's post-initialize stderr flood did not complete"
2665 );
2666 assert!(!server.has_exited().unwrap());
2667 }
2668
2669 mod initialize_wire {
2675
2676 use tempfile::TempDir;
2677 use tokio::io::BufReader;
2678
2679 use super::*;
2680 use crate::test_lsp::{
2681 fake_lsp_client, read_framed_message, write_response as write_success_response,
2682 };
2683
2684 #[tokio::test]
2685 async fn test_initialize_sends_configured_position_encodings() {
2686 let (client, mut server) = fake_lsp_client();
2687
2688 let config = ServerInitConfig::new(
2689 LspServerConfig::rust_analyzer(),
2690 WorkspaceRoots::default(),
2691 PositionEncodings::new(vec![PositionEncoding::Utf32, PositionEncoding::Utf8])
2692 .unwrap(),
2693 std::sync::Arc::default(),
2694 );
2695
2696 let init_task =
2697 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
2698
2699 let mut reader = BufReader::new(&mut server.write_stdout);
2700 let request = read_framed_message(&mut reader).await;
2701
2702 assert_eq!(request["method"], "initialize");
2703 assert_eq!(
2704 request["params"]["capabilities"]["general"]["positionEncodings"],
2705 serde_json::json!(["utf-32", "utf-8"]),
2706 "initialize request must carry the configured encoding order, not the \
2707 hardcoded [UTF8, UTF16] default"
2708 );
2709
2710 write_success_response(
2711 &mut server.read_half_stdin,
2712 &request["id"].clone(),
2713 serde_json::json!({ "capabilities": {} }),
2714 )
2715 .await;
2716
2717 init_task.await.unwrap().unwrap();
2719 }
2720
2721 struct Handshake {
2724 initialize: serde_json::Value,
2725 initialized: serde_json::Value,
2726 configuration: serde_json::Value,
2727 }
2728
2729 async fn handshake_with_settings(settings_json: Option<&str>) -> Handshake {
2730 let (client, mut server) = fake_lsp_client();
2731 let mut server_config = LspServerConfig::rust_analyzer();
2732 server_config.settings = settings_json.map(|json| serde_json::from_str(json).unwrap());
2733 let config = crate::test_lsp::init_config_for(server_config);
2734 let init_task =
2735 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
2736
2737 let mut reader = BufReader::new(&mut server.write_stdout);
2738 let initialize = read_framed_message(&mut reader).await;
2739 assert_eq!(initialize["method"], "initialize");
2740 write_success_response(
2741 &mut server.read_half_stdin,
2742 &initialize["id"].clone(),
2743 serde_json::json!({ "capabilities": {} }),
2744 )
2745 .await;
2746 let initialized = read_framed_message(&mut reader).await;
2747 let configuration = read_framed_message(&mut reader).await;
2748 init_task.await.unwrap().unwrap();
2749 Handshake {
2750 initialize,
2751 initialized,
2752 configuration,
2753 }
2754 }
2755
2756 #[tokio::test]
2757 async fn test_initialize_pushes_null_settings_right_after_initialized() {
2758 let wire = handshake_with_settings(None).await;
2759
2760 assert_eq!(wire.initialized["method"], "initialized");
2761 assert_eq!(
2762 wire.configuration["method"],
2763 "workspace/didChangeConfiguration"
2764 );
2765 assert_eq!(
2766 wire.configuration["params"],
2767 serde_json::json!({ "settings": null })
2768 );
2769 }
2770
2771 #[tokio::test]
2772 async fn test_initialize_pushes_configured_settings_and_advertises_configuration() {
2773 let wire =
2774 handshake_with_settings(Some(r#"{"python.analysis.typeCheckingMode": "strict"}"#))
2775 .await;
2776
2777 assert_eq!(
2778 wire.initialize["params"]["capabilities"]["workspace"]["configuration"],
2779 serde_json::json!(true)
2780 );
2781 assert_eq!(
2782 wire.configuration["params"],
2783 serde_json::json!({
2784 "settings": { "python": { "analysis": { "typeCheckingMode": "strict" } } }
2785 })
2786 );
2787 }
2788
2789 #[tokio::test]
2790 async fn test_initialize_pushes_gopls_flat_and_yaml_url_keys_on_the_wire() {
2791 let wire = handshake_with_settings(Some(
2792 r#"{"gopls":{"ui.semanticTokens":true},
2793 "yaml.schemas":{"https://json.schemastore.org/x.json":"*.yml"}}"#,
2794 ))
2795 .await;
2796
2797 let settings =
2798 serde_json::to_string(&wire.configuration["params"]["settings"]).unwrap();
2799 assert!(
2800 settings.contains(r#""gopls":{"ui.semanticTokens":true}"#),
2801 "{settings}"
2802 );
2803 assert!(
2804 settings.contains(
2805 r#""yaml":{"schemas":{"https://json.schemastore.org/x.json":"*.yml"}}"#
2806 ),
2807 "{settings}"
2808 );
2809 }
2810
2811 #[test]
2812 fn test_workspace_configuration_not_advertised_without_settings() {
2813 let workspace = LspServer::client_capabilities(&PositionEncodings::DEFAULT, None)
2814 .workspace
2815 .unwrap();
2816 assert_eq!(workspace.configuration, None);
2817 }
2818
2819 #[tokio::test]
2820 async fn test_initialize_advertises_stale_request_support() {
2821 let (client, mut server) = fake_lsp_client();
2822
2823 let config = ServerInitConfig::new(
2824 LspServerConfig::rust_analyzer(),
2825 WorkspaceRoots::default(),
2826 PositionEncodings::DEFAULT,
2827 std::sync::Arc::default(),
2828 );
2829
2830 let init_task =
2831 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
2832
2833 let mut reader = BufReader::new(&mut server.write_stdout);
2834 let request = read_framed_message(&mut reader).await;
2835 let params: InitializeParams =
2836 serde_json::from_value(request["params"].clone()).unwrap();
2837 let stale_request_support = params
2838 .capabilities
2839 .general
2840 .unwrap()
2841 .stale_request_support
2842 .unwrap();
2843
2844 assert_eq!(request["method"], "initialize");
2845 assert!(
2846 !stale_request_support.cancel,
2847 "mcpls does not implement active in-flight request cancellation"
2848 );
2849 assert_eq!(
2850 stale_request_support.retry_on_content_modified,
2851 CONTENT_MODIFIED_RETRY_METHODS
2852 .iter()
2853 .map(ToString::to_string)
2854 .collect::<Vec<_>>(),
2855 "the wire-advertised capability must match the methods LspClient::request \
2856 actually retries -32801 for, not drift from it"
2857 );
2858
2859 write_success_response(
2860 &mut server.read_half_stdin,
2861 &request["id"].clone(),
2862 serde_json::json!({ "capabilities": {} }),
2863 )
2864 .await;
2865
2866 init_task.await.unwrap().unwrap();
2867 }
2868
2869 #[tokio::test]
2870 async fn test_initialize_advertises_server_status_notification_support() {
2871 let (client, mut server) = fake_lsp_client();
2872
2873 let config = ServerInitConfig::new(
2874 LspServerConfig::rust_analyzer(),
2875 WorkspaceRoots::default(),
2876 PositionEncodings::DEFAULT,
2877 std::sync::Arc::default(),
2878 );
2879
2880 let init_task =
2881 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
2882
2883 let mut reader = BufReader::new(&mut server.write_stdout);
2884 let request = read_framed_message(&mut reader).await;
2885
2886 assert_eq!(request["method"], "initialize");
2887 assert_eq!(
2888 request["params"]["capabilities"]["experimental"]["serverStatusNotification"],
2889 serde_json::json!(true),
2890 "without this, rust-analyzer never emits experimental/serverStatus and the \
2891 indexing-readiness gate in Translator::wait_for_indexing_ready is a \
2892 permanent no-op"
2893 );
2894
2895 write_success_response(
2896 &mut server.read_half_stdin,
2897 &request["id"].clone(),
2898 serde_json::json!({ "capabilities": {} }),
2899 )
2900 .await;
2901
2902 init_task.await.unwrap().unwrap();
2903 }
2904
2905 #[tokio::test]
2906 async fn test_initialize_advertises_hierarchical_document_symbols() {
2907 let (client, mut server) = fake_lsp_client();
2908
2909 let config = ServerInitConfig::new(
2910 LspServerConfig::rust_analyzer(),
2911 WorkspaceRoots::default(),
2912 PositionEncodings::DEFAULT,
2913 std::sync::Arc::default(),
2914 );
2915
2916 let init_task =
2917 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
2918
2919 let mut reader = BufReader::new(&mut server.write_stdout);
2920 let request = read_framed_message(&mut reader).await;
2921 let params: InitializeParams =
2922 serde_json::from_value(request["params"].clone()).unwrap();
2923 let document_symbol = params
2924 .capabilities
2925 .text_document
2926 .unwrap()
2927 .document_symbol
2928 .unwrap();
2929
2930 assert_eq!(request["method"], "initialize");
2931 assert_eq!(document_symbol.dynamic_registration, Some(false));
2932 assert_eq!(
2933 document_symbol.hierarchical_document_symbol_support,
2934 Some(true)
2935 );
2936 assert_eq!(
2937 document_symbol.symbol_kind.unwrap().value_set,
2938 Some(advertised_symbol_kinds())
2939 );
2940
2941 write_success_response(
2942 &mut server.read_half_stdin,
2943 &request["id"].clone(),
2944 serde_json::json!({ "capabilities": {} }),
2945 )
2946 .await;
2947
2948 init_task.await.unwrap().unwrap();
2949 }
2950
2951 #[tokio::test]
2952 async fn test_initialize_accepts_resolved_dot_workspace_root() {
2953 let temp_dir = TempDir::new().unwrap();
2954 let base = dunce::canonicalize(temp_dir.path()).unwrap();
2955 let workspace_roots =
2956 crate::bridge::WorkspaceRoots::from_paths_with(&[PathBuf::from(".")], || {
2957 Ok(crate::bridge::ProcessCwd::new(base.clone(), None))
2958 })
2959 .unwrap()
2960 .canonical()
2961 .to_vec();
2962 assert_eq!(workspace_roots, vec![base.clone()]);
2963
2964 let (client, mut server) = fake_lsp_client();
2965 let config = ServerInitConfig::new(
2966 LspServerConfig::rust_analyzer(),
2967 WorkspaceRoots::for_test(workspace_roots, vec![]),
2968 PositionEncodings::DEFAULT,
2969 std::sync::Arc::default(),
2970 );
2971
2972 let init_task =
2973 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
2974
2975 let mut reader = BufReader::new(&mut server.write_stdout);
2976 let request = read_framed_message(&mut reader).await;
2977 let expected_uri = try_path_to_uri(&base).unwrap();
2978 assert_eq!(
2979 request["params"]["workspaceFolders"][0]["uri"],
2980 expected_uri.as_ref()
2981 );
2982
2983 write_success_response(
2984 &mut server.read_half_stdin,
2985 &request["id"].clone(),
2986 serde_json::json!({ "capabilities": {} }),
2987 )
2988 .await;
2989
2990 init_task.await.unwrap().unwrap();
2991 }
2992 }
2993
2994 #[test]
2997 fn child_env_var_compares_names_as_the_host_does() {
2998 let parent = |_: &str| Some(std::ffi::OsString::from("/parent"));
2999 let on = |host| {
3000 let mut config = bare_server_config(HashMap::new());
3001 config.env = crate::config::ServerEnv::from_entries(
3002 [(
3003 crate::config::EnvKey::from_static("Path"),
3004 crate::config::EnvValue::from_static("/over"),
3005 )],
3006 host,
3007 )
3008 .unwrap();
3009 child_env_var(&config, "PATH", parent)
3010 };
3011
3012 assert_eq!(on(HostOs::Windows), Some(std::ffi::OsString::from("/over")));
3013 assert_eq!(on(HostOs::Other), Some(std::ffi::OsString::from("/parent")));
3014 }
3015
3016 fn bare_server_config(
3019 env: HashMap<crate::config::EnvKey, crate::config::EnvValue>,
3020 ) -> LspServerConfig {
3021 LspServerConfig {
3022 language_id: LanguageId::from_static("test"),
3023 command: ServerCommand::from_static("irrelevant-for-build-command").into(),
3024 args: crate::config::ServerArgs::default(),
3025 env: crate::config::ServerEnv::from_entries(env, HostOs::CURRENT).unwrap(),
3026 file_patterns: vec![],
3027 initialization_options: None,
3028 settings: None,
3029 timeout_seconds: TimeoutSecs::new(5).unwrap(),
3030 request_timeout_seconds: TimeoutSecs::new(5).unwrap(),
3031 heuristics: None,
3032 name: None,
3033 handles: None,
3034 indexing: crate::bridge::IndexingPolicy::Auto,
3035 }
3036 }
3037
3038 fn effective_envs(command: &Command) -> HashMap<String, String> {
3042 command
3043 .as_std()
3044 .get_envs()
3045 .filter_map(|(k, v)| {
3046 v.map(|v| {
3047 (
3048 k.to_string_lossy().into_owned(),
3049 v.to_string_lossy().into_owned(),
3050 )
3051 })
3052 })
3053 .collect()
3054 }
3055
3056 #[test]
3060 fn test_build_command_excludes_non_allowlisted_parent_env_vars() {
3061 let config = bare_server_config(HashMap::new());
3062 let command =
3063 LspServer::build_command(&config, &ChildWorkingDir::Inherit, |key| match key {
3064 "PATH" => Some("/parent/bin".into()),
3065 "MCPLS_TEST_LEAK_CANARY" => Some("should-not-reach-child".into()),
3066 _ => None,
3067 });
3068
3069 let envs = effective_envs(&command);
3070
3071 assert!(
3072 !envs.contains_key("MCPLS_TEST_LEAK_CANARY"),
3073 "non-allowlisted parent env var leaked into child command: {envs:?}"
3074 );
3075
3076 #[cfg(unix)]
3087 assert!(
3088 format!("{:?}", command.as_std()).starts_with("env -i "),
3089 "build_command must call .env_clear() so the child doesn't inherit the full parent environment"
3090 );
3091 }
3092
3093 #[test]
3096 fn test_build_command_passes_through_allowlisted_env_vars() {
3097 let config = bare_server_config(HashMap::new());
3098 let command = LspServer::build_command(&config, &ChildWorkingDir::Inherit, |key| {
3099 (key == "PATH").then(|| "/parent/bin".into())
3100 });
3101
3102 let envs = effective_envs(&command);
3103
3104 assert_eq!(envs.get("PATH"), Some(&"/parent/bin".to_string()));
3105 }
3106
3107 #[test]
3108 fn test_build_command_inherits_working_dir_unless_fixed() {
3109 let config = bare_server_config(HashMap::new());
3110 let inherited = LspServer::build_command(&config, &ChildWorkingDir::Inherit, |_| None);
3111 assert_eq!(inherited.as_std().get_current_dir(), None);
3112
3113 let dir = PathBuf::from("/fixed/dir");
3114 let fixed =
3115 LspServer::build_command(&config, &ChildWorkingDir::Fixed(dir.clone()), |_| None);
3116 assert_eq!(fixed.as_std().get_current_dir(), Some(dir.as_path()));
3117 }
3118
3119 #[test]
3120 fn test_server_init_config_defaults_to_inherited_working_dir() {
3121 let config = ServerInitConfig::new(
3122 LspServerConfig::rust_analyzer(),
3123 WorkspaceRoots::default(),
3124 PositionEncodings::DEFAULT,
3125 std::sync::Arc::default(),
3126 );
3127 assert_eq!(config.child_working_dir(), &ChildWorkingDir::Inherit);
3128 let fixed = config.with_child_working_dir(ChildWorkingDir::Fixed(PathBuf::from("/x")));
3129 assert_eq!(
3130 fixed.child_working_dir(),
3131 &ChildWorkingDir::Fixed(PathBuf::from("/x"))
3132 );
3133 }
3134
3135 #[tokio::test]
3136 async fn test_for_respawn_keeps_the_config_and_pinned_tsserver() {
3137 let mut server_config = LspServerConfig::typescript();
3138 server_config.initialization_options =
3139 Some(serde_json::json!({"tsserver": {"path": "/pin/tsserver.js"}}));
3140 let config = ServerInitConfig::new(
3141 server_config,
3142 WorkspaceRoots::default(),
3143 PositionEncodings::DEFAULT,
3144 std::sync::Arc::default(),
3145 );
3146 assert_eq!(
3147 config.pinned_tsserver(),
3148 Some(PathBuf::from("/pin/tsserver.js"))
3149 );
3150
3151 let respawn = config.for_respawn().await.unwrap();
3152 assert_eq!(
3153 respawn.server_config().initialization_options,
3154 config.server_config().initialization_options
3155 );
3156 assert_eq!(respawn.pinned_tsserver(), config.pinned_tsserver());
3157 }
3158
3159 #[tokio::test]
3162 async fn test_for_respawn_replans_an_auto_pinned_server_off_the_runtime() {
3163 let mut server_config = LspServerConfig::typescript();
3164 server_config.initialization_options =
3165 Some(serde_json::json!({"tsserver": {"path": "/mcpls-test/stale/tsserver.js"}}));
3166 let config = ServerInitConfig::new(
3167 server_config,
3168 WorkspaceRoots::default(),
3169 PositionEncodings::DEFAULT,
3170 std::sync::Arc::default(),
3171 )
3172 .with_auto_pin(PathBuf::from("/mcpls-test/stale/tsserver.js"));
3173
3174 let respawn = config.for_respawn().await.unwrap();
3175
3176 assert_eq!(respawn.server_config().id(), config.server_config().id());
3177 assert_ne!(respawn.pinned_tsserver(), config.pinned_tsserver());
3178 }
3179
3180 fn untrusted_init_naming_a_tool_argument() -> (tempfile::TempDir, PathBuf, ServerInitConfig) {
3183 let dir = tempfile::TempDir::new().unwrap();
3184 let workspace = dunce::canonicalize(dir.path()).unwrap();
3185 let tool = workspace.join("tool");
3186 std::fs::write(&tool, "").unwrap();
3187 let roots = WorkspaceRoots::from_paths(std::slice::from_ref(&workspace)).unwrap();
3188 let mut server_config = LspServerConfig::rust_analyzer();
3189 server_config.command = ServerCommand::from_static("mcpls-test-no-such-server").into();
3190 server_config.args =
3191 crate::config::ServerArgs::new([tool.to_string_lossy().into_owned()]).unwrap();
3192 let command = server_config.command.server_command().clone();
3193 let init = ServerInitConfig::new(
3194 server_config,
3195 roots.clone(),
3196 PositionEncodings::DEFAULT,
3197 std::sync::Arc::default(),
3198 )
3199 .with_trust(SpawnTrust::Untrusted(UntrustedVetting::new(roots, command)));
3200 (dir, tool, init)
3201 }
3202
3203 fn become_executable(tool: &Path) {
3205 #[cfg(unix)]
3206 {
3207 use std::os::unix::fs::PermissionsExt as _;
3208 std::fs::set_permissions(tool, std::fs::Permissions::from_mode(0o755)).unwrap();
3209 }
3210 #[cfg(windows)]
3211 std::fs::write(tool.with_added_extension("exe"), "").unwrap();
3212 }
3213
3214 fn refused_argument(error: &Error) -> bool {
3215 matches!(
3216 error,
3217 Error::ServerFailedToStart(failure) if matches!(
3218 failure.reason,
3219 StartupFailure::RefusedUntrustedWorkspace(
3220 UntrustedRefusal::WorkspaceExecutableArgument { index: 0, .. }
3221 )
3222 )
3223 )
3224 }
3225
3226 #[tokio::test]
3228 async fn test_spawn_refuses_an_argument_that_became_executable_after_planning() {
3229 let (_dir, tool, init) = untrusted_init_naming_a_tool_argument();
3230 init.vet_arguments().unwrap();
3231
3232 become_executable(&tool);
3233
3234 let Err(error) = LspServer::spawn(init).await else {
3235 panic!("the spawn must be refused");
3236 };
3237 assert!(refused_argument(&error), "{error:?}");
3238 }
3239
3240 #[tokio::test]
3242 async fn test_for_respawn_refuses_an_argument_that_became_executable_after_planning() {
3243 let (_dir, tool, init) = untrusted_init_naming_a_tool_argument();
3244 let unchanged = init.for_respawn().await.unwrap();
3245 assert_eq!(unchanged.server_config().args, init.server_config().args);
3246
3247 become_executable(&tool);
3248
3249 let Err(error) = init.for_respawn().await else {
3250 panic!("the respawn plan must be refused");
3251 };
3252 assert!(refused_argument(&error), "{error:?}");
3253 }
3254
3255 #[tokio::test]
3256 async fn test_a_trusted_config_is_never_vetted_at_spawn_time() {
3257 let (_dir, tool, init) = untrusted_init_naming_a_tool_argument();
3258 let trusted = ServerInitConfig::new(
3259 init.server_config().clone(),
3260 init.workspace_roots().clone(),
3261 PositionEncodings::DEFAULT,
3262 std::sync::Arc::default(),
3263 );
3264 become_executable(&tool);
3265
3266 trusted.vet_arguments().unwrap();
3267 trusted.vet_arguments_off_runtime().await.unwrap();
3268 trusted.for_respawn().await.unwrap();
3269 }
3270
3271 #[tokio::test(start_paused = true)]
3274 async fn test_terminate_sends_exit_after_a_shutdown_answer_that_is_not_null() {
3275 use tokio::io::BufReader;
3276
3277 use crate::test_lsp::{fake_lsp_client, read_framed_message, write_response};
3278
3279 let (client, mut fake) = fake_lsp_client();
3280 let mut server = LspServer {
3281 client,
3282 capabilities: lsp_types::ServerCapabilities::default(),
3283 position_encoding: PositionEncodingKind::UTF8,
3284 child: None,
3285 init_config: crate::test_lsp::init_config_for(LspServerConfig::rust_analyzer()),
3286 };
3287 let terminating = tokio::spawn(async move {
3288 server
3289 .terminate(SHUTDOWN_TIMEOUT, ExitGrace::WithinBudget)
3290 .await
3291 });
3292 let mut wire = BufReader::new(&mut fake.write_stdout);
3293
3294 let request = read_framed_message(&mut wire).await;
3295 assert_eq!(request["method"], "shutdown");
3296 write_response(
3297 &mut fake.read_half_stdin,
3298 &request["id"],
3299 serde_json::json!({}),
3300 )
3301 .await;
3302 let exit = read_framed_message(&mut wire).await;
3303
3304 assert_eq!(exit["method"], "exit");
3305 assert_matches!(terminating.await.unwrap(), Ok(()));
3306 }
3307
3308 #[test]
3311 fn test_build_command_includes_configured_env_vars() {
3312 let mut env = HashMap::new();
3313 env.insert(
3314 crate::config::EnvKey::from_static("MCPLS_TEST_CONFIGURED"),
3315 crate::config::EnvValue::from_static("from-server-config"),
3316 );
3317 let config = bare_server_config(env);
3318 let command = LspServer::build_command(&config, &ChildWorkingDir::Inherit, |_| None);
3319
3320 let envs = effective_envs(&command);
3321
3322 assert_eq!(
3323 envs.get("MCPLS_TEST_CONFIGURED"),
3324 Some(&"from-server-config".to_string())
3325 );
3326 }
3327
3328 #[test]
3332 fn test_build_command_configured_env_overrides_allowlisted_var() {
3333 let mut env = HashMap::new();
3334 env.insert(
3335 crate::config::EnvKey::from_static("PATH"),
3336 crate::config::EnvValue::from_static("/configured/override/path"),
3337 );
3338 let config = bare_server_config(env);
3339 let command = LspServer::build_command(&config, &ChildWorkingDir::Inherit, |key| {
3340 (key == "PATH").then(|| "/parent/bin".into())
3341 });
3342
3343 let envs = effective_envs(&command);
3344
3345 assert_eq!(
3346 envs.get("PATH"),
3347 Some(&"/configured/override/path".to_string())
3348 );
3349 }
3350
3351 #[tokio::test]
3358 async fn test_settling_dead_diagnostics_server_hands_the_route_to_the_catch_all() {
3359 use crate::bridge::Translator;
3360 use crate::config::{ServerId, ToolKind, ToolRouter};
3361
3362 let pylsp_id = ServerId::from_static("pylsp");
3363 let configs = vec![
3364 LspServerConfig {
3365 language_id: LanguageId::from_static("python"),
3366 command: ServerCommand::from_static("pyright-langserver").into(),
3367 args: crate::config::ServerArgs::default(),
3368 env: crate::config::ServerEnv::default(),
3369 file_patterns: vec![],
3370 initialization_options: None,
3371 settings: None,
3372 timeout_seconds: TimeoutSecs::new(30).unwrap(),
3373 request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
3374 heuristics: None,
3375 name: Some(ServerId::from_static("pyright-diag")),
3376 handles: Some(ToolSet::new(vec![ToolKind::Diagnostics]).unwrap()),
3377 indexing: crate::bridge::IndexingPolicy::Auto,
3378 },
3379 LspServerConfig {
3380 language_id: LanguageId::from_static("python"),
3381 command: ServerCommand::from_static("pylsp").into(),
3382 args: crate::config::ServerArgs::default(),
3383 env: crate::config::ServerEnv::default(),
3384 file_patterns: vec![],
3385 initialization_options: None,
3386 settings: None,
3387 timeout_seconds: TimeoutSecs::new(30).unwrap(),
3388 request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
3389 heuristics: None,
3390 name: Some(ServerId::from_static("pylsp")),
3391 handles: None,
3392 indexing: crate::bridge::IndexingPolicy::Auto,
3393 },
3394 ];
3395 let router = ToolRouter::from_configs(&configs).unwrap();
3396 let translator = Translator::new().with_router(router);
3397 translator.set_expected_servers(
3398 [ServerId::from_static("pyright-diag"), pylsp_id.clone()]
3399 .into_iter()
3400 .collect(),
3401 );
3402
3403 translator.settle_failed(&ServerSpawnFailure {
3404 server_id: ServerId::from_static("pyright-diag"),
3405 language_id: LanguageId::from_static("python"),
3406 command: ServerCommand::from_static("pyright-langserver"),
3407 reason: StartupFailure::InitTaskPanicked,
3408 });
3409 translator.settle_started(fake_lsp_server_with_config(configs[1].clone()));
3410
3411 assert!(
3412 translator.is_diagnostics_route(&LanguageId::from_static("python"), &pylsp_id),
3413 "pylsp must inherit the diagnostics route once pyright-diag is known dead"
3414 );
3415 }
3416}