Skip to main content

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            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        // Register lock file watchers using patterns from all ecosystems
263        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        // Spawn background cleanup task for cold start rate limiter
272        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                // Clean up entries older than 5 minutes
278                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        // Use ecosystem registry to check if we support this file type
298        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            // Use ecosystem registry to check if we support this file type
313            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        // Clone config before async call to release lock early
381        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        // Clone config before async call to release lock early
427        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        // Verify text document sync
500        assert!(caps.text_document_sync.is_some());
501
502        // Verify completion provider
503        assert!(caps.completion_provider.is_some());
504        let completion = caps.completion_provider.unwrap();
505        assert!(!completion.resolve_provider.unwrap()); // resolve_provider is disabled
506
507        // Verify hover provider
508        assert!(caps.hover_provider.is_some());
509
510        // Verify inlay hints
511        assert!(caps.inlay_hint_provider.is_some());
512
513        // Verify diagnostics
514        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        // Backend should be created successfully
521        // This is a minimal smoke test
522    }
523
524    #[tokio::test]
525    async fn test_initialize_without_options() {
526        let (_service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
527        // Should initialize successfully with default config
528        // Integration tests will test actual LSP protocol
529    }
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}