deps_core/
handler.rs

1//! Generic LSP handler infrastructure.
2//!
3//! Provides traits and generic functions for implementing LSP operations
4//! (inlay hints, hover, etc.) across different package ecosystems.
5//!
6//! # Deprecation Notice
7//!
8//! This module is being phased out in favor of the new `Ecosystem` trait.
9//! The `EcosystemHandler` trait will be removed in a future version.
10//! New implementations should use `crate::ecosystem::Ecosystem` instead.
11
12use crate::HttpCache;
13use crate::parser::DependencyInfo;
14use crate::registry::{PackageRegistry, VersionInfo};
15use async_trait::async_trait;
16use futures::future::join_all;
17use std::collections::HashMap;
18use std::sync::Arc;
19use tower_lsp_server::ls_types::{
20    InlayHint, InlayHintKind, InlayHintLabel, InlayHintLabelPart, MarkupContent, MarkupKind, Range,
21};
22
23/// Maximum number of versions to display in hover tooltips.
24const MAX_VERSIONS_IN_HOVER: usize = 8;
25
26/// Maximum number of features to display in hover tooltips.
27const MAX_FEATURES_IN_HOVER: usize = 10;
28
29/// Maximum number of versions to offer in code action suggestions.
30const MAX_CODE_ACTION_VERSIONS: usize = 5;
31
32/// Generic handler for LSP operations across ecosystems.
33///
34/// This trait uses Generic Associated Types (GATs) to provide
35/// a unified interface for handlers while maintaining strong typing.
36///
37/// Implementors provide ecosystem-specific behavior (registry access,
38/// URL construction, version matching) while the generic handler
39/// functions provide the common LSP logic.
40///
41/// # Examples
42///
43/// ```no_run
44/// use deps_core::{EcosystemHandler, HttpCache, PackageRegistry, DependencyInfo};
45/// use async_trait::async_trait;
46/// use std::sync::Arc;
47///
48/// # #[derive(Clone)] struct MyVersion { version: String, yanked: bool }
49/// # impl deps_core::VersionInfo for MyVersion {
50/// #     fn version_string(&self) -> &str { &self.version }
51/// #     fn is_yanked(&self) -> bool { self.yanked }
52/// # }
53/// # #[derive(Clone)] struct MyMetadata { name: String }
54/// # impl deps_core::PackageMetadata for MyMetadata {
55/// #     fn name(&self) -> &str { &self.name }
56/// #     fn description(&self) -> Option<&str> { None }
57/// #     fn repository(&self) -> Option<&str> { None }
58/// #     fn documentation(&self) -> Option<&str> { None }
59/// #     fn latest_version(&self) -> &str { "1.0.0" }
60/// # }
61/// # #[derive(Clone)] struct MyDependency { name: String }
62/// # impl DependencyInfo for MyDependency {
63/// #     fn name(&self) -> &str { &self.name }
64/// #     fn name_range(&self) -> tower_lsp_server::ls_types::Range { tower_lsp_server::ls_types::Range::default() }
65/// #     fn version_requirement(&self) -> Option<&str> { None }
66/// #     fn version_range(&self) -> Option<tower_lsp_server::ls_types::Range> { None }
67/// #     fn source(&self) -> deps_core::parser::DependencySource { deps_core::parser::DependencySource::Registry }
68/// # }
69/// # #[derive(Clone)] struct MyRegistry;
70/// # #[async_trait]
71/// # impl PackageRegistry for MyRegistry {
72/// #     type Version = MyVersion;
73/// #     type Metadata = MyMetadata;
74/// #     type VersionReq = String;
75/// #     async fn get_versions(&self, _name: &str) -> deps_core::error::Result<Vec<Self::Version>> { Ok(vec![]) }
76/// #     async fn get_latest_matching(&self, _name: &str, _req: &Self::VersionReq) -> deps_core::error::Result<Option<Self::Version>> { Ok(None) }
77/// #     async fn search(&self, _query: &str, _limit: usize) -> deps_core::error::Result<Vec<Self::Metadata>> { Ok(vec![]) }
78/// # }
79/// struct MyHandler {
80///     registry: MyRegistry,
81/// }
82///
83/// #[async_trait]
84/// impl EcosystemHandler for MyHandler {
85///     type Registry = MyRegistry;
86///     type Dependency = MyDependency;
87///     type UnifiedDep = MyDependency; // In real implementation, this would be UnifiedDependency enum
88///
89///     fn new(_cache: Arc<HttpCache>) -> Self {
90///         Self {
91///             registry: MyRegistry,
92///         }
93///     }
94///
95///     fn registry(&self) -> &Self::Registry {
96///         &self.registry
97///     }
98///
99///     fn extract_dependency(dep: &Self::UnifiedDep) -> Option<&Self::Dependency> {
100///         // In real implementation, match on the enum variant
101///         Some(dep)
102///     }
103///
104///     fn package_url(name: &str) -> String {
105///         format!("https://myregistry.org/package/{}", name)
106///     }
107///
108///     fn ecosystem_display_name() -> &'static str {
109///         "MyRegistry"
110///     }
111///
112///     fn is_version_latest(version_req: &str, latest: &str) -> bool {
113///         version_req == latest
114///     }
115///
116///     fn format_version_for_edit(_dep: &Self::Dependency, version: &str) -> String {
117///         format!("\"{}\"", version)
118///     }
119///
120///     fn is_deprecated(version: &MyVersion) -> bool {
121///         version.yanked
122///     }
123///
124///     fn is_valid_version_syntax(_version_req: &str) -> bool {
125///         true
126///     }
127///
128///     fn parse_version_req(version_req: &str) -> Option<String> {
129///         Some(version_req.to_string())
130///     }
131/// }
132/// ```
133#[async_trait]
134pub trait EcosystemHandler: Send + Sync + Sized {
135    /// Registry type for this ecosystem.
136    type Registry: PackageRegistry + Clone;
137
138    /// Dependency type for this ecosystem.
139    type Dependency: DependencyInfo;
140
141    /// Unified dependency type (typically deps_lsp::document::UnifiedDependency).
142    ///
143    /// This is an associated type to avoid unsafe transmute when extracting
144    /// ecosystem-specific dependencies from the unified enum.
145    type UnifiedDep;
146
147    /// Create a new handler with the given cache.
148    fn new(cache: Arc<HttpCache>) -> Self;
149
150    /// Get the registry instance.
151    fn registry(&self) -> &Self::Registry;
152
153    /// Extract typed dependency from a unified dependency enum.
154    ///
155    /// Returns Some if the unified dependency matches this handler's ecosystem,
156    /// None otherwise.
157    fn extract_dependency(dep: &Self::UnifiedDep) -> Option<&Self::Dependency>;
158
159    /// Package URL for this ecosystem (e.g., crates.io, npmjs.com).
160    ///
161    /// Used in inlay hint commands and hover tooltips.
162    fn package_url(name: &str) -> String;
163
164    /// Display name for the ecosystem (e.g., "crates.io", "PyPI").
165    ///
166    /// Used in LSP command titles.
167    fn ecosystem_display_name() -> &'static str;
168
169    /// Check if version is latest (ecosystem-specific logic).
170    ///
171    /// Returns true if the latest version satisfies the version requirement,
172    /// meaning the dependency is up-to-date within its constraint.
173    fn is_version_latest(version_req: &str, latest: &str) -> bool;
174
175    /// Format a version string for editing in the manifest.
176    ///
177    /// Different ecosystems have different formatting conventions:
178    /// - Cargo: `"1.0.0"` (bare semver)
179    /// - npm: `"1.0.0"` (bare version, caret added by package manager)
180    /// - PyPI PEP 621: `>=1.0.0` (no quotes in array)
181    /// - PyPI Poetry: `"^1.0.0"` (caret in quotes)
182    fn format_version_for_edit(dep: &Self::Dependency, version: &str) -> String;
183
184    /// Check if a version is deprecated/yanked.
185    ///
186    /// Returns true if the version should be filtered out from suggestions.
187    fn is_deprecated(version: &<Self::Registry as PackageRegistry>::Version) -> bool;
188
189    /// Validate version requirement syntax.
190    ///
191    /// Returns true if the version requirement is valid for this ecosystem.
192    /// Used for diagnostic validation (semver for Cargo, PEP 440 for PyPI, etc.)
193    fn is_valid_version_syntax(version_req: &str) -> bool;
194
195    /// Parse a version requirement string into the registry's VersionReq type.
196    ///
197    /// Returns None if the version requirement is invalid.
198    fn parse_version_req(
199        version_req: &str,
200    ) -> Option<<Self::Registry as PackageRegistry>::VersionReq>;
201
202    /// Get lock file provider for this ecosystem.
203    ///
204    /// Returns `None` if the ecosystem doesn't support lock files.
205    /// Default implementation returns `None`.
206    ///
207    /// # Examples
208    ///
209    /// ```ignore
210    /// // Override in handler implementation:
211    /// fn lockfile_provider(&self) -> Option<Arc<dyn LockFileProvider>> {
212    ///     Some(Arc::new(MyLockParser))
213    /// }
214    /// ```
215    fn lockfile_provider(&self) -> Option<Arc<dyn crate::lockfile::LockFileProvider>> {
216        None
217    }
218}
219
220/// Configuration for inlay hint display.
221///
222/// This is a simplified version to avoid circular dependencies.
223/// The actual type comes from deps-lsp/config.rs.
224pub struct InlayHintsConfig {
225    pub enabled: bool,
226    pub up_to_date_text: String,
227    pub needs_update_text: String,
228}
229
230impl Default for InlayHintsConfig {
231    fn default() -> Self {
232        Self {
233            enabled: true,
234            up_to_date_text: "✅".to_string(),
235            needs_update_text: "❌ {}".to_string(),
236        }
237    }
238}
239
240/// Helper trait for accessing version string from unified version types.
241///
242/// Allows generic code to work with UnifiedVersion without circular dependency.
243pub trait VersionStringGetter {
244    fn version_string(&self) -> &str;
245}
246
247/// Helper trait for checking if a version is yanked.
248///
249/// Allows generic code to work with UnifiedVersion without circular dependency.
250pub trait YankedChecker {
251    fn is_yanked(&self) -> bool;
252}
253
254/// Generic inlay hints generator.
255///
256/// Handles the common logic of fetching versions, checking cache,
257/// and creating hints. Ecosystem-specific behavior is delegated
258/// to the EcosystemHandler trait.
259///
260/// # Type Parameters
261///
262/// * `H` - Ecosystem handler type
263/// * `UnifiedVer` - Unified version enum (typically UnifiedVersion from deps-lsp)
264///
265/// # Arguments
266///
267/// * `handler` - Ecosystem-specific handler instance
268/// * `dependencies` - List of dependencies to process
269/// * `cached_versions` - Previously cached version information
270/// * `resolved_versions` - Resolved versions from lock file
271/// * `config` - Display configuration
272///
273/// # Returns
274///
275/// Vector of inlay hints for the LSP client.
276pub async fn generate_inlay_hints<H, UnifiedVer>(
277    handler: &H,
278    dependencies: &[H::UnifiedDep],
279    cached_versions: &HashMap<String, UnifiedVer>,
280    resolved_versions: &HashMap<String, String>,
281    config: &InlayHintsConfig,
282) -> Vec<InlayHint>
283where
284    H: EcosystemHandler,
285    UnifiedVer: VersionStringGetter + YankedChecker,
286{
287    // Pre-allocate with estimated capacity
288    let mut cached_deps = Vec::with_capacity(dependencies.len());
289    let mut fetch_deps = Vec::with_capacity(dependencies.len());
290
291    for dep in dependencies {
292        let Some(typed_dep) = H::extract_dependency(dep) else {
293            continue;
294        };
295
296        let Some(version_req) = typed_dep.version_requirement() else {
297            continue;
298        };
299        let Some(version_range) = typed_dep.version_range() else {
300            continue;
301        };
302
303        let name = typed_dep.name();
304        if let Some(cached) = cached_versions.get(name) {
305            cached_deps.push((
306                name.to_string(),
307                version_req.to_string(),
308                version_range,
309                cached.version_string().to_string(),
310                cached.is_yanked(),
311            ));
312        } else {
313            fetch_deps.push((name.to_string(), version_req.to_string(), version_range));
314        }
315    }
316
317    let registry = handler.registry().clone();
318    let futures: Vec<_> = fetch_deps
319        .into_iter()
320        .map(|(name, version_req, version_range)| {
321            let registry = registry.clone();
322            async move {
323                let result = registry.get_versions(&name).await;
324                (name, version_req, version_range, result)
325            }
326        })
327        .collect();
328
329    let fetch_results = join_all(futures).await;
330
331    let mut hints = Vec::new();
332
333    for (name, version_req, version_range, latest_version, is_yanked) in cached_deps {
334        if is_yanked {
335            continue;
336        }
337        // Use resolved version from lock file if available, otherwise fall back to requirement
338        let version_to_compare = resolved_versions
339            .get(&name)
340            .map(String::as_str)
341            .unwrap_or(&version_req);
342        let is_latest = H::is_version_latest(version_to_compare, &latest_version);
343        hints.push(create_hint::<H>(
344            &name,
345            version_range,
346            &latest_version,
347            is_latest,
348            config,
349        ));
350    }
351
352    for (name, version_req, version_range, result) in fetch_results {
353        let Ok(versions): std::result::Result<Vec<<H::Registry as PackageRegistry>::Version>, _> =
354            result
355        else {
356            tracing::warn!("Failed to fetch versions for {}", name);
357            continue;
358        };
359
360        let Some(latest) = versions
361            .iter()
362            .find(|v: &&<H::Registry as PackageRegistry>::Version| !v.is_yanked())
363        else {
364            tracing::warn!("No non-yanked versions found for '{}'", name);
365            continue;
366        };
367
368        // Use resolved version from lock file if available, otherwise fall back to requirement
369        let version_to_compare = resolved_versions
370            .get(&name)
371            .map(String::as_str)
372            .unwrap_or(&version_req);
373        let is_latest = H::is_version_latest(version_to_compare, latest.version_string());
374        hints.push(create_hint::<H>(
375            &name,
376            version_range,
377            latest.version_string(),
378            is_latest,
379            config,
380        ));
381    }
382
383    hints
384}
385
386#[inline]
387fn create_hint<H: EcosystemHandler>(
388    name: &str,
389    version_range: Range,
390    latest_version: &str,
391    is_latest: bool,
392    config: &InlayHintsConfig,
393) -> InlayHint {
394    let label_text = if is_latest {
395        config.up_to_date_text.clone()
396    } else {
397        config.needs_update_text.replace("{}", latest_version)
398    };
399
400    let url = H::package_url(name);
401    let tooltip_content = format!(
402        "[{}]({}) - {}\n\nLatest: **{}**",
403        name, url, url, latest_version
404    );
405
406    InlayHint {
407        position: version_range.end,
408        label: InlayHintLabel::LabelParts(vec![InlayHintLabelPart {
409            value: label_text,
410            tooltip: Some(
411                tower_lsp_server::ls_types::InlayHintLabelPartTooltip::MarkupContent(
412                    MarkupContent {
413                        kind: MarkupKind::Markdown,
414                        value: tooltip_content,
415                    },
416                ),
417            ),
418            location: None,
419            command: Some(tower_lsp_server::ls_types::Command {
420                title: format!("Open on {}", H::ecosystem_display_name()),
421                command: "vscode.open".into(),
422                arguments: Some(vec![serde_json::json!(url)]),
423            }),
424        }]),
425        kind: Some(InlayHintKind::TYPE),
426        text_edits: None,
427        tooltip: None,
428        padding_left: Some(true),
429        padding_right: None,
430        data: None,
431    }
432}
433
434/// Generic hover generator.
435///
436/// Fetches version information and generates markdown hover content
437/// with version list and features (if supported).
438///
439/// # Type Parameters
440///
441/// * `H` - Ecosystem handler type
442///
443/// # Arguments
444///
445/// * `handler` - Ecosystem handler instance
446/// * `dep` - Dependency to generate hover for
447/// * `resolved_version` - Optional resolved version from lock file (preferred over manifest version)
448pub async fn generate_hover<H>(
449    handler: &H,
450    dep: &H::UnifiedDep,
451    resolved_version: Option<&str>,
452) -> Option<tower_lsp_server::ls_types::Hover>
453where
454    H: EcosystemHandler,
455{
456    use tower_lsp_server::ls_types::{Hover, HoverContents};
457
458    let typed_dep = H::extract_dependency(dep)?;
459    let registry = handler.registry();
460    let versions: Vec<<H::Registry as PackageRegistry>::Version> =
461        registry.get_versions(typed_dep.name()).await.ok()?;
462    let latest: &<H::Registry as PackageRegistry>::Version = versions.first()?;
463
464    let url = H::package_url(typed_dep.name());
465    let mut markdown = format!("# [{}]({})\n\n", typed_dep.name(), url);
466
467    if let Some(version) = resolved_version.or(typed_dep.version_requirement()) {
468        markdown.push_str(&format!("**Current**: `{}`\n\n", version));
469    }
470
471    if latest.is_yanked() {
472        markdown.push_str("⚠️ **Warning**: This version has been yanked\n\n");
473    }
474
475    markdown.push_str("**Versions** *(use Cmd+. to update)*:\n");
476    for (i, version) in versions.iter().take(MAX_VERSIONS_IN_HOVER).enumerate() {
477        if i == 0 {
478            markdown.push_str(&format!("- {} *(latest)*\n", version.version_string()));
479        } else {
480            markdown.push_str(&format!("- {}\n", version.version_string()));
481        }
482    }
483    if versions.len() > MAX_VERSIONS_IN_HOVER {
484        markdown.push_str(&format!(
485            "- *...and {} more*\n",
486            versions.len() - MAX_VERSIONS_IN_HOVER
487        ));
488    }
489
490    let features = latest.features();
491    if !features.is_empty() {
492        markdown.push_str("\n**Features**:\n");
493        for feature in features.iter().take(MAX_FEATURES_IN_HOVER) {
494            markdown.push_str(&format!("- `{}`\n", feature));
495        }
496        if features.len() > MAX_FEATURES_IN_HOVER {
497            markdown.push_str(&format!(
498                "- *...and {} more*\n",
499                features.len() - MAX_FEATURES_IN_HOVER
500            ));
501        }
502    }
503
504    Some(Hover {
505        contents: HoverContents::Markup(MarkupContent {
506            kind: MarkupKind::Markdown,
507            value: markdown,
508        }),
509        range: Some(typed_dep.name_range()),
510    })
511}
512
513/// Configuration for diagnostics display.
514///
515/// This is a simplified version to avoid circular dependencies.
516pub struct DiagnosticsConfig {
517    pub unknown_severity: tower_lsp_server::ls_types::DiagnosticSeverity,
518    pub yanked_severity: tower_lsp_server::ls_types::DiagnosticSeverity,
519    pub outdated_severity: tower_lsp_server::ls_types::DiagnosticSeverity,
520}
521
522impl Default for DiagnosticsConfig {
523    fn default() -> Self {
524        use tower_lsp_server::ls_types::DiagnosticSeverity;
525        Self {
526            unknown_severity: DiagnosticSeverity::WARNING,
527            yanked_severity: DiagnosticSeverity::WARNING,
528            outdated_severity: DiagnosticSeverity::HINT,
529        }
530    }
531}
532
533/// Generic code actions generator.
534///
535/// Fetches available versions and generates "Update to version X" quick fixes.
536///
537/// # Type Parameters
538///
539/// * `H` - Ecosystem handler type
540///
541/// # Arguments
542///
543/// * `handler` - Ecosystem-specific handler instance
544/// * `dependencies` - List of dependencies with version ranges
545/// * `uri` - Document URI
546/// * `selected_range` - Range selected by user for code actions
547///
548/// # Returns
549///
550/// Vector of code actions (quick fixes) for the LSP client.
551pub async fn generate_code_actions<H>(
552    handler: &H,
553    dependencies: &[H::UnifiedDep],
554    uri: &tower_lsp_server::ls_types::Uri,
555    selected_range: Range,
556) -> Vec<tower_lsp_server::ls_types::CodeActionOrCommand>
557where
558    H: EcosystemHandler,
559{
560    use tower_lsp_server::ls_types::{
561        CodeAction, CodeActionKind, CodeActionOrCommand, TextEdit, WorkspaceEdit,
562    };
563
564    let mut deps_to_check = Vec::new();
565    for dep in dependencies {
566        let Some(typed_dep) = H::extract_dependency(dep) else {
567            continue;
568        };
569
570        let Some(version_range) = typed_dep.version_range() else {
571            continue;
572        };
573
574        // Check if this dependency's version range overlaps with cursor position
575        if !ranges_overlap(version_range, selected_range) {
576            continue;
577        }
578
579        deps_to_check.push((typed_dep, version_range));
580    }
581
582    if deps_to_check.is_empty() {
583        return vec![];
584    }
585
586    let registry = handler.registry().clone();
587    let futures: Vec<_> = deps_to_check
588        .iter()
589        .map(|(dep, version_range)| {
590            let name = dep.name().to_string();
591            let version_range = *version_range;
592            let registry = registry.clone();
593            async move {
594                let versions = registry.get_versions(&name).await;
595                (name, dep, version_range, versions)
596            }
597        })
598        .collect();
599
600    let results = join_all(futures).await;
601
602    let mut actions = Vec::new();
603    for (name, dep, version_range, versions_result) in results {
604        let Ok(versions) = versions_result else {
605            tracing::warn!("Failed to fetch versions for {}", name);
606            continue;
607        };
608
609        for (i, version) in versions
610            .iter()
611            .filter(|v| !H::is_deprecated(v))
612            .take(MAX_CODE_ACTION_VERSIONS)
613            .enumerate()
614        {
615            let new_text = H::format_version_for_edit(dep, version.version_string());
616
617            let mut edits = std::collections::HashMap::new();
618            edits.insert(
619                uri.clone(),
620                vec![TextEdit {
621                    range: version_range,
622                    new_text,
623                }],
624            );
625
626            let title = if i == 0 {
627                format!("Update {} to {} (latest)", name, version.version_string())
628            } else {
629                format!("Update {} to {}", name, version.version_string())
630            };
631
632            actions.push(CodeActionOrCommand::CodeAction(CodeAction {
633                title,
634                kind: Some(CodeActionKind::REFACTOR),
635                edit: Some(WorkspaceEdit {
636                    changes: Some(edits),
637                    ..Default::default()
638                }),
639                is_preferred: Some(i == 0),
640                ..Default::default()
641            }));
642        }
643    }
644
645    actions
646}
647
648fn ranges_overlap(a: Range, b: Range) -> bool {
649    !(a.end.line < b.start.line
650        || (a.end.line == b.start.line && a.end.character < b.start.character)
651        || b.end.line < a.start.line
652        || (b.end.line == a.start.line && b.end.character < a.start.character))
653}
654
655/// Generic diagnostics generator.
656///
657/// Checks dependencies for issues:
658/// - Unknown packages (not found in registry)
659/// - Invalid version syntax
660/// - Yanked/deprecated versions
661/// - Outdated versions
662///
663/// # Type Parameters
664///
665/// * `H` - Ecosystem handler type
666///
667/// # Arguments
668///
669/// * `handler` - Ecosystem-specific handler instance
670/// * `dependencies` - List of dependencies to check
671/// * `config` - Diagnostic severity configuration
672///
673/// # Returns
674///
675/// Vector of LSP diagnostics.
676pub async fn generate_diagnostics<H>(
677    handler: &H,
678    dependencies: &[H::UnifiedDep],
679    config: &DiagnosticsConfig,
680) -> Vec<tower_lsp_server::ls_types::Diagnostic>
681where
682    H: EcosystemHandler,
683{
684    use tower_lsp_server::ls_types::{Diagnostic, DiagnosticSeverity};
685
686    let mut deps_to_check = Vec::new();
687    for dep in dependencies {
688        let Some(typed_dep) = H::extract_dependency(dep) else {
689            continue;
690        };
691        deps_to_check.push(typed_dep);
692    }
693
694    if deps_to_check.is_empty() {
695        return vec![];
696    }
697
698    let registry = handler.registry().clone();
699    let futures: Vec<_> = deps_to_check
700        .iter()
701        .map(|dep| {
702            let name = dep.name().to_string();
703            let registry = registry.clone();
704            async move {
705                let versions = registry.get_versions(&name).await;
706                (name, versions)
707            }
708        })
709        .collect();
710
711    let version_results = join_all(futures).await;
712
713    let mut diagnostics = Vec::new();
714
715    for (i, dep) in deps_to_check.iter().enumerate() {
716        let (name, version_result) = &version_results[i];
717
718        let versions = match version_result {
719            Ok(v) => v,
720            Err(_) => {
721                diagnostics.push(Diagnostic {
722                    range: dep.name_range(),
723                    severity: Some(config.unknown_severity),
724                    message: format!("Unknown package '{}'", name),
725                    source: Some("deps-lsp".into()),
726                    ..Default::default()
727                });
728                continue;
729            }
730        };
731
732        if let Some(version_req) = dep.version_requirement()
733            && let Some(version_range) = dep.version_range()
734        {
735            let Some(parsed_version_req) = H::parse_version_req(version_req) else {
736                diagnostics.push(Diagnostic {
737                    range: version_range,
738                    severity: Some(DiagnosticSeverity::ERROR),
739                    message: format!("Invalid version requirement '{}'", version_req),
740                    source: Some("deps-lsp".into()),
741                    ..Default::default()
742                });
743                continue;
744            };
745
746            let matching = handler
747                .registry()
748                .get_latest_matching(name, &parsed_version_req)
749                .await
750                .ok()
751                .flatten();
752
753            if let Some(current) = &matching
754                && H::is_deprecated(current)
755            {
756                diagnostics.push(Diagnostic {
757                    range: version_range,
758                    severity: Some(config.yanked_severity),
759                    message: "This version has been yanked".into(),
760                    source: Some("deps-lsp".into()),
761                    ..Default::default()
762                });
763            }
764
765            let latest = versions
766                .iter()
767                .find(|v| !H::is_deprecated(v) && !v.is_prerelease());
768            if let (Some(latest), Some(current)) = (latest, &matching)
769                && latest.version_string() != current.version_string()
770            {
771                diagnostics.push(Diagnostic {
772                    range: version_range,
773                    severity: Some(config.outdated_severity),
774                    message: format!("Newer version available: {}", latest.version_string()),
775                    source: Some("deps-lsp".into()),
776                    ..Default::default()
777                });
778            }
779        }
780    }
781
782    diagnostics
783}
784
785#[cfg(test)]
786mod tests {
787    use super::*;
788    use crate::registry::PackageMetadata;
789    use tower_lsp_server::ls_types::{Position, Range};
790
791    #[derive(Clone)]
792    struct MockVersion {
793        version: String,
794        yanked: bool,
795        features: Vec<String>,
796    }
797
798    impl VersionInfo for MockVersion {
799        fn version_string(&self) -> &str {
800            &self.version
801        }
802
803        fn is_yanked(&self) -> bool {
804            self.yanked
805        }
806
807        fn features(&self) -> Vec<String> {
808            self.features.clone()
809        }
810    }
811
812    #[derive(Clone)]
813    struct MockMetadata {
814        name: String,
815        description: Option<String>,
816        latest: String,
817    }
818
819    impl PackageMetadata for MockMetadata {
820        fn name(&self) -> &str {
821            &self.name
822        }
823
824        fn description(&self) -> Option<&str> {
825            self.description.as_deref()
826        }
827
828        fn repository(&self) -> Option<&str> {
829            None
830        }
831
832        fn documentation(&self) -> Option<&str> {
833            None
834        }
835
836        fn latest_version(&self) -> &str {
837            &self.latest
838        }
839    }
840
841    #[derive(Clone)]
842    struct MockDependency {
843        name: String,
844        version_req: Option<String>,
845        version_range: Option<Range>,
846        name_range: Range,
847    }
848
849    impl crate::parser::DependencyInfo for MockDependency {
850        fn name(&self) -> &str {
851            &self.name
852        }
853
854        fn name_range(&self) -> Range {
855            self.name_range
856        }
857
858        fn version_requirement(&self) -> Option<&str> {
859            self.version_req.as_deref()
860        }
861
862        fn version_range(&self) -> Option<Range> {
863            self.version_range
864        }
865
866        fn source(&self) -> crate::parser::DependencySource {
867            crate::parser::DependencySource::Registry
868        }
869    }
870
871    struct MockRegistry {
872        versions: std::collections::HashMap<String, Vec<MockVersion>>,
873    }
874
875    impl Clone for MockRegistry {
876        fn clone(&self) -> Self {
877            Self {
878                versions: self.versions.clone(),
879            }
880        }
881    }
882
883    #[async_trait]
884    impl crate::registry::PackageRegistry for MockRegistry {
885        type Version = MockVersion;
886        type Metadata = MockMetadata;
887        type VersionReq = String;
888
889        async fn get_versions(&self, name: &str) -> crate::error::Result<Vec<Self::Version>> {
890            self.versions.get(name).cloned().ok_or_else(|| {
891                use std::io::{Error as IoError, ErrorKind};
892                crate::DepsError::Io(IoError::new(ErrorKind::NotFound, "package not found"))
893            })
894        }
895
896        async fn get_latest_matching(
897            &self,
898            name: &str,
899            req: &Self::VersionReq,
900        ) -> crate::error::Result<Option<Self::Version>> {
901            Ok(self
902                .versions
903                .get(name)
904                .and_then(|versions| versions.iter().find(|v| v.version == *req).cloned()))
905        }
906
907        async fn search(
908            &self,
909            _query: &str,
910            _limit: usize,
911        ) -> crate::error::Result<Vec<Self::Metadata>> {
912            Ok(vec![])
913        }
914    }
915
916    struct MockHandler {
917        registry: MockRegistry,
918    }
919
920    #[async_trait]
921    impl EcosystemHandler for MockHandler {
922        type Registry = MockRegistry;
923        type Dependency = MockDependency;
924        type UnifiedDep = MockDependency;
925
926        fn new(_cache: Arc<HttpCache>) -> Self {
927            let mut versions = std::collections::HashMap::new();
928            versions.insert(
929                "serde".to_string(),
930                vec![
931                    MockVersion {
932                        version: "1.0.195".to_string(),
933                        yanked: false,
934                        features: vec!["derive".to_string(), "alloc".to_string()],
935                    },
936                    MockVersion {
937                        version: "1.0.194".to_string(),
938                        yanked: false,
939                        features: vec![],
940                    },
941                ],
942            );
943            versions.insert(
944                "yanked-pkg".to_string(),
945                vec![MockVersion {
946                    version: "1.0.0".to_string(),
947                    yanked: true,
948                    features: vec![],
949                }],
950            );
951
952            Self {
953                registry: MockRegistry { versions },
954            }
955        }
956
957        fn registry(&self) -> &Self::Registry {
958            &self.registry
959        }
960
961        fn extract_dependency(dep: &Self::UnifiedDep) -> Option<&Self::Dependency> {
962            Some(dep)
963        }
964
965        fn package_url(name: &str) -> String {
966            format!("https://test.io/pkg/{}", name)
967        }
968
969        fn ecosystem_display_name() -> &'static str {
970            "Test Registry"
971        }
972
973        fn is_version_latest(version_req: &str, latest: &str) -> bool {
974            version_req == latest
975        }
976
977        fn format_version_for_edit(_dep: &Self::Dependency, version: &str) -> String {
978            format!("\"{}\"", version)
979        }
980
981        fn is_deprecated(version: &MockVersion) -> bool {
982            version.yanked
983        }
984
985        fn is_valid_version_syntax(_version_req: &str) -> bool {
986            true
987        }
988
989        fn parse_version_req(version_req: &str) -> Option<String> {
990            Some(version_req.to_string())
991        }
992    }
993
994    impl VersionStringGetter for MockVersion {
995        fn version_string(&self) -> &str {
996            &self.version
997        }
998    }
999
1000    impl YankedChecker for MockVersion {
1001        fn is_yanked(&self) -> bool {
1002            self.yanked
1003        }
1004    }
1005
1006    #[test]
1007    fn test_inlay_hints_config_default() {
1008        let config = InlayHintsConfig::default();
1009        assert!(config.enabled);
1010        assert_eq!(config.up_to_date_text, "✅");
1011        assert_eq!(config.needs_update_text, "❌ {}");
1012    }
1013
1014    #[tokio::test]
1015    async fn test_generate_inlay_hints_cached() {
1016        let cache = Arc::new(HttpCache::new());
1017        let handler = MockHandler::new(cache);
1018
1019        let deps = vec![MockDependency {
1020            name: "serde".to_string(),
1021            version_req: Some("1.0.195".to_string()),
1022            version_range: Some(Range {
1023                start: Position {
1024                    line: 0,
1025                    character: 10,
1026                },
1027                end: Position {
1028                    line: 0,
1029                    character: 20,
1030                },
1031            }),
1032            name_range: Range::default(),
1033        }];
1034
1035        let mut cached_versions = HashMap::new();
1036        cached_versions.insert(
1037            "serde".to_string(),
1038            MockVersion {
1039                version: "1.0.195".to_string(),
1040                yanked: false,
1041                features: vec![],
1042            },
1043        );
1044
1045        let config = InlayHintsConfig::default();
1046        let resolved_versions: HashMap<String, String> = HashMap::new();
1047        let hints = generate_inlay_hints(
1048            &handler,
1049            &deps,
1050            &cached_versions,
1051            &resolved_versions,
1052            &config,
1053        )
1054        .await;
1055
1056        assert_eq!(hints.len(), 1);
1057        assert_eq!(hints[0].position.line, 0);
1058        assert_eq!(hints[0].position.character, 20);
1059    }
1060
1061    #[tokio::test]
1062    async fn test_generate_inlay_hints_fetch() {
1063        let cache = Arc::new(HttpCache::new());
1064        let handler = MockHandler::new(cache);
1065
1066        let deps = vec![MockDependency {
1067            name: "serde".to_string(),
1068            version_req: Some("1.0.0".to_string()),
1069            version_range: Some(Range {
1070                start: Position {
1071                    line: 0,
1072                    character: 10,
1073                },
1074                end: Position {
1075                    line: 0,
1076                    character: 20,
1077                },
1078            }),
1079            name_range: Range::default(),
1080        }];
1081
1082        let cached_versions: HashMap<String, MockVersion> = HashMap::new();
1083        let config = InlayHintsConfig::default();
1084        let resolved_versions: HashMap<String, String> = HashMap::new();
1085        let hints = generate_inlay_hints(
1086            &handler,
1087            &deps,
1088            &cached_versions,
1089            &resolved_versions,
1090            &config,
1091        )
1092        .await;
1093
1094        assert_eq!(hints.len(), 1);
1095    }
1096
1097    #[tokio::test]
1098    async fn test_generate_inlay_hints_skips_yanked() {
1099        let cache = Arc::new(HttpCache::new());
1100        let handler = MockHandler::new(cache);
1101
1102        let deps = vec![MockDependency {
1103            name: "serde".to_string(),
1104            version_req: Some("1.0.195".to_string()),
1105            version_range: Some(Range {
1106                start: Position {
1107                    line: 0,
1108                    character: 10,
1109                },
1110                end: Position {
1111                    line: 0,
1112                    character: 20,
1113                },
1114            }),
1115            name_range: Range::default(),
1116        }];
1117
1118        let mut cached_versions = HashMap::new();
1119        cached_versions.insert(
1120            "serde".to_string(),
1121            MockVersion {
1122                version: "1.0.195".to_string(),
1123                yanked: true,
1124                features: vec![],
1125            },
1126        );
1127
1128        let config = InlayHintsConfig::default();
1129        let resolved_versions: HashMap<String, String> = HashMap::new();
1130        let hints = generate_inlay_hints(
1131            &handler,
1132            &deps,
1133            &cached_versions,
1134            &resolved_versions,
1135            &config,
1136        )
1137        .await;
1138
1139        assert_eq!(hints.len(), 0);
1140    }
1141
1142    #[tokio::test]
1143    async fn test_generate_inlay_hints_no_version_range() {
1144        let cache = Arc::new(HttpCache::new());
1145        let handler = MockHandler::new(cache);
1146
1147        let deps = vec![MockDependency {
1148            name: "serde".to_string(),
1149            version_req: Some("1.0.195".to_string()),
1150            version_range: None,
1151            name_range: Range::default(),
1152        }];
1153
1154        let cached_versions: HashMap<String, MockVersion> = HashMap::new();
1155        let config = InlayHintsConfig::default();
1156        let resolved_versions: HashMap<String, String> = HashMap::new();
1157        let hints = generate_inlay_hints(
1158            &handler,
1159            &deps,
1160            &cached_versions,
1161            &resolved_versions,
1162            &config,
1163        )
1164        .await;
1165
1166        assert_eq!(hints.len(), 0);
1167    }
1168
1169    #[tokio::test]
1170    async fn test_generate_inlay_hints_no_version_req() {
1171        let cache = Arc::new(HttpCache::new());
1172        let handler = MockHandler::new(cache);
1173
1174        let deps = vec![MockDependency {
1175            name: "serde".to_string(),
1176            version_req: None,
1177            version_range: Some(Range {
1178                start: Position {
1179                    line: 0,
1180                    character: 10,
1181                },
1182                end: Position {
1183                    line: 0,
1184                    character: 20,
1185                },
1186            }),
1187            name_range: Range::default(),
1188        }];
1189
1190        let cached_versions: HashMap<String, MockVersion> = HashMap::new();
1191        let config = InlayHintsConfig::default();
1192        let resolved_versions: HashMap<String, String> = HashMap::new();
1193        let hints = generate_inlay_hints(
1194            &handler,
1195            &deps,
1196            &cached_versions,
1197            &resolved_versions,
1198            &config,
1199        )
1200        .await;
1201
1202        assert_eq!(hints.len(), 0);
1203    }
1204
1205    #[test]
1206    fn test_create_hint_up_to_date() {
1207        let config = InlayHintsConfig::default();
1208        let range = Range {
1209            start: Position {
1210                line: 5,
1211                character: 10,
1212            },
1213            end: Position {
1214                line: 5,
1215                character: 20,
1216            },
1217        };
1218
1219        let hint = create_hint::<MockHandler>("serde", range, "1.0.195", true, &config);
1220
1221        assert_eq!(hint.position, range.end);
1222        if let InlayHintLabel::LabelParts(parts) = hint.label {
1223            assert_eq!(parts[0].value, "✅");
1224        } else {
1225            panic!("Expected LabelParts");
1226        }
1227    }
1228
1229    #[test]
1230    fn test_create_hint_needs_update() {
1231        let config = InlayHintsConfig::default();
1232        let range = Range {
1233            start: Position {
1234                line: 5,
1235                character: 10,
1236            },
1237            end: Position {
1238                line: 5,
1239                character: 20,
1240            },
1241        };
1242
1243        let hint = create_hint::<MockHandler>("serde", range, "1.0.200", false, &config);
1244
1245        assert_eq!(hint.position, range.end);
1246        if let InlayHintLabel::LabelParts(parts) = hint.label {
1247            assert_eq!(parts[0].value, "❌ 1.0.200");
1248        } else {
1249            panic!("Expected LabelParts");
1250        }
1251    }
1252
1253    #[test]
1254    fn test_create_hint_custom_config() {
1255        let config = InlayHintsConfig {
1256            enabled: true,
1257            up_to_date_text: "OK".to_string(),
1258            needs_update_text: "UPDATE: {}".to_string(),
1259        };
1260        let range = Range {
1261            start: Position {
1262                line: 0,
1263                character: 0,
1264            },
1265            end: Position {
1266                line: 0,
1267                character: 10,
1268            },
1269        };
1270
1271        let hint = create_hint::<MockHandler>("test", range, "2.0.0", false, &config);
1272
1273        if let InlayHintLabel::LabelParts(parts) = hint.label {
1274            assert_eq!(parts[0].value, "UPDATE: 2.0.0");
1275        } else {
1276            panic!("Expected LabelParts");
1277        }
1278    }
1279
1280    #[tokio::test]
1281    async fn test_generate_hover() {
1282        let cache = Arc::new(HttpCache::new());
1283        let handler = MockHandler::new(cache);
1284
1285        let dep = MockDependency {
1286            name: "serde".to_string(),
1287            version_req: Some("1.0.0".to_string()),
1288            version_range: Some(Range::default()),
1289            name_range: Range {
1290                start: Position {
1291                    line: 0,
1292                    character: 0,
1293                },
1294                end: Position {
1295                    line: 0,
1296                    character: 5,
1297                },
1298            },
1299        };
1300
1301        let hover = generate_hover(&handler, &dep, None).await;
1302
1303        assert!(hover.is_some());
1304        let hover = hover.unwrap();
1305
1306        if let tower_lsp_server::ls_types::HoverContents::Markup(content) = hover.contents {
1307            assert!(content.value.contains("serde"));
1308            assert!(content.value.contains("1.0.195"));
1309            assert!(content.value.contains("Current"));
1310            assert!(content.value.contains("Features"));
1311            assert!(content.value.contains("derive"));
1312        } else {
1313            panic!("Expected Markup content");
1314        }
1315    }
1316
1317    #[tokio::test]
1318    async fn test_generate_hover_yanked_version() {
1319        let cache = Arc::new(HttpCache::new());
1320        let handler = MockHandler::new(cache);
1321
1322        let dep = MockDependency {
1323            name: "yanked-pkg".to_string(),
1324            version_req: Some("1.0.0".to_string()),
1325            version_range: Some(Range::default()),
1326            name_range: Range::default(),
1327        };
1328
1329        let hover = generate_hover(&handler, &dep, None).await;
1330
1331        assert!(hover.is_some());
1332        let hover = hover.unwrap();
1333
1334        if let tower_lsp_server::ls_types::HoverContents::Markup(content) = hover.contents {
1335            assert!(content.value.contains("Warning"));
1336            assert!(content.value.contains("yanked"));
1337        } else {
1338            panic!("Expected Markup content");
1339        }
1340    }
1341
1342    #[tokio::test]
1343    async fn test_generate_hover_no_versions() {
1344        let cache = Arc::new(HttpCache::new());
1345        let handler = MockHandler::new(cache);
1346
1347        let dep = MockDependency {
1348            name: "nonexistent".to_string(),
1349            version_req: Some("1.0.0".to_string()),
1350            version_range: Some(Range::default()),
1351            name_range: Range::default(),
1352        };
1353
1354        let hover = generate_hover(&handler, &dep, None).await;
1355        assert!(hover.is_none());
1356    }
1357
1358    #[tokio::test]
1359    async fn test_generate_hover_no_version_req() {
1360        let cache = Arc::new(HttpCache::new());
1361        let handler = MockHandler::new(cache);
1362
1363        let dep = MockDependency {
1364            name: "serde".to_string(),
1365            version_req: None,
1366            version_range: Some(Range::default()),
1367            name_range: Range::default(),
1368        };
1369
1370        let hover = generate_hover(&handler, &dep, None).await;
1371
1372        assert!(hover.is_some());
1373        let hover = hover.unwrap();
1374
1375        if let tower_lsp_server::ls_types::HoverContents::Markup(content) = hover.contents {
1376            assert!(!content.value.contains("Current"));
1377        } else {
1378            panic!("Expected Markup content");
1379        }
1380    }
1381
1382    #[tokio::test]
1383    async fn test_generate_hover_with_resolved_version() {
1384        let cache = Arc::new(HttpCache::new());
1385        let handler = MockHandler::new(cache);
1386
1387        let dep = MockDependency {
1388            name: "serde".to_string(),
1389            version_req: Some("1.0".to_string()), // Manifest has short version
1390            version_range: Some(Range::default()),
1391            name_range: Range {
1392                start: Position {
1393                    line: 0,
1394                    character: 0,
1395                },
1396                end: Position {
1397                    line: 0,
1398                    character: 5,
1399                },
1400            },
1401        };
1402
1403        // Pass resolved version from lock file (full version)
1404        let hover = generate_hover(&handler, &dep, Some("1.0.195")).await;
1405
1406        assert!(hover.is_some());
1407        let hover = hover.unwrap();
1408
1409        if let tower_lsp_server::ls_types::HoverContents::Markup(content) = hover.contents {
1410            // Should show the resolved version (1.0.195) not manifest version (1.0)
1411            assert!(content.value.contains("**Current**: `1.0.195`"));
1412            assert!(!content.value.contains("**Current**: `1.0`"));
1413        } else {
1414            panic!("Expected Markup content");
1415        }
1416    }
1417
1418    #[tokio::test]
1419    async fn test_generate_code_actions_empty_when_up_to_date() {
1420        use tower_lsp_server::ls_types::Uri;
1421
1422        let cache = Arc::new(HttpCache::new());
1423        let handler = MockHandler::new(cache);
1424
1425        let deps = vec![MockDependency {
1426            name: "serde".to_string(),
1427            version_req: Some("1.0.195".to_string()),
1428            version_range: Some(Range {
1429                start: Position {
1430                    line: 0,
1431                    character: 10,
1432                },
1433                end: Position {
1434                    line: 0,
1435                    character: 20,
1436                },
1437            }),
1438            name_range: Range::default(),
1439        }];
1440
1441        let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
1442        let selected_range = Range {
1443            start: Position {
1444                line: 0,
1445                character: 15,
1446            },
1447            end: Position {
1448                line: 0,
1449                character: 15,
1450            },
1451        };
1452
1453        let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1454
1455        assert!(!actions.is_empty());
1456    }
1457
1458    #[tokio::test]
1459    async fn test_generate_code_actions_update_outdated() {
1460        use tower_lsp_server::ls_types::{CodeActionOrCommand, Uri};
1461
1462        let cache = Arc::new(HttpCache::new());
1463        let handler = MockHandler::new(cache);
1464
1465        let deps = vec![MockDependency {
1466            name: "serde".to_string(),
1467            version_req: Some("1.0.0".to_string()),
1468            version_range: Some(Range {
1469                start: Position {
1470                    line: 0,
1471                    character: 10,
1472                },
1473                end: Position {
1474                    line: 0,
1475                    character: 20,
1476                },
1477            }),
1478            name_range: Range::default(),
1479        }];
1480
1481        let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
1482        let selected_range = Range {
1483            start: Position {
1484                line: 0,
1485                character: 15,
1486            },
1487            end: Position {
1488                line: 0,
1489                character: 15,
1490            },
1491        };
1492
1493        let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1494
1495        assert!(!actions.is_empty());
1496        assert!(actions.len() <= 5);
1497
1498        if let CodeActionOrCommand::CodeAction(action) = &actions[0] {
1499            assert!(action.title.contains("1.0.195"));
1500            assert!(action.title.contains("latest"));
1501            assert_eq!(action.is_preferred, Some(true));
1502        } else {
1503            panic!("Expected CodeAction");
1504        }
1505    }
1506
1507    #[tokio::test]
1508    async fn test_generate_code_actions_missing_version_range() {
1509        use tower_lsp_server::ls_types::Uri;
1510
1511        let cache = Arc::new(HttpCache::new());
1512        let handler = MockHandler::new(cache);
1513
1514        let deps = vec![MockDependency {
1515            name: "serde".to_string(),
1516            version_req: Some("1.0.0".to_string()),
1517            version_range: None,
1518            name_range: Range::default(),
1519        }];
1520
1521        let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
1522        let selected_range = Range {
1523            start: Position {
1524                line: 0,
1525                character: 15,
1526            },
1527            end: Position {
1528                line: 0,
1529                character: 15,
1530            },
1531        };
1532
1533        let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1534
1535        assert_eq!(actions.len(), 0);
1536    }
1537
1538    #[tokio::test]
1539    async fn test_generate_code_actions_no_overlap() {
1540        use tower_lsp_server::ls_types::Uri;
1541
1542        let cache = Arc::new(HttpCache::new());
1543        let handler = MockHandler::new(cache);
1544
1545        let deps = vec![MockDependency {
1546            name: "serde".to_string(),
1547            version_req: Some("1.0.0".to_string()),
1548            version_range: Some(Range {
1549                start: Position {
1550                    line: 0,
1551                    character: 10,
1552                },
1553                end: Position {
1554                    line: 0,
1555                    character: 20,
1556                },
1557            }),
1558            name_range: Range::default(),
1559        }];
1560
1561        let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
1562        let selected_range = Range {
1563            start: Position {
1564                line: 5,
1565                character: 0,
1566            },
1567            end: Position {
1568                line: 5,
1569                character: 10,
1570            },
1571        };
1572
1573        let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1574
1575        assert_eq!(actions.len(), 0);
1576    }
1577
1578    #[tokio::test]
1579    async fn test_generate_code_actions_filters_deprecated() {
1580        use tower_lsp_server::ls_types::{CodeActionOrCommand, Uri};
1581
1582        let cache = Arc::new(HttpCache::new());
1583        let handler = MockHandler::new(cache);
1584
1585        let deps = vec![MockDependency {
1586            name: "yanked-pkg".to_string(),
1587            version_req: Some("1.0.0".to_string()),
1588            version_range: Some(Range {
1589                start: Position {
1590                    line: 0,
1591                    character: 10,
1592                },
1593                end: Position {
1594                    line: 0,
1595                    character: 20,
1596                },
1597            }),
1598            name_range: Range::default(),
1599        }];
1600
1601        let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
1602        let selected_range = Range {
1603            start: Position {
1604                line: 0,
1605                character: 15,
1606            },
1607            end: Position {
1608                line: 0,
1609                character: 15,
1610            },
1611        };
1612
1613        let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1614
1615        assert_eq!(actions.len(), 0);
1616
1617        for action in actions {
1618            if let CodeActionOrCommand::CodeAction(a) = action {
1619                assert!(!a.title.contains("1.0.0"));
1620            }
1621        }
1622    }
1623
1624    #[test]
1625    fn test_ranges_overlap_basic() {
1626        let range_a = Range {
1627            start: Position {
1628                line: 0,
1629                character: 10,
1630            },
1631            end: Position {
1632                line: 0,
1633                character: 20,
1634            },
1635        };
1636
1637        let range_b = Range {
1638            start: Position {
1639                line: 0,
1640                character: 15,
1641            },
1642            end: Position {
1643                line: 0,
1644                character: 25,
1645            },
1646        };
1647
1648        assert!(ranges_overlap(range_a, range_b));
1649    }
1650
1651    #[test]
1652    fn test_ranges_no_overlap() {
1653        let range_a = Range {
1654            start: Position {
1655                line: 0,
1656                character: 10,
1657            },
1658            end: Position {
1659                line: 0,
1660                character: 20,
1661            },
1662        };
1663
1664        let range_b = Range {
1665            start: Position {
1666                line: 0,
1667                character: 25,
1668            },
1669            end: Position {
1670                line: 0,
1671                character: 30,
1672            },
1673        };
1674
1675        assert!(!ranges_overlap(range_a, range_b));
1676    }
1677
1678    #[tokio::test]
1679    async fn test_generate_diagnostics_valid_version() {
1680        let cache = Arc::new(HttpCache::new());
1681        let handler = MockHandler::new(cache);
1682
1683        let deps = vec![MockDependency {
1684            name: "serde".to_string(),
1685            version_req: Some("1.0.195".to_string()),
1686            version_range: Some(Range {
1687                start: Position {
1688                    line: 0,
1689                    character: 10,
1690                },
1691                end: Position {
1692                    line: 0,
1693                    character: 20,
1694                },
1695            }),
1696            name_range: Range::default(),
1697        }];
1698
1699        let config = DiagnosticsConfig::default();
1700        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1701
1702        assert_eq!(diagnostics.len(), 0);
1703    }
1704
1705    #[tokio::test]
1706    async fn test_generate_diagnostics_deprecated_version() {
1707        use tower_lsp_server::ls_types::DiagnosticSeverity;
1708
1709        let cache = Arc::new(HttpCache::new());
1710        let handler = MockHandler::new(cache);
1711
1712        let deps = vec![MockDependency {
1713            name: "yanked-pkg".to_string(),
1714            version_req: Some("1.0.0".to_string()),
1715            version_range: Some(Range {
1716                start: Position {
1717                    line: 0,
1718                    character: 10,
1719                },
1720                end: Position {
1721                    line: 0,
1722                    character: 20,
1723                },
1724            }),
1725            name_range: Range::default(),
1726        }];
1727
1728        let config = DiagnosticsConfig::default();
1729        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1730
1731        assert_eq!(diagnostics.len(), 1);
1732        assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
1733        assert!(diagnostics[0].message.contains("yanked"));
1734    }
1735
1736    #[tokio::test]
1737    async fn test_generate_diagnostics_unknown_package() {
1738        use tower_lsp_server::ls_types::DiagnosticSeverity;
1739
1740        let cache = Arc::new(HttpCache::new());
1741        let handler = MockHandler::new(cache);
1742
1743        let deps = vec![MockDependency {
1744            name: "nonexistent".to_string(),
1745            version_req: Some("1.0.0".to_string()),
1746            version_range: Some(Range {
1747                start: Position {
1748                    line: 0,
1749                    character: 10,
1750                },
1751                end: Position {
1752                    line: 0,
1753                    character: 20,
1754                },
1755            }),
1756            name_range: Range {
1757                start: Position {
1758                    line: 0,
1759                    character: 0,
1760                },
1761                end: Position {
1762                    line: 0,
1763                    character: 10,
1764                },
1765            },
1766        }];
1767
1768        let config = DiagnosticsConfig::default();
1769        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1770
1771        assert_eq!(diagnostics.len(), 1);
1772        assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
1773        assert!(diagnostics[0].message.contains("Unknown package"));
1774        assert!(diagnostics[0].message.contains("nonexistent"));
1775    }
1776
1777    #[tokio::test]
1778    async fn test_generate_diagnostics_missing_version() {
1779        let cache = Arc::new(HttpCache::new());
1780        let handler = MockHandler::new(cache);
1781
1782        let deps = vec![MockDependency {
1783            name: "serde".to_string(),
1784            version_req: None,
1785            version_range: None,
1786            name_range: Range::default(),
1787        }];
1788
1789        let config = DiagnosticsConfig::default();
1790        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1791
1792        assert_eq!(diagnostics.len(), 0);
1793    }
1794
1795    #[tokio::test]
1796    async fn test_generate_diagnostics_outdated_version() {
1797        use tower_lsp_server::ls_types::DiagnosticSeverity;
1798
1799        let cache = Arc::new(HttpCache::new());
1800        let mut handler = MockHandler::new(cache);
1801
1802        handler.registry.versions.insert(
1803            "outdated-pkg".to_string(),
1804            vec![
1805                MockVersion {
1806                    version: "2.0.0".to_string(),
1807                    yanked: false,
1808                    features: vec![],
1809                },
1810                MockVersion {
1811                    version: "1.0.0".to_string(),
1812                    yanked: false,
1813                    features: vec![],
1814                },
1815            ],
1816        );
1817
1818        let deps = vec![MockDependency {
1819            name: "outdated-pkg".to_string(),
1820            version_req: Some("1.0.0".to_string()),
1821            version_range: Some(Range {
1822                start: Position {
1823                    line: 0,
1824                    character: 10,
1825                },
1826                end: Position {
1827                    line: 0,
1828                    character: 20,
1829                },
1830            }),
1831            name_range: Range::default(),
1832        }];
1833
1834        let config = DiagnosticsConfig::default();
1835        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1836
1837        assert_eq!(diagnostics.len(), 1);
1838        assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::HINT));
1839        assert!(diagnostics[0].message.contains("Newer version available"));
1840        assert!(diagnostics[0].message.contains("2.0.0"));
1841    }
1842
1843    #[test]
1844    fn test_diagnostics_config_default() {
1845        use tower_lsp_server::ls_types::DiagnosticSeverity;
1846
1847        let config = DiagnosticsConfig::default();
1848        assert_eq!(config.unknown_severity, DiagnosticSeverity::WARNING);
1849        assert_eq!(config.yanked_severity, DiagnosticSeverity::WARNING);
1850        assert_eq!(config.outdated_severity, DiagnosticSeverity::HINT);
1851    }
1852
1853    #[tokio::test]
1854    async fn test_generate_diagnostics_ignores_prerelease() {
1855        let cache = Arc::new(HttpCache::new());
1856        let mut handler = MockHandler::new(cache);
1857
1858        handler.registry.versions.insert(
1859            "test-pkg".to_string(),
1860            vec![
1861                MockVersion {
1862                    version: "4.0.0-alpha.13".to_string(),
1863                    yanked: false,
1864                    features: vec![],
1865                },
1866                MockVersion {
1867                    version: "3.7.4".to_string(),
1868                    yanked: false,
1869                    features: vec![],
1870                },
1871                MockVersion {
1872                    version: "3.4.2".to_string(),
1873                    yanked: false,
1874                    features: vec![],
1875                },
1876            ],
1877        );
1878
1879        let deps = vec![MockDependency {
1880            name: "test-pkg".to_string(),
1881            version_req: Some("3.4.2".to_string()),
1882            version_range: Some(Range {
1883                start: Position {
1884                    line: 0,
1885                    character: 10,
1886                },
1887                end: Position {
1888                    line: 0,
1889                    character: 20,
1890                },
1891            }),
1892            name_range: Range::default(),
1893        }];
1894
1895        let config = DiagnosticsConfig::default();
1896        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1897
1898        assert_eq!(diagnostics.len(), 1);
1899        assert!(diagnostics[0].message.contains("Newer version available"));
1900        assert!(diagnostics[0].message.contains("3.7.4"));
1901        assert!(!diagnostics[0].message.contains("4.0.0-alpha"));
1902    }
1903
1904    #[tokio::test]
1905    async fn test_generate_diagnostics_no_warning_when_latest_is_prerelease() {
1906        let cache = Arc::new(HttpCache::new());
1907        let mut handler = MockHandler::new(cache);
1908
1909        handler.registry.versions.insert(
1910            "prerelease-pkg".to_string(),
1911            vec![
1912                MockVersion {
1913                    version: "2.0.0-beta.1".to_string(),
1914                    yanked: false,
1915                    features: vec![],
1916                },
1917                MockVersion {
1918                    version: "1.5.0".to_string(),
1919                    yanked: false,
1920                    features: vec![],
1921                },
1922            ],
1923        );
1924
1925        let deps = vec![MockDependency {
1926            name: "prerelease-pkg".to_string(),
1927            version_req: Some("1.5.0".to_string()),
1928            version_range: Some(Range {
1929                start: Position {
1930                    line: 0,
1931                    character: 10,
1932                },
1933                end: Position {
1934                    line: 0,
1935                    character: 20,
1936                },
1937            }),
1938            name_range: Range::default(),
1939        }];
1940
1941        let config = DiagnosticsConfig::default();
1942        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1943
1944        assert_eq!(diagnostics.len(), 0);
1945    }
1946}