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 offset_encoding: None,
245 })
246 }
247
248 async fn initialized(&self, _: InitializedParams) {
249 tracing::info!("deps-lsp server initialized");
250 self.client
251 .log_message(
252 MessageType::INFO,
253 format!(
254 "deps-lsp v{} ({} {})",
255 env!("CARGO_PKG_VERSION"),
256 env!("GIT_HASH"),
257 env!("BUILD_TIME")
258 ),
259 )
260 .await;
261
262 let patterns = self.state.ecosystem_registry.all_lockfile_patterns();
264 if let Err(e) = file_watcher::register_lock_file_watchers(&self.client, &patterns).await {
265 tracing::warn!("Failed to register file watchers: {}", e);
266 self.client
267 .log_message(MessageType::WARNING, format!("File watching disabled: {e}"))
268 .await;
269 }
270
271 let state_clone = Arc::clone(&self.state);
273 tokio::spawn(async move {
274 let mut interval = tokio::time::interval(std::time::Duration::from_secs(60));
275 loop {
276 interval.tick().await;
277 state_clone
279 .cold_start_limiter
280 .cleanup_old_entries(std::time::Duration::from_secs(300));
281 tracing::trace!("Cleaned up old cold start rate limit entries");
282 }
283 });
284 }
285
286 async fn shutdown(&self) -> Result<()> {
287 tracing::info!("shutting down deps-lsp server");
288 Ok(())
289 }
290
291 async fn did_open(&self, params: DidOpenTextDocumentParams) {
292 let uri = params.text_document.uri;
293 let content = params.text_document.text;
294
295 tracing::info!("document opened: {:?}", uri);
296
297 if self.state.ecosystem_registry.get_for_uri(&uri).is_none() {
299 tracing::debug!("unsupported file type: {:?}", uri);
300 return;
301 }
302
303 self.handle_open(uri, content).await;
304 }
305
306 async fn did_change(&self, params: DidChangeTextDocumentParams) {
307 let uri = params.text_document.uri;
308
309 if let Some(change) = params.content_changes.first() {
310 let content = change.text.clone();
311
312 if self.state.ecosystem_registry.get_for_uri(&uri).is_none() {
314 tracing::debug!("unsupported file type: {:?}", uri);
315 return;
316 }
317
318 self.handle_change(uri, content).await;
319 }
320 }
321
322 async fn did_close(&self, params: DidCloseTextDocumentParams) {
323 let uri = params.text_document.uri;
324 tracing::info!("document closed: {:?}", uri);
325
326 self.state.remove_document(&uri);
327 self.state.cancel_background_task(&uri).await;
328 }
329
330 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
331 tracing::debug!("Received {} file change events", params.changes.len());
332
333 for change in params.changes {
334 let Some(path) = change.uri.to_file_path() else {
335 tracing::warn!("Invalid file path in change event: {:?}", change.uri);
336 continue;
337 };
338
339 let Some(filename) = file_watcher::extract_lockfile_name(&path) else {
340 continue;
341 };
342
343 let Some(ecosystem) = self.state.ecosystem_registry.get_for_lockfile(filename) else {
344 tracing::debug!("Skipping non-lock-file change: {}", filename);
345 continue;
346 };
347
348 tracing::info!(
349 "Lock file changed: {} (ecosystem: {})",
350 filename,
351 ecosystem.id()
352 );
353
354 self.state.lockfile_cache.invalidate(&path);
355 self.handle_lockfile_change(&path, ecosystem.id()).await;
356 }
357 }
358
359 async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
360 Ok(hover::handle_hover(
361 Arc::clone(&self.state),
362 params,
363 self.client.clone(),
364 Arc::clone(&self.config),
365 )
366 .await)
367 }
368
369 async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
370 Ok(completion::handle_completion(
371 Arc::clone(&self.state),
372 params,
373 self.client.clone(),
374 Arc::clone(&self.config),
375 )
376 .await)
377 }
378
379 async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
380 let inlay_config = { self.config.read().await.inlay_hints.clone() };
382 let range = params.range;
383
384 let hints: Vec<_> = inlay_hints::handle_inlay_hints(
385 Arc::clone(&self.state),
386 params,
387 &inlay_config,
388 self.client.clone(),
389 Arc::clone(&self.config),
390 )
391 .await
392 .into_iter()
393 .filter(|h| h.position.line >= range.start.line && h.position.line <= range.end.line)
394 .collect();
395
396 Ok(Some(hints))
397 }
398
399 async fn code_action(
400 &self,
401 params: CodeActionParams,
402 ) -> Result<Option<Vec<tower_lsp_server::ls_types::CodeActionOrCommand>>> {
403 tracing::info!(
404 "code_action request: uri={:?}, range={:?}",
405 params.text_document.uri,
406 params.range
407 );
408 let actions = code_actions::handle_code_actions(
409 Arc::clone(&self.state),
410 params,
411 self.client.clone(),
412 Arc::clone(&self.config),
413 )
414 .await;
415 tracing::info!("code_action response: {} actions", actions.len());
416 Ok(Some(actions))
417 }
418
419 async fn diagnostic(
420 &self,
421 params: DocumentDiagnosticParams,
422 ) -> Result<DocumentDiagnosticReportResult> {
423 let uri = params.text_document.uri;
424 tracing::info!("diagnostic request for: {:?}", uri);
425
426 let diagnostics_config = { self.config.read().await.diagnostics.clone() };
428
429 let items = diagnostics::handle_diagnostics(
430 Arc::clone(&self.state),
431 &uri,
432 &diagnostics_config,
433 self.client.clone(),
434 Arc::clone(&self.config),
435 )
436 .await;
437
438 tracing::info!("returning {} diagnostics", items.len());
439
440 Ok(DocumentDiagnosticReportResult::Report(
441 DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
442 related_documents: None,
443 full_document_diagnostic_report: FullDocumentDiagnosticReport {
444 result_id: None,
445 items,
446 },
447 }),
448 ))
449 }
450
451 async fn execute_command(
452 &self,
453 params: ExecuteCommandParams,
454 ) -> Result<Option<serde_json::Value>> {
455 tracing::info!("execute_command: {:?}", params.command);
456
457 if params.command == commands::UPDATE_VERSION
458 && let Some(args) = params.arguments.first()
459 && let Ok(update_args) = serde_json::from_value::<UpdateVersionArgs>(args.clone())
460 {
461 let mut edits = HashMap::new();
462 edits.insert(
463 update_args.uri.clone(),
464 vec![TextEdit {
465 range: update_args.range,
466 new_text: format!("\"{}\"", update_args.version),
467 }],
468 );
469
470 let edit = WorkspaceEdit {
471 changes: Some(edits),
472 ..Default::default()
473 };
474
475 if let Err(e) = self.client.apply_edit(edit).await {
476 tracing::error!("Failed to apply edit: {:?}", e);
477 }
478 }
479
480 Ok(None)
481 }
482}
483
484#[derive(serde::Deserialize)]
485struct UpdateVersionArgs {
486 uri: Uri,
487 range: Range,
488 version: String,
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494
495 #[test]
496 fn test_server_capabilities() {
497 let caps = Backend::server_capabilities();
498
499 assert!(caps.text_document_sync.is_some());
501
502 assert!(caps.completion_provider.is_some());
504 let completion = caps.completion_provider.unwrap();
505 assert!(!completion.resolve_provider.unwrap()); assert!(caps.hover_provider.is_some());
509
510 assert!(caps.inlay_hint_provider.is_some());
512
513 assert!(caps.diagnostic_provider.is_some());
515 }
516
517 #[tokio::test]
518 async fn test_backend_creation() {
519 let (_service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
520 }
523
524 #[tokio::test]
525 async fn test_initialize_without_options() {
526 let (_service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
527 }
530
531 #[test]
532 fn test_server_capabilities_text_document_sync() {
533 let caps = Backend::server_capabilities();
534
535 match caps.text_document_sync {
536 Some(TextDocumentSyncCapability::Kind(kind)) => {
537 assert_eq!(kind, TextDocumentSyncKind::FULL);
538 }
539 _ => panic!("Expected text document sync kind to be FULL"),
540 }
541 }
542
543 #[test]
544 fn test_server_capabilities_completion_triggers() {
545 let caps = Backend::server_capabilities();
546
547 let completion = caps
548 .completion_provider
549 .expect("completion provider should exist");
550 let triggers = completion
551 .trigger_characters
552 .expect("trigger characters should exist");
553
554 assert!(triggers.contains(&"\"".to_string()));
555 assert!(triggers.contains(&"=".to_string()));
556 assert!(triggers.contains(&".".to_string()));
557 assert_eq!(triggers.len(), 3);
558 }
559
560 #[test]
561 fn test_server_capabilities_code_actions() {
562 let caps = Backend::server_capabilities();
563
564 match caps.code_action_provider {
565 Some(CodeActionProviderCapability::Options(opts)) => {
566 let kinds = opts
567 .code_action_kinds
568 .expect("code action kinds should exist");
569 assert!(kinds.contains(&tower_lsp_server::ls_types::CodeActionKind::REFACTOR));
570 }
571 _ => panic!("Expected code action provider options"),
572 }
573 }
574
575 #[test]
576 fn test_server_capabilities_diagnostics_config() {
577 let caps = Backend::server_capabilities();
578
579 match caps.diagnostic_provider {
580 Some(DiagnosticServerCapabilities::Options(opts)) => {
581 assert_eq!(opts.identifier, Some("deps".to_string()));
582 assert!(!opts.inter_file_dependencies);
583 assert!(!opts.workspace_diagnostics);
584 }
585 _ => panic!("Expected diagnostic options"),
586 }
587 }
588
589 #[test]
590 fn test_server_capabilities_execute_command() {
591 let caps = Backend::server_capabilities();
592
593 let execute = caps
594 .execute_command_provider
595 .expect("execute command provider should exist");
596 assert!(
597 execute
598 .commands
599 .contains(&commands::UPDATE_VERSION.to_string())
600 );
601 }
602
603 #[test]
604 fn test_commands_constants() {
605 assert_eq!(commands::UPDATE_VERSION, "deps-lsp.updateVersion");
606 }
607
608 #[tokio::test]
609 async fn test_backend_state_initialization() {
610 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
611 let backend = service.inner();
612
613 assert_eq!(backend.state.documents.len(), 0);
614 }
615
616 #[tokio::test]
617 async fn test_backend_config_initialization() {
618 let (service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
619 let backend = service.inner();
620
621 let config = backend.config.read().await;
622 assert!(config.inlay_hints.enabled);
623 }
624
625 #[test]
626 fn test_update_version_args_deserialization() {
627 let json = serde_json::json!({
628 "uri": "file:///test/Cargo.toml",
629 "range": {
630 "start": {"line": 5, "character": 10},
631 "end": {"line": 5, "character": 15}
632 },
633 "version": "1.0.0"
634 });
635
636 let args: UpdateVersionArgs = serde_json::from_value(json).unwrap();
637 assert_eq!(args.version, "1.0.0");
638 assert_eq!(args.range.start.line, 5);
639 assert_eq!(args.range.start.character, 10);
640 }
641}