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::lsp_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::lsp_types::Range { tower_lsp::lsp_types::Range::default() }
65/// #     fn version_requirement(&self) -> Option<&str> { None }
66/// #     fn version_range(&self) -> Option<tower_lsp::lsp_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::lsp_types::InlayHintLabelPartTooltip::MarkupContent(MarkupContent {
412                    kind: MarkupKind::Markdown,
413                    value: tooltip_content,
414                }),
415            ),
416            location: None,
417            command: Some(tower_lsp::lsp_types::Command {
418                title: format!("Open on {}", H::ecosystem_display_name()),
419                command: "vscode.open".into(),
420                arguments: Some(vec![serde_json::json!(url)]),
421            }),
422        }]),
423        kind: Some(InlayHintKind::TYPE),
424        text_edits: None,
425        tooltip: None,
426        padding_left: Some(true),
427        padding_right: None,
428        data: None,
429    }
430}
431
432/// Generic hover generator.
433///
434/// Fetches version information and generates markdown hover content
435/// with version list and features (if supported).
436///
437/// # Type Parameters
438///
439/// * `H` - Ecosystem handler type
440///
441/// # Arguments
442///
443/// * `handler` - Ecosystem handler instance
444/// * `dep` - Dependency to generate hover for
445/// * `resolved_version` - Optional resolved version from lock file (preferred over manifest version)
446pub async fn generate_hover<H>(
447    handler: &H,
448    dep: &H::UnifiedDep,
449    resolved_version: Option<&str>,
450) -> Option<tower_lsp::lsp_types::Hover>
451where
452    H: EcosystemHandler,
453{
454    use tower_lsp::lsp_types::{Hover, HoverContents};
455
456    let typed_dep = H::extract_dependency(dep)?;
457    let registry = handler.registry();
458    let versions: Vec<<H::Registry as PackageRegistry>::Version> =
459        registry.get_versions(typed_dep.name()).await.ok()?;
460    let latest: &<H::Registry as PackageRegistry>::Version = versions.first()?;
461
462    let url = H::package_url(typed_dep.name());
463    let mut markdown = format!("# [{}]({})\n\n", typed_dep.name(), url);
464
465    if let Some(version) = resolved_version.or(typed_dep.version_requirement()) {
466        markdown.push_str(&format!("**Current**: `{}`\n\n", version));
467    }
468
469    if latest.is_yanked() {
470        markdown.push_str("⚠️ **Warning**: This version has been yanked\n\n");
471    }
472
473    markdown.push_str("**Versions** *(use Cmd+. to update)*:\n");
474    for (i, version) in versions.iter().take(MAX_VERSIONS_IN_HOVER).enumerate() {
475        if i == 0 {
476            markdown.push_str(&format!("- {} *(latest)*\n", version.version_string()));
477        } else {
478            markdown.push_str(&format!("- {}\n", version.version_string()));
479        }
480    }
481    if versions.len() > MAX_VERSIONS_IN_HOVER {
482        markdown.push_str(&format!(
483            "- *...and {} more*\n",
484            versions.len() - MAX_VERSIONS_IN_HOVER
485        ));
486    }
487
488    let features = latest.features();
489    if !features.is_empty() {
490        markdown.push_str("\n**Features**:\n");
491        for feature in features.iter().take(MAX_FEATURES_IN_HOVER) {
492            markdown.push_str(&format!("- `{}`\n", feature));
493        }
494        if features.len() > MAX_FEATURES_IN_HOVER {
495            markdown.push_str(&format!(
496                "- *...and {} more*\n",
497                features.len() - MAX_FEATURES_IN_HOVER
498            ));
499        }
500    }
501
502    Some(Hover {
503        contents: HoverContents::Markup(MarkupContent {
504            kind: MarkupKind::Markdown,
505            value: markdown,
506        }),
507        range: Some(typed_dep.name_range()),
508    })
509}
510
511/// Configuration for diagnostics display.
512///
513/// This is a simplified version to avoid circular dependencies.
514pub struct DiagnosticsConfig {
515    pub unknown_severity: tower_lsp::lsp_types::DiagnosticSeverity,
516    pub yanked_severity: tower_lsp::lsp_types::DiagnosticSeverity,
517    pub outdated_severity: tower_lsp::lsp_types::DiagnosticSeverity,
518}
519
520impl Default for DiagnosticsConfig {
521    fn default() -> Self {
522        use tower_lsp::lsp_types::DiagnosticSeverity;
523        Self {
524            unknown_severity: DiagnosticSeverity::WARNING,
525            yanked_severity: DiagnosticSeverity::WARNING,
526            outdated_severity: DiagnosticSeverity::HINT,
527        }
528    }
529}
530
531/// Generic code actions generator.
532///
533/// Fetches available versions and generates "Update to version X" quick fixes.
534///
535/// # Type Parameters
536///
537/// * `H` - Ecosystem handler type
538///
539/// # Arguments
540///
541/// * `handler` - Ecosystem-specific handler instance
542/// * `dependencies` - List of dependencies with version ranges
543/// * `uri` - Document URI
544/// * `selected_range` - Range selected by user for code actions
545///
546/// # Returns
547///
548/// Vector of code actions (quick fixes) for the LSP client.
549pub async fn generate_code_actions<H>(
550    handler: &H,
551    dependencies: &[H::UnifiedDep],
552    uri: &tower_lsp::lsp_types::Url,
553    selected_range: Range,
554) -> Vec<tower_lsp::lsp_types::CodeActionOrCommand>
555where
556    H: EcosystemHandler,
557{
558    use tower_lsp::lsp_types::{
559        CodeAction, CodeActionKind, CodeActionOrCommand, TextEdit, WorkspaceEdit,
560    };
561
562    let mut deps_to_check = Vec::new();
563    for dep in dependencies {
564        let Some(typed_dep) = H::extract_dependency(dep) else {
565            continue;
566        };
567
568        let Some(version_range) = typed_dep.version_range() else {
569            continue;
570        };
571
572        // Check if this dependency's version range overlaps with cursor position
573        if !ranges_overlap(version_range, selected_range) {
574            continue;
575        }
576
577        deps_to_check.push((typed_dep, version_range));
578    }
579
580    if deps_to_check.is_empty() {
581        return vec![];
582    }
583
584    let registry = handler.registry().clone();
585    let futures: Vec<_> = deps_to_check
586        .iter()
587        .map(|(dep, version_range)| {
588            let name = dep.name().to_string();
589            let version_range = *version_range;
590            let registry = registry.clone();
591            async move {
592                let versions = registry.get_versions(&name).await;
593                (name, dep, version_range, versions)
594            }
595        })
596        .collect();
597
598    let results = join_all(futures).await;
599
600    let mut actions = Vec::new();
601    for (name, dep, version_range, versions_result) in results {
602        let Ok(versions) = versions_result else {
603            tracing::warn!("Failed to fetch versions for {}", name);
604            continue;
605        };
606
607        for (i, version) in versions
608            .iter()
609            .filter(|v| !H::is_deprecated(v))
610            .take(MAX_CODE_ACTION_VERSIONS)
611            .enumerate()
612        {
613            let new_text = H::format_version_for_edit(dep, version.version_string());
614
615            let mut edits = std::collections::HashMap::new();
616            edits.insert(
617                uri.clone(),
618                vec![TextEdit {
619                    range: version_range,
620                    new_text,
621                }],
622            );
623
624            let title = if i == 0 {
625                format!("Update {} to {} (latest)", name, version.version_string())
626            } else {
627                format!("Update {} to {}", name, version.version_string())
628            };
629
630            actions.push(CodeActionOrCommand::CodeAction(CodeAction {
631                title,
632                kind: Some(CodeActionKind::REFACTOR),
633                edit: Some(WorkspaceEdit {
634                    changes: Some(edits),
635                    ..Default::default()
636                }),
637                is_preferred: Some(i == 0),
638                ..Default::default()
639            }));
640        }
641    }
642
643    actions
644}
645
646fn ranges_overlap(a: Range, b: Range) -> bool {
647    !(a.end.line < b.start.line
648        || (a.end.line == b.start.line && a.end.character < b.start.character)
649        || b.end.line < a.start.line
650        || (b.end.line == a.start.line && b.end.character < a.start.character))
651}
652
653/// Generic diagnostics generator.
654///
655/// Checks dependencies for issues:
656/// - Unknown packages (not found in registry)
657/// - Invalid version syntax
658/// - Yanked/deprecated versions
659/// - Outdated versions
660///
661/// # Type Parameters
662///
663/// * `H` - Ecosystem handler type
664///
665/// # Arguments
666///
667/// * `handler` - Ecosystem-specific handler instance
668/// * `dependencies` - List of dependencies to check
669/// * `config` - Diagnostic severity configuration
670///
671/// # Returns
672///
673/// Vector of LSP diagnostics.
674pub async fn generate_diagnostics<H>(
675    handler: &H,
676    dependencies: &[H::UnifiedDep],
677    config: &DiagnosticsConfig,
678) -> Vec<tower_lsp::lsp_types::Diagnostic>
679where
680    H: EcosystemHandler,
681{
682    use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
683
684    let mut deps_to_check = Vec::new();
685    for dep in dependencies {
686        let Some(typed_dep) = H::extract_dependency(dep) else {
687            continue;
688        };
689        deps_to_check.push(typed_dep);
690    }
691
692    if deps_to_check.is_empty() {
693        return vec![];
694    }
695
696    let registry = handler.registry().clone();
697    let futures: Vec<_> = deps_to_check
698        .iter()
699        .map(|dep| {
700            let name = dep.name().to_string();
701            let registry = registry.clone();
702            async move {
703                let versions = registry.get_versions(&name).await;
704                (name, versions)
705            }
706        })
707        .collect();
708
709    let version_results = join_all(futures).await;
710
711    let mut diagnostics = Vec::new();
712
713    for (i, dep) in deps_to_check.iter().enumerate() {
714        let (name, version_result) = &version_results[i];
715
716        let versions = match version_result {
717            Ok(v) => v,
718            Err(_) => {
719                diagnostics.push(Diagnostic {
720                    range: dep.name_range(),
721                    severity: Some(config.unknown_severity),
722                    message: format!("Unknown package '{}'", name),
723                    source: Some("deps-lsp".into()),
724                    ..Default::default()
725                });
726                continue;
727            }
728        };
729
730        if let Some(version_req) = dep.version_requirement()
731            && let Some(version_range) = dep.version_range()
732        {
733            let Some(parsed_version_req) = H::parse_version_req(version_req) else {
734                diagnostics.push(Diagnostic {
735                    range: version_range,
736                    severity: Some(DiagnosticSeverity::ERROR),
737                    message: format!("Invalid version requirement '{}'", version_req),
738                    source: Some("deps-lsp".into()),
739                    ..Default::default()
740                });
741                continue;
742            };
743
744            let matching = handler
745                .registry()
746                .get_latest_matching(name, &parsed_version_req)
747                .await
748                .ok()
749                .flatten();
750
751            if let Some(current) = &matching
752                && H::is_deprecated(current)
753            {
754                diagnostics.push(Diagnostic {
755                    range: version_range,
756                    severity: Some(config.yanked_severity),
757                    message: "This version has been yanked".into(),
758                    source: Some("deps-lsp".into()),
759                    ..Default::default()
760                });
761            }
762
763            let latest = versions
764                .iter()
765                .find(|v| !H::is_deprecated(v) && !v.is_prerelease());
766            if let (Some(latest), Some(current)) = (latest, &matching)
767                && latest.version_string() != current.version_string()
768            {
769                diagnostics.push(Diagnostic {
770                    range: version_range,
771                    severity: Some(config.outdated_severity),
772                    message: format!("Newer version available: {}", latest.version_string()),
773                    source: Some("deps-lsp".into()),
774                    ..Default::default()
775                });
776            }
777        }
778    }
779
780    diagnostics
781}
782
783#[cfg(test)]
784mod tests {
785    use super::*;
786    use crate::registry::PackageMetadata;
787    use tower_lsp::lsp_types::{Position, Range};
788
789    #[derive(Clone)]
790    struct MockVersion {
791        version: String,
792        yanked: bool,
793        features: Vec<String>,
794    }
795
796    impl VersionInfo for MockVersion {
797        fn version_string(&self) -> &str {
798            &self.version
799        }
800
801        fn is_yanked(&self) -> bool {
802            self.yanked
803        }
804
805        fn features(&self) -> Vec<String> {
806            self.features.clone()
807        }
808    }
809
810    #[derive(Clone)]
811    struct MockMetadata {
812        name: String,
813        description: Option<String>,
814        latest: String,
815    }
816
817    impl PackageMetadata for MockMetadata {
818        fn name(&self) -> &str {
819            &self.name
820        }
821
822        fn description(&self) -> Option<&str> {
823            self.description.as_deref()
824        }
825
826        fn repository(&self) -> Option<&str> {
827            None
828        }
829
830        fn documentation(&self) -> Option<&str> {
831            None
832        }
833
834        fn latest_version(&self) -> &str {
835            &self.latest
836        }
837    }
838
839    #[derive(Clone)]
840    struct MockDependency {
841        name: String,
842        version_req: Option<String>,
843        version_range: Option<Range>,
844        name_range: Range,
845    }
846
847    impl crate::parser::DependencyInfo for MockDependency {
848        fn name(&self) -> &str {
849            &self.name
850        }
851
852        fn name_range(&self) -> Range {
853            self.name_range
854        }
855
856        fn version_requirement(&self) -> Option<&str> {
857            self.version_req.as_deref()
858        }
859
860        fn version_range(&self) -> Option<Range> {
861            self.version_range
862        }
863
864        fn source(&self) -> crate::parser::DependencySource {
865            crate::parser::DependencySource::Registry
866        }
867    }
868
869    struct MockRegistry {
870        versions: std::collections::HashMap<String, Vec<MockVersion>>,
871    }
872
873    impl Clone for MockRegistry {
874        fn clone(&self) -> Self {
875            Self {
876                versions: self.versions.clone(),
877            }
878        }
879    }
880
881    #[async_trait]
882    impl crate::registry::PackageRegistry for MockRegistry {
883        type Version = MockVersion;
884        type Metadata = MockMetadata;
885        type VersionReq = String;
886
887        async fn get_versions(&self, name: &str) -> crate::error::Result<Vec<Self::Version>> {
888            self.versions.get(name).cloned().ok_or_else(|| {
889                use std::io::{Error as IoError, ErrorKind};
890                crate::DepsError::Io(IoError::new(ErrorKind::NotFound, "package not found"))
891            })
892        }
893
894        async fn get_latest_matching(
895            &self,
896            name: &str,
897            req: &Self::VersionReq,
898        ) -> crate::error::Result<Option<Self::Version>> {
899            Ok(self
900                .versions
901                .get(name)
902                .and_then(|versions| versions.iter().find(|v| v.version == *req).cloned()))
903        }
904
905        async fn search(
906            &self,
907            _query: &str,
908            _limit: usize,
909        ) -> crate::error::Result<Vec<Self::Metadata>> {
910            Ok(vec![])
911        }
912    }
913
914    struct MockHandler {
915        registry: MockRegistry,
916    }
917
918    #[async_trait]
919    impl EcosystemHandler for MockHandler {
920        type Registry = MockRegistry;
921        type Dependency = MockDependency;
922        type UnifiedDep = MockDependency;
923
924        fn new(_cache: Arc<HttpCache>) -> Self {
925            let mut versions = std::collections::HashMap::new();
926            versions.insert(
927                "serde".to_string(),
928                vec![
929                    MockVersion {
930                        version: "1.0.195".to_string(),
931                        yanked: false,
932                        features: vec!["derive".to_string(), "alloc".to_string()],
933                    },
934                    MockVersion {
935                        version: "1.0.194".to_string(),
936                        yanked: false,
937                        features: vec![],
938                    },
939                ],
940            );
941            versions.insert(
942                "yanked-pkg".to_string(),
943                vec![MockVersion {
944                    version: "1.0.0".to_string(),
945                    yanked: true,
946                    features: vec![],
947                }],
948            );
949
950            Self {
951                registry: MockRegistry { versions },
952            }
953        }
954
955        fn registry(&self) -> &Self::Registry {
956            &self.registry
957        }
958
959        fn extract_dependency(dep: &Self::UnifiedDep) -> Option<&Self::Dependency> {
960            Some(dep)
961        }
962
963        fn package_url(name: &str) -> String {
964            format!("https://test.io/pkg/{}", name)
965        }
966
967        fn ecosystem_display_name() -> &'static str {
968            "Test Registry"
969        }
970
971        fn is_version_latest(version_req: &str, latest: &str) -> bool {
972            version_req == latest
973        }
974
975        fn format_version_for_edit(_dep: &Self::Dependency, version: &str) -> String {
976            format!("\"{}\"", version)
977        }
978
979        fn is_deprecated(version: &MockVersion) -> bool {
980            version.yanked
981        }
982
983        fn is_valid_version_syntax(_version_req: &str) -> bool {
984            true
985        }
986
987        fn parse_version_req(version_req: &str) -> Option<String> {
988            Some(version_req.to_string())
989        }
990    }
991
992    impl VersionStringGetter for MockVersion {
993        fn version_string(&self) -> &str {
994            &self.version
995        }
996    }
997
998    impl YankedChecker for MockVersion {
999        fn is_yanked(&self) -> bool {
1000            self.yanked
1001        }
1002    }
1003
1004    #[test]
1005    fn test_inlay_hints_config_default() {
1006        let config = InlayHintsConfig::default();
1007        assert!(config.enabled);
1008        assert_eq!(config.up_to_date_text, "✅");
1009        assert_eq!(config.needs_update_text, "❌ {}");
1010    }
1011
1012    #[tokio::test]
1013    async fn test_generate_inlay_hints_cached() {
1014        let cache = Arc::new(HttpCache::new());
1015        let handler = MockHandler::new(cache);
1016
1017        let deps = vec![MockDependency {
1018            name: "serde".to_string(),
1019            version_req: Some("1.0.195".to_string()),
1020            version_range: Some(Range {
1021                start: Position {
1022                    line: 0,
1023                    character: 10,
1024                },
1025                end: Position {
1026                    line: 0,
1027                    character: 20,
1028                },
1029            }),
1030            name_range: Range::default(),
1031        }];
1032
1033        let mut cached_versions = HashMap::new();
1034        cached_versions.insert(
1035            "serde".to_string(),
1036            MockVersion {
1037                version: "1.0.195".to_string(),
1038                yanked: false,
1039                features: vec![],
1040            },
1041        );
1042
1043        let config = InlayHintsConfig::default();
1044        let resolved_versions: HashMap<String, String> = HashMap::new();
1045        let hints = generate_inlay_hints(
1046            &handler,
1047            &deps,
1048            &cached_versions,
1049            &resolved_versions,
1050            &config,
1051        )
1052        .await;
1053
1054        assert_eq!(hints.len(), 1);
1055        assert_eq!(hints[0].position.line, 0);
1056        assert_eq!(hints[0].position.character, 20);
1057    }
1058
1059    #[tokio::test]
1060    async fn test_generate_inlay_hints_fetch() {
1061        let cache = Arc::new(HttpCache::new());
1062        let handler = MockHandler::new(cache);
1063
1064        let deps = vec![MockDependency {
1065            name: "serde".to_string(),
1066            version_req: Some("1.0.0".to_string()),
1067            version_range: Some(Range {
1068                start: Position {
1069                    line: 0,
1070                    character: 10,
1071                },
1072                end: Position {
1073                    line: 0,
1074                    character: 20,
1075                },
1076            }),
1077            name_range: Range::default(),
1078        }];
1079
1080        let cached_versions: HashMap<String, MockVersion> = HashMap::new();
1081        let config = InlayHintsConfig::default();
1082        let resolved_versions: HashMap<String, String> = HashMap::new();
1083        let hints = generate_inlay_hints(
1084            &handler,
1085            &deps,
1086            &cached_versions,
1087            &resolved_versions,
1088            &config,
1089        )
1090        .await;
1091
1092        assert_eq!(hints.len(), 1);
1093    }
1094
1095    #[tokio::test]
1096    async fn test_generate_inlay_hints_skips_yanked() {
1097        let cache = Arc::new(HttpCache::new());
1098        let handler = MockHandler::new(cache);
1099
1100        let deps = vec![MockDependency {
1101            name: "serde".to_string(),
1102            version_req: Some("1.0.195".to_string()),
1103            version_range: Some(Range {
1104                start: Position {
1105                    line: 0,
1106                    character: 10,
1107                },
1108                end: Position {
1109                    line: 0,
1110                    character: 20,
1111                },
1112            }),
1113            name_range: Range::default(),
1114        }];
1115
1116        let mut cached_versions = HashMap::new();
1117        cached_versions.insert(
1118            "serde".to_string(),
1119            MockVersion {
1120                version: "1.0.195".to_string(),
1121                yanked: true,
1122                features: vec![],
1123            },
1124        );
1125
1126        let config = InlayHintsConfig::default();
1127        let resolved_versions: HashMap<String, String> = HashMap::new();
1128        let hints = generate_inlay_hints(
1129            &handler,
1130            &deps,
1131            &cached_versions,
1132            &resolved_versions,
1133            &config,
1134        )
1135        .await;
1136
1137        assert_eq!(hints.len(), 0);
1138    }
1139
1140    #[tokio::test]
1141    async fn test_generate_inlay_hints_no_version_range() {
1142        let cache = Arc::new(HttpCache::new());
1143        let handler = MockHandler::new(cache);
1144
1145        let deps = vec![MockDependency {
1146            name: "serde".to_string(),
1147            version_req: Some("1.0.195".to_string()),
1148            version_range: None,
1149            name_range: Range::default(),
1150        }];
1151
1152        let cached_versions: HashMap<String, MockVersion> = HashMap::new();
1153        let config = InlayHintsConfig::default();
1154        let resolved_versions: HashMap<String, String> = HashMap::new();
1155        let hints = generate_inlay_hints(
1156            &handler,
1157            &deps,
1158            &cached_versions,
1159            &resolved_versions,
1160            &config,
1161        )
1162        .await;
1163
1164        assert_eq!(hints.len(), 0);
1165    }
1166
1167    #[tokio::test]
1168    async fn test_generate_inlay_hints_no_version_req() {
1169        let cache = Arc::new(HttpCache::new());
1170        let handler = MockHandler::new(cache);
1171
1172        let deps = vec![MockDependency {
1173            name: "serde".to_string(),
1174            version_req: None,
1175            version_range: Some(Range {
1176                start: Position {
1177                    line: 0,
1178                    character: 10,
1179                },
1180                end: Position {
1181                    line: 0,
1182                    character: 20,
1183                },
1184            }),
1185            name_range: Range::default(),
1186        }];
1187
1188        let cached_versions: HashMap<String, MockVersion> = HashMap::new();
1189        let config = InlayHintsConfig::default();
1190        let resolved_versions: HashMap<String, String> = HashMap::new();
1191        let hints = generate_inlay_hints(
1192            &handler,
1193            &deps,
1194            &cached_versions,
1195            &resolved_versions,
1196            &config,
1197        )
1198        .await;
1199
1200        assert_eq!(hints.len(), 0);
1201    }
1202
1203    #[test]
1204    fn test_create_hint_up_to_date() {
1205        let config = InlayHintsConfig::default();
1206        let range = Range {
1207            start: Position {
1208                line: 5,
1209                character: 10,
1210            },
1211            end: Position {
1212                line: 5,
1213                character: 20,
1214            },
1215        };
1216
1217        let hint = create_hint::<MockHandler>("serde", range, "1.0.195", true, &config);
1218
1219        assert_eq!(hint.position, range.end);
1220        if let InlayHintLabel::LabelParts(parts) = hint.label {
1221            assert_eq!(parts[0].value, "✅");
1222        } else {
1223            panic!("Expected LabelParts");
1224        }
1225    }
1226
1227    #[test]
1228    fn test_create_hint_needs_update() {
1229        let config = InlayHintsConfig::default();
1230        let range = Range {
1231            start: Position {
1232                line: 5,
1233                character: 10,
1234            },
1235            end: Position {
1236                line: 5,
1237                character: 20,
1238            },
1239        };
1240
1241        let hint = create_hint::<MockHandler>("serde", range, "1.0.200", false, &config);
1242
1243        assert_eq!(hint.position, range.end);
1244        if let InlayHintLabel::LabelParts(parts) = hint.label {
1245            assert_eq!(parts[0].value, "❌ 1.0.200");
1246        } else {
1247            panic!("Expected LabelParts");
1248        }
1249    }
1250
1251    #[test]
1252    fn test_create_hint_custom_config() {
1253        let config = InlayHintsConfig {
1254            enabled: true,
1255            up_to_date_text: "OK".to_string(),
1256            needs_update_text: "UPDATE: {}".to_string(),
1257        };
1258        let range = Range {
1259            start: Position {
1260                line: 0,
1261                character: 0,
1262            },
1263            end: Position {
1264                line: 0,
1265                character: 10,
1266            },
1267        };
1268
1269        let hint = create_hint::<MockHandler>("test", range, "2.0.0", false, &config);
1270
1271        if let InlayHintLabel::LabelParts(parts) = hint.label {
1272            assert_eq!(parts[0].value, "UPDATE: 2.0.0");
1273        } else {
1274            panic!("Expected LabelParts");
1275        }
1276    }
1277
1278    #[tokio::test]
1279    async fn test_generate_hover() {
1280        let cache = Arc::new(HttpCache::new());
1281        let handler = MockHandler::new(cache);
1282
1283        let dep = MockDependency {
1284            name: "serde".to_string(),
1285            version_req: Some("1.0.0".to_string()),
1286            version_range: Some(Range::default()),
1287            name_range: Range {
1288                start: Position {
1289                    line: 0,
1290                    character: 0,
1291                },
1292                end: Position {
1293                    line: 0,
1294                    character: 5,
1295                },
1296            },
1297        };
1298
1299        let hover = generate_hover(&handler, &dep, None).await;
1300
1301        assert!(hover.is_some());
1302        let hover = hover.unwrap();
1303
1304        if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1305            assert!(content.value.contains("serde"));
1306            assert!(content.value.contains("1.0.195"));
1307            assert!(content.value.contains("Current"));
1308            assert!(content.value.contains("Features"));
1309            assert!(content.value.contains("derive"));
1310        } else {
1311            panic!("Expected Markup content");
1312        }
1313    }
1314
1315    #[tokio::test]
1316    async fn test_generate_hover_yanked_version() {
1317        let cache = Arc::new(HttpCache::new());
1318        let handler = MockHandler::new(cache);
1319
1320        let dep = MockDependency {
1321            name: "yanked-pkg".to_string(),
1322            version_req: Some("1.0.0".to_string()),
1323            version_range: Some(Range::default()),
1324            name_range: Range::default(),
1325        };
1326
1327        let hover = generate_hover(&handler, &dep, None).await;
1328
1329        assert!(hover.is_some());
1330        let hover = hover.unwrap();
1331
1332        if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1333            assert!(content.value.contains("Warning"));
1334            assert!(content.value.contains("yanked"));
1335        } else {
1336            panic!("Expected Markup content");
1337        }
1338    }
1339
1340    #[tokio::test]
1341    async fn test_generate_hover_no_versions() {
1342        let cache = Arc::new(HttpCache::new());
1343        let handler = MockHandler::new(cache);
1344
1345        let dep = MockDependency {
1346            name: "nonexistent".to_string(),
1347            version_req: Some("1.0.0".to_string()),
1348            version_range: Some(Range::default()),
1349            name_range: Range::default(),
1350        };
1351
1352        let hover = generate_hover(&handler, &dep, None).await;
1353        assert!(hover.is_none());
1354    }
1355
1356    #[tokio::test]
1357    async fn test_generate_hover_no_version_req() {
1358        let cache = Arc::new(HttpCache::new());
1359        let handler = MockHandler::new(cache);
1360
1361        let dep = MockDependency {
1362            name: "serde".to_string(),
1363            version_req: None,
1364            version_range: Some(Range::default()),
1365            name_range: Range::default(),
1366        };
1367
1368        let hover = generate_hover(&handler, &dep, None).await;
1369
1370        assert!(hover.is_some());
1371        let hover = hover.unwrap();
1372
1373        if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1374            assert!(!content.value.contains("Current"));
1375        } else {
1376            panic!("Expected Markup content");
1377        }
1378    }
1379
1380    #[tokio::test]
1381    async fn test_generate_hover_with_resolved_version() {
1382        let cache = Arc::new(HttpCache::new());
1383        let handler = MockHandler::new(cache);
1384
1385        let dep = MockDependency {
1386            name: "serde".to_string(),
1387            version_req: Some("1.0".to_string()), // Manifest has short version
1388            version_range: Some(Range::default()),
1389            name_range: Range {
1390                start: Position {
1391                    line: 0,
1392                    character: 0,
1393                },
1394                end: Position {
1395                    line: 0,
1396                    character: 5,
1397                },
1398            },
1399        };
1400
1401        // Pass resolved version from lock file (full version)
1402        let hover = generate_hover(&handler, &dep, Some("1.0.195")).await;
1403
1404        assert!(hover.is_some());
1405        let hover = hover.unwrap();
1406
1407        if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1408            // Should show the resolved version (1.0.195) not manifest version (1.0)
1409            assert!(content.value.contains("**Current**: `1.0.195`"));
1410            assert!(!content.value.contains("**Current**: `1.0`"));
1411        } else {
1412            panic!("Expected Markup content");
1413        }
1414    }
1415
1416    #[tokio::test]
1417    async fn test_generate_code_actions_empty_when_up_to_date() {
1418        use tower_lsp::lsp_types::Url;
1419
1420        let cache = Arc::new(HttpCache::new());
1421        let handler = MockHandler::new(cache);
1422
1423        let deps = vec![MockDependency {
1424            name: "serde".to_string(),
1425            version_req: Some("1.0.195".to_string()),
1426            version_range: Some(Range {
1427                start: Position {
1428                    line: 0,
1429                    character: 10,
1430                },
1431                end: Position {
1432                    line: 0,
1433                    character: 20,
1434                },
1435            }),
1436            name_range: Range::default(),
1437        }];
1438
1439        let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1440        let selected_range = Range {
1441            start: Position {
1442                line: 0,
1443                character: 15,
1444            },
1445            end: Position {
1446                line: 0,
1447                character: 15,
1448            },
1449        };
1450
1451        let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1452
1453        assert!(!actions.is_empty());
1454    }
1455
1456    #[tokio::test]
1457    async fn test_generate_code_actions_update_outdated() {
1458        use tower_lsp::lsp_types::{CodeActionOrCommand, Url};
1459
1460        let cache = Arc::new(HttpCache::new());
1461        let handler = MockHandler::new(cache);
1462
1463        let deps = vec![MockDependency {
1464            name: "serde".to_string(),
1465            version_req: Some("1.0.0".to_string()),
1466            version_range: Some(Range {
1467                start: Position {
1468                    line: 0,
1469                    character: 10,
1470                },
1471                end: Position {
1472                    line: 0,
1473                    character: 20,
1474                },
1475            }),
1476            name_range: Range::default(),
1477        }];
1478
1479        let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1480        let selected_range = Range {
1481            start: Position {
1482                line: 0,
1483                character: 15,
1484            },
1485            end: Position {
1486                line: 0,
1487                character: 15,
1488            },
1489        };
1490
1491        let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1492
1493        assert!(!actions.is_empty());
1494        assert!(actions.len() <= 5);
1495
1496        if let CodeActionOrCommand::CodeAction(action) = &actions[0] {
1497            assert!(action.title.contains("1.0.195"));
1498            assert!(action.title.contains("latest"));
1499            assert_eq!(action.is_preferred, Some(true));
1500        } else {
1501            panic!("Expected CodeAction");
1502        }
1503    }
1504
1505    #[tokio::test]
1506    async fn test_generate_code_actions_missing_version_range() {
1507        use tower_lsp::lsp_types::Url;
1508
1509        let cache = Arc::new(HttpCache::new());
1510        let handler = MockHandler::new(cache);
1511
1512        let deps = vec![MockDependency {
1513            name: "serde".to_string(),
1514            version_req: Some("1.0.0".to_string()),
1515            version_range: None,
1516            name_range: Range::default(),
1517        }];
1518
1519        let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1520        let selected_range = Range {
1521            start: Position {
1522                line: 0,
1523                character: 15,
1524            },
1525            end: Position {
1526                line: 0,
1527                character: 15,
1528            },
1529        };
1530
1531        let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1532
1533        assert_eq!(actions.len(), 0);
1534    }
1535
1536    #[tokio::test]
1537    async fn test_generate_code_actions_no_overlap() {
1538        use tower_lsp::lsp_types::Url;
1539
1540        let cache = Arc::new(HttpCache::new());
1541        let handler = MockHandler::new(cache);
1542
1543        let deps = vec![MockDependency {
1544            name: "serde".to_string(),
1545            version_req: Some("1.0.0".to_string()),
1546            version_range: Some(Range {
1547                start: Position {
1548                    line: 0,
1549                    character: 10,
1550                },
1551                end: Position {
1552                    line: 0,
1553                    character: 20,
1554                },
1555            }),
1556            name_range: Range::default(),
1557        }];
1558
1559        let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1560        let selected_range = Range {
1561            start: Position {
1562                line: 5,
1563                character: 0,
1564            },
1565            end: Position {
1566                line: 5,
1567                character: 10,
1568            },
1569        };
1570
1571        let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1572
1573        assert_eq!(actions.len(), 0);
1574    }
1575
1576    #[tokio::test]
1577    async fn test_generate_code_actions_filters_deprecated() {
1578        use tower_lsp::lsp_types::{CodeActionOrCommand, Url};
1579
1580        let cache = Arc::new(HttpCache::new());
1581        let handler = MockHandler::new(cache);
1582
1583        let deps = vec![MockDependency {
1584            name: "yanked-pkg".to_string(),
1585            version_req: Some("1.0.0".to_string()),
1586            version_range: Some(Range {
1587                start: Position {
1588                    line: 0,
1589                    character: 10,
1590                },
1591                end: Position {
1592                    line: 0,
1593                    character: 20,
1594                },
1595            }),
1596            name_range: Range::default(),
1597        }];
1598
1599        let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1600        let selected_range = Range {
1601            start: Position {
1602                line: 0,
1603                character: 15,
1604            },
1605            end: Position {
1606                line: 0,
1607                character: 15,
1608            },
1609        };
1610
1611        let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1612
1613        assert_eq!(actions.len(), 0);
1614
1615        for action in actions {
1616            if let CodeActionOrCommand::CodeAction(a) = action {
1617                assert!(!a.title.contains("1.0.0"));
1618            }
1619        }
1620    }
1621
1622    #[test]
1623    fn test_ranges_overlap_basic() {
1624        let range_a = Range {
1625            start: Position {
1626                line: 0,
1627                character: 10,
1628            },
1629            end: Position {
1630                line: 0,
1631                character: 20,
1632            },
1633        };
1634
1635        let range_b = Range {
1636            start: Position {
1637                line: 0,
1638                character: 15,
1639            },
1640            end: Position {
1641                line: 0,
1642                character: 25,
1643            },
1644        };
1645
1646        assert!(ranges_overlap(range_a, range_b));
1647    }
1648
1649    #[test]
1650    fn test_ranges_no_overlap() {
1651        let range_a = Range {
1652            start: Position {
1653                line: 0,
1654                character: 10,
1655            },
1656            end: Position {
1657                line: 0,
1658                character: 20,
1659            },
1660        };
1661
1662        let range_b = Range {
1663            start: Position {
1664                line: 0,
1665                character: 25,
1666            },
1667            end: Position {
1668                line: 0,
1669                character: 30,
1670            },
1671        };
1672
1673        assert!(!ranges_overlap(range_a, range_b));
1674    }
1675
1676    #[tokio::test]
1677    async fn test_generate_diagnostics_valid_version() {
1678        let cache = Arc::new(HttpCache::new());
1679        let handler = MockHandler::new(cache);
1680
1681        let deps = vec![MockDependency {
1682            name: "serde".to_string(),
1683            version_req: Some("1.0.195".to_string()),
1684            version_range: Some(Range {
1685                start: Position {
1686                    line: 0,
1687                    character: 10,
1688                },
1689                end: Position {
1690                    line: 0,
1691                    character: 20,
1692                },
1693            }),
1694            name_range: Range::default(),
1695        }];
1696
1697        let config = DiagnosticsConfig::default();
1698        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1699
1700        assert_eq!(diagnostics.len(), 0);
1701    }
1702
1703    #[tokio::test]
1704    async fn test_generate_diagnostics_deprecated_version() {
1705        use tower_lsp::lsp_types::DiagnosticSeverity;
1706
1707        let cache = Arc::new(HttpCache::new());
1708        let handler = MockHandler::new(cache);
1709
1710        let deps = vec![MockDependency {
1711            name: "yanked-pkg".to_string(),
1712            version_req: Some("1.0.0".to_string()),
1713            version_range: Some(Range {
1714                start: Position {
1715                    line: 0,
1716                    character: 10,
1717                },
1718                end: Position {
1719                    line: 0,
1720                    character: 20,
1721                },
1722            }),
1723            name_range: Range::default(),
1724        }];
1725
1726        let config = DiagnosticsConfig::default();
1727        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1728
1729        assert_eq!(diagnostics.len(), 1);
1730        assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
1731        assert!(diagnostics[0].message.contains("yanked"));
1732    }
1733
1734    #[tokio::test]
1735    async fn test_generate_diagnostics_unknown_package() {
1736        use tower_lsp::lsp_types::DiagnosticSeverity;
1737
1738        let cache = Arc::new(HttpCache::new());
1739        let handler = MockHandler::new(cache);
1740
1741        let deps = vec![MockDependency {
1742            name: "nonexistent".to_string(),
1743            version_req: Some("1.0.0".to_string()),
1744            version_range: Some(Range {
1745                start: Position {
1746                    line: 0,
1747                    character: 10,
1748                },
1749                end: Position {
1750                    line: 0,
1751                    character: 20,
1752                },
1753            }),
1754            name_range: Range {
1755                start: Position {
1756                    line: 0,
1757                    character: 0,
1758                },
1759                end: Position {
1760                    line: 0,
1761                    character: 10,
1762                },
1763            },
1764        }];
1765
1766        let config = DiagnosticsConfig::default();
1767        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1768
1769        assert_eq!(diagnostics.len(), 1);
1770        assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
1771        assert!(diagnostics[0].message.contains("Unknown package"));
1772        assert!(diagnostics[0].message.contains("nonexistent"));
1773    }
1774
1775    #[tokio::test]
1776    async fn test_generate_diagnostics_missing_version() {
1777        let cache = Arc::new(HttpCache::new());
1778        let handler = MockHandler::new(cache);
1779
1780        let deps = vec![MockDependency {
1781            name: "serde".to_string(),
1782            version_req: None,
1783            version_range: None,
1784            name_range: Range::default(),
1785        }];
1786
1787        let config = DiagnosticsConfig::default();
1788        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1789
1790        assert_eq!(diagnostics.len(), 0);
1791    }
1792
1793    #[tokio::test]
1794    async fn test_generate_diagnostics_outdated_version() {
1795        use tower_lsp::lsp_types::DiagnosticSeverity;
1796
1797        let cache = Arc::new(HttpCache::new());
1798        let mut handler = MockHandler::new(cache);
1799
1800        handler.registry.versions.insert(
1801            "outdated-pkg".to_string(),
1802            vec![
1803                MockVersion {
1804                    version: "2.0.0".to_string(),
1805                    yanked: false,
1806                    features: vec![],
1807                },
1808                MockVersion {
1809                    version: "1.0.0".to_string(),
1810                    yanked: false,
1811                    features: vec![],
1812                },
1813            ],
1814        );
1815
1816        let deps = vec![MockDependency {
1817            name: "outdated-pkg".to_string(),
1818            version_req: Some("1.0.0".to_string()),
1819            version_range: Some(Range {
1820                start: Position {
1821                    line: 0,
1822                    character: 10,
1823                },
1824                end: Position {
1825                    line: 0,
1826                    character: 20,
1827                },
1828            }),
1829            name_range: Range::default(),
1830        }];
1831
1832        let config = DiagnosticsConfig::default();
1833        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1834
1835        assert_eq!(diagnostics.len(), 1);
1836        assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::HINT));
1837        assert!(diagnostics[0].message.contains("Newer version available"));
1838        assert!(diagnostics[0].message.contains("2.0.0"));
1839    }
1840
1841    #[test]
1842    fn test_diagnostics_config_default() {
1843        use tower_lsp::lsp_types::DiagnosticSeverity;
1844
1845        let config = DiagnosticsConfig::default();
1846        assert_eq!(config.unknown_severity, DiagnosticSeverity::WARNING);
1847        assert_eq!(config.yanked_severity, DiagnosticSeverity::WARNING);
1848        assert_eq!(config.outdated_severity, DiagnosticSeverity::HINT);
1849    }
1850
1851    #[tokio::test]
1852    async fn test_generate_diagnostics_ignores_prerelease() {
1853        let cache = Arc::new(HttpCache::new());
1854        let mut handler = MockHandler::new(cache);
1855
1856        handler.registry.versions.insert(
1857            "test-pkg".to_string(),
1858            vec![
1859                MockVersion {
1860                    version: "4.0.0-alpha.13".to_string(),
1861                    yanked: false,
1862                    features: vec![],
1863                },
1864                MockVersion {
1865                    version: "3.7.4".to_string(),
1866                    yanked: false,
1867                    features: vec![],
1868                },
1869                MockVersion {
1870                    version: "3.4.2".to_string(),
1871                    yanked: false,
1872                    features: vec![],
1873                },
1874            ],
1875        );
1876
1877        let deps = vec![MockDependency {
1878            name: "test-pkg".to_string(),
1879            version_req: Some("3.4.2".to_string()),
1880            version_range: Some(Range {
1881                start: Position {
1882                    line: 0,
1883                    character: 10,
1884                },
1885                end: Position {
1886                    line: 0,
1887                    character: 20,
1888                },
1889            }),
1890            name_range: Range::default(),
1891        }];
1892
1893        let config = DiagnosticsConfig::default();
1894        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1895
1896        assert_eq!(diagnostics.len(), 1);
1897        assert!(diagnostics[0].message.contains("Newer version available"));
1898        assert!(diagnostics[0].message.contains("3.7.4"));
1899        assert!(!diagnostics[0].message.contains("4.0.0-alpha"));
1900    }
1901
1902    #[tokio::test]
1903    async fn test_generate_diagnostics_no_warning_when_latest_is_prerelease() {
1904        let cache = Arc::new(HttpCache::new());
1905        let mut handler = MockHandler::new(cache);
1906
1907        handler.registry.versions.insert(
1908            "prerelease-pkg".to_string(),
1909            vec![
1910                MockVersion {
1911                    version: "2.0.0-beta.1".to_string(),
1912                    yanked: false,
1913                    features: vec![],
1914                },
1915                MockVersion {
1916                    version: "1.5.0".to_string(),
1917                    yanked: false,
1918                    features: vec![],
1919                },
1920            ],
1921        );
1922
1923        let deps = vec![MockDependency {
1924            name: "prerelease-pkg".to_string(),
1925            version_req: Some("1.5.0".to_string()),
1926            version_range: Some(Range {
1927                start: Position {
1928                    line: 0,
1929                    character: 10,
1930                },
1931                end: Position {
1932                    line: 0,
1933                    character: 20,
1934                },
1935            }),
1936            name_range: Range::default(),
1937        }];
1938
1939        let config = DiagnosticsConfig::default();
1940        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1941
1942        assert_eq!(diagnostics.len(), 0);
1943    }
1944}