1use crate::config::DepsConfig;
2use crate::document::{ServerState, handle_document_change, handle_document_open};
3use crate::file_watcher;
4use crate::handlers::{code_actions, completion, diagnostics, hover, inlay_hints};
5use std::collections::HashMap;
6use std::sync::Arc;
7use tokio::sync::RwLock;
8use tower_lsp_server::ls_types::{
9 CodeActionOptions, CodeActionParams, CodeActionProviderCapability, CompletionOptions,
10 CompletionParams, CompletionResponse, DiagnosticOptions, DiagnosticServerCapabilities,
11 DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidCloseTextDocumentParams,
12 DidOpenTextDocumentParams, DocumentDiagnosticParams, DocumentDiagnosticReport,
13 DocumentDiagnosticReportResult, ExecuteCommandOptions, ExecuteCommandParams,
14 FullDocumentDiagnosticReport, Hover, HoverParams, HoverProviderCapability, InitializeParams,
15 InitializeResult, InitializedParams, InlayHint, InlayHintParams, MessageType, OneOf, Range,
16 RelatedFullDocumentDiagnosticReport, ServerCapabilities, ServerInfo,
17 TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Uri, WorkspaceEdit,
18};
19use tower_lsp_server::{Client, LanguageServer, jsonrpc::Result};
20
21mod commands {
23 pub(super) const UPDATE_VERSION: &str = "deps-lsp.updateVersion";
25}
26
27pub struct Backend {
28 pub(crate) client: Client,
29 state: Arc<ServerState>,
30 config: Arc<RwLock<DepsConfig>>,
31 client_capabilities: Arc<RwLock<Option<tower_lsp_server::ls_types::ClientCapabilities>>>,
32}
33
34impl Backend {
35 pub fn new(client: Client) -> Self {
36 Self {
37 client,
38 state: Arc::new(ServerState::new()),
39 config: Arc::new(RwLock::new(DepsConfig::default())),
40 client_capabilities: Arc::new(RwLock::new(None)),
41 }
42 }
43
44 #[doc(hidden)]
46 pub const fn client(&self) -> &Client {
47 &self.client
48 }
49
50 async fn handle_open(&self, uri: tower_lsp_server::ls_types::Uri, content: String) {
52 match handle_document_open(
53 uri.clone(),
54 content,
55 Arc::clone(&self.state),
56 self.client.clone(),
57 Arc::clone(&self.config),
58 )
59 .await
60 {
61 Ok(task) => {
62 self.state.spawn_background_task(uri, task).await;
63 }
64 Err(e) => {
65 tracing::error!("failed to open document {:?}: {}", uri, e);
66 self.client
67 .log_message(MessageType::ERROR, format!("Parse error: {e}"))
68 .await;
69 }
70 }
71 }
72
73 async fn handle_change(&self, uri: tower_lsp_server::ls_types::Uri, content: String) {
75 match handle_document_change(
76 uri.clone(),
77 content,
78 Arc::clone(&self.state),
79 self.client.clone(),
80 Arc::clone(&self.config),
81 )
82 .await
83 {
84 Ok(task) => {
85 self.state.spawn_background_task(uri, task).await;
86 }
87 Err(e) => {
88 tracing::error!("failed to process document change {:?}: {}", uri, e);
89 }
90 }
91 }
92
93 async fn handle_lockfile_change(&self, lockfile_path: &std::path::Path, ecosystem_id: &str) {
94 let Some(ecosystem) = self.state.ecosystem_registry.get(ecosystem_id) else {
95 tracing::error!("Unknown ecosystem: {}", ecosystem_id);
96 return;
97 };
98
99 let Some(lock_provider) = ecosystem.lockfile_provider() else {
100 tracing::warn!("Ecosystem {} has no lock file provider", ecosystem_id);
101 return;
102 };
103
104 let affected_uris: Vec<Uri> = self
106 .state
107 .documents
108 .iter()
109 .filter_map(|entry| {
110 let uri = entry.key();
111 let doc = entry.value();
112 if doc.ecosystem_id != ecosystem_id {
113 return None;
114 }
115 let doc_lockfile = lock_provider.locate_lockfile(uri)?;
116 if doc_lockfile == lockfile_path {
117 Some(uri.clone())
118 } else {
119 None
120 }
121 })
122 .collect();
123
124 if affected_uris.is_empty() {
125 tracing::debug!(
126 "No open manifests affected by lock file: {}",
127 lockfile_path.display()
128 );
129 return;
130 }
131
132 tracing::info!(
133 "Updating {} manifest(s) affected by lock file change",
134 affected_uris.len()
135 );
136
137 let resolved_versions = match self
139 .state
140 .lockfile_cache
141 .get_or_parse(lock_provider.as_ref(), lockfile_path)
142 .await
143 {
144 Ok(packages) => packages
145 .iter()
146 .map(|(name, pkg)| (name.clone(), pkg.version.clone()))
147 .collect::<HashMap<String, String>>(),
148 Err(e) => {
149 tracing::error!("Failed to reload lock file: {}", e);
150 self.client
151 .log_message(
152 MessageType::ERROR,
153 format!("Failed to reload lock file: {e}"),
154 )
155 .await;
156 HashMap::new()
157 }
158 };
159
160 let config = self.config.read().await;
161
162 for uri in affected_uris {
163 if let Some(mut doc) = self.state.documents.get_mut(&uri) {
164 doc.update_resolved_versions(resolved_versions.clone());
165 }
166
167 let items = diagnostics::handle_diagnostics(
168 Arc::clone(&self.state),
169 &uri,
170 &config.diagnostics,
171 self.client.clone(),
172 Arc::clone(&self.config),
173 )
174 .await;
175
176 self.client.publish_diagnostics(uri, items, None).await;
177 }
178
179 if let Err(e) = self.client.inlay_hint_refresh().await {
180 tracing::debug!("inlay_hint_refresh not supported: {:?}", e);
181 }
182 }
183
184 #[allow(dead_code)]
186 async fn supports_progress(&self) -> bool {
187 let caps = self.client_capabilities.read().await;
188 caps.as_ref()
189 .and_then(|c| c.window.as_ref())
190 .and_then(|w| w.work_done_progress)
191 .unwrap_or(false)
192 }
193
194 fn server_capabilities() -> ServerCapabilities {
195 ServerCapabilities {
196 text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
197 completion_provider: Some(CompletionOptions {
198 trigger_characters: Some(vec!["\"".into(), "=".into(), ".".into()]),
199 resolve_provider: Some(false),
200 ..Default::default()
201 }),
202 hover_provider: Some(HoverProviderCapability::Simple(true)),
203 inlay_hint_provider: Some(OneOf::Left(true)),
204 code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
205 code_action_kinds: Some(vec![tower_lsp_server::ls_types::CodeActionKind::REFACTOR]),
206 ..Default::default()
207 })),
208 diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
209 identifier: Some("deps".into()),
210 inter_file_dependencies: false,
211 workspace_diagnostics: false,
212 ..Default::default()
213 })),
214 execute_command_provider: Some(ExecuteCommandOptions {
215 commands: vec![commands::UPDATE_VERSION.into()],
216 ..Default::default()
217 }),
218 ..Default::default()
219 }
220 }
221}
222
223impl LanguageServer for Backend {
224 async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
225 tracing::info!("initializing deps-lsp server");
226
227 *self.client_capabilities.write().await = Some(params.capabilities.clone());
229
230 if let Some(init_options) = params.initialization_options
232 && let Ok(config) = serde_json::from_value::<DepsConfig>(init_options)
233 {
234 tracing::debug!("loaded configuration: {:?}", config);
235 *self.config.write().await = config;
236 }
237
238 Ok(InitializeResult {
239 capabilities: Self::server_capabilities(),
240 server_info: Some(ServerInfo {
241 name: "deps-lsp".into(),
242 version: Some(env!("CARGO_PKG_VERSION").into()),
243 }),
244 })
245 }
246
247 async fn initialized(&self, _: InitializedParams) {
248 tracing::info!("deps-lsp server initialized");
249 self.client
250 .log_message(
251 MessageType::INFO,
252 format!(
253 "deps-lsp v{} ({} {})",
254 env!("CARGO_PKG_VERSION"),
255 env!("GIT_HASH"),
256 env!("BUILD_TIME")
257 ),
258 )
259 .await;
260
261 let patterns = self.state.ecosystem_registry.all_lockfile_patterns();
263 if let Err(e) = file_watcher::register_lock_file_watchers(&self.client, &patterns).await {
264 tracing::warn!("Failed to register file watchers: {}", e);
265 self.client
266 .log_message(MessageType::WARNING, format!("File watching disabled: {e}"))
267 .await;
268 }
269
270 let state_clone = Arc::clone(&self.state);
272 tokio::spawn(async move {
273 let mut interval = tokio::time::interval(std::time::Duration::from_secs(60));
274 loop {
275 interval.tick().await;
276 state_clone
278 .cold_start_limiter
279 .cleanup_old_entries(std::time::Duration::from_secs(300));
280 tracing::trace!("Cleaned up old cold start rate limit entries");
281 }
282 });
283 }
284
285 async fn shutdown(&self) -> Result<()> {
286 tracing::info!("shutting down deps-lsp server");
287 Ok(())
288 }
289
290 async fn did_open(&self, params: DidOpenTextDocumentParams) {
291 let uri = params.text_document.uri;
292 let content = params.text_document.text;
293
294 tracing::info!("document opened: {:?}", uri);
295
296 if self.state.ecosystem_registry.get_for_uri(&uri).is_none() {
298 tracing::debug!("unsupported file type: {:?}", uri);
299 return;
300 }
301
302 self.handle_open(uri, content).await;
303 }
304
305 async fn did_change(&self, params: DidChangeTextDocumentParams) {
306 let uri = params.text_document.uri;
307
308 if let Some(change) = params.content_changes.first() {
309 let content = change.text.clone();
310
311 if self.state.ecosystem_registry.get_for_uri(&uri).is_none() {
313 tracing::debug!("unsupported file type: {:?}", uri);
314 return;
315 }
316
317 self.handle_change(uri, content).await;
318 }
319 }
320
321 async fn did_close(&self, params: DidCloseTextDocumentParams) {
322 let uri = params.text_document.uri;
323 tracing::info!("document closed: {:?}", uri);
324
325 self.state.remove_document(&uri);
326 self.state.cancel_background_task(&uri).await;
327 }
328
329 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
330 tracing::debug!("Received {} file change events", params.changes.len());
331
332 for change in params.changes {
333 let Some(path) = change.uri.to_file_path() else {
334 tracing::warn!("Invalid file path in change event: {:?}", change.uri);
335 continue;
336 };
337
338 let Some(filename) = file_watcher::extract_lockfile_name(&path) else {
339 continue;
340 };
341
342 let Some(ecosystem) = self.state.ecosystem_registry.get_for_lockfile(filename) else {
343 tracing::debug!("Skipping non-lock-file change: {}", filename);
344 continue;
345 };
346
347 tracing::info!(
348 "Lock file changed: {} (ecosystem: {})",
349 filename,
350 ecosystem.id()
351 );
352
353 self.state.lockfile_cache.invalidate(&path);
354 self.handle_lockfile_change(&path, ecosystem.id()).await;
355 }
356 }
357
358 async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
359 Ok(hover::handle_hover(
360 Arc::clone(&self.state),
361 params,
362 self.client.clone(),
363 Arc::clone(&self.config),
364 )
365 .await)
366 }
367
368 async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
369 Ok(completion::handle_completion(
370 Arc::clone(&self.state),
371 params,
372 self.client.clone(),
373 Arc::clone(&self.config),
374 )
375 .await)
376 }
377
378 async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
379 let inlay_config = { self.config.read().await.inlay_hints.clone() };
381 let range = params.range;
382
383 let hints: Vec<_> = inlay_hints::handle_inlay_hints(
384 Arc::clone(&self.state),
385 params,
386 &inlay_config,
387 self.client.clone(),
388 Arc::clone(&self.config),
389 )
390 .await
391 .into_iter()
392 .filter(|h| h.position.line >= range.start.line && h.position.line <= range.end.line)
393 .collect();
394
395 Ok(Some(hints))
396 }
397
398 async fn code_action(
399 &self,
400 params: CodeActionParams,
401 ) -> Result<Option<Vec<tower_lsp_server::ls_types::CodeActionOrCommand>>> {
402 tracing::info!(
403 "code_action request: uri={:?}, range={:?}",
404 params.text_document.uri,
405 params.range
406 );
407 let actions = code_actions::handle_code_actions(
408 Arc::clone(&self.state),
409 params,
410 self.client.clone(),
411 Arc::clone(&self.config),
412 )
413 .await;
414 tracing::info!("code_action response: {} actions", actions.len());
415 Ok(Some(actions))
416 }
417
418 async fn diagnostic(
419 &self,
420 params: DocumentDiagnosticParams,
421 ) -> Result<DocumentDiagnosticReportResult> {
422 let uri = params.text_document.uri;
423 tracing::info!("diagnostic request for: {:?}", uri);
424
425 let diagnostics_config = { self.config.read().await.diagnostics.clone() };
427
428 let items = diagnostics::handle_diagnostics(
429 Arc::clone(&self.state),
430 &uri,
431 &diagnostics_config,
432 self.client.clone(),
433 Arc::clone(&self.config),
434 )
435 .await;
436
437 tracing::info!("returning {} diagnostics", items.len());
438
439 Ok(DocumentDiagnosticReportResult::Report(
440 DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
441 related_documents: None,
442 full_document_diagnostic_report: FullDocumentDiagnosticReport {
443 result_id: None,
444 items,
445 },
446 }),
447 ))
448 }
449
450 async fn execute_command(
451 &self,
452 params: ExecuteCommandParams,
453 ) -> Result<Option<serde_json::Value>> {
454 tracing::info!("execute_command: {:?}", params.command);
455
456 if params.command == commands::UPDATE_VERSION
457 && let Some(args) = params.arguments.first()
458 && let Ok(update_args) = serde_json::from_value::<UpdateVersionArgs>(args.clone())
459 {
460 let mut edits = HashMap::new();
461 edits.insert(
462 update_args.uri.clone(),
463 vec![TextEdit {
464 range: update_args.range,
465 new_text: format!("\"{}\"", update_args.version),
466 }],
467 );
468
469 let edit = WorkspaceEdit {
470 changes: Some(edits),
471 ..Default::default()
472 };
473
474 if let Err(e) = self.client.apply_edit(edit).await {
475 tracing::error!("Failed to apply edit: {:?}", e);
476 }
477 }
478
479 Ok(None)
480 }
481}
482
483#[derive(serde::Deserialize)]
484struct UpdateVersionArgs {
485 uri: Uri,
486 range: Range,
487 version: String,
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
495 fn test_server_capabilities() {
496 let caps = Backend::server_capabilities();
497
498 assert!(caps.text_document_sync.is_some());
500
501 assert!(caps.completion_provider.is_some());
503 let completion = caps.completion_provider.unwrap();
504 assert!(!completion.resolve_provider.unwrap()); assert!(caps.hover_provider.is_some());
508
509 assert!(caps.inlay_hint_provider.is_some());
511
512 assert!(caps.diagnostic_provider.is_some());
514 }
515
516 #[tokio::test]
517 async fn test_backend_creation() {
518 let (_service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
519 }
522
523 #[tokio::test]
524 async fn test_initialize_without_options() {
525 let (_service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
526 }
529
530 #[test]
531 fn test_server_capabilities_text_document_sync() {
532 let caps = Backend::server_capabilities();
533
534 match caps.text_document_sync {
535 Some(TextDocumentSyncCapability::Kind(kind)) => {
536 assert_eq!(kind, TextDocumentSyncKind::FULL);
537 }
538 _ => panic!("Expected text document sync kind to be FULL"),
539 }
540 }
541
542 #[test]
543 fn test_server_capabilities_completion_triggers() {
544 let caps = Backend::server_capabilities();
545
546 let completion = caps
547 .completion_provider
548 .expect("completion provider should exist");
549 let triggers = completion
550 .trigger_characters
551 .expect("trigger characters should exist");
552
553 assert!(triggers.contains(&"\"".to_string()));
554 assert!(triggers.contains(&"=".to_string()));
555 assert!(triggers.contains(&".".to_string()));
556 assert_eq!(triggers.len(), 3);
557 }
558
559 #[test]
560 fn test_server_capabilities_code_actions() {
561 let caps = Backend::server_capabilities();
562
563 match caps.code_action_provider {
564 Some(CodeActionProviderCapability::Options(opts)) => {
565 let kinds = opts
566 .code_action_kinds
567 .expect("code action kinds should exist");
568 assert!(kinds.contains(&tower_lsp_server::ls_types::CodeActionKind::REFACTOR));
569 }
570 _ => panic!("Expected code action provider options"),
571 }
572 }
573
574 #[test]
575 fn test_server_capabilities_diagnostics_config() {
576 let caps = Backend::server_capabilities();
577
578 match caps.diagnostic_provider {
579 Some(DiagnosticServerCapabilities::Options(opts)) => {
580 assert_eq!(opts.identifier, Some("deps".to_string()));
581 assert!(!opts.inter_file_dependencies);
582 assert!(!opts.workspace_diagnostics);
583 }
584 _ => panic!("Expected diagnostic options"),
585 }
586 }
587
588 #[test]
589 fn test_server_capabilities_execute_command() {
590 let caps = Backend::server_capabilities();
591
592 let execute = caps
593 .execute_command_provider
594 .expect("execute command provider should exist");
595 assert!(
596 execute
597 .commands
598 .contains(&commands::UPDATE_VERSION.to_string())
599 );
600 }
601
602 #[test]
603 fn test_commands_constants() {
604 assert_eq!(commands::UPDATE_VERSION, "deps-lsp.updateVersion");
605 }
606
607 #[tokio::test]
608 async fn test_backend_state_initialization() {
609 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
610 let backend = service.inner();
611
612 assert_eq!(backend.state.documents.len(), 0);
613 }
614
615 #[tokio::test]
616 async fn test_backend_config_initialization() {
617 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
618 let backend = service.inner();
619
620 let config = backend.config.read().await;
621 assert!(config.inlay_hints.enabled);
622 }
623
624 #[test]
625 fn test_update_version_args_deserialization() {
626 let json = serde_json::json!({
627 "uri": "file:///test/Cargo.toml",
628 "range": {
629 "start": {"line": 5, "character": 10},
630 "end": {"line": 5, "character": 15}
631 },
632 "version": "1.0.0"
633 });
634
635 let args: UpdateVersionArgs = serde_json::from_value(json).unwrap();
636 assert_eq!(args.version, "1.0.0");
637 assert_eq!(args.range.start.line, 5);
638 assert_eq!(args.range.start.character, 10);
639 }
640}