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