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.iter().find(|v| !H::is_deprecated(v));
764            if let (Some(latest), Some(current)) = (latest, &matching)
765                && latest.version_string() != current.version_string()
766            {
767                diagnostics.push(Diagnostic {
768                    range: version_range,
769                    severity: Some(config.outdated_severity),
770                    message: format!("Newer version available: {}", latest.version_string()),
771                    source: Some("deps-lsp".into()),
772                    ..Default::default()
773                });
774            }
775        }
776    }
777
778    diagnostics
779}
780
781#[cfg(test)]
782mod tests {
783    use super::*;
784    use crate::registry::PackageMetadata;
785    use tower_lsp::lsp_types::{Position, Range};
786
787    #[derive(Clone)]
788    struct MockVersion {
789        version: String,
790        yanked: bool,
791        features: Vec<String>,
792    }
793
794    impl VersionInfo for MockVersion {
795        fn version_string(&self) -> &str {
796            &self.version
797        }
798
799        fn is_yanked(&self) -> bool {
800            self.yanked
801        }
802
803        fn features(&self) -> Vec<String> {
804            self.features.clone()
805        }
806    }
807
808    #[derive(Clone)]
809    struct MockMetadata {
810        name: String,
811        description: Option<String>,
812        latest: String,
813    }
814
815    impl PackageMetadata for MockMetadata {
816        fn name(&self) -> &str {
817            &self.name
818        }
819
820        fn description(&self) -> Option<&str> {
821            self.description.as_deref()
822        }
823
824        fn repository(&self) -> Option<&str> {
825            None
826        }
827
828        fn documentation(&self) -> Option<&str> {
829            None
830        }
831
832        fn latest_version(&self) -> &str {
833            &self.latest
834        }
835    }
836
837    #[derive(Clone)]
838    struct MockDependency {
839        name: String,
840        version_req: Option<String>,
841        version_range: Option<Range>,
842        name_range: Range,
843    }
844
845    impl crate::parser::DependencyInfo for MockDependency {
846        fn name(&self) -> &str {
847            &self.name
848        }
849
850        fn name_range(&self) -> Range {
851            self.name_range
852        }
853
854        fn version_requirement(&self) -> Option<&str> {
855            self.version_req.as_deref()
856        }
857
858        fn version_range(&self) -> Option<Range> {
859            self.version_range
860        }
861
862        fn source(&self) -> crate::parser::DependencySource {
863            crate::parser::DependencySource::Registry
864        }
865    }
866
867    struct MockRegistry {
868        versions: std::collections::HashMap<String, Vec<MockVersion>>,
869    }
870
871    impl Clone for MockRegistry {
872        fn clone(&self) -> Self {
873            Self {
874                versions: self.versions.clone(),
875            }
876        }
877    }
878
879    #[async_trait]
880    impl crate::registry::PackageRegistry for MockRegistry {
881        type Version = MockVersion;
882        type Metadata = MockMetadata;
883        type VersionReq = String;
884
885        async fn get_versions(&self, name: &str) -> crate::error::Result<Vec<Self::Version>> {
886            self.versions.get(name).cloned().ok_or_else(|| {
887                use std::io::{Error as IoError, ErrorKind};
888                crate::DepsError::Io(IoError::new(ErrorKind::NotFound, "package not found"))
889            })
890        }
891
892        async fn get_latest_matching(
893            &self,
894            name: &str,
895            req: &Self::VersionReq,
896        ) -> crate::error::Result<Option<Self::Version>> {
897            Ok(self
898                .versions
899                .get(name)
900                .and_then(|versions| versions.iter().find(|v| v.version == *req).cloned()))
901        }
902
903        async fn search(
904            &self,
905            _query: &str,
906            _limit: usize,
907        ) -> crate::error::Result<Vec<Self::Metadata>> {
908            Ok(vec![])
909        }
910    }
911
912    struct MockHandler {
913        registry: MockRegistry,
914    }
915
916    #[async_trait]
917    impl EcosystemHandler for MockHandler {
918        type Registry = MockRegistry;
919        type Dependency = MockDependency;
920        type UnifiedDep = MockDependency;
921
922        fn new(_cache: Arc<HttpCache>) -> Self {
923            let mut versions = std::collections::HashMap::new();
924            versions.insert(
925                "serde".to_string(),
926                vec![
927                    MockVersion {
928                        version: "1.0.195".to_string(),
929                        yanked: false,
930                        features: vec!["derive".to_string(), "alloc".to_string()],
931                    },
932                    MockVersion {
933                        version: "1.0.194".to_string(),
934                        yanked: false,
935                        features: vec![],
936                    },
937                ],
938            );
939            versions.insert(
940                "yanked-pkg".to_string(),
941                vec![MockVersion {
942                    version: "1.0.0".to_string(),
943                    yanked: true,
944                    features: vec![],
945                }],
946            );
947
948            Self {
949                registry: MockRegistry { versions },
950            }
951        }
952
953        fn registry(&self) -> &Self::Registry {
954            &self.registry
955        }
956
957        fn extract_dependency(dep: &Self::UnifiedDep) -> Option<&Self::Dependency> {
958            Some(dep)
959        }
960
961        fn package_url(name: &str) -> String {
962            format!("https://test.io/pkg/{}", name)
963        }
964
965        fn ecosystem_display_name() -> &'static str {
966            "Test Registry"
967        }
968
969        fn is_version_latest(version_req: &str, latest: &str) -> bool {
970            version_req == latest
971        }
972
973        fn format_version_for_edit(_dep: &Self::Dependency, version: &str) -> String {
974            format!("\"{}\"", version)
975        }
976
977        fn is_deprecated(version: &MockVersion) -> bool {
978            version.yanked
979        }
980
981        fn is_valid_version_syntax(_version_req: &str) -> bool {
982            true
983        }
984
985        fn parse_version_req(version_req: &str) -> Option<String> {
986            Some(version_req.to_string())
987        }
988    }
989
990    impl VersionStringGetter for MockVersion {
991        fn version_string(&self) -> &str {
992            &self.version
993        }
994    }
995
996    impl YankedChecker for MockVersion {
997        fn is_yanked(&self) -> bool {
998            self.yanked
999        }
1000    }
1001
1002    #[test]
1003    fn test_inlay_hints_config_default() {
1004        let config = InlayHintsConfig::default();
1005        assert!(config.enabled);
1006        assert_eq!(config.up_to_date_text, "✅");
1007        assert_eq!(config.needs_update_text, "❌ {}");
1008    }
1009
1010    #[tokio::test]
1011    async fn test_generate_inlay_hints_cached() {
1012        let cache = Arc::new(HttpCache::new());
1013        let handler = MockHandler::new(cache);
1014
1015        let deps = vec![MockDependency {
1016            name: "serde".to_string(),
1017            version_req: Some("1.0.195".to_string()),
1018            version_range: Some(Range {
1019                start: Position {
1020                    line: 0,
1021                    character: 10,
1022                },
1023                end: Position {
1024                    line: 0,
1025                    character: 20,
1026                },
1027            }),
1028            name_range: Range::default(),
1029        }];
1030
1031        let mut cached_versions = HashMap::new();
1032        cached_versions.insert(
1033            "serde".to_string(),
1034            MockVersion {
1035                version: "1.0.195".to_string(),
1036                yanked: false,
1037                features: vec![],
1038            },
1039        );
1040
1041        let config = InlayHintsConfig::default();
1042        let resolved_versions: HashMap<String, String> = HashMap::new();
1043        let hints = generate_inlay_hints(
1044            &handler,
1045            &deps,
1046            &cached_versions,
1047            &resolved_versions,
1048            &config,
1049        )
1050        .await;
1051
1052        assert_eq!(hints.len(), 1);
1053        assert_eq!(hints[0].position.line, 0);
1054        assert_eq!(hints[0].position.character, 20);
1055    }
1056
1057    #[tokio::test]
1058    async fn test_generate_inlay_hints_fetch() {
1059        let cache = Arc::new(HttpCache::new());
1060        let handler = MockHandler::new(cache);
1061
1062        let deps = vec![MockDependency {
1063            name: "serde".to_string(),
1064            version_req: Some("1.0.0".to_string()),
1065            version_range: Some(Range {
1066                start: Position {
1067                    line: 0,
1068                    character: 10,
1069                },
1070                end: Position {
1071                    line: 0,
1072                    character: 20,
1073                },
1074            }),
1075            name_range: Range::default(),
1076        }];
1077
1078        let cached_versions: HashMap<String, MockVersion> = HashMap::new();
1079        let config = InlayHintsConfig::default();
1080        let resolved_versions: HashMap<String, String> = HashMap::new();
1081        let hints = generate_inlay_hints(
1082            &handler,
1083            &deps,
1084            &cached_versions,
1085            &resolved_versions,
1086            &config,
1087        )
1088        .await;
1089
1090        assert_eq!(hints.len(), 1);
1091    }
1092
1093    #[tokio::test]
1094    async fn test_generate_inlay_hints_skips_yanked() {
1095        let cache = Arc::new(HttpCache::new());
1096        let handler = MockHandler::new(cache);
1097
1098        let deps = vec![MockDependency {
1099            name: "serde".to_string(),
1100            version_req: Some("1.0.195".to_string()),
1101            version_range: Some(Range {
1102                start: Position {
1103                    line: 0,
1104                    character: 10,
1105                },
1106                end: Position {
1107                    line: 0,
1108                    character: 20,
1109                },
1110            }),
1111            name_range: Range::default(),
1112        }];
1113
1114        let mut cached_versions = HashMap::new();
1115        cached_versions.insert(
1116            "serde".to_string(),
1117            MockVersion {
1118                version: "1.0.195".to_string(),
1119                yanked: true,
1120                features: vec![],
1121            },
1122        );
1123
1124        let config = InlayHintsConfig::default();
1125        let resolved_versions: HashMap<String, String> = HashMap::new();
1126        let hints = generate_inlay_hints(
1127            &handler,
1128            &deps,
1129            &cached_versions,
1130            &resolved_versions,
1131            &config,
1132        )
1133        .await;
1134
1135        assert_eq!(hints.len(), 0);
1136    }
1137
1138    #[tokio::test]
1139    async fn test_generate_inlay_hints_no_version_range() {
1140        let cache = Arc::new(HttpCache::new());
1141        let handler = MockHandler::new(cache);
1142
1143        let deps = vec![MockDependency {
1144            name: "serde".to_string(),
1145            version_req: Some("1.0.195".to_string()),
1146            version_range: None,
1147            name_range: Range::default(),
1148        }];
1149
1150        let cached_versions: HashMap<String, MockVersion> = HashMap::new();
1151        let config = InlayHintsConfig::default();
1152        let resolved_versions: HashMap<String, String> = HashMap::new();
1153        let hints = generate_inlay_hints(
1154            &handler,
1155            &deps,
1156            &cached_versions,
1157            &resolved_versions,
1158            &config,
1159        )
1160        .await;
1161
1162        assert_eq!(hints.len(), 0);
1163    }
1164
1165    #[tokio::test]
1166    async fn test_generate_inlay_hints_no_version_req() {
1167        let cache = Arc::new(HttpCache::new());
1168        let handler = MockHandler::new(cache);
1169
1170        let deps = vec![MockDependency {
1171            name: "serde".to_string(),
1172            version_req: None,
1173            version_range: Some(Range {
1174                start: Position {
1175                    line: 0,
1176                    character: 10,
1177                },
1178                end: Position {
1179                    line: 0,
1180                    character: 20,
1181                },
1182            }),
1183            name_range: Range::default(),
1184        }];
1185
1186        let cached_versions: HashMap<String, MockVersion> = HashMap::new();
1187        let config = InlayHintsConfig::default();
1188        let resolved_versions: HashMap<String, String> = HashMap::new();
1189        let hints = generate_inlay_hints(
1190            &handler,
1191            &deps,
1192            &cached_versions,
1193            &resolved_versions,
1194            &config,
1195        )
1196        .await;
1197
1198        assert_eq!(hints.len(), 0);
1199    }
1200
1201    #[test]
1202    fn test_create_hint_up_to_date() {
1203        let config = InlayHintsConfig::default();
1204        let range = Range {
1205            start: Position {
1206                line: 5,
1207                character: 10,
1208            },
1209            end: Position {
1210                line: 5,
1211                character: 20,
1212            },
1213        };
1214
1215        let hint = create_hint::<MockHandler>("serde", range, "1.0.195", true, &config);
1216
1217        assert_eq!(hint.position, range.end);
1218        if let InlayHintLabel::LabelParts(parts) = hint.label {
1219            assert_eq!(parts[0].value, "✅");
1220        } else {
1221            panic!("Expected LabelParts");
1222        }
1223    }
1224
1225    #[test]
1226    fn test_create_hint_needs_update() {
1227        let config = InlayHintsConfig::default();
1228        let range = Range {
1229            start: Position {
1230                line: 5,
1231                character: 10,
1232            },
1233            end: Position {
1234                line: 5,
1235                character: 20,
1236            },
1237        };
1238
1239        let hint = create_hint::<MockHandler>("serde", range, "1.0.200", false, &config);
1240
1241        assert_eq!(hint.position, range.end);
1242        if let InlayHintLabel::LabelParts(parts) = hint.label {
1243            assert_eq!(parts[0].value, "❌ 1.0.200");
1244        } else {
1245            panic!("Expected LabelParts");
1246        }
1247    }
1248
1249    #[test]
1250    fn test_create_hint_custom_config() {
1251        let config = InlayHintsConfig {
1252            enabled: true,
1253            up_to_date_text: "OK".to_string(),
1254            needs_update_text: "UPDATE: {}".to_string(),
1255        };
1256        let range = Range {
1257            start: Position {
1258                line: 0,
1259                character: 0,
1260            },
1261            end: Position {
1262                line: 0,
1263                character: 10,
1264            },
1265        };
1266
1267        let hint = create_hint::<MockHandler>("test", range, "2.0.0", false, &config);
1268
1269        if let InlayHintLabel::LabelParts(parts) = hint.label {
1270            assert_eq!(parts[0].value, "UPDATE: 2.0.0");
1271        } else {
1272            panic!("Expected LabelParts");
1273        }
1274    }
1275
1276    #[tokio::test]
1277    async fn test_generate_hover() {
1278        let cache = Arc::new(HttpCache::new());
1279        let handler = MockHandler::new(cache);
1280
1281        let dep = MockDependency {
1282            name: "serde".to_string(),
1283            version_req: Some("1.0.0".to_string()),
1284            version_range: Some(Range::default()),
1285            name_range: Range {
1286                start: Position {
1287                    line: 0,
1288                    character: 0,
1289                },
1290                end: Position {
1291                    line: 0,
1292                    character: 5,
1293                },
1294            },
1295        };
1296
1297        let hover = generate_hover(&handler, &dep, None).await;
1298
1299        assert!(hover.is_some());
1300        let hover = hover.unwrap();
1301
1302        if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1303            assert!(content.value.contains("serde"));
1304            assert!(content.value.contains("1.0.195"));
1305            assert!(content.value.contains("Current"));
1306            assert!(content.value.contains("Features"));
1307            assert!(content.value.contains("derive"));
1308        } else {
1309            panic!("Expected Markup content");
1310        }
1311    }
1312
1313    #[tokio::test]
1314    async fn test_generate_hover_yanked_version() {
1315        let cache = Arc::new(HttpCache::new());
1316        let handler = MockHandler::new(cache);
1317
1318        let dep = MockDependency {
1319            name: "yanked-pkg".to_string(),
1320            version_req: Some("1.0.0".to_string()),
1321            version_range: Some(Range::default()),
1322            name_range: Range::default(),
1323        };
1324
1325        let hover = generate_hover(&handler, &dep, None).await;
1326
1327        assert!(hover.is_some());
1328        let hover = hover.unwrap();
1329
1330        if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1331            assert!(content.value.contains("Warning"));
1332            assert!(content.value.contains("yanked"));
1333        } else {
1334            panic!("Expected Markup content");
1335        }
1336    }
1337
1338    #[tokio::test]
1339    async fn test_generate_hover_no_versions() {
1340        let cache = Arc::new(HttpCache::new());
1341        let handler = MockHandler::new(cache);
1342
1343        let dep = MockDependency {
1344            name: "nonexistent".to_string(),
1345            version_req: Some("1.0.0".to_string()),
1346            version_range: Some(Range::default()),
1347            name_range: Range::default(),
1348        };
1349
1350        let hover = generate_hover(&handler, &dep, None).await;
1351        assert!(hover.is_none());
1352    }
1353
1354    #[tokio::test]
1355    async fn test_generate_hover_no_version_req() {
1356        let cache = Arc::new(HttpCache::new());
1357        let handler = MockHandler::new(cache);
1358
1359        let dep = MockDependency {
1360            name: "serde".to_string(),
1361            version_req: None,
1362            version_range: Some(Range::default()),
1363            name_range: Range::default(),
1364        };
1365
1366        let hover = generate_hover(&handler, &dep, None).await;
1367
1368        assert!(hover.is_some());
1369        let hover = hover.unwrap();
1370
1371        if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1372            assert!(!content.value.contains("Current"));
1373        } else {
1374            panic!("Expected Markup content");
1375        }
1376    }
1377
1378    #[tokio::test]
1379    async fn test_generate_hover_with_resolved_version() {
1380        let cache = Arc::new(HttpCache::new());
1381        let handler = MockHandler::new(cache);
1382
1383        let dep = MockDependency {
1384            name: "serde".to_string(),
1385            version_req: Some("1.0".to_string()), // Manifest has short version
1386            version_range: Some(Range::default()),
1387            name_range: Range {
1388                start: Position {
1389                    line: 0,
1390                    character: 0,
1391                },
1392                end: Position {
1393                    line: 0,
1394                    character: 5,
1395                },
1396            },
1397        };
1398
1399        // Pass resolved version from lock file (full version)
1400        let hover = generate_hover(&handler, &dep, Some("1.0.195")).await;
1401
1402        assert!(hover.is_some());
1403        let hover = hover.unwrap();
1404
1405        if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1406            // Should show the resolved version (1.0.195) not manifest version (1.0)
1407            assert!(content.value.contains("**Current**: `1.0.195`"));
1408            assert!(!content.value.contains("**Current**: `1.0`"));
1409        } else {
1410            panic!("Expected Markup content");
1411        }
1412    }
1413
1414    #[tokio::test]
1415    async fn test_generate_code_actions_empty_when_up_to_date() {
1416        use tower_lsp::lsp_types::Url;
1417
1418        let cache = Arc::new(HttpCache::new());
1419        let handler = MockHandler::new(cache);
1420
1421        let deps = vec![MockDependency {
1422            name: "serde".to_string(),
1423            version_req: Some("1.0.195".to_string()),
1424            version_range: Some(Range {
1425                start: Position {
1426                    line: 0,
1427                    character: 10,
1428                },
1429                end: Position {
1430                    line: 0,
1431                    character: 20,
1432                },
1433            }),
1434            name_range: Range::default(),
1435        }];
1436
1437        let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1438        let selected_range = Range {
1439            start: Position {
1440                line: 0,
1441                character: 15,
1442            },
1443            end: Position {
1444                line: 0,
1445                character: 15,
1446            },
1447        };
1448
1449        let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1450
1451        assert!(!actions.is_empty());
1452    }
1453
1454    #[tokio::test]
1455    async fn test_generate_code_actions_update_outdated() {
1456        use tower_lsp::lsp_types::{CodeActionOrCommand, Url};
1457
1458        let cache = Arc::new(HttpCache::new());
1459        let handler = MockHandler::new(cache);
1460
1461        let deps = vec![MockDependency {
1462            name: "serde".to_string(),
1463            version_req: Some("1.0.0".to_string()),
1464            version_range: Some(Range {
1465                start: Position {
1466                    line: 0,
1467                    character: 10,
1468                },
1469                end: Position {
1470                    line: 0,
1471                    character: 20,
1472                },
1473            }),
1474            name_range: Range::default(),
1475        }];
1476
1477        let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1478        let selected_range = Range {
1479            start: Position {
1480                line: 0,
1481                character: 15,
1482            },
1483            end: Position {
1484                line: 0,
1485                character: 15,
1486            },
1487        };
1488
1489        let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1490
1491        assert!(!actions.is_empty());
1492        assert!(actions.len() <= 5);
1493
1494        if let CodeActionOrCommand::CodeAction(action) = &actions[0] {
1495            assert!(action.title.contains("1.0.195"));
1496            assert!(action.title.contains("latest"));
1497            assert_eq!(action.is_preferred, Some(true));
1498        } else {
1499            panic!("Expected CodeAction");
1500        }
1501    }
1502
1503    #[tokio::test]
1504    async fn test_generate_code_actions_missing_version_range() {
1505        use tower_lsp::lsp_types::Url;
1506
1507        let cache = Arc::new(HttpCache::new());
1508        let handler = MockHandler::new(cache);
1509
1510        let deps = vec![MockDependency {
1511            name: "serde".to_string(),
1512            version_req: Some("1.0.0".to_string()),
1513            version_range: None,
1514            name_range: Range::default(),
1515        }];
1516
1517        let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1518        let selected_range = Range {
1519            start: Position {
1520                line: 0,
1521                character: 15,
1522            },
1523            end: Position {
1524                line: 0,
1525                character: 15,
1526            },
1527        };
1528
1529        let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1530
1531        assert_eq!(actions.len(), 0);
1532    }
1533
1534    #[tokio::test]
1535    async fn test_generate_code_actions_no_overlap() {
1536        use tower_lsp::lsp_types::Url;
1537
1538        let cache = Arc::new(HttpCache::new());
1539        let handler = MockHandler::new(cache);
1540
1541        let deps = vec![MockDependency {
1542            name: "serde".to_string(),
1543            version_req: Some("1.0.0".to_string()),
1544            version_range: Some(Range {
1545                start: Position {
1546                    line: 0,
1547                    character: 10,
1548                },
1549                end: Position {
1550                    line: 0,
1551                    character: 20,
1552                },
1553            }),
1554            name_range: Range::default(),
1555        }];
1556
1557        let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1558        let selected_range = Range {
1559            start: Position {
1560                line: 5,
1561                character: 0,
1562            },
1563            end: Position {
1564                line: 5,
1565                character: 10,
1566            },
1567        };
1568
1569        let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1570
1571        assert_eq!(actions.len(), 0);
1572    }
1573
1574    #[tokio::test]
1575    async fn test_generate_code_actions_filters_deprecated() {
1576        use tower_lsp::lsp_types::{CodeActionOrCommand, Url};
1577
1578        let cache = Arc::new(HttpCache::new());
1579        let handler = MockHandler::new(cache);
1580
1581        let deps = vec![MockDependency {
1582            name: "yanked-pkg".to_string(),
1583            version_req: Some("1.0.0".to_string()),
1584            version_range: Some(Range {
1585                start: Position {
1586                    line: 0,
1587                    character: 10,
1588                },
1589                end: Position {
1590                    line: 0,
1591                    character: 20,
1592                },
1593            }),
1594            name_range: Range::default(),
1595        }];
1596
1597        let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1598        let selected_range = Range {
1599            start: Position {
1600                line: 0,
1601                character: 15,
1602            },
1603            end: Position {
1604                line: 0,
1605                character: 15,
1606            },
1607        };
1608
1609        let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1610
1611        assert_eq!(actions.len(), 0);
1612
1613        for action in actions {
1614            if let CodeActionOrCommand::CodeAction(a) = action {
1615                assert!(!a.title.contains("1.0.0"));
1616            }
1617        }
1618    }
1619
1620    #[test]
1621    fn test_ranges_overlap_basic() {
1622        let range_a = Range {
1623            start: Position {
1624                line: 0,
1625                character: 10,
1626            },
1627            end: Position {
1628                line: 0,
1629                character: 20,
1630            },
1631        };
1632
1633        let range_b = Range {
1634            start: Position {
1635                line: 0,
1636                character: 15,
1637            },
1638            end: Position {
1639                line: 0,
1640                character: 25,
1641            },
1642        };
1643
1644        assert!(ranges_overlap(range_a, range_b));
1645    }
1646
1647    #[test]
1648    fn test_ranges_no_overlap() {
1649        let range_a = Range {
1650            start: Position {
1651                line: 0,
1652                character: 10,
1653            },
1654            end: Position {
1655                line: 0,
1656                character: 20,
1657            },
1658        };
1659
1660        let range_b = Range {
1661            start: Position {
1662                line: 0,
1663                character: 25,
1664            },
1665            end: Position {
1666                line: 0,
1667                character: 30,
1668            },
1669        };
1670
1671        assert!(!ranges_overlap(range_a, range_b));
1672    }
1673
1674    #[tokio::test]
1675    async fn test_generate_diagnostics_valid_version() {
1676        let cache = Arc::new(HttpCache::new());
1677        let handler = MockHandler::new(cache);
1678
1679        let deps = vec![MockDependency {
1680            name: "serde".to_string(),
1681            version_req: Some("1.0.195".to_string()),
1682            version_range: Some(Range {
1683                start: Position {
1684                    line: 0,
1685                    character: 10,
1686                },
1687                end: Position {
1688                    line: 0,
1689                    character: 20,
1690                },
1691            }),
1692            name_range: Range::default(),
1693        }];
1694
1695        let config = DiagnosticsConfig::default();
1696        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1697
1698        assert_eq!(diagnostics.len(), 0);
1699    }
1700
1701    #[tokio::test]
1702    async fn test_generate_diagnostics_deprecated_version() {
1703        use tower_lsp::lsp_types::DiagnosticSeverity;
1704
1705        let cache = Arc::new(HttpCache::new());
1706        let handler = MockHandler::new(cache);
1707
1708        let deps = vec![MockDependency {
1709            name: "yanked-pkg".to_string(),
1710            version_req: Some("1.0.0".to_string()),
1711            version_range: Some(Range {
1712                start: Position {
1713                    line: 0,
1714                    character: 10,
1715                },
1716                end: Position {
1717                    line: 0,
1718                    character: 20,
1719                },
1720            }),
1721            name_range: Range::default(),
1722        }];
1723
1724        let config = DiagnosticsConfig::default();
1725        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1726
1727        assert_eq!(diagnostics.len(), 1);
1728        assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
1729        assert!(diagnostics[0].message.contains("yanked"));
1730    }
1731
1732    #[tokio::test]
1733    async fn test_generate_diagnostics_unknown_package() {
1734        use tower_lsp::lsp_types::DiagnosticSeverity;
1735
1736        let cache = Arc::new(HttpCache::new());
1737        let handler = MockHandler::new(cache);
1738
1739        let deps = vec![MockDependency {
1740            name: "nonexistent".to_string(),
1741            version_req: Some("1.0.0".to_string()),
1742            version_range: Some(Range {
1743                start: Position {
1744                    line: 0,
1745                    character: 10,
1746                },
1747                end: Position {
1748                    line: 0,
1749                    character: 20,
1750                },
1751            }),
1752            name_range: Range {
1753                start: Position {
1754                    line: 0,
1755                    character: 0,
1756                },
1757                end: Position {
1758                    line: 0,
1759                    character: 10,
1760                },
1761            },
1762        }];
1763
1764        let config = DiagnosticsConfig::default();
1765        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1766
1767        assert_eq!(diagnostics.len(), 1);
1768        assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
1769        assert!(diagnostics[0].message.contains("Unknown package"));
1770        assert!(diagnostics[0].message.contains("nonexistent"));
1771    }
1772
1773    #[tokio::test]
1774    async fn test_generate_diagnostics_missing_version() {
1775        let cache = Arc::new(HttpCache::new());
1776        let handler = MockHandler::new(cache);
1777
1778        let deps = vec![MockDependency {
1779            name: "serde".to_string(),
1780            version_req: None,
1781            version_range: None,
1782            name_range: Range::default(),
1783        }];
1784
1785        let config = DiagnosticsConfig::default();
1786        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1787
1788        assert_eq!(diagnostics.len(), 0);
1789    }
1790
1791    #[tokio::test]
1792    async fn test_generate_diagnostics_outdated_version() {
1793        use tower_lsp::lsp_types::DiagnosticSeverity;
1794
1795        let cache = Arc::new(HttpCache::new());
1796        let mut handler = MockHandler::new(cache);
1797
1798        handler.registry.versions.insert(
1799            "outdated-pkg".to_string(),
1800            vec![
1801                MockVersion {
1802                    version: "2.0.0".to_string(),
1803                    yanked: false,
1804                    features: vec![],
1805                },
1806                MockVersion {
1807                    version: "1.0.0".to_string(),
1808                    yanked: false,
1809                    features: vec![],
1810                },
1811            ],
1812        );
1813
1814        let deps = vec![MockDependency {
1815            name: "outdated-pkg".to_string(),
1816            version_req: Some("1.0.0".to_string()),
1817            version_range: Some(Range {
1818                start: Position {
1819                    line: 0,
1820                    character: 10,
1821                },
1822                end: Position {
1823                    line: 0,
1824                    character: 20,
1825                },
1826            }),
1827            name_range: Range::default(),
1828        }];
1829
1830        let config = DiagnosticsConfig::default();
1831        let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1832
1833        assert_eq!(diagnostics.len(), 1);
1834        assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::HINT));
1835        assert!(diagnostics[0].message.contains("Newer version available"));
1836        assert!(diagnostics[0].message.contains("2.0.0"));
1837    }
1838
1839    #[test]
1840    fn test_diagnostics_config_default() {
1841        use tower_lsp::lsp_types::DiagnosticSeverity;
1842
1843        let config = DiagnosticsConfig::default();
1844        assert_eq!(config.unknown_severity, DiagnosticSeverity::WARNING);
1845        assert_eq!(config.yanked_severity, DiagnosticSeverity::WARNING);
1846        assert_eq!(config.outdated_severity, DiagnosticSeverity::HINT);
1847    }
1848}