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::{
40 CONTENT_MODIFIED_RETRY_METHODS, ManagedEnvVar, NotificationInbox, NotificationSink, ParentEnv,
41 PublishReader, ServerMessage, process_env, tsserver_pin,
42};
43use crate::redaction::Redactions;
44
45const ENV_PASSTHROUGH: &[&str] = &[
58 ManagedEnvVar::Path.name(),
59 ManagedEnvVar::Home.name(),
60 ManagedEnvVar::UserProfile.name(),
61 "TMPDIR",
62 "TEMP",
63 "TMP",
64];
65
66const CHILD_EXIT_GRACE: Duration = Duration::from_secs(3);
70
71const MIN_SWEEP_BUDGET: Duration = Duration::from_secs(3);
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum ExitGrace {
80 WithinBudget,
83 IfAnswered(Duration),
87}
88
89const EARLY_EXIT_PROBE: Duration = Duration::from_millis(500);
92
93const NOTIFICATION_CHANNEL_CAPACITY: usize = 256;
101
102const LIFECYCLE_CHANNEL_CAPACITY: usize = 128;
112
113pub const SUPPORTED_SYMBOL_KINDS: [(SymbolKind, &str); 26] = [
119 (SymbolKind::File, "file"),
120 (SymbolKind::Module, "module"),
121 (SymbolKind::Namespace, "namespace"),
122 (SymbolKind::Package, "package"),
123 (SymbolKind::Class, "class"),
124 (SymbolKind::Method, "method"),
125 (SymbolKind::Property, "property"),
126 (SymbolKind::Field, "field"),
127 (SymbolKind::Constructor, "constructor"),
128 (SymbolKind::Enum, "enum"),
129 (SymbolKind::Interface, "interface"),
130 (SymbolKind::Function, "function"),
131 (SymbolKind::Variable, "variable"),
132 (SymbolKind::Constant, "constant"),
133 (SymbolKind::String, "string"),
134 (SymbolKind::Number, "number"),
135 (SymbolKind::Boolean, "boolean"),
136 (SymbolKind::Array, "array"),
137 (SymbolKind::Object, "object"),
138 (SymbolKind::Key, "key"),
139 (SymbolKind::Null, "null"),
140 (SymbolKind::EnumMember, "enummember"),
141 (SymbolKind::Struct, "struct"),
142 (SymbolKind::Event, "event"),
143 (SymbolKind::Operator, "operator"),
144 (SymbolKind::TypeParameter, "typeparameter"),
145];
146
147fn advertised_symbol_kinds() -> Vec<SymbolKind> {
149 SUPPORTED_SYMBOL_KINDS
150 .iter()
151 .map(|&(kind, _)| kind)
152 .collect()
153}
154
155#[cfg(windows)]
165const ENV_PASSTHROUGH_WINDOWS: &[&str] = &[
166 ManagedEnvVar::NoDefaultCurrentDirectoryInExePath.name(),
167 "SystemRoot",
168 "SystemDrive",
169 "windir",
170 "APPDATA",
171 "LOCALAPPDATA",
172 "ProgramData",
173 "ProgramFiles",
174 "COMSPEC",
175 "PATHEXT",
176 "NUMBER_OF_PROCESSORS",
177 "USERNAME",
178];
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub enum ServerState {
183 Uninitialized,
185 Initializing,
187 Ready,
189 ShuttingDown,
191 Shutdown,
193}
194
195impl ServerState {
196 #[must_use]
198 pub const fn is_ready(&self) -> bool {
199 matches!(self, Self::Ready)
200 }
201
202 #[must_use]
204 pub const fn can_accept_requests(&self) -> bool {
205 matches!(self, Self::Ready)
206 }
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, Default)]
226pub enum ChildWorkingDir {
227 #[default]
229 Inherit,
230 Fixed(PathBuf),
232}
233
234#[derive(Debug, Clone)]
239struct AutoPin {
240 tsserver: PathBuf,
241 vetting: Option<tsserver_pin::UntrustedVetting>,
243}
244
245#[derive(Debug, Clone)]
251pub struct ServerInitConfig {
252 server_config: LspServerConfig,
253 workspace_roots: WorkspaceRoots,
254 position_encodings: PositionEncodings,
255 redactions: Arc<Redactions>,
256 child_working_dir: ChildWorkingDir,
257 auto_pin: Option<AutoPin>,
258}
259
260impl ServerInitConfig {
261 #[must_use]
295 pub const fn new(
296 server_config: LspServerConfig,
297 workspace_roots: WorkspaceRoots,
298 position_encodings: PositionEncodings,
299 redactions: Arc<Redactions>,
300 ) -> Self {
301 Self {
302 server_config,
303 workspace_roots,
304 position_encodings,
305 redactions,
306 child_working_dir: ChildWorkingDir::Inherit,
307 auto_pin: None,
308 }
309 }
310
311 #[must_use]
314 pub(crate) fn with_auto_pin(
315 mut self,
316 tsserver: PathBuf,
317 vetting: Option<tsserver_pin::UntrustedVetting>,
318 ) -> Self {
319 self.auto_pin = Some(AutoPin { tsserver, vetting });
320 self
321 }
322
323 #[must_use]
346 pub fn with_child_working_dir(mut self, dir: ChildWorkingDir) -> Self {
347 self.child_working_dir = dir;
348 self
349 }
350
351 #[must_use]
353 pub const fn server_config(&self) -> &LspServerConfig {
354 &self.server_config
355 }
356
357 #[must_use]
359 pub const fn workspace_roots(&self) -> &WorkspaceRoots {
360 &self.workspace_roots
361 }
362
363 #[must_use]
365 pub const fn position_encodings(&self) -> &PositionEncodings {
366 &self.position_encodings
367 }
368
369 #[must_use]
371 pub const fn redactions(&self) -> &Arc<Redactions> {
372 &self.redactions
373 }
374
375 #[must_use]
377 pub const fn child_working_dir(&self) -> &ChildWorkingDir {
378 &self.child_working_dir
379 }
380
381 #[cfg(all(test, unix))]
383 pub(crate) const fn server_config_mut(&mut self) -> &mut LspServerConfig {
384 &mut self.server_config
385 }
386
387 #[must_use]
389 pub fn pinned_tsserver(&self) -> Option<PathBuf> {
390 tsserver_pin::configured_tsserver_path(self.server_config.initialization_options.as_ref())
391 }
392
393 pub(crate) async fn for_respawn(&self) -> Result<Self> {
409 if self.auto_pin.is_none() {
410 return Ok(self.clone());
411 }
412 let config = self.clone();
413 crate::on_blocking_pool(move || config.plan_respawn())
414 .await
415 .map_err(|source| Error::TaskFailed {
416 task: crate::error::BackgroundTask::ServerPlanning,
417 source,
418 })?
419 }
420
421 pub(crate) fn plan_respawn(&self) -> Result<Self> {
424 let Some(pin) = self
425 .auto_pin
426 .as_ref()
427 .filter(|pin| dunce::canonicalize(&pin.tsserver).ok().as_ref() != Some(&pin.tsserver))
428 else {
429 return Ok(self.clone());
430 };
431 let mut unpinned = self.server_config.clone();
432 unpinned.initialization_options = None;
433 let plan = tsserver_pin::plan_typescript(unpinned, process_env);
434 if let Some(vetting) = &pin.vetting {
435 plan.vet_untrusted(vetting)
436 .map_err(|refusal| self.refusal(refusal))?;
437 }
438 let (server_config, tsserver) = plan.apply(&self.workspace_roots);
439 let auto_pin = tsserver.map(|tsserver| AutoPin {
440 tsserver,
441 vetting: pin.vetting.clone(),
442 });
443 Ok(Self {
444 server_config,
445 auto_pin,
446 ..self.clone()
447 })
448 }
449
450 fn refusal(&self, refusal: UntrustedRefusal) -> Error {
451 Error::ServerFailedToStart(Box::new(ServerSpawnFailure {
452 server_id: self.server_config.id(),
453 language_id: self.server_config.language_id.clone(),
454 command: self.server_config.command.server_command().clone(),
455 reason: StartupFailure::RefusedUntrustedWorkspace(refusal),
456 }))
457 }
458}
459
460#[derive(Debug)]
465pub enum ServerStartOutcome {
466 Started(Box<LspServer>),
468 Failed(ServerSpawnFailure),
470}
471
472pub struct LspServer {
474 client: LspClient,
475 capabilities: ServerCapabilities,
476 position_encoding: PositionEncodingKind,
477 notification_rx: mpsc::Receiver<ServerMessage>,
481 publish_rx: PublishReader,
486 lifecycle_rx: mpsc::Receiver<LspNotification>,
495 child: Option<ServerProcess>,
506 init_config: ServerInitConfig,
509}
510
511impl std::fmt::Debug for LspServer {
512 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
513 f.debug_struct("LspServer")
514 .field("client", &self.client)
515 .field("capabilities", &self.capabilities)
516 .field("position_encoding", &self.position_encoding)
517 .field("notification_rx", &"<channel>")
518 .field("publish_rx", &"<mailbox>")
519 .field("lifecycle_rx", &"<channel>")
520 .field("child", &"<process>")
521 .field("id", &self.init_config.server_config.id())
522 .finish()
523 }
524}
525
526pub fn child_env_var(
532 config: &LspServerConfig,
533 key: &str,
534 parent_env: impl ParentEnv,
535) -> Option<std::ffi::OsString> {
536 config
537 .env
538 .get(key)
539 .map(std::ffi::OsString::from)
540 .or_else(|| parent_env(key))
541}
542
543impl LspServer {
544 pub(crate) const fn init_config(&self) -> &ServerInitConfig {
546 &self.init_config
547 }
548
549 #[cfg(test)]
551 #[cfg(unix)]
552 pub(crate) fn set_init_config(&mut self, config: ServerInitConfig) {
553 self.init_config = config;
554 }
555
556 fn take_notification_rx(&mut self) -> tokio::sync::mpsc::Receiver<ServerMessage> {
558 let (_, dummy) = tokio::sync::mpsc::channel(1);
559 std::mem::replace(&mut self.notification_rx, dummy)
560 }
561
562 pub fn take_notification_inbox(&mut self) -> NotificationInbox {
565 NotificationInbox::new(self.take_notification_rx(), self.take_publish_rx())
566 }
567
568 fn take_publish_rx(&mut self) -> PublishReader {
571 std::mem::replace(&mut self.publish_rx, PublishReader::closed())
572 }
573
574 pub fn take_lifecycle_rx(&mut self) -> tokio::sync::mpsc::Receiver<LspNotification> {
579 let (_, dummy) = tokio::sync::mpsc::channel(1);
580 std::mem::replace(&mut self.lifecycle_rx, dummy)
581 }
582
583 pub async fn spawn(config: ServerInitConfig) -> Result<Self> {
603 let own = Redactions::for_server(config.server_config(), current_environment());
604 let redactions = Arc::new(Redactions::union([config.redactions().as_ref(), &own]));
605 Self::log_spawn(config.server_config(), &redactions);
606
607 let command = Self::build_command(
608 config.server_config(),
609 config.child_working_dir(),
610 process_env,
611 );
612
613 let passthrough_present = {
618 let base = ENV_PASSTHROUGH
619 .iter()
620 .filter(|key| std::env::var_os(key).is_some())
621 .count();
622 #[cfg(windows)]
623 let windows = ENV_PASSTHROUGH_WINDOWS
624 .iter()
625 .filter(|key| std::env::var_os(key).is_some())
626 .count();
627 #[cfg(not(windows))]
628 let windows = 0;
629 base.saturating_add(windows)
630 };
631 debug!(
632 "Effective LSP server env: {passthrough_present} allowlisted key(s) present, \
633 {} configured override(s) applied",
634 config.server_config.env.len()
635 );
636
637 let mut child = ServerProcess::spawn(command)
638 .map_err(|e| spawn_error(config.server_config.command.server_command().clone(), e))?;
639
640 let stdin = child
641 .take_stdin()
642 .ok_or(Error::StdioCapture(StdioStream::Stdin))?;
643 let stdout = child
644 .take_stdout()
645 .ok_or(Error::StdioCapture(StdioStream::Stdout))?;
646
647 let stderr = child
648 .take_stderr()
649 .ok_or(Error::StdioCapture(StdioStream::Stderr))?;
650 let stderr_capture = StderrCapture::start(stderr);
651
652 let transport = LspTransport::with_redactions(stdin, stdout, Arc::clone(&redactions));
653 let (notification_tx, notification_rx) = mpsc::channel(NOTIFICATION_CHANNEL_CAPACITY);
654 let (notification_sink, publish_rx) = NotificationSink::new(notification_tx);
655 let (lifecycle_tx, lifecycle_rx) = mpsc::channel(LIFECYCLE_CHANNEL_CAPACITY);
656 let client = LspClient::from_transport_with_notifications(
657 config.server_config.clone(),
658 transport,
659 notification_sink,
660 lifecycle_tx,
661 Arc::clone(&redactions),
662 );
663 #[cfg(unix)]
664 let process_id = initialize_process_id(child.binding());
665 #[cfg(windows)]
666 let process_id = Some(mcpls_process_id());
667 let (capabilities, position_encoding) =
668 match Self::initialize_as(&client, &config, process_id).await {
669 Ok(negotiated) => negotiated,
670 Err(init_error) if is_connection_loss(&init_error) => {
671 return Err(Self::connection_loss_error(
672 &config,
673 &mut child,
674 stderr_capture,
675 &redactions,
676 init_error,
677 )
678 .await);
679 }
680 Err(Error::LspInitFailed { phase, cause, .. }) => {
681 let stderr = stderr_capture.finish(EofWait::Grace, &redactions).await;
685 let hint = Self::init_failure_hint(&config).await;
686 return Err(Error::LspInitFailed {
687 phase,
688 cause: Box::new(redactions.sanitize_error(*cause)),
689 hint,
690 stderr,
691 });
692 }
693 Err(init_error) => return Err(redactions.sanitize_error(init_error)),
694 };
695
696 info!("LSP server initialized successfully");
697
698 Ok(Self {
699 client,
700 capabilities,
701 position_encoding,
702 notification_rx,
703 publish_rx,
704 lifecycle_rx,
705 child: Some(child),
706 init_config: config,
707 })
708 }
709
710 async fn connection_loss_error(
713 config: &ServerInitConfig,
714 child: &mut ServerProcess,
715 stderr_capture: StderrCapture,
716 redactions: &Redactions,
717 init_error: Error,
718 ) -> Error {
719 let exit_status = early_exit_status(child).await;
720 let eof_wait = if exit_status.is_some() {
721 EofWait::Grace
722 } else {
723 EofWait::Skip
724 };
725 let stderr = stderr_capture.finish(eof_wait, redactions).await;
726 let hint = Self::init_failure_hint(config).await;
727 match exit_status {
728 Some(status) => Error::ServerExitedDuringInit {
729 command: config.server_config.command.server_command().clone(),
730 exit_code: status.code(),
731 hint,
732 stderr,
733 },
734 None => Error::LspInitFailed {
735 phase: InitPhase::Initialize,
736 cause: Box::new(redactions.sanitize_error(init_error)),
737 hint,
738 stderr,
739 },
740 }
741 }
742
743 async fn init_failure_hint(config: &ServerInitConfig) -> Option<InitFailureHint> {
749 let config = config.clone();
750 let hint = crate::on_blocking_pool(move || {
751 tsserver_pin::init_failure_hint(
752 config.server_config(),
753 config.workspace_roots(),
754 process_env,
755 )
756 })
757 .await;
758 hint.unwrap_or_else(|error| {
759 debug!("Dropped the init failure hint: {error}");
760 None
761 })
762 }
763
764 fn log_spawn(server_config: &LspServerConfig, redactions: &Redactions) {
767 info!(
768 "Spawning LSP server: {} ({} arg(s))",
769 server_config.command,
770 server_config.args.len()
771 );
772 debug!(
773 "LSP server args: {:?}",
774 server_config
775 .args
776 .iter()
777 .map(|arg| redactions.apply(arg))
778 .collect::<Vec<_>>()
779 );
780 }
781
782 fn build_command(
792 config: &LspServerConfig,
793 working_dir: &ChildWorkingDir,
794 parent_env: impl ParentEnv,
795 ) -> Command {
796 let mut command = Command::new(&config.command);
797 command.args(&config.args).env_clear();
798 if let ChildWorkingDir::Fixed(dir) = working_dir {
799 command.current_dir(dir);
800 }
801
802 for key in ENV_PASSTHROUGH {
803 if let Some(value) = child_env_var(config, key, &parent_env) {
804 command.env(key, value);
805 }
806 }
807 #[cfg(windows)]
808 for key in ENV_PASSTHROUGH_WINDOWS {
809 if let Some(value) = child_env_var(config, key, &parent_env) {
810 command.env(key, value);
811 }
812 }
813
814 command
815 .envs(config.env.iter())
816 .stdin(Stdio::piped())
817 .stdout(Stdio::piped())
818 .stderr(Stdio::piped());
819
820 command
821 }
822
823 fn folding_range_capabilities() -> lsp_types::FoldingRangeClientCapabilities {
826 lsp_types::FoldingRangeClientCapabilities {
827 dynamic_registration: Some(false),
828 line_folding_only: Some(false),
829 folding_range_kind: Some(lsp_types::ClientFoldingRangeKindOptions {
830 value_set: Some(vec![
831 lsp_types::FoldingRangeKind::Comment,
832 lsp_types::FoldingRangeKind::Imports,
833 lsp_types::FoldingRangeKind::Region,
834 ]),
835 }),
836 folding_range: Some(lsp_types::ClientFoldingRangeOptions {
837 collapsed_text: Some(true),
838 }),
839 ..Default::default()
840 }
841 }
842
843 fn client_capabilities(
858 position_encodings: &PositionEncodings,
859 settings: Option<&LspSettings>,
860 ) -> ClientCapabilities {
861 ClientCapabilities {
862 general: Some(GeneralClientCapabilities {
863 position_encodings: Some(
864 position_encodings
865 .as_slice()
866 .iter()
867 .map(|encoding| encoding.to_kind())
868 .collect(),
869 ),
870 stale_request_support: Some(StaleRequestSupportOptions {
871 cancel: false,
874 retry_on_content_modified: CONTENT_MODIFIED_RETRY_METHODS
875 .iter()
876 .map(ToString::to_string)
877 .collect(),
878 }),
879 ..Default::default()
880 }),
881 text_document: Some(lsp_types::TextDocumentClientCapabilities {
882 document_symbol: Some(lsp_types::DocumentSymbolClientCapabilities {
883 dynamic_registration: Some(false),
884 symbol_kind: Some(lsp_types::ClientSymbolKindOptions {
885 value_set: Some(advertised_symbol_kinds()),
886 }),
887 hierarchical_document_symbol_support: Some(true),
888 ..Default::default()
889 }),
890 hover: Some(lsp_types::HoverClientCapabilities {
891 dynamic_registration: Some(false),
892 content_format: Some(vec![
893 lsp_types::MarkupKind::Markdown,
894 lsp_types::MarkupKind::PlainText,
895 ]),
896 }),
897 definition: Some(lsp_types::DefinitionClientCapabilities {
898 dynamic_registration: Some(false),
899 link_support: Some(true),
900 }),
901 declaration: Some(lsp_types::DeclarationClientCapabilities {
902 dynamic_registration: Some(false),
903 link_support: Some(true),
904 }),
905 references: Some(lsp_types::ReferenceClientCapabilities {
906 dynamic_registration: Some(false),
907 }),
908 rename: Some(lsp_types::RenameClientCapabilities {
911 dynamic_registration: Some(false),
912 prepare_support: Some(true),
913 prepare_support_default_behavior: Some(
914 lsp_types::PrepareSupportDefaultBehavior::Identifier,
915 ),
916 ..Default::default()
917 }),
918 folding_range: Some(Self::folding_range_capabilities()),
919 publish_diagnostics: Some(lsp_types::PublishDiagnosticsClientCapabilities {
922 version_support: Some(true),
923 ..Default::default()
924 }),
925 selection_range: Some(lsp_types::SelectionRangeClientCapabilities {
926 dynamic_registration: Some(false),
927 }),
928 code_action: Some(lsp_types::CodeActionClientCapabilities {
929 dynamic_registration: Some(false),
930 data_support: Some(true),
931 resolve_support: Some(lsp_types::ClientCodeActionResolveOptions {
932 properties: vec!["edit".to_string()],
933 }),
934 code_action_literal_support: Some(lsp_types::ClientCodeActionLiteralOptions {
937 code_action_kind: lsp_types::ClientCodeActionKindOptions {
938 value_set: std::iter::once(lsp_types::CodeActionKind::Empty)
939 .chain(
940 crate::bridge::CodeActionKindFilter::ALL
941 .map(lsp_types::CodeActionKind::from),
942 )
943 .collect(),
944 },
945 }),
946 ..Default::default()
947 }),
948 ..Default::default()
949 }),
950 workspace: Some(lsp_types::WorkspaceClientCapabilities {
951 workspace_folders: Some(true),
952 configuration: settings.map(|_| true),
953 ..Default::default()
954 }),
955 window: Some(lsp_types::WindowClientCapabilities {
957 work_done_progress: Some(true),
958 ..Default::default()
959 }),
960 experimental: Some(serde_json::json!({ "serverStatusNotification": true })),
962 ..Default::default()
963 }
964 }
965
966 #[cfg(test)]
979 async fn initialize(
980 client: &LspClient,
981 config: &ServerInitConfig,
982 ) -> Result<(ServerCapabilities, PositionEncodingKind)> {
983 Self::initialize_as(client, config, Some(mcpls_process_id())).await
984 }
985
986 async fn initialize_as(
991 client: &LspClient,
992 config: &ServerInitConfig,
993 process_id: Option<i32>,
994 ) -> Result<(ServerCapabilities, PositionEncodingKind)> {
995 debug!("Sending initialize request");
996 if config.server_config().initialization_options.is_some()
997 && config.server_config().settings.is_some()
998 {
999 warn!(
1000 "server `{}` has both initialization_options and settings: servers that pull \
1001 workspace/configuration (rust-analyzer, jdtls) may replace the options with \
1002 the settings",
1003 config.server_config.command
1004 );
1005 }
1006
1007 let workspace_folders: Vec<WorkspaceFolder> = config
1008 .workspace_roots()
1009 .canonical()
1010 .iter()
1011 .map(|root| workspace_folder(root))
1012 .collect::<Result<Vec<_>>>()?;
1013
1014 let params = InitializeParams {
1015 process_id,
1016 #[allow(
1017 deprecated,
1018 reason = "`root_uri` is deprecated but the struct literal must still set it"
1019 )]
1020 root_uri: None,
1021 initialization_options: config.server_config().initialization_options.clone(),
1022 capabilities: Self::client_capabilities(
1023 &config.position_encodings,
1024 config.server_config.settings.as_ref(),
1025 ),
1026 client_info: Some(ClientInfo {
1027 name: "mcpls".to_string(),
1028 version: Some(env!("CARGO_PKG_VERSION").to_string()),
1029 }),
1030 workspace_folders_initialize_params: lsp_types::WorkspaceFoldersInitializeParams {
1031 workspace_folders: Some(lsp_types::WorkspaceFolders::WorkspaceFolderList(
1032 workspace_folders,
1033 )),
1034 },
1035 ..Default::default()
1036 };
1037
1038 let result: InitializeResult = client
1042 .request_typed::<InitializeRequest>(
1043 params,
1044 config.server_config.timeout_seconds.as_duration(),
1045 )
1046 .await
1047 .map_err(|e| {
1048 if is_connection_loss(&e) {
1049 e
1050 } else {
1051 Error::LspInitFailed {
1052 phase: InitPhase::Initialize,
1053 cause: Box::new(e),
1054 hint: None,
1055 stderr: None,
1056 }
1057 }
1058 })?;
1059
1060 let position_encoding = result
1061 .capabilities
1062 .position_encoding
1063 .clone()
1064 .unwrap_or(PositionEncodingKind::UTF16);
1065
1066 debug!(
1067 "Server capabilities received, encoding: {:?}",
1068 position_encoding
1069 );
1070
1071 notify_handshake::<InitializedNotification>(
1072 client,
1073 InitPhase::Initialized,
1074 InitializedParams {},
1075 )
1076 .await?;
1077 notify_handshake::<DidChangeConfigurationNotification>(
1078 client,
1079 InitPhase::DidChangeConfiguration,
1080 DidChangeConfigurationParams {
1081 settings: config
1082 .server_config
1083 .settings
1084 .as_ref()
1085 .map_or(serde_json::Value::Null, LspSettings::to_value),
1086 },
1087 )
1088 .await?;
1089
1090 Ok((result.capabilities, position_encoding))
1091 }
1092
1093 #[must_use]
1095 pub const fn capabilities(&self) -> &ServerCapabilities {
1096 &self.capabilities
1097 }
1098
1099 #[must_use]
1101 pub fn position_encoding(&self) -> PositionEncodingKind {
1102 self.position_encoding.clone()
1103 }
1104
1105 #[must_use]
1107 pub const fn client(&self) -> &LspClient {
1108 &self.client
1109 }
1110
1111 pub fn has_exited(&mut self) -> Result<bool> {
1129 match &mut self.child {
1130 Some(child) => Ok(child.try_wait()?.is_some()),
1131 None => Ok(false),
1132 }
1133 }
1134
1135 pub(crate) fn is_dead(&mut self) -> Result<bool> {
1147 if self.has_exited()? {
1148 return Ok(true);
1149 }
1150 Ok(self.child.is_some() && self.client.is_message_loop_finished())
1151 }
1152
1153 pub async fn shutdown(mut self) -> Result<()> {
1175 self.terminate(SHUTDOWN_TIMEOUT, ExitGrace::WithinBudget)
1176 .await
1177 }
1178
1179 #[allow(
1198 clippy::arithmetic_side_effects,
1199 reason = "SHUTDOWN_TIMEOUT and CHILD_EXIT_GRACE are small constants"
1200 )]
1201 pub(crate) async fn terminate(&mut self, budget: Duration, grace: ExitGrace) -> Result<()> {
1202 debug!("Shutting down LSP server");
1203
1204 let deadline = Instant::now() + budget;
1205 let client = &mut self.client;
1206 let shutdown_request_timeout = Duration::from_secs(5).min(budget);
1207
1208 let handshake: Result<MarkOutcome> = timeout_at(deadline, async {
1209 let _: serde_json::Value = client
1210 .request(
1211 ShutdownRequest::METHOD.as_str(),
1212 (),
1213 shutdown_request_timeout,
1214 )
1215 .await?;
1216 #[cfg(unix)]
1217 let mark = match self.child.as_mut() {
1218 Some(child) => child.mark_escapees().await,
1219 None => MarkOutcome::Confirmed,
1220 };
1221 #[cfg(windows)]
1222 let mark = MarkOutcome::Confirmed;
1223 if mark == MarkOutcome::Confirmed {
1224 client.notify_typed::<ExitNotification>(()).await?;
1225 }
1226 Ok(mark)
1227 })
1228 .await
1229 .unwrap_or(Err(Error::ShutdownTimeout));
1230 let mark = handshake
1231 .as_ref()
1232 .map_or(MarkOutcome::Confirmed, |mark| *mark);
1233 let handshake = handshake.map(drop);
1234 let handshake = match client.shutdown_until(deadline).await {
1235 Ok(()) => handshake,
1236 Err(e) => handshake.and(Err(e)),
1237 };
1238
1239 if let Some(child) = &mut self.child {
1240 let child_deadline = match grace {
1241 _ if mark != MarkOutcome::Confirmed => Instant::now(),
1242 ExitGrace::WithinBudget => deadline.min(Instant::now() + CHILD_EXIT_GRACE),
1243 ExitGrace::IfAnswered(grace) if handshake.is_ok() => Instant::now() + grace,
1244 ExitGrace::IfAnswered(_) => Instant::now(),
1245 };
1246 match timeout_at(child_deadline, child.wait()).await {
1247 Ok(Ok(status)) => {
1248 debug!(
1249 ?status,
1250 "LSP server process exited after `exit` notification"
1251 );
1252 }
1253 Ok(Err(e)) => warn!(error = %e, "failed to wait for LSP server process exit"),
1254 Err(_) => warn!(
1255 timeout = ?CHILD_EXIT_GRACE,
1256 "LSP server process did not exit within grace period after `exit` \
1257 notification, killing it"
1258 ),
1259 }
1260 let sweep_budget = deadline
1261 .saturating_duration_since(Instant::now())
1262 .clamp(MIN_SWEEP_BUDGET, crate::lsp::LIFELINE_SWEEP_BUDGET);
1263 child.terminate_tree(sweep_budget).await;
1264 }
1265
1266 handshake?;
1267 info!("LSP server shut down successfully");
1268 Ok(())
1269 }
1270
1271 pub(crate) async fn start_contained(config: &ServerInitConfig) -> ServerStartOutcome {
1278 Box::pin(contain(config, Self::spawn(config.clone()))).await
1279 }
1280}
1281
1282async fn contain(
1285 config: &ServerInitConfig,
1286 start: impl std::future::Future<Output = Result<LspServer>>,
1287) -> ServerStartOutcome {
1288 let server_id = config.server_config.id();
1289 let language_id = config.server_config.language_id.clone();
1290 let command = config.server_config.command.server_command().clone();
1291 let started = Instant::now();
1292
1293 let reason = match crate::util::catch_panic(start).await {
1294 Ok(Ok(server)) => {
1295 info!(
1296 "Successfully spawned LSP server: {} ({}) in {:?}",
1297 server_id,
1298 command,
1299 started.elapsed()
1300 );
1301 return ServerStartOutcome::Started(Box::new(server));
1302 }
1303 Ok(Err(e)) => {
1304 tracing::error!(
1305 "Failed to spawn LSP server: {} ({}) after {:?}: {}",
1306 server_id,
1307 command,
1308 started.elapsed(),
1309 e
1310 );
1311 StartupFailure::Spawn(Arc::new(e))
1312 }
1313 Err(panicked) => {
1314 tracing::error!(
1315 "Starting LSP server {} ({}) panicked after {:?}: {}",
1316 server_id,
1317 command,
1318 started.elapsed(),
1319 panicked.message()
1320 );
1321 StartupFailure::InitTaskPanicked
1322 }
1323 };
1324 ServerStartOutcome::Failed(ServerSpawnFailure {
1325 server_id,
1326 language_id,
1327 command,
1328 reason,
1329 })
1330}
1331
1332fn mcpls_process_id() -> i32 {
1333 i32::try_from(std::process::id()).unwrap_or(i32::MAX)
1334}
1335
1336#[cfg(unix)]
1339fn initialize_process_id(binding: Binding) -> Option<i32> {
1340 match binding {
1341 Binding::Bound => None,
1342 Binding::Unbound => Some(mcpls_process_id()),
1343 }
1344}
1345
1346async fn notify_handshake<N>(client: &LspClient, phase: InitPhase, params: N::Params) -> Result<()>
1349where
1350 N: lsp_types::Notification,
1351{
1352 client
1353 .notify_typed::<N>(params)
1354 .await
1355 .map_err(|e| Error::LspInitFailed {
1356 phase,
1357 cause: Box::new(e),
1358 hint: None,
1359 stderr: None,
1360 })
1361}
1362
1363const fn is_connection_loss(error: &Error) -> bool {
1366 matches!(
1367 error,
1368 Error::ServerTerminated
1369 | Error::TaskFailed {
1370 task: BackgroundTask::LspReceiver,
1371 ..
1372 }
1373 )
1374}
1375
1376async fn early_exit_status(child: &mut ServerProcess) -> Option<std::process::ExitStatus> {
1379 timeout(EARLY_EXIT_PROBE, child.wait()).await.ok()?.ok()
1380}
1381
1382fn spawn_error(command: ServerCommand, source: std::io::Error) -> Error {
1385 if source.kind() == std::io::ErrorKind::NotFound {
1386 Error::ServerNotFound { command, source }
1387 } else {
1388 Error::ServerSpawnFailed { command, source }
1389 }
1390}
1391
1392pub fn current_environment() -> Vec<(String, String)> {
1394 std::env::vars_os()
1395 .filter_map(|(name, value)| Some((name.into_string().ok()?, value.into_string().ok()?)))
1396 .collect()
1397}
1398
1399fn workspace_folder(root: &Path) -> Result<WorkspaceFolder> {
1405 let uri = try_path_to_uri(root).ok_or_else(|| Error::PathToUri(root.to_path_buf()))?;
1406 Ok(WorkspaceFolder {
1407 uri,
1408 name: root
1409 .file_name()
1410 .and_then(|n| n.to_str())
1411 .unwrap_or("workspace")
1412 .to_string(),
1413 })
1414}
1415
1416#[cfg(test)]
1429pub fn fake_lsp_server() -> LspServer {
1430 fake_lsp_server_with_config(LspServerConfig::pyright())
1431}
1432
1433#[cfg(test)]
1436pub fn fake_lsp_server_with_config(server_config: LspServerConfig) -> LspServer {
1437 let transport = crate::test_lsp::inert_transport();
1438 let client = LspClient::from_transport(server_config.clone(), transport);
1439 let (_, mock_notification_rx) = mpsc::channel(1);
1440 let (_, mock_lifecycle_rx) = mpsc::channel(1);
1441 LspServer {
1442 client,
1443 capabilities: lsp_types::ServerCapabilities::default(),
1444 position_encoding: PositionEncodingKind::UTF8,
1445 notification_rx: mock_notification_rx,
1446 publish_rx: PublishReader::closed(),
1447 lifecycle_rx: mock_lifecycle_rx,
1448 child: None,
1449 init_config: crate::test_lsp::init_config_for(server_config),
1450 }
1451}
1452
1453#[cfg(test)]
1456#[cfg(unix)]
1457pub fn fake_lsp_server_with_dead_loop_and_live_child() -> LspServer {
1458 let child = tokio::process::Command::new("sleep")
1459 .arg("30")
1460 .kill_on_drop(true)
1461 .spawn()
1462 .unwrap_or_else(|e| panic!("failed to spawn sleep: {e}"));
1463 let mut server = fake_lsp_server();
1464 server.child = Some(ServerProcess::from_unbound(child));
1465 server
1466}
1467
1468#[cfg(test)]
1469impl LspServer {
1470 pub(crate) fn new_for_test(capabilities: ServerCapabilities) -> Self {
1482 Self::new_for_test_with_encoding(capabilities, PositionEncodingKind::UTF16)
1483 }
1484
1485 pub(crate) fn new_for_test_with_encoding(
1490 capabilities: ServerCapabilities,
1491 position_encoding: PositionEncodingKind,
1492 ) -> Self {
1493 let client = LspClient::new(LspServerConfig::rust_analyzer());
1494 let (_, notification_rx) = mpsc::channel(1);
1495 let (_, lifecycle_rx) = mpsc::channel(1);
1496
1497 Self {
1498 client,
1499 capabilities,
1500 position_encoding,
1501 notification_rx,
1502 publish_rx: PublishReader::closed(),
1503 lifecycle_rx,
1504 child: None,
1505 init_config: crate::test_lsp::init_config_for(LspServerConfig::rust_analyzer()),
1506 }
1507 }
1508}
1509
1510#[cfg(test)]
1511mod tests {
1512 use std::assert_matches;
1513 use std::collections::HashMap;
1514
1515 use super::*;
1516 use crate::bridge::PositionEncoding;
1517 use crate::config::{FilePattern, LanguageId, TimeoutSecs, ToolSet};
1518 use crate::lsp::HostOs;
1519
1520 #[test]
1521 fn test_client_capabilities_offer_configured_encodings_in_order() {
1522 let encodings =
1523 PositionEncodings::new(vec![PositionEncoding::Utf32, PositionEncoding::Utf8]).unwrap();
1524 let general = LspServer::client_capabilities(&encodings, None)
1525 .general
1526 .unwrap();
1527 assert_eq!(
1528 general.position_encodings,
1529 Some(vec![
1530 PositionEncodingKind::UTF32,
1531 PositionEncodingKind::UTF8
1532 ])
1533 );
1534 }
1535
1536 #[test]
1542 fn test_client_capabilities_advertises_work_done_progress() {
1543 let capabilities = LspServer::client_capabilities(&PositionEncodings::DEFAULT, None);
1544
1545 assert_eq!(
1546 capabilities.window.and_then(|w| w.work_done_progress),
1547 Some(true)
1548 );
1549 }
1550
1551 #[test]
1555 fn test_client_capabilities_advertises_prepare_rename_support() {
1556 let rename = LspServer::client_capabilities(&PositionEncodings::DEFAULT, None)
1557 .text_document
1558 .and_then(|t| t.rename)
1559 .unwrap();
1560
1561 assert_eq!(rename.prepare_support, Some(true));
1562 assert_eq!(
1563 rename.prepare_support_default_behavior,
1564 Some(lsp_types::PrepareSupportDefaultBehavior::Identifier)
1565 );
1566 }
1567
1568 #[test]
1569 fn test_client_capabilities_advertises_folding_range_with_columns_and_collapsed_text() {
1570 let folding = LspServer::client_capabilities(&PositionEncodings::DEFAULT, None)
1571 .text_document
1572 .and_then(|t| t.folding_range)
1573 .unwrap();
1574
1575 assert_eq!(folding.line_folding_only, Some(false));
1576 assert_eq!(
1577 folding
1578 .folding_range
1579 .and_then(|options| options.collapsed_text),
1580 Some(true)
1581 );
1582 }
1583
1584 #[test]
1585 fn test_client_capabilities_advertises_selection_range() {
1586 let capabilities = LspServer::client_capabilities(&PositionEncodings::DEFAULT, None)
1587 .text_document
1588 .and_then(|t| t.selection_range)
1589 .unwrap();
1590
1591 assert_eq!(capabilities.dynamic_registration, Some(false));
1592 }
1593
1594 #[test]
1595 fn test_client_capabilities_advertises_publish_diagnostics_with_version_support_only() {
1596 let capabilities = LspServer::client_capabilities(&PositionEncodings::DEFAULT, None)
1597 .text_document
1598 .and_then(|t| t.publish_diagnostics)
1599 .unwrap();
1600
1601 assert_eq!(
1602 capabilities,
1603 lsp_types::PublishDiagnosticsClientCapabilities {
1604 version_support: Some(true),
1605 ..Default::default()
1606 }
1607 );
1608 }
1609
1610 #[test]
1611 fn test_client_capabilities_do_not_advertise_pull_diagnostics() {
1612 let text_document = LspServer::client_capabilities(&PositionEncodings::DEFAULT, None)
1613 .text_document
1614 .unwrap();
1615
1616 assert!(text_document.diagnostic.is_none());
1617 }
1618
1619 #[test]
1620 fn test_server_state_ready() {
1621 assert!(ServerState::Ready.is_ready());
1622 assert!(ServerState::Ready.can_accept_requests());
1623 }
1624
1625 #[test]
1626 fn test_server_state_uninitialized() {
1627 assert!(!ServerState::Uninitialized.is_ready());
1628 assert!(!ServerState::Uninitialized.can_accept_requests());
1629 }
1630
1631 #[test]
1632 fn test_server_state_initializing() {
1633 assert!(!ServerState::Initializing.is_ready());
1634 assert!(!ServerState::Initializing.can_accept_requests());
1635 }
1636
1637 #[test]
1638 fn test_workspace_folder_encodes_fragment_char() {
1639 #[cfg(windows)]
1642 let (root, expected) = (
1643 Path::new(r"C:\home\me\dev\#work"),
1644 "file:///C:/home/me/dev/%23work",
1645 );
1646 #[cfg(not(windows))]
1647 let (root, expected) = (
1648 Path::new("/home/me/dev/#work"),
1649 "file:///home/me/dev/%23work",
1650 );
1651
1652 let folder = workspace_folder(root).unwrap();
1653
1654 assert_eq!(folder.uri.as_ref(), expected);
1655 assert_eq!(folder.name, "#work");
1656 }
1657
1658 #[test]
1659 fn test_workspace_folder_encodes_bracket_chars() {
1660 #[cfg(windows)]
1661 let (root, expected) = (
1662 Path::new(r"C:\home\me\dev\[env]"),
1663 "file:///C:/home/me/dev/%5Benv%5D",
1664 );
1665 #[cfg(not(windows))]
1666 let (root, expected) = (
1667 Path::new("/home/me/dev/[env]"),
1668 "file:///home/me/dev/%5Benv%5D",
1669 );
1670
1671 let folder = workspace_folder(root).unwrap();
1672
1673 assert_eq!(folder.uri.as_ref(), expected);
1674 assert_eq!(folder.name, "[env]");
1675 }
1676
1677 #[test]
1678 fn test_workspace_folder_rejects_relative_root() {
1679 let err = workspace_folder(Path::new("relative/root")).unwrap_err();
1680 assert_matches!(err, Error::PathToUri(_), "got {err:?}");
1681 }
1682
1683 #[test]
1684 fn test_server_state_shutting_down() {
1685 assert!(!ServerState::ShuttingDown.is_ready());
1686 assert!(!ServerState::ShuttingDown.can_accept_requests());
1687 }
1688
1689 #[test]
1690 fn test_server_state_shutdown() {
1691 assert!(!ServerState::Shutdown.is_ready());
1692 assert!(!ServerState::Shutdown.can_accept_requests());
1693 }
1694
1695 #[test]
1696 fn test_server_state_equality() {
1697 assert_eq!(ServerState::Ready, ServerState::Ready);
1698 assert_ne!(ServerState::Ready, ServerState::Uninitialized);
1699 assert_eq!(ServerState::Shutdown, ServerState::Shutdown);
1700 }
1701
1702 #[test]
1703 fn test_server_state_clone() {
1704 let state = ServerState::Ready;
1705 let cloned = state;
1706 assert_eq!(state, cloned);
1707 }
1708
1709 #[test]
1710 fn test_server_state_debug() {
1711 let state = ServerState::Ready;
1712 let debug_str = format!("{state:?}");
1713 assert!(debug_str.contains("Ready"));
1714 }
1715
1716 #[test]
1717 fn test_server_init_config_clone() {
1718 let config = ServerInitConfig::new(
1719 {
1720 let mut c = LspServerConfig::rust_analyzer();
1721 c.initialization_options = Some(serde_json::json!({"key": "value"}));
1722 c
1723 },
1724 WorkspaceRoots::for_test(vec![PathBuf::from("/tmp/workspace")], vec![]),
1725 PositionEncodings::DEFAULT,
1726 std::sync::Arc::default(),
1727 );
1728
1729 #[allow(clippy::redundant_clone, reason = "the test exercises `Clone` itself")]
1730 let cloned = config.clone();
1731 assert_eq!(cloned.server_config().language_id, "rust");
1732 assert_eq!(cloned.workspace_roots().canonical().len(), 1);
1733 }
1734
1735 #[test]
1736 fn test_server_init_config_debug() {
1737 let config = ServerInitConfig::new(
1738 LspServerConfig::pyright(),
1739 WorkspaceRoots::default(),
1740 PositionEncodings::DEFAULT,
1741 std::sync::Arc::default(),
1742 );
1743
1744 let debug_str = format!("{config:?}");
1745 assert!(debug_str.contains("python"));
1746 assert!(debug_str.contains("pyright"));
1747 }
1748
1749 #[test]
1750 fn test_server_init_config_with_options() {
1751 let init_opts = serde_json::json!({
1752 "settings": {
1753 "python": {
1754 "analysis": {
1755 "typeCheckingMode": "strict"
1756 }
1757 }
1758 }
1759 });
1760
1761 let mut env = crate::config::ServerEnv::default();
1762 env.insert(
1763 crate::config::EnvKey::from_static("PYTHONPATH"),
1764 "/usr/lib".to_string(),
1765 );
1766
1767 let config = ServerInitConfig::new(
1768 LspServerConfig {
1769 language_id: LanguageId::from_static("python"),
1770 command: ServerCommand::from_static("pyright-langserver").into(),
1771 args: vec!["--stdio".to_string()],
1772 env,
1773 file_patterns: vec![FilePattern::from_static("**/*.py")],
1774 initialization_options: Some(init_opts),
1775 settings: None,
1776 timeout_seconds: TimeoutSecs::new(10).unwrap(),
1777 request_timeout_seconds: TimeoutSecs::new(10).unwrap(),
1778 heuristics: None,
1779 name: None,
1780 handles: None,
1781 indexing: crate::bridge::IndexingPolicy::Auto,
1782 },
1783 WorkspaceRoots::for_test(vec![PathBuf::from("/workspace")], vec![]),
1784 PositionEncodings::DEFAULT,
1785 std::sync::Arc::default(),
1786 );
1787
1788 assert!(config.server_config().initialization_options.is_some());
1789 assert_eq!(config.workspace_roots().canonical().len(), 1);
1790 }
1791
1792 #[test]
1793 fn test_server_init_config_empty_workspace() {
1794 let config = ServerInitConfig::new(
1795 LspServerConfig::typescript(),
1796 WorkspaceRoots::default(),
1797 PositionEncodings::DEFAULT,
1798 std::sync::Arc::default(),
1799 );
1800
1801 assert_eq!(config.workspace_roots().canonical().len(), 0);
1802 }
1803
1804 #[test]
1805 fn test_server_init_config_multiple_workspaces() {
1806 let config = ServerInitConfig::new(
1807 LspServerConfig::rust_analyzer(),
1808 WorkspaceRoots::for_test(
1809 vec![
1810 PathBuf::from("/workspace1"),
1811 PathBuf::from("/workspace2"),
1812 PathBuf::from("/workspace3"),
1813 ],
1814 vec![],
1815 ),
1816 PositionEncodings::DEFAULT,
1817 std::sync::Arc::default(),
1818 );
1819
1820 assert_eq!(config.workspace_roots().canonical().len(), 3);
1821 }
1822
1823 #[cfg(unix)]
1830 #[tokio::test]
1831 async fn test_has_exited_reflects_child_process_state() {
1832 use lsp_types::ServerCapabilities;
1833
1834 let mut mock_child = tokio::process::Command::new("sleep")
1835 .arg("2")
1836 .stdin(Stdio::piped())
1837 .stdout(Stdio::piped())
1838 .kill_on_drop(true)
1839 .spawn()
1840 .unwrap();
1841
1842 let mock_stdin = mock_child.stdin.take().unwrap();
1843 let mock_stdout = mock_child.stdout.take().unwrap();
1844
1845 let transport = LspTransport::new(mock_stdin, mock_stdout);
1846 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
1847 let (_, mock_notification_rx) = mpsc::channel(1);
1848 let (_, mock_lifecycle_rx) = mpsc::channel(1);
1849
1850 let mut server = LspServer {
1851 client,
1852 capabilities: ServerCapabilities::default(),
1853 position_encoding: PositionEncodingKind::UTF8,
1854 notification_rx: mock_notification_rx,
1855 publish_rx: PublishReader::closed(),
1856 lifecycle_rx: mock_lifecycle_rx,
1857 child: Some(ServerProcess::from_unbound(mock_child)),
1858 init_config: crate::test_lsp::init_config_for(LspServerConfig::rust_analyzer()),
1859 };
1860
1861 assert!(
1862 !server.has_exited().unwrap(),
1863 "freshly spawned `sleep 2` should still be running"
1864 );
1865
1866 server.child.as_mut().unwrap().kill().await.unwrap();
1867 assert!(
1870 server.has_exited().unwrap(),
1871 "killed child must report as exited"
1872 );
1873 }
1874
1875 #[cfg(unix)]
1877 #[tokio::test]
1878 async fn test_is_dead_when_message_loop_finished_but_child_alive() {
1879 let mut server = fake_lsp_server_with_dead_loop_and_live_child();
1880
1881 tokio::time::timeout(Duration::from_secs(2), async {
1882 while !server.client.is_message_loop_finished() {
1883 tokio::time::sleep(Duration::from_millis(10)).await;
1884 }
1885 })
1886 .await
1887 .unwrap();
1888
1889 assert!(!server.has_exited().unwrap());
1890 assert!(server.is_dead().unwrap());
1891 }
1892
1893 #[tokio::test(start_paused = true)]
1897 async fn test_shutdown_reports_handshake_timeout_within_deadline() {
1898 let (client, _fake_server) = crate::test_lsp::fake_lsp_client();
1899 let probe = client.clone();
1900 let (_, notification_rx) = mpsc::channel(1);
1901 let (_, lifecycle_rx) = mpsc::channel(1);
1902 let server = LspServer {
1903 client,
1904 capabilities: lsp_types::ServerCapabilities::default(),
1905 position_encoding: PositionEncodingKind::UTF8,
1906 notification_rx,
1907 publish_rx: PublishReader::closed(),
1908 lifecycle_rx,
1909 child: None,
1910 init_config: crate::test_lsp::init_config_for(LspServerConfig::rust_analyzer()),
1911 };
1912
1913 let started = Instant::now();
1914 let result = server.shutdown().await;
1915
1916 assert_matches!(result, Err(Error::Timeout(_)), "got {result:?}");
1917 assert!(started.elapsed() <= SHUTDOWN_TIMEOUT);
1918 assert_matches!(probe.state().await, ServerState::Shutdown);
1919 }
1920
1921 #[tokio::test]
1922 async fn test_is_dead_false_for_childless_fixture() {
1923 let mut server = fake_lsp_server();
1924 tokio::time::sleep(Duration::from_millis(50)).await;
1925 assert!(!server.is_dead().unwrap());
1926 }
1927
1928 #[tokio::test]
1929 async fn test_lsp_server_getters() {
1930 use lsp_types::ServerCapabilities;
1931
1932 let transport = crate::test_lsp::inert_transport();
1933 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
1934 let (_, mock_notification_rx) = mpsc::channel(1);
1935 let (_, mock_lifecycle_rx) = mpsc::channel(1);
1936
1937 let server = LspServer {
1938 client,
1939 capabilities: ServerCapabilities::default(),
1940 position_encoding: PositionEncodingKind::UTF8,
1941 notification_rx: mock_notification_rx,
1942 publish_rx: PublishReader::closed(),
1943 lifecycle_rx: mock_lifecycle_rx,
1944 child: None,
1945 init_config: crate::test_lsp::init_config_for(LspServerConfig::rust_analyzer()),
1946 };
1947
1948 assert_eq!(server.position_encoding(), PositionEncodingKind::UTF8);
1949 assert!(server.capabilities().text_document_sync.is_none());
1950
1951 let debug_str = format!("{server:?}");
1952 assert!(debug_str.contains("LspServer"));
1953 assert!(debug_str.contains("<process>"));
1954 }
1955
1956 #[test]
1957 fn test_spawn_error_classifies_not_found() {
1958 use std::io::{Error as IoError, ErrorKind};
1959
1960 let missing = spawn_error(
1961 ServerCommand::from_static("x"),
1962 IoError::from(ErrorKind::NotFound),
1963 );
1964 assert_matches!(missing, Error::ServerNotFound { .. });
1965 let denied = spawn_error(
1966 ServerCommand::from_static("x"),
1967 IoError::from(ErrorKind::PermissionDenied),
1968 );
1969 assert_matches!(denied, Error::ServerSpawnFailed { .. });
1970 }
1971
1972 #[tokio::test]
1973 async fn test_spawn_nonexistent_command_is_server_not_found() {
1974 let mut server_config = LspServerConfig::rust_analyzer();
1975 server_config.command = ServerCommand::from_static("nonexistent-lsp-cmd-xyz").into();
1976 let config = ServerInitConfig::new(
1977 server_config,
1978 WorkspaceRoots::default(),
1979 PositionEncodings::DEFAULT,
1980 std::sync::Arc::default(),
1981 );
1982 let err = LspServer::spawn(config).await.unwrap_err();
1983 assert_matches!(err, Error::ServerNotFound { .. }, "got {err:?}");
1984 }
1985
1986 #[tokio::test]
1987 async fn test_is_connection_loss_excludes_server_replies() {
1988 let join_error = tokio::spawn(async { panic!("receiver died") })
1989 .await
1990 .unwrap_err();
1991 assert!(is_connection_loss(&Error::ServerTerminated));
1992 assert!(is_connection_loss(&Error::TaskFailed {
1993 task: BackgroundTask::LspReceiver,
1994 source: join_error,
1995 }));
1996 assert!(!is_connection_loss(&Error::StdioCapture(
1997 StdioStream::Stdin
1998 )));
1999 assert!(!is_connection_loss(&Error::LspServerError {
2000 code: -32603,
2001 message: "bad".to_string(),
2002 data: None,
2003 }));
2004 }
2005
2006 #[cfg(unix)]
2009 #[tokio::test]
2010 async fn test_spawn_keeps_servers_own_initialize_error_when_it_exits_afterwards() {
2011 let dir = tempfile::TempDir::new().unwrap();
2012 let config = crate::test_lsp::sh_script_init_config(
2013 dir.path(),
2014 &crate::test_lsp::with_read_preamble(
2015 r#"body='{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"rejected by server"}}'
2016printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
2017"#,
2018 ),
2019 );
2020
2021 let err = LspServer::spawn(config).await.unwrap_err();
2022
2023 assert_matches!(
2024 &err,
2025 Error::LspInitFailed { phase: InitPhase::Initialize, cause, .. }
2026 if matches!(**cause, Error::LspServerError { code: -32603, .. }),
2027 "got {err:?}"
2028 );
2029 assert!(err.to_string().contains("rejected by server"), "{err}");
2030 }
2031
2032 #[cfg(unix)]
2035 #[tokio::test]
2036 async fn test_spawn_connection_loss_with_live_child_is_lsp_init_failed() {
2037 let dir = tempfile::TempDir::new().unwrap();
2038 let config = crate::test_lsp::sh_script_init_config(dir.path(), "exec 1>&-\nsleep 5\n");
2039
2040 let err = LspServer::spawn(config).await.unwrap_err();
2041
2042 assert_matches!(
2043 &err,
2044 Error::LspInitFailed {
2045 phase: InitPhase::Initialize,
2046 ..
2047 },
2048 "got {err:?}"
2049 );
2050 assert!(
2051 err.to_string().contains("Initialize request failed"),
2052 "{err}"
2053 );
2054 }
2055
2056 #[cfg(unix)]
2057 #[tokio::test]
2058 async fn test_spawn_exiting_before_initialize_reply_is_server_exited_during_init() {
2059 let dir = tempfile::TempDir::new().unwrap();
2060 let config = crate::test_lsp::sh_script_init_config(dir.path(), "exit 1\n");
2061
2062 let err = LspServer::spawn(config).await.unwrap_err();
2063
2064 assert_matches!(
2065 err,
2066 Error::ServerExitedDuringInit {
2067 exit_code: Some(1),
2068 ..
2069 },
2070 "got {err:?}"
2071 );
2072 }
2073
2074 #[cfg(unix)]
2075 #[test]
2076 fn test_unbound_server_gets_the_real_process_id() {
2077 assert_eq!(
2078 initialize_process_id(Binding::Unbound),
2079 Some(i32::try_from(std::process::id()).unwrap())
2080 );
2081 }
2082
2083 #[cfg(unix)]
2084 #[test]
2085 fn test_bound_server_gets_no_process_id() {
2086 assert_eq!(initialize_process_id(Binding::Bound), None);
2087 }
2088
2089 #[cfg(unix)]
2092 #[tokio::test]
2093 async fn test_spawn_sends_null_process_id_while_a_lifeline_is_bound() {
2094 let dir = tempfile::TempDir::new().unwrap();
2095 let dump = dir.path().join("request");
2096 let script = format!(
2097 "read -r header\nread -r _\nhead -c \"$(printf %s \"$header\" | tr -dc 0-9)\" > '{}'\nexit 1\n",
2098 dump.display()
2099 );
2100 let config = crate::test_lsp::sh_script_init_config(dir.path(), &script);
2101
2102 LspServer::spawn(config).await.unwrap_err();
2103
2104 let request = std::fs::read_to_string(&dump).unwrap();
2105 assert!(request.contains(r#""processId":null"#), "got {request}");
2106 }
2107
2108 #[cfg(unix)]
2110 #[tokio::test]
2111 async fn test_spawn_exit_during_init_carries_stderr() {
2112 let dir = tempfile::TempDir::new().unwrap();
2113 let config = crate::test_lsp::sh_script_init_config(
2114 dir.path(),
2115 "echo 'fatal: bad toolchain' >&2\nexit 1\n",
2116 );
2117
2118 let err = LspServer::spawn(config).await.unwrap_err();
2119
2120 let Error::ServerExitedDuringInit {
2121 stderr: Some(stderr),
2122 ..
2123 } = &err
2124 else {
2125 panic!("got {err:?}");
2126 };
2127 assert_eq!(stderr.head(), "fatal: bad toolchain");
2128 assert!(err.to_string().contains("stderr: fatal: bad toolchain"));
2129 }
2130
2131 #[cfg(unix)]
2134 #[tokio::test]
2135 async fn test_spawn_rejection_keeps_stderr_written_just_before_exit() {
2136 let dir = tempfile::TempDir::new().unwrap();
2137 let config = crate::test_lsp::sh_script_init_config(
2138 dir.path(),
2139 &crate::test_lsp::with_read_preamble(
2140 r#"body='{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"rejected by server"}}'
2141printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
2142# Shell-only pause: an external `sleep` can take longer than the 100 ms EOF grace just to start on a loaded macOS runner.
2143i=0; while [ $i -lt 300 ]; do i=$((i+1)); done
2144echo 'fatal: bad toolchain' >&2
2145"#,
2146 ),
2147 );
2148
2149 let err = LspServer::spawn(config).await.unwrap_err();
2150
2151 let Error::LspInitFailed {
2152 stderr: Some(stderr),
2153 ..
2154 } = &err
2155 else {
2156 panic!("got {err:?}");
2157 };
2158 assert_eq!(stderr.head(), "fatal: bad toolchain");
2159 }
2160
2161 #[cfg(unix)]
2164 #[tokio::test]
2165 async fn test_spawn_initialize_timeout_carries_stderr() {
2166 let dir = tempfile::TempDir::new().unwrap();
2167 let mut config = crate::test_lsp::sh_script_init_config(
2168 dir.path(),
2169 "echo 'indexing forever' >&2\nsleep 5\n",
2170 );
2171 config.server_config.timeout_seconds = TimeoutSecs::new(1).unwrap();
2172
2173 let err = LspServer::spawn(config).await.unwrap_err();
2174
2175 let Error::LspInitFailed {
2176 stderr: Some(stderr),
2177 ..
2178 } = &err
2179 else {
2180 panic!("got {err:?}");
2181 };
2182 assert_eq!(stderr.head(), "indexing forever");
2183 }
2184
2185 #[cfg(unix)]
2189 #[tokio::test]
2190 async fn test_spawn_hides_another_servers_secret_from_stderr() {
2191 let dir = tempfile::TempDir::new().unwrap();
2192 let mut config = crate::test_lsp::sh_script_init_config(
2193 dir.path(),
2194 "echo \"seen=$OTHER_VALUE own=$API_TOKEN\" >&2\nexit 1\n",
2195 );
2196 config.server_config.env.insert(
2197 crate::config::EnvKey::from_static("OTHER_VALUE"),
2198 "bravo-secret-222".to_string(),
2199 );
2200 config.server_config.env.insert(
2201 crate::config::EnvKey::from_static("API_TOKEN"),
2202 "s3cr3t-value".to_string(),
2203 );
2204 config.redactions = std::sync::Arc::new(Redactions::new([(
2205 "B_TOKEN".to_owned(),
2206 "bravo-secret-222".to_owned(),
2207 )]));
2208
2209 let err = LspServer::spawn(config).await.unwrap_err();
2210
2211 let text = err.to_string();
2212 assert!(text.contains("seen=[redacted:B_TOKEN]"), "{text}");
2213 assert!(text.contains("own=[redacted:API_TOKEN]"), "{text}");
2214 }
2215
2216 #[cfg(unix)]
2218 #[tokio::test]
2219 async fn test_spawn_stderr_redacts_configured_env_values() {
2220 let dir = tempfile::TempDir::new().unwrap();
2221 let mut config = crate::test_lsp::sh_script_init_config(
2222 dir.path(),
2223 "echo \"token=$API_TOKEN toolchain=$RUSTUP_TOOLCHAIN\" >&2\nexit 1\n",
2224 );
2225 config.server_config.env.insert(
2226 crate::config::EnvKey::from_static("RUSTUP_TOOLCHAIN"),
2227 "nightly-2024-01-01".to_string(),
2228 );
2229 config.server_config.env.insert(
2230 crate::config::EnvKey::from_static("API_TOKEN"),
2231 "s3cr3t-value".to_string(),
2232 );
2233
2234 let err = LspServer::spawn(config).await.unwrap_err();
2235
2236 let text = err.to_string();
2237 assert!(text.contains("token=[redacted:API_TOKEN]"), "{text}");
2238 assert!(text.contains("toolchain=nightly-2024-01-01"), "{text}");
2239 assert!(!text.contains("s3cr3t-value"), "{text}");
2240 }
2241
2242 #[test]
2243 fn test_log_spawn_hides_argument_values() {
2244 use tracing_subscriber::prelude::*;
2245
2246 use crate::test_lsp::CapturedLogs;
2247
2248 let mut config = LspServerConfig::rust_analyzer();
2249 config.args = vec!["--api-key=SuperSecretArg456".to_string()];
2250 let redactions = Redactions::for_server(&config, std::iter::empty());
2251 let logs = CapturedLogs::default();
2252 let info_only = tracing_subscriber::registry()
2253 .with(tracing_subscriber::filter::LevelFilter::INFO)
2254 .with(logs.clone());
2255
2256 tracing::subscriber::with_default(info_only, || {
2257 LspServer::log_spawn(&config, &redactions);
2258 });
2259
2260 let output = logs.messages().join("\n");
2261 assert!(output.contains("(1 arg(s))"), "{output}");
2262 assert!(!output.contains("SuperSecretArg456"), "{output}");
2263 }
2264
2265 #[tokio::test]
2267 async fn contain_attributes_a_panic_to_its_own_server() {
2268 let config = crate::test_lsp::init_config_for(LspServerConfig::rust_analyzer());
2269
2270 let outcome = contain(&config, async { panic!("start boom") }).await;
2271
2272 assert_matches!(
2273 outcome,
2274 ServerStartOutcome::Failed(failure)
2275 if failure.server_id == config.server_config.id()
2276 && matches!(failure.reason, StartupFailure::InitTaskPanicked)
2277 );
2278 }
2279
2280 #[tokio::test]
2281 async fn contain_reports_a_spawn_error_as_that_servers_failure() {
2282 let config = crate::test_lsp::init_config_for(LspServerConfig::rust_analyzer());
2283
2284 let outcome = contain(&config, async { Err(Error::ServerTerminated) }).await;
2285
2286 assert_matches!(
2287 outcome,
2288 ServerStartOutcome::Failed(failure) if matches!(failure.reason, StartupFailure::Spawn(_))
2289 );
2290 }
2291
2292 #[cfg(unix)]
2295 #[tokio::test]
2296 async fn test_spawn_redacts_secrets_in_server_log_and_show_messages() {
2297 let dir = tempfile::TempDir::new().unwrap();
2298 let script = r#"body='{"jsonrpc":"2.0","id":1,"result":{"capabilities":{}}}'
2299printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
2300body="{\"jsonrpc\":\"2.0\",\"method\":\"window/logMessage\",\"params\":{\"type\":3,\"message\":\"env=$API_TOKEN arg=$1\"}}"
2301printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
2302body="{\"jsonrpc\":\"2.0\",\"method\":\"window/showMessage\",\"params\":{\"type\":3,\"message\":\"env=$API_TOKEN\"}}"
2303printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
2304sleep 5
2305"#;
2306 let mut config = crate::test_lsp::sh_script_init_config(
2307 dir.path(),
2308 &crate::test_lsp::with_read_preamble(script),
2309 );
2310 config
2311 .server_config
2312 .args
2313 .push("--api-key=SuperSecretArg456".to_string());
2314 config.server_config.env.insert(
2315 crate::config::EnvKey::from_static("API_TOKEN"),
2316 "SuperSecretValue123".to_string(),
2317 );
2318
2319 let mut server = LspServer::spawn(config).await.unwrap();
2320 let mut rx = server.take_notification_rx();
2321 let mut texts = Vec::new();
2322 for _ in 0..2 {
2323 let notification = tokio::time::timeout(Duration::from_secs(5), rx.recv())
2324 .await
2325 .unwrap()
2326 .unwrap();
2327 texts.push(format!("{notification:?}"));
2328 }
2329
2330 let joined = texts.join("\n");
2331 assert!(!joined.contains("SuperSecretValue123"), "{joined}");
2332 assert!(!joined.contains("SuperSecretArg456"), "{joined}");
2333 assert!(joined.contains("[redacted:API_TOKEN]"), "{joined}");
2334 assert!(joined.contains("[redacted:api-key]"), "{joined}");
2335 }
2336
2337 #[cfg(unix)]
2340 #[tokio::test]
2341 async fn test_spawn_redacts_secrets_in_initialize_decode_error() {
2342 let dir = tempfile::TempDir::new().unwrap();
2343 let script = r#"body='{"jsonrpc":"2.0","id":1,"result":{"capabilities":"SuperSecretValue123"}}'
2344printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
2345sleep 5
2346"#;
2347 let mut config = crate::test_lsp::sh_script_init_config(
2348 dir.path(),
2349 &crate::test_lsp::with_read_preamble(script),
2350 );
2351 config.server_config.env.insert(
2352 crate::config::EnvKey::from_static("API_TOKEN"),
2353 "SuperSecretValue123".to_string(),
2354 );
2355
2356 let err = LspServer::spawn(config).await.unwrap_err();
2357
2358 let text = err.to_string();
2359 assert!(!text.contains("SuperSecretValue123"), "{text}");
2360 }
2361
2362 #[cfg(unix)]
2364 #[tokio::test]
2365 async fn test_spawn_redacts_secrets_in_initialize_error_message() {
2366 let dir = tempfile::TempDir::new().unwrap();
2367 let script = r#"body="{\"jsonrpc\":\"2.0\",\"id\":1,\"error\":{\"code\":-32603,\"message\":\"bad token $API_TOKEN\"}}"
2368printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
2369sleep 5
2370"#;
2371 let mut config = crate::test_lsp::sh_script_init_config(
2372 dir.path(),
2373 &crate::test_lsp::with_read_preamble(script),
2374 );
2375 config.server_config.env.insert(
2376 crate::config::EnvKey::from_static("API_TOKEN"),
2377 "SuperSecretValue123".to_string(),
2378 );
2379
2380 let err = LspServer::spawn(config).await.unwrap_err();
2381
2382 let text = err.to_string();
2383 assert!(text.contains("[redacted:API_TOKEN]"), "{text}");
2384 assert!(!text.contains("SuperSecretValue123"), "{text}");
2385 }
2386
2387 #[cfg(unix)]
2391 #[tokio::test]
2392 async fn test_spawn_keeps_draining_stderr_after_initialize() {
2393 let dir = tempfile::TempDir::new().unwrap();
2394 let marker = dir.path().join("flooded");
2395 let script = format!(
2396 r#"head -c 200000 /dev/zero | tr '\0' 'e' >&2
2397body='{{"jsonrpc":"2.0","id":1,"result":{{"capabilities":{{}}}}}}'
2398printf 'Content-Length: %d\r\n\r\n%s' ${{#body}} "$body"
2399sleep 0.3
2400head -c 200000 /dev/zero | tr '\0' 'e' >&2 && echo done > '{}'
2401sleep 5
2402"#,
2403 marker.display()
2404 );
2405 let config = crate::test_lsp::sh_script_init_config(
2406 dir.path(),
2407 &crate::test_lsp::with_read_preamble(&script),
2408 );
2409
2410 let mut server = LspServer::spawn(config).await.unwrap();
2411
2412 let flooded = tokio::time::timeout(Duration::from_secs(5), async {
2413 while !marker.exists() {
2414 tokio::time::sleep(Duration::from_millis(20)).await;
2415 }
2416 })
2417 .await;
2418 assert!(
2419 flooded.is_ok(),
2420 "server's post-initialize stderr flood did not complete"
2421 );
2422 assert!(!server.has_exited().unwrap());
2423 }
2424
2425 mod initialize_wire {
2431
2432 use tempfile::TempDir;
2433 use tokio::io::BufReader;
2434
2435 use super::*;
2436 use crate::test_lsp::{
2437 fake_lsp_client, read_framed_message, write_response as write_success_response,
2438 };
2439
2440 #[tokio::test]
2441 async fn test_initialize_sends_configured_position_encodings() {
2442 let (client, mut server) = fake_lsp_client();
2443
2444 let config = ServerInitConfig::new(
2445 LspServerConfig::rust_analyzer(),
2446 WorkspaceRoots::default(),
2447 PositionEncodings::new(vec![PositionEncoding::Utf32, PositionEncoding::Utf8])
2448 .unwrap(),
2449 std::sync::Arc::default(),
2450 );
2451
2452 let init_task =
2453 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
2454
2455 let mut reader = BufReader::new(&mut server.write_stdout);
2456 let request = read_framed_message(&mut reader).await;
2457
2458 assert_eq!(request["method"], "initialize");
2459 assert_eq!(
2460 request["params"]["capabilities"]["general"]["positionEncodings"],
2461 serde_json::json!(["utf-32", "utf-8"]),
2462 "initialize request must carry the configured encoding order, not the \
2463 hardcoded [UTF8, UTF16] default"
2464 );
2465
2466 write_success_response(
2467 &mut server.read_half_stdin,
2468 &request["id"].clone(),
2469 serde_json::json!({ "capabilities": {} }),
2470 )
2471 .await;
2472
2473 init_task.await.unwrap().unwrap();
2475 }
2476
2477 struct Handshake {
2480 initialize: serde_json::Value,
2481 initialized: serde_json::Value,
2482 configuration: serde_json::Value,
2483 }
2484
2485 async fn handshake_with_settings(settings_json: Option<&str>) -> Handshake {
2486 let (client, mut server) = fake_lsp_client();
2487 let mut server_config = LspServerConfig::rust_analyzer();
2488 server_config.settings = settings_json.map(|json| serde_json::from_str(json).unwrap());
2489 let config = crate::test_lsp::init_config_for(server_config);
2490 let init_task =
2491 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
2492
2493 let mut reader = BufReader::new(&mut server.write_stdout);
2494 let initialize = read_framed_message(&mut reader).await;
2495 assert_eq!(initialize["method"], "initialize");
2496 write_success_response(
2497 &mut server.read_half_stdin,
2498 &initialize["id"].clone(),
2499 serde_json::json!({ "capabilities": {} }),
2500 )
2501 .await;
2502 let initialized = read_framed_message(&mut reader).await;
2503 let configuration = read_framed_message(&mut reader).await;
2504 init_task.await.unwrap().unwrap();
2505 Handshake {
2506 initialize,
2507 initialized,
2508 configuration,
2509 }
2510 }
2511
2512 #[tokio::test]
2513 async fn test_initialize_pushes_null_settings_right_after_initialized() {
2514 let wire = handshake_with_settings(None).await;
2515
2516 assert_eq!(wire.initialized["method"], "initialized");
2517 assert_eq!(
2518 wire.configuration["method"],
2519 "workspace/didChangeConfiguration"
2520 );
2521 assert_eq!(
2522 wire.configuration["params"],
2523 serde_json::json!({ "settings": null })
2524 );
2525 }
2526
2527 #[tokio::test]
2528 async fn test_initialize_pushes_configured_settings_and_advertises_configuration() {
2529 let wire =
2530 handshake_with_settings(Some(r#"{"python.analysis.typeCheckingMode": "strict"}"#))
2531 .await;
2532
2533 assert_eq!(
2534 wire.initialize["params"]["capabilities"]["workspace"]["configuration"],
2535 serde_json::json!(true)
2536 );
2537 assert_eq!(
2538 wire.configuration["params"],
2539 serde_json::json!({
2540 "settings": { "python": { "analysis": { "typeCheckingMode": "strict" } } }
2541 })
2542 );
2543 }
2544
2545 #[tokio::test]
2546 async fn test_initialize_pushes_gopls_flat_and_yaml_url_keys_on_the_wire() {
2547 let wire = handshake_with_settings(Some(
2548 r#"{"gopls":{"ui.semanticTokens":true},
2549 "yaml.schemas":{"https://json.schemastore.org/x.json":"*.yml"}}"#,
2550 ))
2551 .await;
2552
2553 let settings =
2554 serde_json::to_string(&wire.configuration["params"]["settings"]).unwrap();
2555 assert!(
2556 settings.contains(r#""gopls":{"ui.semanticTokens":true}"#),
2557 "{settings}"
2558 );
2559 assert!(
2560 settings.contains(
2561 r#""yaml":{"schemas":{"https://json.schemastore.org/x.json":"*.yml"}}"#
2562 ),
2563 "{settings}"
2564 );
2565 }
2566
2567 #[test]
2568 fn test_workspace_configuration_not_advertised_without_settings() {
2569 let workspace = LspServer::client_capabilities(&PositionEncodings::DEFAULT, None)
2570 .workspace
2571 .unwrap();
2572 assert_eq!(workspace.configuration, None);
2573 }
2574
2575 #[tokio::test]
2576 async fn test_initialize_advertises_stale_request_support() {
2577 let (client, mut server) = fake_lsp_client();
2578
2579 let config = ServerInitConfig::new(
2580 LspServerConfig::rust_analyzer(),
2581 WorkspaceRoots::default(),
2582 PositionEncodings::DEFAULT,
2583 std::sync::Arc::default(),
2584 );
2585
2586 let init_task =
2587 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
2588
2589 let mut reader = BufReader::new(&mut server.write_stdout);
2590 let request = read_framed_message(&mut reader).await;
2591 let params: InitializeParams =
2592 serde_json::from_value(request["params"].clone()).unwrap();
2593 let stale_request_support = params
2594 .capabilities
2595 .general
2596 .unwrap()
2597 .stale_request_support
2598 .unwrap();
2599
2600 assert_eq!(request["method"], "initialize");
2601 assert!(
2602 !stale_request_support.cancel,
2603 "mcpls does not implement active in-flight request cancellation"
2604 );
2605 assert_eq!(
2606 stale_request_support.retry_on_content_modified,
2607 CONTENT_MODIFIED_RETRY_METHODS
2608 .iter()
2609 .map(ToString::to_string)
2610 .collect::<Vec<_>>(),
2611 "the wire-advertised capability must match the methods LspClient::request \
2612 actually retries -32801 for, not drift from it"
2613 );
2614
2615 write_success_response(
2616 &mut server.read_half_stdin,
2617 &request["id"].clone(),
2618 serde_json::json!({ "capabilities": {} }),
2619 )
2620 .await;
2621
2622 init_task.await.unwrap().unwrap();
2623 }
2624
2625 #[tokio::test]
2626 async fn test_initialize_advertises_server_status_notification_support() {
2627 let (client, mut server) = fake_lsp_client();
2628
2629 let config = ServerInitConfig::new(
2630 LspServerConfig::rust_analyzer(),
2631 WorkspaceRoots::default(),
2632 PositionEncodings::DEFAULT,
2633 std::sync::Arc::default(),
2634 );
2635
2636 let init_task =
2637 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
2638
2639 let mut reader = BufReader::new(&mut server.write_stdout);
2640 let request = read_framed_message(&mut reader).await;
2641
2642 assert_eq!(request["method"], "initialize");
2643 assert_eq!(
2644 request["params"]["capabilities"]["experimental"]["serverStatusNotification"],
2645 serde_json::json!(true),
2646 "without this, rust-analyzer never emits experimental/serverStatus and the \
2647 indexing-readiness gate in Translator::wait_for_indexing_ready is a \
2648 permanent no-op"
2649 );
2650
2651 write_success_response(
2652 &mut server.read_half_stdin,
2653 &request["id"].clone(),
2654 serde_json::json!({ "capabilities": {} }),
2655 )
2656 .await;
2657
2658 init_task.await.unwrap().unwrap();
2659 }
2660
2661 #[tokio::test]
2662 async fn test_initialize_advertises_hierarchical_document_symbols() {
2663 let (client, mut server) = fake_lsp_client();
2664
2665 let config = ServerInitConfig::new(
2666 LspServerConfig::rust_analyzer(),
2667 WorkspaceRoots::default(),
2668 PositionEncodings::DEFAULT,
2669 std::sync::Arc::default(),
2670 );
2671
2672 let init_task =
2673 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
2674
2675 let mut reader = BufReader::new(&mut server.write_stdout);
2676 let request = read_framed_message(&mut reader).await;
2677 let params: InitializeParams =
2678 serde_json::from_value(request["params"].clone()).unwrap();
2679 let document_symbol = params
2680 .capabilities
2681 .text_document
2682 .unwrap()
2683 .document_symbol
2684 .unwrap();
2685
2686 assert_eq!(request["method"], "initialize");
2687 assert_eq!(document_symbol.dynamic_registration, Some(false));
2688 assert_eq!(
2689 document_symbol.hierarchical_document_symbol_support,
2690 Some(true)
2691 );
2692 assert_eq!(
2693 document_symbol.symbol_kind.unwrap().value_set,
2694 Some(advertised_symbol_kinds())
2695 );
2696
2697 write_success_response(
2698 &mut server.read_half_stdin,
2699 &request["id"].clone(),
2700 serde_json::json!({ "capabilities": {} }),
2701 )
2702 .await;
2703
2704 init_task.await.unwrap().unwrap();
2705 }
2706
2707 #[tokio::test]
2708 async fn test_initialize_accepts_resolved_dot_workspace_root() {
2709 let temp_dir = TempDir::new().unwrap();
2710 let base = dunce::canonicalize(temp_dir.path()).unwrap();
2711 let workspace_roots =
2712 crate::bridge::WorkspaceRoots::from_paths_with(&[PathBuf::from(".")], || {
2713 Ok(crate::bridge::ProcessCwd::new(base.clone(), None))
2714 })
2715 .unwrap()
2716 .canonical()
2717 .to_vec();
2718 assert_eq!(workspace_roots, vec![base.clone()]);
2719
2720 let (client, mut server) = fake_lsp_client();
2721 let config = ServerInitConfig::new(
2722 LspServerConfig::rust_analyzer(),
2723 WorkspaceRoots::for_test(workspace_roots, vec![]),
2724 PositionEncodings::DEFAULT,
2725 std::sync::Arc::default(),
2726 );
2727
2728 let init_task =
2729 tokio::spawn(async move { LspServer::initialize(&client, &config).await });
2730
2731 let mut reader = BufReader::new(&mut server.write_stdout);
2732 let request = read_framed_message(&mut reader).await;
2733 let expected_uri = try_path_to_uri(&base).unwrap();
2734 assert_eq!(
2735 request["params"]["workspaceFolders"][0]["uri"],
2736 expected_uri.as_ref()
2737 );
2738
2739 write_success_response(
2740 &mut server.read_half_stdin,
2741 &request["id"].clone(),
2742 serde_json::json!({ "capabilities": {} }),
2743 )
2744 .await;
2745
2746 init_task.await.unwrap().unwrap();
2747 }
2748 }
2749
2750 #[test]
2753 fn child_env_var_compares_names_as_the_host_does() {
2754 let parent = |_: &str| Some(std::ffi::OsString::from("/parent"));
2755 let on = |host| {
2756 let mut config = bare_server_config(HashMap::new());
2757 config.env = crate::config::ServerEnv::from_entries(
2758 [(
2759 crate::config::EnvKey::from_static("Path"),
2760 "/over".to_string(),
2761 )],
2762 host,
2763 )
2764 .unwrap();
2765 child_env_var(&config, "PATH", parent)
2766 };
2767
2768 assert_eq!(on(HostOs::Windows), Some(std::ffi::OsString::from("/over")));
2769 assert_eq!(on(HostOs::Other), Some(std::ffi::OsString::from("/parent")));
2770 }
2771
2772 fn bare_server_config(env: HashMap<crate::config::EnvKey, String>) -> LspServerConfig {
2775 LspServerConfig {
2776 language_id: LanguageId::from_static("test"),
2777 command: ServerCommand::from_static("irrelevant-for-build-command").into(),
2778 args: vec![],
2779 env: crate::config::ServerEnv::from_entries(env, HostOs::CURRENT).unwrap(),
2780 file_patterns: vec![],
2781 initialization_options: None,
2782 settings: None,
2783 timeout_seconds: TimeoutSecs::new(5).unwrap(),
2784 request_timeout_seconds: TimeoutSecs::new(5).unwrap(),
2785 heuristics: None,
2786 name: None,
2787 handles: None,
2788 indexing: crate::bridge::IndexingPolicy::Auto,
2789 }
2790 }
2791
2792 fn effective_envs(command: &Command) -> HashMap<String, String> {
2796 command
2797 .as_std()
2798 .get_envs()
2799 .filter_map(|(k, v)| {
2800 v.map(|v| {
2801 (
2802 k.to_string_lossy().into_owned(),
2803 v.to_string_lossy().into_owned(),
2804 )
2805 })
2806 })
2807 .collect()
2808 }
2809
2810 #[test]
2814 fn test_build_command_excludes_non_allowlisted_parent_env_vars() {
2815 let config = bare_server_config(HashMap::new());
2816 let command =
2817 LspServer::build_command(&config, &ChildWorkingDir::Inherit, |key| match key {
2818 "PATH" => Some("/parent/bin".into()),
2819 "MCPLS_TEST_LEAK_CANARY" => Some("should-not-reach-child".into()),
2820 _ => None,
2821 });
2822
2823 let envs = effective_envs(&command);
2824
2825 assert!(
2826 !envs.contains_key("MCPLS_TEST_LEAK_CANARY"),
2827 "non-allowlisted parent env var leaked into child command: {envs:?}"
2828 );
2829
2830 #[cfg(unix)]
2841 assert!(
2842 format!("{:?}", command.as_std()).starts_with("env -i "),
2843 "build_command must call .env_clear() so the child doesn't inherit the full parent environment"
2844 );
2845 }
2846
2847 #[test]
2850 fn test_build_command_passes_through_allowlisted_env_vars() {
2851 let config = bare_server_config(HashMap::new());
2852 let command = LspServer::build_command(&config, &ChildWorkingDir::Inherit, |key| {
2853 (key == "PATH").then(|| "/parent/bin".into())
2854 });
2855
2856 let envs = effective_envs(&command);
2857
2858 assert_eq!(envs.get("PATH"), Some(&"/parent/bin".to_string()));
2859 }
2860
2861 #[test]
2862 fn test_build_command_inherits_working_dir_unless_fixed() {
2863 let config = bare_server_config(HashMap::new());
2864 let inherited = LspServer::build_command(&config, &ChildWorkingDir::Inherit, |_| None);
2865 assert_eq!(inherited.as_std().get_current_dir(), None);
2866
2867 let dir = PathBuf::from("/fixed/dir");
2868 let fixed =
2869 LspServer::build_command(&config, &ChildWorkingDir::Fixed(dir.clone()), |_| None);
2870 assert_eq!(fixed.as_std().get_current_dir(), Some(dir.as_path()));
2871 }
2872
2873 #[test]
2874 fn test_server_init_config_defaults_to_inherited_working_dir() {
2875 let config = ServerInitConfig::new(
2876 LspServerConfig::rust_analyzer(),
2877 WorkspaceRoots::default(),
2878 PositionEncodings::DEFAULT,
2879 std::sync::Arc::default(),
2880 );
2881 assert_eq!(config.child_working_dir(), &ChildWorkingDir::Inherit);
2882 let fixed = config.with_child_working_dir(ChildWorkingDir::Fixed(PathBuf::from("/x")));
2883 assert_eq!(
2884 fixed.child_working_dir(),
2885 &ChildWorkingDir::Fixed(PathBuf::from("/x"))
2886 );
2887 }
2888
2889 #[tokio::test]
2890 async fn test_for_respawn_keeps_the_config_and_pinned_tsserver() {
2891 let mut server_config = LspServerConfig::typescript();
2892 server_config.initialization_options =
2893 Some(serde_json::json!({"tsserver": {"path": "/pin/tsserver.js"}}));
2894 let config = ServerInitConfig::new(
2895 server_config,
2896 WorkspaceRoots::default(),
2897 PositionEncodings::DEFAULT,
2898 std::sync::Arc::default(),
2899 );
2900 assert_eq!(
2901 config.pinned_tsserver(),
2902 Some(PathBuf::from("/pin/tsserver.js"))
2903 );
2904
2905 let respawn = config.for_respawn().await.unwrap();
2906 assert_eq!(
2907 respawn.server_config().initialization_options,
2908 config.server_config().initialization_options
2909 );
2910 assert_eq!(respawn.pinned_tsserver(), config.pinned_tsserver());
2911 }
2912
2913 #[tokio::test]
2916 async fn test_for_respawn_replans_an_auto_pinned_server_off_the_runtime() {
2917 let mut server_config = LspServerConfig::typescript();
2918 server_config.initialization_options =
2919 Some(serde_json::json!({"tsserver": {"path": "/mcpls-test/stale/tsserver.js"}}));
2920 let config = ServerInitConfig::new(
2921 server_config,
2922 WorkspaceRoots::default(),
2923 PositionEncodings::DEFAULT,
2924 std::sync::Arc::default(),
2925 )
2926 .with_auto_pin(PathBuf::from("/mcpls-test/stale/tsserver.js"), None);
2927
2928 let respawn = config.for_respawn().await.unwrap();
2929
2930 assert_eq!(respawn.server_config().id(), config.server_config().id());
2931 assert_ne!(respawn.pinned_tsserver(), config.pinned_tsserver());
2932 }
2933
2934 #[test]
2937 fn test_build_command_includes_configured_env_vars() {
2938 let mut env = HashMap::new();
2939 env.insert(
2940 crate::config::EnvKey::from_static("MCPLS_TEST_CONFIGURED"),
2941 "from-server-config".to_string(),
2942 );
2943 let config = bare_server_config(env);
2944 let command = LspServer::build_command(&config, &ChildWorkingDir::Inherit, |_| None);
2945
2946 let envs = effective_envs(&command);
2947
2948 assert_eq!(
2949 envs.get("MCPLS_TEST_CONFIGURED"),
2950 Some(&"from-server-config".to_string())
2951 );
2952 }
2953
2954 #[test]
2958 fn test_build_command_configured_env_overrides_allowlisted_var() {
2959 let mut env = HashMap::new();
2960 env.insert(
2961 crate::config::EnvKey::from_static("PATH"),
2962 "/configured/override/path".to_string(),
2963 );
2964 let config = bare_server_config(env);
2965 let command = LspServer::build_command(&config, &ChildWorkingDir::Inherit, |key| {
2966 (key == "PATH").then(|| "/parent/bin".into())
2967 });
2968
2969 let envs = effective_envs(&command);
2970
2971 assert_eq!(
2972 envs.get("PATH"),
2973 Some(&"/configured/override/path".to_string())
2974 );
2975 }
2976
2977 #[tokio::test]
2984 async fn test_settling_dead_diagnostics_server_hands_the_route_to_the_catch_all() {
2985 use crate::bridge::Translator;
2986 use crate::config::{ServerId, ToolKind, ToolRouter};
2987
2988 let pylsp_id = ServerId::from_static("pylsp");
2989 let configs = vec![
2990 LspServerConfig {
2991 language_id: LanguageId::from_static("python"),
2992 command: ServerCommand::from_static("pyright-langserver").into(),
2993 args: vec![],
2994 env: crate::config::ServerEnv::default(),
2995 file_patterns: vec![],
2996 initialization_options: None,
2997 settings: None,
2998 timeout_seconds: TimeoutSecs::new(30).unwrap(),
2999 request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
3000 heuristics: None,
3001 name: Some(ServerId::from_static("pyright-diag")),
3002 handles: Some(ToolSet::new(vec![ToolKind::Diagnostics]).unwrap()),
3003 indexing: crate::bridge::IndexingPolicy::Auto,
3004 },
3005 LspServerConfig {
3006 language_id: LanguageId::from_static("python"),
3007 command: ServerCommand::from_static("pylsp").into(),
3008 args: vec![],
3009 env: crate::config::ServerEnv::default(),
3010 file_patterns: vec![],
3011 initialization_options: None,
3012 settings: None,
3013 timeout_seconds: TimeoutSecs::new(30).unwrap(),
3014 request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
3015 heuristics: None,
3016 name: Some(ServerId::from_static("pylsp")),
3017 handles: None,
3018 indexing: crate::bridge::IndexingPolicy::Auto,
3019 },
3020 ];
3021 let router = ToolRouter::from_configs(&configs).unwrap();
3022 let translator = Translator::new().with_router(router);
3023 translator.set_expected_servers(
3024 [ServerId::from_static("pyright-diag"), pylsp_id.clone()]
3025 .into_iter()
3026 .collect(),
3027 );
3028
3029 translator.settle_failed(&ServerSpawnFailure {
3030 server_id: ServerId::from_static("pyright-diag"),
3031 language_id: LanguageId::from_static("python"),
3032 command: ServerCommand::from_static("pyright-langserver"),
3033 reason: StartupFailure::InitTaskPanicked,
3034 });
3035 translator.settle_started(fake_lsp_server_with_config(configs[1].clone()));
3036
3037 assert!(
3038 translator.is_diagnostics_route(&LanguageId::from_static("python"), &pylsp_id),
3039 "pylsp must inherit the diagnostics route once pyright-diag is known dead"
3040 );
3041 }
3042}