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        })
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        // Register lock file watchers using patterns from all ecosystems
254        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        // Spawn background cleanup task for cold start rate limiter
263        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                // Clean up entries older than 5 minutes
269                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        // Use ecosystem registry to check if we support this file type
289        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            // Use ecosystem registry to check if we support this file type
304            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        // Clone config before async call to release lock early
372        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        // Clone config before async call to release lock early
414        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        // Verify text document sync
487        assert!(caps.text_document_sync.is_some());
488
489        // Verify completion provider
490        assert!(caps.completion_provider.is_some());
491        let completion = caps.completion_provider.unwrap();
492        assert!(!completion.resolve_provider.unwrap()); // resolve_provider is disabled
493
494        // Verify hover provider
495        assert!(caps.hover_provider.is_some());
496
497        // Verify inlay hints
498        assert!(caps.inlay_hint_provider.is_some());
499
500        // Verify diagnostics
501        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        // Backend should be created successfully
508        // This is a minimal smoke test
509    }
510
511    #[tokio::test]
512    async fn test_initialize_without_options() {
513        let (_service, _socket) = tower_lsp_server::LspService::build(Backend::new).finish();
514        // Should initialize successfully with default config
515        // Integration tests will test actual LSP protocol
516    }
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}