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