deps_lsp/
server.rs

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
21/// LSP command identifiers.
22mod commands {
23    /// Command to update a dependency version.
24    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    /// Get a reference to the LSP client (primarily for testing/benchmarking).
45    #[doc(hidden)]
46    pub const fn client(&self) -> &Client {
47        &self.client
48    }
49
50    /// Handles opening a document using unified ecosystem registry.
51    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    /// Handles changes to a document using unified ecosystem registry.
74    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        // Find all open documents using this lock file
105        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        // Reload lock file (cache was invalidated, so this re-parses)
138        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    /// Check if client supports work done progress.
185    #[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        // Store client capabilities
228        *self.client_capabilities.write().await = Some(params.capabilities.clone());
229
230        // Parse initialization options
231        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        // Register lock file watchers using patterns from all ecosystems
262        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        // Spawn background cleanup task for cold start rate limiter
271        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                // Clean up entries older than 5 minutes
277                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        // Use ecosystem registry to check if we support this file type
297        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            // Use ecosystem registry to check if we support this file type
312            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        // Clone config before async call to release lock early
380        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        // Clone config before async call to release lock early
426        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        // Verify text document sync
499        assert!(caps.text_document_sync.is_some());
500
501        // Verify completion provider
502        assert!(caps.completion_provider.is_some());
503        let completion = caps.completion_provider.unwrap();
504        assert!(!completion.resolve_provider.unwrap()); // resolve_provider is disabled
505
506        // Verify hover provider
507        assert!(caps.hover_provider.is_some());
508
509        // Verify inlay hints
510        assert!(caps.inlay_hint_provider.is_some());
511
512        // Verify diagnostics
513        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        // Backend should be created successfully
520        // This is a minimal smoke test
521    }
522
523    #[tokio::test]
524    async fn test_initialize_without_options() {
525        let (_service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
526        // Should initialize successfully with default config
527        // Integration tests will test actual LSP protocol
528    }
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}