1use crate::config::DepsConfig;
2use crate::document::{
3 CLIENT_REFRESH_TIMEOUT, ServerState, handle_document_change, handle_document_open,
4};
5use crate::file_watcher;
6use crate::handlers::{
7 code_actions, code_lens, completion, diagnostics, document_link, hover, inlay_hints,
8};
9use deps_core::{PackageName, is_safe_version_string};
10use std::collections::HashMap;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13use tower_lsp_server::ls_types::{
14 CodeActionOptions, CodeActionParams, CodeActionProviderCapability, CodeLens, CodeLensOptions,
15 CodeLensParams, CompletionOptions, CompletionOptionsCompletionItem, CompletionParams,
16 CompletionResponse, DiagnosticOptions, DiagnosticServerCapabilities,
17 DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
18 DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentChanges,
19 DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportResult,
20 DocumentLink, DocumentLinkOptions, DocumentLinkParams, ExecuteCommandOptions,
21 ExecuteCommandParams, FullDocumentDiagnosticReport, Hover, HoverParams,
22 HoverProviderCapability, InitializeParams, InitializeResult, InitializedParams, InlayHint,
23 InlayHintParams, MessageType, OneOf, OptionalVersionedTextDocumentIdentifier, Range,
24 Registration, RelatedFullDocumentDiagnosticReport, ServerCapabilities, ServerInfo,
25 TextDocumentEdit, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Uri,
26 WorkspaceEdit,
27};
28use tower_lsp_server::{Client, LanguageServer, jsonrpc::Result};
29
30mod commands {
32 pub(super) const UPDATE_VERSION: &str = "deps-lsp.updateVersion";
34 pub(super) const UPDATE_ALL_OUTDATED: &str = crate::handlers::code_lens::COMMAND_ID;
37}
38
39fn parse_config(value: serde_json::Value) -> Option<DepsConfig> {
56 match serde_json::from_value::<DepsConfig>(value) {
57 Ok(config) => Some(config),
58 Err(e) => {
59 tracing::warn!(
60 "failed to parse deps-lsp configuration: {e} (keeping previous configuration)"
61 );
62 None
63 }
64 }
65}
66
67#[cfg(feature = "gitlab-ci")]
79async fn warn_if_gitlab_instance_host_invalid(
80 client: &Client,
81 raw: &str,
82 policy: &deps_core::net_policy::RegistryAccessPolicy,
83) {
84 if let Err(error) = deps_gitlab_ci::GitlabHost::parse(raw, policy) {
85 client
86 .show_message(
87 MessageType::WARNING,
88 format!(
89 "deps-lsp: registries.gitlab_instance_host value '{raw}' is invalid \
90 ({error}) and will be ignored — instance-host resolution stays \
91 unresolved and GITLAB_TOKEN will not be sent to gitlab.com or any other \
92 host until this is corrected"
93 ),
94 )
95 .await;
96 }
97}
98
99pub struct Backend {
100 pub(crate) client: Client,
101 state: Arc<ServerState>,
102 config: Arc<RwLock<DepsConfig>>,
103 client_capabilities: Arc<RwLock<Option<tower_lsp_server::ls_types::ClientCapabilities>>>,
104}
105
106impl Backend {
107 pub fn new(client: Client) -> Self {
108 Self {
109 client,
110 state: Arc::new(ServerState::new()),
111 config: Arc::new(RwLock::new(DepsConfig::default())),
112 client_capabilities: Arc::new(RwLock::new(None)),
113 }
114 }
115
116 #[doc(hidden)]
118 pub const fn client(&self) -> &Client {
119 &self.client
120 }
121
122 async fn handle_open(
124 &self,
125 uri: tower_lsp_server::ls_types::Uri,
126 content: String,
127 version: i32,
128 ) {
129 match handle_document_open(
130 uri.clone(),
131 content,
132 Some(version),
133 Arc::clone(&self.state),
134 self.client.clone(),
135 Arc::clone(&self.config),
136 )
137 .await
138 {
139 Ok(task) => {
140 self.state.spawn_background_task(uri, task).await;
141 }
142 Err(e) => {
143 tracing::error!("failed to open document {:?}: {}", uri, e);
144 self.client
145 .log_message(MessageType::ERROR, format!("Parse error: {e}"))
146 .await;
147 }
148 }
149 }
150
151 async fn handle_change(
153 &self,
154 uri: tower_lsp_server::ls_types::Uri,
155 content: String,
156 version: i32,
157 ) {
158 match handle_document_change(
159 uri.clone(),
160 content,
161 Some(version),
162 Arc::clone(&self.state),
163 self.client.clone(),
164 Arc::clone(&self.config),
165 )
166 .await
167 {
168 Ok(task) => {
169 self.state.spawn_background_task(uri, task).await;
170 }
171 Err(e) => {
172 tracing::error!("failed to process document change {:?}: {}", uri, e);
173 self.client
177 .log_message(MessageType::ERROR, format!("Change rejected: {e}"))
178 .await;
179 }
180 }
181 }
182
183 async fn handle_lockfile_change(&self, lockfile_path: &std::path::Path, ecosystem_id: &str) {
184 let Some(ecosystem) = self.state.ecosystem_registry.get(ecosystem_id) else {
185 tracing::error!("Unknown ecosystem: {}", ecosystem_id);
186 return;
187 };
188
189 let Some(lock_provider) = ecosystem.lockfile_provider() else {
190 tracing::warn!("Ecosystem {} has no lock file provider", ecosystem_id);
191 return;
192 };
193
194 let affected_uris: Vec<Uri> = self
196 .state
197 .documents
198 .iter()
199 .filter_map(|entry| {
200 let uri = entry.key();
201 let doc = entry.value();
202 if doc.ecosystem_id() != ecosystem_id {
203 return None;
204 }
205 let doc_lockfile = lock_provider.locate_lockfile(uri)?;
206 if doc_lockfile == lockfile_path {
207 Some(uri.clone())
208 } else {
209 None
210 }
211 })
212 .collect();
213
214 if affected_uris.is_empty() {
215 tracing::debug!(
216 "No open manifests affected by lock file: {}",
217 lockfile_path.display()
218 );
219 return;
220 }
221
222 tracing::info!(
223 "Updating {} manifest(s) affected by lock file change",
224 affected_uris.len()
225 );
226
227 let resolved_versions = match self
229 .state
230 .lockfile_cache
231 .get_or_parse(lock_provider.as_ref(), lockfile_path)
232 .await
233 {
234 Ok(packages) => packages
235 .iter()
236 .map(|(name, pkg)| (PackageName::new(name.as_str()), pkg.version.clone().into()))
237 .collect::<HashMap<PackageName, deps_core::ConcreteVersion>>(),
238 Err(e) => {
239 tracing::error!("Failed to reload lock file: {}", e);
240 self.client
241 .log_message(
242 MessageType::ERROR,
243 format!("Failed to reload lock file: {e}"),
244 )
245 .await;
246 HashMap::new()
247 }
248 };
249
250 let (freshness, severities, offline) = {
257 let config = self.config.read().await;
258 (
259 config.freshness.to_settings(),
260 config.diagnostics.to_severities(),
261 config.network.offline,
262 )
263 };
264
265 for uri in affected_uris {
266 if let Some(mut doc) = self.state.documents.get_mut(&uri) {
267 doc.update_resolved_versions(resolved_versions.clone());
268 }
269
270 let items = diagnostics::generate_diagnostics_internal(
271 Arc::clone(&self.state),
272 &uri,
273 freshness,
274 severities,
275 offline,
276 )
277 .await;
278
279 self.client.publish_diagnostics(uri, items, None).await;
280 }
281
282 self.state.spawn_refresh_requests(&self.client);
285 }
286
287 async fn handle_watched_config_change(&self, ecosystem_id: &'static str) {
313 crate::document::reparse::reparse_open_documents(
314 crate::config::ReparseScope::Ecosystems(vec![ecosystem_id]),
315 crate::document::RefetchPolicy::Diff,
316 "watched config file change",
317 Arc::clone(&self.state),
318 self.client.clone(),
319 Arc::clone(&self.config),
320 )
321 .await;
322 }
323
324 async fn supports_progress(&self) -> bool {
326 let caps = self.client_capabilities.read().await;
327 caps.as_ref()
328 .and_then(|c| c.window.as_ref())
329 .and_then(|w| w.work_done_progress)
330 .unwrap_or(false)
331 }
332
333 async fn did_change_configuration_dynamic_registration_supported(&self) -> bool {
337 let caps = self.client_capabilities.read().await;
338 caps.as_ref()
339 .and_then(|c| c.workspace.as_ref())
340 .and_then(|w| w.did_change_configuration.as_ref())
341 .and_then(|d| d.dynamic_registration)
342 .unwrap_or(false)
343 }
344
345 async fn diagnostic_refresh_supported(&self) -> bool {
349 let caps = self.client_capabilities.read().await;
350 caps.as_ref()
351 .and_then(|c| c.workspace.as_ref())
352 .and_then(|w| w.diagnostics.as_ref())
353 .and_then(|d| d.refresh_support)
354 .unwrap_or(false)
355 }
356
357 async fn inlay_hint_refresh_supported(&self) -> bool {
361 let caps = self.client_capabilities.read().await;
362 caps.as_ref()
363 .and_then(|c| c.workspace.as_ref())
364 .and_then(|w| w.inlay_hint.as_ref())
365 .and_then(|h| h.refresh_support)
366 .unwrap_or(false)
367 }
368
369 async fn code_lens_refresh_supported(&self) -> bool {
372 let caps = self.client_capabilities.read().await;
373 caps.as_ref()
374 .and_then(|c| c.workspace.as_ref())
375 .and_then(|w| w.code_lens.as_ref())
376 .and_then(|c| c.refresh_support)
377 .unwrap_or(false)
378 }
379
380 fn server_capabilities() -> ServerCapabilities {
381 ServerCapabilities {
382 text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
383 completion_provider: Some(CompletionOptions {
384 trigger_characters: Some(vec!["\"".into(), "=".into(), ".".into()]),
385 resolve_provider: Some(false),
386 completion_item: Some(CompletionOptionsCompletionItem {
387 label_details_support: Some(true),
388 }),
389 ..Default::default()
390 }),
391 hover_provider: Some(HoverProviderCapability::Simple(true)),
392 inlay_hint_provider: Some(OneOf::Left(true)),
393 code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
394 code_action_kinds: Some(vec![
395 tower_lsp_server::ls_types::CodeActionKind::REFACTOR,
396 tower_lsp_server::ls_types::CodeActionKind::QUICKFIX,
397 ]),
398 ..Default::default()
399 })),
400 code_lens_provider: Some(CodeLensOptions {
401 resolve_provider: Some(false),
402 }),
403 document_link_provider: Some(DocumentLinkOptions {
404 resolve_provider: Some(false),
405 work_done_progress_options: Default::default(),
406 }),
407 diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
408 identifier: Some("deps".into()),
409 inter_file_dependencies: false,
410 workspace_diagnostics: false,
411 ..Default::default()
412 })),
413 execute_command_provider: Some(ExecuteCommandOptions {
414 commands: vec![
415 commands::UPDATE_VERSION.into(),
416 commands::UPDATE_ALL_OUTDATED.into(),
417 ],
418 ..Default::default()
419 }),
420 ..Default::default()
421 }
422 }
423}
424
425impl LanguageServer for Backend {
426 async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
427 tracing::info!("initializing deps-lsp server");
428
429 *self.client_capabilities.write().await = Some(params.capabilities.clone());
431 self.state
432 .set_progress_supported(self.supports_progress().await);
433 let inlay_hint_refresh_supported = self.inlay_hint_refresh_supported().await;
434 self.state
435 .set_inlay_hint_refresh_supported(inlay_hint_refresh_supported);
436 let code_lens_refresh_supported = self.code_lens_refresh_supported().await;
437 self.state
438 .set_code_lens_refresh_supported(code_lens_refresh_supported);
439 let diagnostic_refresh_supported = self.diagnostic_refresh_supported().await;
443 self.state
444 .set_diagnostic_refresh_supported(diagnostic_refresh_supported);
445 if !inlay_hint_refresh_supported {
449 tracing::debug!(
450 "client did not declare workspace.inlayHint.refreshSupport; inlay hints won't auto-refresh after background fetches"
451 );
452 }
453 if !code_lens_refresh_supported {
454 tracing::debug!(
455 "client did not declare workspace.codeLens.refreshSupport; code lenses won't auto-refresh after background fetches"
456 );
457 }
458
459 if let Some(init_options) = params.initialization_options
461 && let Some(config) = parse_config(init_options)
462 {
463 tracing::debug!("loaded configuration: {:?}", config);
464 self.state
465 .cache
466 .set_registry_policy(config.registries.workspace_registries.to_policy());
467 self.state.nuget_user_profile_sources.store(
468 config.registries.nuget_user_profile_sources,
469 std::sync::atomic::Ordering::Relaxed,
470 );
471 let gitlab_instance_host = (!config.registries.gitlab_instance_host.is_empty())
472 .then(|| config.registries.gitlab_instance_host.clone());
473 #[cfg(feature = "gitlab-ci")]
474 if let Some(raw) = &gitlab_instance_host {
475 warn_if_gitlab_instance_host_invalid(
476 &self.client,
477 raw,
478 &self.state.registry_policy,
479 )
480 .await;
481 }
482 *self
483 .state
484 .gitlab_instance_host
485 .write()
486 .expect("gitlab_instance_host lock poisoned") = gitlab_instance_host;
487 self.state.cache.set_offline(config.network.offline);
488 self.state.cache.set_cache_enabled(config.cache.enabled);
489 self.state
490 .cold_start_limiter
491 .set_min_interval(std::time::Duration::from_millis(
492 config.cold_start.rate_limit_ms,
493 ));
494 *self.config.write().await = config;
495 }
496
497 Ok(InitializeResult {
498 capabilities: Self::server_capabilities(),
499 server_info: Some(ServerInfo {
500 name: "deps-lsp".into(),
501 version: Some(env!("CARGO_PKG_VERSION").into()),
502 }),
503 offset_encoding: None,
504 })
505 }
506
507 async fn initialized(&self, _: InitializedParams) {
508 tracing::info!("deps-lsp server initialized");
509 self.client
510 .log_message(
511 MessageType::INFO,
512 format!(
513 "deps-lsp v{} ({} {})",
514 env!("CARGO_PKG_VERSION"),
515 env!("GIT_HASH"),
516 env!("BUILD_TIME")
517 ),
518 )
519 .await;
520
521 let state_clone = Arc::clone(&self.state);
527 let cleanup_task = tokio::spawn(async move {
528 let mut interval = tokio::time::interval(std::time::Duration::from_mins(1));
529 loop {
530 interval.tick().await;
531 state_clone
532 .cold_start_limiter
533 .cleanup_old_entries(std::time::Duration::from_mins(5));
534 tracing::trace!("Cleaned up old cold start rate limit entries");
535 }
536 });
537 tokio::spawn(async move {
538 let Err(e) = cleanup_task.await;
540 tracing::error!("Cold start rate limiter cleanup task exited unexpectedly: {e}");
541 });
542
543 let mut patterns = self.state.ecosystem_registry.all_lockfile_patterns();
551 patterns.extend(self.state.ecosystem_registry.all_watched_config_patterns());
552 match tokio::time::timeout(
553 CLIENT_REFRESH_TIMEOUT,
554 file_watcher::register_lock_file_watchers(&self.client, &patterns),
555 )
556 .await
557 {
558 Ok(Ok(())) => {}
559 Ok(Err(e)) => {
560 tracing::warn!("Failed to register file watchers: {}", e);
561 self.client
562 .log_message(MessageType::WARNING, format!("File watching disabled: {e}"))
563 .await;
564 }
565 Err(_) => {
566 tracing::warn!("Timed out registering file watchers");
567 self.client
568 .log_message(
569 MessageType::WARNING,
570 "File watching disabled: registration timed out".to_string(),
571 )
572 .await;
573 }
574 }
575
576 if self
581 .did_change_configuration_dynamic_registration_supported()
582 .await
583 {
584 let registration = Registration {
585 id: "deps-lsp-did-change-configuration".to_string(),
586 method: "workspace/didChangeConfiguration".to_string(),
587 register_options: None,
588 };
589 match tokio::time::timeout(
590 CLIENT_REFRESH_TIMEOUT,
591 self.client.register_capability(vec![registration]),
592 )
593 .await
594 {
595 Ok(Ok(())) => {}
596 Ok(Err(e)) => tracing::warn!("Failed to register didChangeConfiguration: {}", e),
597 Err(_) => tracing::warn!("Timed out registering didChangeConfiguration"),
598 }
599 }
600 }
601
602 async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
621 if params.settings.is_null() {
622 tracing::debug!(
623 "workspace/didChangeConfiguration received null settings; the \
624 workspace/configuration pull form is not implemented, ignoring"
625 );
626 return;
627 }
628
629 let Some(config) = parse_config(params.settings) else {
630 return;
631 };
632
633 tracing::info!("configuration updated via workspace/didChangeConfiguration");
634
635 let workspace_registries_policy = config.registries.workspace_registries.to_policy();
643 let nuget_user_profile_sources = config.registries.nuget_user_profile_sources;
644 let offline = config.network.offline;
645 let cache_enabled = config.cache.enabled;
646 let cold_start_rate_limit_ms = config.cold_start.rate_limit_ms;
647 let gitlab_instance_host = (!config.registries.gitlab_instance_host.is_empty())
648 .then(|| config.registries.gitlab_instance_host.clone());
649
650 let scope = {
655 let mut guard = self.config.write().await;
656 let scope = crate::config::reparse_scope(
657 &guard,
658 &config,
659 &self.state.workspace_registry_ecosystems,
660 );
661 *guard = config;
662 scope
663 };
664
665 self.state
666 .cache
667 .set_registry_policy(workspace_registries_policy);
668 self.state.nuget_user_profile_sources.store(
669 nuget_user_profile_sources,
670 std::sync::atomic::Ordering::Relaxed,
671 );
672 #[cfg(feature = "gitlab-ci")]
673 if let Some(raw) = &gitlab_instance_host {
674 warn_if_gitlab_instance_host_invalid(&self.client, raw, &self.state.registry_policy)
675 .await;
676 }
677 *self
678 .state
679 .gitlab_instance_host
680 .write()
681 .expect("gitlab_instance_host lock poisoned") = gitlab_instance_host;
682 self.state.cache.set_offline(offline);
685 self.state.cache.set_cache_enabled(cache_enabled);
686 self.state
687 .cold_start_limiter
688 .set_min_interval(std::time::Duration::from_millis(cold_start_rate_limit_ms));
689
690 match scope {
691 Some(scope) => {
692 let generation = self.state.queue_reparse(scope);
696 let state = Arc::clone(&self.state);
697 let client = self.client.clone();
698 let config = Arc::clone(&self.config);
699 tokio::spawn(async move {
700 tokio::time::sleep(crate::document::reparse::RECONFIGURE_DEBOUNCE).await;
701 let superseded = state.config_generation() != generation;
702 if superseded
710 && !state
711 .pending_reparse_overdue(crate::document::reparse::MAX_DEBOUNCE_WAIT)
712 {
713 return;
714 }
715 let Some(scope) = state.take_pending_reparse() else {
716 return;
717 };
718 crate::document::reparse::reparse_open_documents(
719 scope,
720 crate::document::RefetchPolicy::AllDependencies,
721 "workspace/didChangeConfiguration",
722 state,
723 client,
724 config,
725 )
726 .await;
727 });
728 }
729 None => {
730 if self.diagnostic_refresh_supported().await {
737 match tokio::time::timeout(
738 CLIENT_REFRESH_TIMEOUT,
739 self.client.workspace_diagnostic_refresh(),
740 )
741 .await
742 {
743 Ok(Ok(())) => {}
744 Ok(Err(e)) => {
745 tracing::debug!("workspace/diagnostic/refresh failed: {:?}", e);
746 }
747 Err(_) => tracing::debug!("workspace/diagnostic/refresh timed out"),
748 }
749 }
750 }
751 }
752 }
753
754 fn shutdown(&self) -> impl std::future::Future<Output = Result<()>> + Send {
755 tracing::info!("shutting down deps-lsp server");
756 std::future::ready(Ok(()))
757 }
758
759 async fn did_open(&self, params: DidOpenTextDocumentParams) {
760 let uri = params.text_document.uri;
761 let content = params.text_document.text;
762 let version = params.text_document.version;
763
764 tracing::info!("document opened: {:?}", uri);
765
766 if self.state.ecosystem_registry.get_for_uri(&uri).is_none() {
768 tracing::debug!("unsupported file type: {:?}", uri);
769 return;
770 }
771
772 self.handle_open(uri, content, version).await;
773 }
774
775 async fn did_change(&self, params: DidChangeTextDocumentParams) {
776 let uri = params.text_document.uri;
777 let version = params.text_document.version;
778
779 if let Some(change) = params.content_changes.first() {
780 let content = change.text.clone();
781
782 if self.state.ecosystem_registry.get_for_uri(&uri).is_none() {
784 tracing::debug!("unsupported file type: {:?}", uri);
785 return;
786 }
787
788 self.handle_change(uri, content, version).await;
789 }
790 }
791
792 async fn did_close(&self, params: DidCloseTextDocumentParams) {
793 let uri = params.text_document.uri;
794 tracing::info!("document closed: {:?}", uri);
795
796 self.state.remove_document(&uri);
797 self.state.cancel_background_task(&uri).await;
798 }
799
800 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
801 tracing::debug!("Received {} file change events", params.changes.len());
802
803 for change in params.changes {
804 let Some(path) = change.uri.to_file_path() else {
805 tracing::warn!("Invalid file path in change event: {:?}", change.uri);
806 continue;
807 };
808
809 let Some(filename) = file_watcher::extract_lockfile_name(&path) else {
810 continue;
811 };
812
813 if let Some(ecosystem) = self.state.ecosystem_registry.get_for_lockfile(filename) {
814 tracing::info!(
815 "Lock file changed: {} (ecosystem: {})",
816 filename,
817 ecosystem.id()
818 );
819
820 self.state.lockfile_cache.invalidate(&path);
821 self.handle_lockfile_change(&path, ecosystem.id()).await;
822 continue;
823 }
824
825 if let Some(ecosystem) = self
826 .state
827 .ecosystem_registry
828 .get_for_watched_config(filename)
829 {
830 tracing::info!(
831 "Watched config file changed: {} (ecosystem: {})",
832 filename,
833 ecosystem.id()
834 );
835
836 self.handle_watched_config_change(ecosystem.id()).await;
841 continue;
842 }
843
844 tracing::debug!("Skipping unrecognized watched-file change: {}", filename);
845 }
846 }
847
848 async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
849 Ok(hover::handle_hover(
850 Arc::clone(&self.state),
851 params,
852 self.client.clone(),
853 Arc::clone(&self.config),
854 )
855 .await)
856 }
857
858 async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
859 Ok(completion::handle_completion(
860 Arc::clone(&self.state),
861 params,
862 self.client.clone(),
863 Arc::clone(&self.config),
864 )
865 .await)
866 }
867
868 async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
869 let inlay_config = { self.config.read().await.inlay_hints.clone() };
871 let range = params.range;
872
873 let hints: Vec<_> = inlay_hints::handle_inlay_hints(
874 Arc::clone(&self.state),
875 params,
876 &inlay_config,
877 self.client.clone(),
878 Arc::clone(&self.config),
879 )
880 .await
881 .into_iter()
882 .filter(|h| h.position.line >= range.start.line && h.position.line <= range.end.line)
883 .collect();
884
885 Ok(Some(hints))
886 }
887
888 async fn code_action(
889 &self,
890 params: CodeActionParams,
891 ) -> Result<Option<Vec<tower_lsp_server::ls_types::CodeActionOrCommand>>> {
892 tracing::info!(
893 "code_action request: uri={:?}, range={:?}",
894 params.text_document.uri,
895 params.range
896 );
897 let actions = code_actions::handle_code_actions(
898 Arc::clone(&self.state),
899 params,
900 self.client.clone(),
901 Arc::clone(&self.config),
902 )
903 .await;
904 tracing::info!("code_action response: {} actions", actions.len());
905 Ok(Some(actions))
906 }
907
908 async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
909 let enabled = { self.config.read().await.code_lens.enabled };
910 let lenses = code_lens::handle_code_lens(
911 Arc::clone(&self.state),
912 params,
913 enabled,
914 self.client.clone(),
915 Arc::clone(&self.config),
916 )
917 .await;
918 Ok(Some(lenses))
919 }
920
921 async fn document_link(&self, params: DocumentLinkParams) -> Result<Option<Vec<DocumentLink>>> {
922 let links = document_link::handle_document_link(
923 Arc::clone(&self.state),
924 params,
925 self.client.clone(),
926 Arc::clone(&self.config),
927 )
928 .await;
929 Ok(Some(links))
930 }
931
932 async fn diagnostic(
933 &self,
934 params: DocumentDiagnosticParams,
935 ) -> Result<DocumentDiagnosticReportResult> {
936 let uri = params.text_document.uri;
937 tracing::info!("diagnostic request for: {:?}", uri);
938
939 let diagnostics_config = { self.config.read().await.diagnostics.clone() };
941
942 let items = diagnostics::handle_diagnostics(
943 Arc::clone(&self.state),
944 &uri,
945 &diagnostics_config,
946 self.client.clone(),
947 Arc::clone(&self.config),
948 )
949 .await;
950
951 tracing::info!("returning {} diagnostics", items.len());
952
953 Ok(DocumentDiagnosticReportResult::Report(
954 DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
955 related_documents: None,
956 full_document_diagnostic_report: FullDocumentDiagnosticReport {
957 result_id: None,
958 items,
959 },
960 }),
961 ))
962 }
963
964 async fn execute_command(
965 &self,
966 params: ExecuteCommandParams,
967 ) -> Result<Option<serde_json::Value>> {
968 tracing::info!("execute_command: {:?}", params.command);
969
970 if params.command == commands::UPDATE_VERSION
971 && let Some(args) = params.arguments.first()
972 && let Ok(update_args) = serde_json::from_value::<UpdateVersionArgs>(args.clone())
973 {
974 if let Some(edit) = build_update_version_edit(&update_args) {
975 match tokio::time::timeout(CLIENT_REFRESH_TIMEOUT, self.client.apply_edit(edit))
976 .await
977 {
978 Ok(Ok(_)) => {}
979 Ok(Err(e)) => tracing::error!("Failed to apply edit: {:?}", e),
980 Err(_) => tracing::warn!(
981 "apply_edit for deps-lsp.updateVersion timed out after {CLIENT_REFRESH_TIMEOUT:?}"
982 ),
983 }
984 }
985 } else if params.command == commands::UPDATE_ALL_OUTDATED
986 && let Some(args) = params.arguments.first()
987 && let Ok(update_args) = serde_json::from_value::<UpdateAllOutdatedArgs>(args.clone())
988 {
989 self.execute_update_all_outdated(update_args.uri).await;
990 }
991
992 Ok(None)
993 }
994}
995
996impl Backend {
997 async fn warn_update_all_outdated_not_ready(&self) {
1001 self.client
1002 .show_message(
1003 MessageType::WARNING,
1004 "deps-lsp: dependency data is not ready for this document",
1005 )
1006 .await;
1007 }
1008
1009 #[allow(clippy::await_holding_invalid_type)]
1041 async fn execute_update_all_outdated(&self, uri: Uri) {
1042 let Some(doc) = self.state.get_document(&uri) else {
1043 self.warn_update_all_outdated_not_ready().await;
1044 return;
1045 };
1046
1047 if !doc.is_ready_for_batch_update() {
1048 drop(doc);
1049 self.warn_update_all_outdated_not_ready().await;
1050 return;
1051 }
1052
1053 let Some(ecosystem) = self.state.ecosystem_registry.get(doc.ecosystem_id()) else {
1054 tracing::warn!("Unknown ecosystem for {:?}", uri);
1055 drop(doc);
1056 self.warn_update_all_outdated_not_ready().await;
1057 return;
1058 };
1059
1060 let Some(parse_result) = doc.parse_result() else {
1061 tracing::warn!("No parse result for {:?}", uri);
1062 drop(doc);
1063 self.warn_update_all_outdated_not_ready().await;
1064 return;
1065 };
1066
1067 let edits = deps_core::collect_update_all_edits(
1068 parse_result,
1069 &doc.content,
1070 deps_core::VersionData::new(&doc.cached_versions, &doc.resolved_versions),
1071 ecosystem.formatter(),
1072 );
1073 let version = doc.version;
1074 drop(doc);
1075
1076 if edits.is_empty() {
1077 self.client
1082 .show_message(
1083 MessageType::INFO,
1084 "deps-lsp: no outdated dependencies to update",
1085 )
1086 .await;
1087 return;
1088 }
1089
1090 let supports_document_changes = self
1091 .client_capabilities
1092 .read()
1093 .await
1094 .as_ref()
1095 .and_then(|c| c.workspace.as_ref())
1096 .and_then(|w| w.workspace_edit.as_ref())
1097 .and_then(|we| we.document_changes)
1098 .unwrap_or(false);
1099
1100 let edit = build_update_all_outdated_edit(&uri, version, edits, supports_document_changes);
1101
1102 match tokio::time::timeout(CLIENT_REFRESH_TIMEOUT, self.client.apply_edit(edit)).await {
1103 Ok(Ok(response)) if response.applied => {}
1104 Ok(Ok(response)) => {
1105 tracing::warn!(
1106 "workspace/applyEdit for {:?} was rejected: {:?}",
1107 uri,
1108 response.failure_reason
1109 );
1110 self.client
1111 .show_message(
1112 MessageType::WARNING,
1113 "deps-lsp: failed to apply dependency updates",
1114 )
1115 .await;
1116 }
1117 Ok(Err(e)) => {
1118 tracing::error!("Failed to apply edit for {:?}: {:?}", uri, e);
1119 self.client
1120 .show_message(
1121 MessageType::WARNING,
1122 "deps-lsp: failed to apply dependency updates",
1123 )
1124 .await;
1125 }
1126 Err(_) => {
1127 tracing::warn!(
1128 "apply_edit for {:?} timed out after {CLIENT_REFRESH_TIMEOUT:?}",
1129 uri
1130 );
1131 self.client
1132 .show_message(
1133 MessageType::WARNING,
1134 format!(
1135 "deps-lsp: the editor did not respond to the update within {CLIENT_REFRESH_TIMEOUT:?}"
1136 ),
1137 )
1138 .await;
1139 }
1140 }
1141 }
1142}
1143
1144#[derive(serde::Deserialize)]
1145struct UpdateVersionArgs {
1146 uri: Uri,
1147 range: Range,
1148 version: String,
1149}
1150
1151fn build_update_version_edit(args: &UpdateVersionArgs) -> Option<WorkspaceEdit> {
1156 if !is_safe_version_string(&args.version) {
1157 tracing::error!(
1158 version = %args.version,
1159 "deps-lsp.updateVersion: rejecting unsafe version string"
1160 );
1161 return None;
1162 }
1163
1164 let mut edits = HashMap::new();
1165 edits.insert(
1166 args.uri.clone(),
1167 vec![TextEdit {
1168 range: args.range,
1169 new_text: format!("\"{}\"", args.version),
1170 }],
1171 );
1172
1173 Some(WorkspaceEdit {
1174 changes: Some(edits),
1175 ..Default::default()
1176 })
1177}
1178
1179#[derive(serde::Deserialize)]
1183struct UpdateAllOutdatedArgs {
1184 uri: Uri,
1185}
1186
1187fn build_update_all_outdated_edit(
1194 uri: &Uri,
1195 version: Option<i32>,
1196 edits: Vec<TextEdit>,
1197 supports_document_changes: bool,
1198) -> WorkspaceEdit {
1199 if supports_document_changes {
1200 WorkspaceEdit {
1201 document_changes: Some(DocumentChanges::Edits(vec![TextDocumentEdit {
1202 text_document: OptionalVersionedTextDocumentIdentifier {
1203 uri: uri.clone(),
1204 version,
1205 },
1206 edits: edits.into_iter().map(OneOf::Left).collect(),
1207 }])),
1208 ..Default::default()
1209 }
1210 } else {
1211 let mut changes = HashMap::new();
1212 changes.insert(uri.clone(), edits);
1213 WorkspaceEdit {
1214 changes: Some(changes),
1215 ..Default::default()
1216 }
1217 }
1218}
1219
1220#[cfg(test)]
1221mod tests {
1222 use super::*;
1223
1224 use std::assert_matches;
1225
1226 #[test]
1227 fn test_server_capabilities() {
1228 let caps = Backend::server_capabilities();
1229
1230 assert!(caps.text_document_sync.is_some());
1232
1233 assert!(caps.completion_provider.is_some());
1235 let completion = caps.completion_provider.unwrap();
1236 assert!(!completion.resolve_provider.unwrap()); assert!(caps.hover_provider.is_some());
1240
1241 assert!(caps.inlay_hint_provider.is_some());
1243
1244 assert!(caps.diagnostic_provider.is_some());
1246 }
1247
1248 #[tokio::test]
1249 async fn test_backend_creation() {
1250 let (_service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1251 }
1254
1255 #[tokio::test]
1256 async fn test_initialize_without_options() {
1257 let (_service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1258 }
1261
1262 #[test]
1263 fn test_server_capabilities_text_document_sync() {
1264 let caps = Backend::server_capabilities();
1265
1266 match caps.text_document_sync {
1267 Some(TextDocumentSyncCapability::Kind(kind)) => {
1268 assert_eq!(kind, TextDocumentSyncKind::FULL);
1269 }
1270 _ => panic!("Expected text document sync kind to be FULL"),
1271 }
1272 }
1273
1274 #[test]
1275 fn test_server_capabilities_completion_triggers() {
1276 let caps = Backend::server_capabilities();
1277
1278 let completion = caps
1279 .completion_provider
1280 .expect("completion provider should exist");
1281 let triggers = completion
1282 .trigger_characters
1283 .expect("trigger characters should exist");
1284
1285 assert!(triggers.contains(&"\"".to_string()));
1286 assert!(triggers.contains(&"=".to_string()));
1287 assert!(triggers.contains(&".".to_string()));
1288 assert_eq!(triggers.len(), 3);
1289 }
1290
1291 #[test]
1292 fn test_server_capabilities_code_actions() {
1293 let caps = Backend::server_capabilities();
1294
1295 match caps.code_action_provider {
1296 Some(CodeActionProviderCapability::Options(opts)) => {
1297 let kinds = opts
1298 .code_action_kinds
1299 .expect("code action kinds should exist");
1300 assert!(kinds.contains(&tower_lsp_server::ls_types::CodeActionKind::REFACTOR));
1301 assert!(kinds.contains(&tower_lsp_server::ls_types::CodeActionKind::QUICKFIX));
1302 }
1303 _ => panic!("Expected code action provider options"),
1304 }
1305 }
1306
1307 #[test]
1308 fn test_server_capabilities_diagnostics_config() {
1309 let caps = Backend::server_capabilities();
1310
1311 match caps.diagnostic_provider {
1312 Some(DiagnosticServerCapabilities::Options(opts)) => {
1313 assert_eq!(opts.identifier, Some("deps".to_string()));
1314 assert!(!opts.inter_file_dependencies);
1315 assert!(!opts.workspace_diagnostics);
1316 }
1317 _ => panic!("Expected diagnostic options"),
1318 }
1319 }
1320
1321 #[test]
1322 fn test_server_capabilities_execute_command() {
1323 let caps = Backend::server_capabilities();
1324
1325 let execute = caps
1326 .execute_command_provider
1327 .expect("execute command provider should exist");
1328 assert!(
1329 execute
1330 .commands
1331 .contains(&commands::UPDATE_VERSION.to_string())
1332 );
1333 }
1334
1335 #[test]
1336 fn test_commands_constants() {
1337 assert_eq!(commands::UPDATE_VERSION, "deps-lsp.updateVersion");
1338 }
1339
1340 #[tokio::test]
1341 async fn test_backend_state_initialization() {
1342 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1343 let backend = service.inner();
1344
1345 assert_eq!(backend.state.documents.len(), 0);
1346 }
1347
1348 #[tokio::test]
1349 async fn test_backend_config_initialization() {
1350 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1351 let backend = service.inner();
1352
1353 let config = backend.config.read().await;
1354 assert!(config.inlay_hints.enabled);
1355 }
1356
1357 #[tokio::test]
1363 async fn test_watched_config_change_reparses_open_document_with_catalog_dependency() {
1364 use tower_lsp_server::ls_types::{
1365 FileChangeType, FileEvent, HoverContents, Position, TextDocumentIdentifier,
1366 TextDocumentItem, TextDocumentPositionParams,
1367 };
1368
1369 let temp_dir = tempfile::tempdir().unwrap();
1370 let workspace_path = temp_dir.path().join("pnpm-workspace.yaml");
1371 std::fs::write(&workspace_path, "catalog:\n react: ^17.0.0\n").unwrap();
1372
1373 let manifest_path = temp_dir.path().join("package.json");
1374 let content = r#"{"dependencies": {"react": "catalog:"}}"#;
1375 std::fs::write(&manifest_path, content).unwrap();
1376 let uri = Uri::from_file_path(&manifest_path).unwrap();
1377
1378 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1379 let backend = service.inner();
1380
1381 backend
1382 .did_open(DidOpenTextDocumentParams {
1383 text_document: TextDocumentItem {
1384 uri: uri.clone(),
1385 language_id: "json".to_string(),
1386 version: 1,
1387 text: content.to_string(),
1388 },
1389 })
1390 .await;
1391
1392 let hover_params = |uri: Uri| HoverParams {
1393 text_document_position_params: TextDocumentPositionParams {
1394 text_document: TextDocumentIdentifier { uri },
1395 position: Position::new(0, 20), },
1397 work_done_progress_params: Default::default(),
1398 };
1399
1400 let hover = backend
1401 .hover(hover_params(uri.clone()))
1402 .await
1403 .unwrap()
1404 .expect("hover must fire for a catalog-resolved dependency");
1405 let HoverContents::Markup(before) = hover.contents else {
1406 panic!("expected markup hover contents");
1407 };
1408 assert!(before.value.contains("^17.0.0"), "{}", before.value);
1409
1410 let future = std::time::SystemTime::now() + std::time::Duration::from_secs(2);
1413 std::fs::write(&workspace_path, "catalog:\n react: ^18.3.0\n").unwrap();
1414 std::fs::OpenOptions::new()
1415 .write(true)
1416 .open(&workspace_path)
1417 .unwrap()
1418 .set_modified(future)
1419 .unwrap();
1420
1421 backend
1422 .did_change_watched_files(DidChangeWatchedFilesParams {
1423 changes: vec![FileEvent {
1424 uri: Uri::from_file_path(&workspace_path).unwrap(),
1425 typ: FileChangeType::CHANGED,
1426 }],
1427 })
1428 .await;
1429
1430 let hover = backend
1431 .hover(hover_params(uri))
1432 .await
1433 .unwrap()
1434 .expect("hover must still fire after reparse");
1435 let HoverContents::Markup(after) = hover.contents else {
1436 panic!("expected markup hover contents");
1437 };
1438 assert!(
1439 after.value.contains("^18.3.0"),
1440 "watched config file change did not trigger a reparse of the open document: {}",
1441 after.value
1442 );
1443 }
1444
1445 #[test]
1446 fn test_update_version_args_deserialization() {
1447 let json = serde_json::json!({
1448 "uri": "file:///test/Cargo.toml",
1449 "range": {
1450 "start": {"line": 5, "character": 10},
1451 "end": {"line": 5, "character": 15}
1452 },
1453 "version": "1.0.0"
1454 });
1455
1456 let args: UpdateVersionArgs = serde_json::from_value(json).unwrap();
1457 assert_eq!(args.version, "1.0.0");
1458 assert_eq!(args.range.start.line, 5);
1459 assert_eq!(args.range.start.character, 10);
1460 }
1461
1462 #[tokio::test]
1463 async fn test_execute_command_update_version_with_unsafe_version_does_not_panic() {
1464 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1472 let backend = service.inner();
1473
1474 let params = ExecuteCommandParams {
1475 command: commands::UPDATE_VERSION.to_string(),
1476 arguments: vec![serde_json::json!({
1477 "uri": "file:///test/Cargo.toml",
1478 "range": {
1479 "start": {"line": 0, "character": 9},
1480 "end": {"line": 0, "character": 14}
1481 },
1482 "version": "1.2.0\", \"evil\": \"true"
1483 })],
1484 work_done_progress_params: Default::default(),
1485 };
1486
1487 let result = backend.execute_command(params).await;
1488 assert!(result.is_ok());
1489 }
1490
1491 fn update_version_args(version: &str) -> UpdateVersionArgs {
1492 UpdateVersionArgs {
1493 uri: deps_core::test_util::test_uri("/test/Cargo.toml"),
1494 range: Range::default(),
1495 version: version.to_string(),
1496 }
1497 }
1498
1499 #[test]
1500 fn test_build_update_version_edit_rejects_unsafe_version() {
1501 let args = update_version_args("1.2.0\", \"evil\": \"true");
1504 assert!(build_update_version_edit(&args).is_none());
1505 }
1506
1507 #[test]
1508 fn test_build_update_version_edit_accepts_safe_version() {
1509 let args = update_version_args("1.2.0");
1510 let edit = build_update_version_edit(&args).expect("a safe version must produce an edit");
1511
1512 let changes = edit.changes.expect("changes present");
1513 let edits = changes.get(&args.uri).expect("edit for the given uri");
1514 assert_eq!(edits.len(), 1);
1515 assert_eq!(edits[0].range, args.range);
1516 assert_eq!(edits[0].new_text, "\"1.2.0\"");
1517 }
1518
1519 #[test]
1520 fn test_server_capabilities_code_lens() {
1521 let caps = Backend::server_capabilities();
1522 let code_lens = caps
1523 .code_lens_provider
1524 .expect("code lens provider should exist");
1525 assert_eq!(code_lens.resolve_provider, Some(false));
1526 }
1527
1528 #[test]
1529 fn test_server_capabilities_execute_command_includes_update_all_outdated() {
1530 let caps = Backend::server_capabilities();
1531 let execute = caps
1532 .execute_command_provider
1533 .expect("execute command provider should exist");
1534 assert!(
1535 execute
1536 .commands
1537 .contains(&commands::UPDATE_ALL_OUTDATED.to_string())
1538 );
1539 }
1540
1541 #[test]
1542 fn test_commands_update_all_outdated_matches_code_lens_command_id() {
1543 assert_eq!(commands::UPDATE_ALL_OUTDATED, "deps-lsp.updateAllOutdated");
1544 }
1545
1546 #[test]
1547 fn test_update_all_outdated_args_deserialization() {
1548 let json = serde_json::json!({ "uri": "file:///test/Cargo.toml" });
1549 let args: UpdateAllOutdatedArgs = serde_json::from_value(json).unwrap();
1550 assert_eq!(args.uri.as_str(), "file:///test/Cargo.toml");
1551 }
1552
1553 #[test]
1554 fn test_build_update_all_outdated_edit_uses_document_changes_with_version() {
1555 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
1556 let edits = vec![TextEdit {
1557 range: Range::default(),
1558 new_text: "1.2.0".into(),
1559 }];
1560
1561 let edit = build_update_all_outdated_edit(&uri, Some(7), edits, true);
1562
1563 assert!(edit.changes.is_none());
1564 let DocumentChanges::Edits(doc_edits) =
1565 edit.document_changes.expect("document_changes present")
1566 else {
1567 panic!("expected DocumentChanges::Edits variant");
1568 };
1569 assert_eq!(doc_edits.len(), 1);
1570 assert_eq!(doc_edits[0].text_document.uri, uri);
1571 assert_eq!(doc_edits[0].text_document.version, Some(7));
1572 assert_eq!(doc_edits[0].edits.len(), 1);
1573 }
1574
1575 #[test]
1576 fn test_build_update_all_outdated_edit_falls_back_to_changes_map() {
1577 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
1578 let edits = vec![TextEdit {
1579 range: Range::default(),
1580 new_text: "1.2.0".into(),
1581 }];
1582
1583 let edit = build_update_all_outdated_edit(&uri, Some(7), edits, false);
1584
1585 assert!(edit.document_changes.is_none());
1586 let changes = edit.changes.expect("changes present");
1587 assert_eq!(changes.get(&uri).map(Vec::len), Some(1));
1588 }
1589
1590 mod parse_config_tests {
1595 use super::*;
1596
1597 #[test]
1598 fn test_parse_config_accepts_empty_object() {
1599 let config = parse_config(serde_json::json!({})).expect("empty object is valid");
1600 assert!(config.freshness.enabled);
1601 }
1602
1603 #[test]
1604 fn test_parse_config_accepts_recognized_keys() {
1605 let config = parse_config(serde_json::json!({
1606 "freshness": { "cooldown_secs": 60 }
1607 }))
1608 .expect("payload with a recognized key is valid");
1609 assert_eq!(config.freshness.cooldown_secs, 60);
1610 }
1611
1612 #[test]
1617 fn test_parse_config_rejects_section_wrapped_payload() {
1618 let result = parse_config(serde_json::json!({
1619 "deps-lsp": { "freshness": { "cooldown_secs": 60 } }
1620 }));
1621 assert!(
1622 result.is_none(),
1623 "a payload with no recognized top-level key must be rejected, not \
1624 silently accepted as all-defaults"
1625 );
1626 }
1627
1628 #[test]
1635 fn test_parse_config_rejects_mixed_blob_with_one_recognized_key_and_unknown_siblings() {
1636 let result = parse_config(serde_json::json!({
1637 "diagnostics": { "outdated_severity": 1 },
1638 "editor": { "fontSize": 14 },
1639 "python": { "linting": true }
1640 }));
1641 assert!(
1642 result.is_none(),
1643 "a payload with unrecognized sibling keys must be rejected wholesale, \
1644 not accepted because one key happens to match"
1645 );
1646 }
1647
1648 #[test]
1649 fn test_parse_config_rejects_malformed_field_value() {
1650 let result = parse_config(serde_json::json!({ "freshness": "not an object" }));
1651 assert!(result.is_none());
1652 }
1653
1654 #[test]
1655 fn test_parse_config_rejects_non_object_payload() {
1656 let result = parse_config(serde_json::json!(["not", "an", "object"]));
1657 assert!(result.is_none());
1658 }
1659 }
1660
1661 mod capability_support_tests {
1665 use super::*;
1666 use tower_lsp_server::ls_types::{
1667 ClientCapabilities, CodeLensWorkspaceClientCapabilities,
1668 DiagnosticWorkspaceClientCapabilities, DynamicRegistrationClientCapabilities,
1669 InlayHintWorkspaceClientCapabilities, WorkspaceClientCapabilities,
1670 };
1671
1672 #[tokio::test]
1673 async fn test_did_change_configuration_dynamic_registration_supported_true_branch() {
1674 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1675 let backend = service.inner();
1676
1677 *backend.client_capabilities.write().await = Some(ClientCapabilities {
1678 workspace: Some(WorkspaceClientCapabilities {
1679 did_change_configuration: Some(DynamicRegistrationClientCapabilities {
1680 dynamic_registration: Some(true),
1681 }),
1682 ..Default::default()
1683 }),
1684 ..Default::default()
1685 });
1686
1687 assert!(
1688 backend
1689 .did_change_configuration_dynamic_registration_supported()
1690 .await
1691 );
1692 }
1693
1694 #[tokio::test]
1695 async fn test_diagnostic_refresh_supported_true_branch() {
1696 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1697 let backend = service.inner();
1698
1699 *backend.client_capabilities.write().await = Some(ClientCapabilities {
1700 workspace: Some(WorkspaceClientCapabilities {
1701 diagnostics: Some(DiagnosticWorkspaceClientCapabilities {
1702 refresh_support: Some(true),
1703 }),
1704 ..Default::default()
1705 }),
1706 ..Default::default()
1707 });
1708
1709 assert!(backend.diagnostic_refresh_supported().await);
1710 }
1711
1712 #[tokio::test]
1717 async fn test_inlay_hint_refresh_supported_true_branch() {
1718 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1719 let backend = service.inner();
1720
1721 *backend.client_capabilities.write().await = Some(ClientCapabilities {
1722 workspace: Some(WorkspaceClientCapabilities {
1723 inlay_hint: Some(InlayHintWorkspaceClientCapabilities {
1724 refresh_support: Some(true),
1725 }),
1726 ..Default::default()
1727 }),
1728 ..Default::default()
1729 });
1730
1731 assert!(backend.inlay_hint_refresh_supported().await);
1732 }
1733
1734 #[tokio::test]
1735 async fn test_inlay_hint_refresh_supported_false_when_absent() {
1736 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1737 let backend = service.inner();
1738
1739 assert!(!backend.inlay_hint_refresh_supported().await);
1740 }
1741
1742 #[tokio::test]
1743 async fn test_code_lens_refresh_supported_true_branch() {
1744 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1745 let backend = service.inner();
1746
1747 *backend.client_capabilities.write().await = Some(ClientCapabilities {
1748 workspace: Some(WorkspaceClientCapabilities {
1749 code_lens: Some(CodeLensWorkspaceClientCapabilities {
1750 refresh_support: Some(true),
1751 }),
1752 ..Default::default()
1753 }),
1754 ..Default::default()
1755 });
1756
1757 assert!(backend.code_lens_refresh_supported().await);
1758 }
1759
1760 #[tokio::test]
1761 async fn test_code_lens_refresh_supported_false_when_absent() {
1762 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1763 let backend = service.inner();
1764
1765 assert!(!backend.code_lens_refresh_supported().await);
1766 }
1767
1768 #[tokio::test]
1773 async fn test_initialize_propagates_refresh_support_flags_into_state() {
1774 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1775 let backend = service.inner();
1776
1777 assert!(!backend.state.inlay_hint_refresh_supported());
1778 assert!(!backend.state.code_lens_refresh_supported());
1779
1780 let result = backend
1781 .initialize(InitializeParams {
1782 capabilities: ClientCapabilities {
1783 workspace: Some(WorkspaceClientCapabilities {
1784 inlay_hint: Some(InlayHintWorkspaceClientCapabilities {
1785 refresh_support: Some(true),
1786 }),
1787 code_lens: Some(CodeLensWorkspaceClientCapabilities {
1788 refresh_support: Some(true),
1789 }),
1790 ..Default::default()
1791 }),
1792 ..Default::default()
1793 },
1794 ..Default::default()
1795 })
1796 .await;
1797
1798 assert!(result.is_ok());
1799 assert!(backend.state.inlay_hint_refresh_supported());
1800 assert!(backend.state.code_lens_refresh_supported());
1801 }
1802
1803 #[tokio::test]
1804 async fn test_initialize_without_refresh_capabilities_keeps_state_flags_false() {
1805 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1806 let backend = service.inner();
1807
1808 let result = backend.initialize(InitializeParams::default()).await;
1809
1810 assert!(result.is_ok());
1811 assert!(!backend.state.inlay_hint_refresh_supported());
1812 assert!(!backend.state.code_lens_refresh_supported());
1813 }
1814 }
1815
1816 mod initialize_tests {
1817 use super::*;
1818
1819 #[tokio::test]
1825 async fn test_initialize_applies_valid_initialization_options() {
1826 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1827 let backend = service.inner();
1828
1829 let result = backend
1830 .initialize(InitializeParams {
1831 initialization_options: Some(
1832 serde_json::json!({ "freshness": { "cooldown_secs": 60 } }),
1833 ),
1834 ..Default::default()
1835 })
1836 .await;
1837
1838 assert!(result.is_ok());
1839 assert_eq!(backend.config.read().await.freshness.cooldown_secs, 60);
1840 }
1841
1842 #[tokio::test]
1847 async fn test_initialize_keeps_default_config_on_malformed_initialization_options() {
1848 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1849 let backend = service.inner();
1850
1851 let result = backend
1852 .initialize(InitializeParams {
1853 initialization_options: Some(
1854 serde_json::json!({ "deps-lsp": { "freshness": { "cooldown_secs": 60 } } }),
1855 ),
1856 ..Default::default()
1857 })
1858 .await;
1859
1860 assert!(result.is_ok());
1861 assert_eq!(
1862 backend.config.read().await.freshness.cooldown_secs,
1863 deps_core::DEFAULT_COOLDOWN_SECS,
1864 "malformed initializationOptions must not silently change the config"
1865 );
1866 }
1867
1868 #[tokio::test]
1869 async fn test_initialize_without_initialization_options_keeps_defaults() {
1870 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1871 let backend = service.inner();
1872
1873 let result = backend.initialize(InitializeParams::default()).await;
1874
1875 assert!(result.is_ok());
1876 assert!(backend.config.read().await.freshness.enabled);
1877 }
1878 }
1879
1880 mod did_change_configuration_tests {
1881 use super::*;
1882 use tower_lsp_server::ls_types::DidChangeConfigurationParams;
1883
1884 #[tokio::test]
1885 async fn test_did_change_configuration_applies_valid_payload() {
1886 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1887 let backend = service.inner();
1888
1889 backend
1890 .did_change_configuration(DidChangeConfigurationParams {
1891 settings: serde_json::json!({ "freshness": { "cooldown_secs": 60 } }),
1892 })
1893 .await;
1894
1895 assert_eq!(backend.config.read().await.freshness.cooldown_secs, 60);
1896 }
1897
1898 #[tokio::test]
1901 async fn test_did_change_configuration_keeps_previous_on_malformed_payload() {
1902 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1903 let backend = service.inner();
1904
1905 backend
1906 .did_change_configuration(DidChangeConfigurationParams {
1907 settings: serde_json::json!({ "freshness": { "cooldown_secs": 60 } }),
1908 })
1909 .await;
1910 assert_eq!(backend.config.read().await.freshness.cooldown_secs, 60);
1911
1912 backend
1913 .did_change_configuration(DidChangeConfigurationParams {
1914 settings: serde_json::json!({ "deps-lsp": { "freshness": { "cooldown_secs": 999 } } }),
1915 })
1916 .await;
1917
1918 assert_eq!(
1919 backend.config.read().await.freshness.cooldown_secs,
1920 60,
1921 "a malformed/unrecognized payload must not overwrite the previous configuration"
1922 );
1923 }
1924
1925 #[tokio::test]
1929 async fn test_did_change_configuration_null_settings_is_noop() {
1930 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1931 let backend = service.inner();
1932
1933 backend
1934 .did_change_configuration(DidChangeConfigurationParams {
1935 settings: serde_json::json!({ "freshness": { "cooldown_secs": 60 } }),
1936 })
1937 .await;
1938 assert_eq!(backend.config.read().await.freshness.cooldown_secs, 60);
1939
1940 backend
1941 .did_change_configuration(DidChangeConfigurationParams {
1942 settings: serde_json::Value::Null,
1943 })
1944 .await;
1945
1946 assert_eq!(backend.config.read().await.freshness.cooldown_secs, 60);
1947 }
1948
1949 #[tokio::test]
1954 async fn test_did_change_configuration_offline_to_online_transition_resumes_fetching() {
1955 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
1956 let backend = service.inner();
1957
1958 let mut server = mockito::Server::new_async().await;
1959 let url = format!("{}/api/data", server.url());
1960
1961 backend
1962 .did_change_configuration(DidChangeConfigurationParams {
1963 settings: serde_json::json!({ "network": { "offline": true } }),
1964 })
1965 .await;
1966 assert!(backend.state.cache.is_offline());
1967
1968 let blocked_mock = server
1969 .mock("GET", "/api/data")
1970 .with_status(200)
1971 .with_body("must not be fetched")
1972 .expect(0)
1973 .create_async()
1974 .await;
1975 let result = backend.state.cache.get_cached(&url).await;
1976 assert_matches!(result, Err(deps_core::DepsError::Offline { .. }));
1977 blocked_mock.assert_async().await;
1978
1979 backend
1980 .did_change_configuration(DidChangeConfigurationParams {
1981 settings: serde_json::json!({ "network": { "offline": false } }),
1982 })
1983 .await;
1984 assert!(!backend.state.cache.is_offline());
1985
1986 let resumed_mock = server
1987 .mock("GET", "/api/data")
1988 .with_status(200)
1989 .with_body("fetched after returning online")
1990 .expect(1)
1991 .create_async()
1992 .await;
1993 let result = backend.state.cache.get_cached(&url).await.unwrap();
1994 assert_eq!(result.as_ref(), b"fetched after returning online");
1995 resumed_mock.assert_async().await;
1996 }
1997
1998 #[tokio::test]
2003 async fn test_did_change_configuration_updates_cold_start_rate_limit() {
2004 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
2005 let backend = service.inner();
2006 let uri = deps_core::test_util::test_uri("/test.toml");
2007
2008 assert!(backend.state.cold_start_limiter.allow_cold_start(&uri));
2009 assert!(
2010 !backend.state.cold_start_limiter.allow_cold_start(&uri),
2011 "second immediate request blocked under the default 100ms interval"
2012 );
2013
2014 backend
2018 .did_change_configuration(DidChangeConfigurationParams {
2019 settings: serde_json::json!({ "cold_start": { "rate_limit_ms": 0 } }),
2020 })
2021 .await;
2022 assert_eq!(backend.config.read().await.cold_start.rate_limit_ms, 0);
2023
2024 assert!(
2025 backend.state.cold_start_limiter.allow_cold_start(&uri),
2026 "rate_limit_ms: 0 should allow a cold start immediately, with no wait"
2027 );
2028 }
2029
2030 #[cfg(feature = "cargo")]
2046 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
2047 async fn test_no_deadlock_between_config_write_and_concurrent_hover_and_diagnostics() {
2048 use crate::document::DocumentState;
2049 use crate::handlers::{diagnostics, hover};
2050 use deps_core::EcosystemId;
2051 use tokio::sync::Barrier;
2052 use tower_lsp_server::ls_types::{
2053 HoverParams, Position, TextDocumentIdentifier, TextDocumentPositionParams,
2054 };
2055
2056 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
2057 let backend = service.inner();
2058 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
2059
2060 let ecosystem = backend.state.ecosystem_registry.get("cargo").unwrap();
2063 let content = "[dependencies]\nserde = \"1.0.0\"\n".to_string();
2064 let parse_result = ecosystem.parse_manifest(&content, &uri).await.unwrap();
2065 let doc_state =
2066 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
2067 backend.state.update_document(uri.clone(), doc_state);
2068
2069 let barrier = Arc::new(Barrier::new(3));
2070
2071 let hover_task = tokio::spawn({
2072 let state = Arc::clone(&backend.state);
2073 let config = Arc::clone(&backend.config);
2074 let client = backend.client.clone();
2075 let uri = uri.clone();
2076 let barrier = Arc::clone(&barrier);
2077 async move {
2078 barrier.wait().await;
2079 let params = HoverParams {
2083 text_document_position_params: TextDocumentPositionParams {
2084 text_document: TextDocumentIdentifier { uri },
2085 position: Position::new(99, 0),
2086 },
2087 work_done_progress_params: Default::default(),
2088 };
2089 hover::handle_hover(state, params, client, config).await
2090 }
2091 });
2092
2093 let diagnostics_config_snapshot = { backend.config.read().await.diagnostics.clone() };
2094 let diagnostics_task = tokio::spawn({
2095 let state = Arc::clone(&backend.state);
2096 let config = Arc::clone(&backend.config);
2097 let client = backend.client.clone();
2098 let uri = uri.clone();
2099 let barrier = Arc::clone(&barrier);
2100 async move {
2101 barrier.wait().await;
2102 diagnostics::handle_diagnostics(
2103 state,
2104 &uri,
2105 &diagnostics_config_snapshot,
2106 client,
2107 config,
2108 )
2109 .await
2110 }
2111 });
2112
2113 let outcome = tokio::time::timeout(std::time::Duration::from_secs(5), async {
2114 barrier.wait().await;
2115 backend
2116 .did_change_configuration(DidChangeConfigurationParams {
2117 settings: serde_json::json!({ "freshness": { "cooldown_secs": 42 } }),
2118 })
2119 .await;
2120 tokio::join!(hover_task, diagnostics_task)
2121 })
2122 .await
2123 .expect(
2124 "hover/diagnostics must not deadlock against a concurrent \
2125 did_change_configuration write (issue #227 C1)",
2126 );
2127
2128 outcome.0.expect("hover task panicked");
2129 outcome.1.expect("diagnostics task panicked");
2130 assert_eq!(backend.config.read().await.freshness.cooldown_secs, 42);
2131 }
2132
2133 #[cfg(all(feature = "cargo", feature = "nuget"))]
2158 #[tokio::test]
2159 async fn test_rapid_config_changes_coalesce_into_a_union_scope_reparse() {
2160 use crate::document::DocumentState;
2161 use deps_core::{EcosystemId, PackageName, PackageVersions};
2162
2163 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
2164 let backend = service.inner();
2165
2166 let cargo_uri = deps_core::test_util::test_uri("/test/Cargo.toml");
2172 let cargo_ecosystem = backend.state.ecosystem_registry.get("cargo").unwrap();
2173 let cargo_content = "[dependencies]\nserde = \"1.0\"\n".to_string();
2174 let cargo_parse = cargo_ecosystem
2175 .parse_manifest(&cargo_content, &cargo_uri)
2176 .await
2177 .unwrap();
2178 let mut cargo_doc = DocumentState::new_from_parse_result(
2179 EcosystemId::Cargo,
2180 cargo_content,
2181 cargo_parse,
2182 );
2183 cargo_doc.set_version(Some(1));
2184 cargo_doc.update_cached_versions(HashMap::from([(
2185 PackageName::new("serde"),
2186 PackageVersions::latest_only("1.0.999"),
2187 )]));
2188 backend.state.update_document(cargo_uri.clone(), cargo_doc);
2189
2190 let nuget_uri = deps_core::test_util::test_uri("/test/project.csproj");
2191 let nuget_ecosystem = backend.state.ecosystem_registry.get("nuget").unwrap();
2192 let nuget_content = r#"<Project><ItemGroup><PackageReference Include="Newtonsoft.Json" Version="12.0.3" /></ItemGroup></Project>"#.to_string();
2193 let nuget_parse = nuget_ecosystem
2194 .parse_manifest(&nuget_content, &nuget_uri)
2195 .await
2196 .unwrap();
2197 let mut nuget_doc = DocumentState::new_from_parse_result(
2198 EcosystemId::NuGet,
2199 nuget_content,
2200 nuget_parse,
2201 );
2202 nuget_doc.set_version(Some(1));
2203 nuget_doc.update_cached_versions(HashMap::from([(
2204 PackageName::new("Newtonsoft.Json"),
2205 PackageVersions::latest_only("99.0.0"),
2206 )]));
2207 backend.state.update_document(nuget_uri.clone(), nuget_doc);
2208
2209 backend
2212 .did_change_configuration(DidChangeConfigurationParams {
2213 settings: serde_json::json!({ "registries": { "workspace_registries": "off" } }),
2214 })
2215 .await;
2216 backend
2217 .did_change_configuration(DidChangeConfigurationParams {
2218 settings: serde_json::json!({
2219 "registries": {
2220 "workspace_registries": "off",
2221 "nuget_user_profile_sources": true
2222 }
2223 }),
2224 })
2225 .await;
2226
2227 let both_cleared = tokio::time::timeout(std::time::Duration::from_secs(5), async {
2228 loop {
2229 let cargo_cleared = backend
2230 .state
2231 .get_document(&cargo_uri)
2232 .is_some_and(|d| d.cached_versions.is_empty());
2233 let nuget_cleared = backend
2234 .state
2235 .get_document(&nuget_uri)
2236 .is_some_and(|d| d.cached_versions.is_empty());
2237 if cargo_cleared && nuget_cleared {
2238 return;
2239 }
2240 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
2241 }
2242 })
2243 .await;
2244
2245 assert!(
2246 both_cleared.is_ok(),
2247 "both documents' stale cached_versions must be dropped by the coalesced \
2248 reparse — a lost scope would leave one of them untouched"
2249 );
2250 }
2251 }
2252
2253 #[cfg(feature = "cargo")]
2254 mod update_all_outdated_execute_command_tests {
2255 use super::*;
2256 use crate::document::DocumentState;
2257 use deps_core::EcosystemId;
2258
2259 fn command_params(uri: &Uri) -> ExecuteCommandParams {
2260 ExecuteCommandParams {
2261 command: commands::UPDATE_ALL_OUTDATED.to_string(),
2262 arguments: vec![serde_json::json!({ "uri": uri.as_str() })],
2263 work_done_progress_params: Default::default(),
2264 }
2265 }
2266
2267 #[tokio::test]
2268 async fn test_execute_command_update_all_outdated_closed_document_no_op() {
2269 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
2270 let backend = service.inner();
2271 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
2272
2273 assert!(backend.state.get_document(&uri).is_none());
2275
2276 let result = backend.execute_command(command_params(&uri)).await;
2277 assert!(result.is_ok());
2278 assert!(
2279 backend.state.get_document(&uri).is_none(),
2280 "a refused command must not create a document"
2281 );
2282 }
2283
2284 #[tokio::test]
2285 async fn test_execute_command_update_all_outdated_loading_document_no_op() {
2286 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
2287 let backend = service.inner();
2288 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
2289
2290 let ecosystem = backend.state.ecosystem_registry.get("cargo").unwrap();
2291 let content = "[dependencies]\nserde = \"1.0.0\"\n".to_string();
2292 let parse_result = ecosystem.parse_manifest(&content, &uri).await.unwrap();
2293 let mut doc_state = DocumentState::new_from_parse_result(
2294 EcosystemId::Cargo,
2295 content.clone(),
2296 parse_result,
2297 );
2298 doc_state.set_version(Some(1));
2299 doc_state.set_loading();
2300 assert!(!doc_state.is_ready_for_batch_update());
2303 backend.state.update_document(uri.clone(), doc_state);
2304
2305 let result = backend.execute_command(command_params(&uri)).await;
2306 assert!(result.is_ok());
2307 assert_eq!(
2308 backend.state.get_document(&uri).unwrap().content,
2309 content,
2310 "a refused command must not touch document content"
2311 );
2312 }
2313
2314 #[tokio::test]
2315 async fn test_execute_command_update_all_outdated_no_version_no_op() {
2316 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
2320 let backend = service.inner();
2321 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
2322
2323 let ecosystem = backend.state.ecosystem_registry.get("cargo").unwrap();
2324 let content = "[dependencies]\nserde = \"1.0.0\"\n".to_string();
2325 let parse_result = ecosystem.parse_manifest(&content, &uri).await.unwrap();
2326 let mut doc_state =
2327 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
2328 doc_state.set_loaded();
2329 assert!(!doc_state.is_ready_for_batch_update());
2331 backend.state.update_document(uri.clone(), doc_state);
2332
2333 let result = backend.execute_command(command_params(&uri)).await;
2334 assert!(result.is_ok());
2335 }
2336
2337 #[tokio::test]
2338 async fn test_execute_command_update_all_outdated_apply_edit_failure_does_not_panic() {
2339 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
2342 let backend = service.inner();
2343 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
2344
2345 let ecosystem = backend.state.ecosystem_registry.get("cargo").unwrap();
2346 let content = "[dependencies]\nserde = \"1.0.0\"\n".to_string();
2347 let parse_result = ecosystem.parse_manifest(&content, &uri).await.unwrap();
2348 let mut doc_state =
2349 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
2350 doc_state.set_version(Some(1));
2351 doc_state.set_loaded();
2352 assert!(doc_state.is_ready_for_batch_update());
2355 let mut cached = HashMap::new();
2356 cached.insert(
2357 "serde".into(),
2358 deps_core::PackageVersions::latest_only("1.2.0"),
2359 );
2360 doc_state.update_cached_versions(cached);
2361 backend.state.update_document(uri.clone(), doc_state);
2362
2363 let result = backend.execute_command(command_params(&uri)).await;
2364 assert!(result.is_ok());
2365 }
2366 }
2367}