use tower_lsp_server::ls_types::{InlayHint, InlayHintKind, InlayHintLabel, InlayHintTooltip};
use crate::{EcosystemConfig, ParseResult, VersionReq};
use super::{EcosystemFormatter, RequirementStatus, VersionData};
pub fn generate_inlay_hints(
parse_result: &dyn ParseResult,
versions: VersionData<'_>,
loading_state: crate::LoadingState,
config: &EcosystemConfig,
formatter: &dyn EcosystemFormatter,
) -> Vec<InlayHint> {
let deps = parse_result.dependencies();
let mut hints = Vec::with_capacity(deps.len());
for dep in deps {
let Some(version_range) = dep.version_range() else {
continue;
};
let normalized_name = formatter.normalize_package_name(dep.name());
let latest_version = versions
.cached
.get(normalized_name.as_str())
.or_else(|| versions.cached.get(dep.name()))
.map(|v| v.latest.as_str());
let resolved_version: Option<&str> =
if formatter.manifest_requirement_is_resolved_version(dep) {
dep.version_requirement().map(VersionReq::as_str)
} else {
versions
.resolved
.get(normalized_name.as_str())
.or_else(|| versions.resolved.get(dep.name()))
.map(String::as_str)
};
if loading_state == crate::LoadingState::Loading
&& config.show_loading_hints
&& latest_version.is_none()
{
hints.push(InlayHint {
position: version_range.end,
label: InlayHintLabel::String(config.loading_text.clone()),
kind: Some(InlayHintKind::TYPE),
tooltip: Some(InlayHintTooltip::String(
"Fetching latest version...".to_string(),
)),
padding_left: Some(true),
padding_right: None,
text_edits: None,
data: None,
});
continue;
}
let Some(latest) = latest_version else {
if let Some(resolved) = resolved_version
&& config.show_up_to_date_hints
{
hints.push(InlayHint {
position: version_range.end,
label: InlayHintLabel::String(format!(
"{} {}",
config.up_to_date_text, resolved
)),
kind: Some(InlayHintKind::TYPE),
padding_left: Some(true),
padding_right: None,
text_edits: None,
tooltip: None,
data: None,
});
}
continue;
};
let status = if let Some(resolved) = resolved_version {
if resolved == latest {
RequirementStatus::UpToDate
} else {
RequirementStatus::Outdated
}
} else {
match dep.version_requirement() {
Some(version_req) => formatter.requirement_status(version_req, latest),
None => RequirementStatus::Unresolved,
}
};
let label_text = match status {
RequirementStatus::UpToDate => {
if config.show_up_to_date_hints {
if let Some(resolved) = resolved_version {
format!("{} {}", config.up_to_date_text, resolved)
} else {
config.up_to_date_text.clone()
}
} else {
continue;
}
}
RequirementStatus::Outdated => config.needs_update_text.replace("{}", latest),
RequirementStatus::Unresolved => continue,
};
hints.push(InlayHint {
position: version_range.end,
label: InlayHintLabel::String(label_text),
kind: Some(InlayHintKind::TYPE),
padding_left: Some(true),
padding_right: None,
text_edits: None,
tooltip: None,
data: None,
});
}
hints
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lsp_helpers::test_support::*;
use crate::lsp_helpers::*;
#[test]
fn test_inlay_hint_exact_version_shows_update_needed() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let config = EcosystemConfig {
show_up_to_date_hints: true,
up_to_date_text: "✅".to_string(),
needs_update_text: "❌ {}".to_string(),
loading_text: "⏳".to_string(),
show_loading_hints: true,
};
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "=2.0.12".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("serde".into(), PackageVersions::latest_only("2.1.1"));
let mut resolved_versions = HashMap::new();
resolved_versions.insert("serde".into(), "2.0.12".to_string());
let hints = generate_inlay_hints(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
crate::LoadingState::Loaded,
&config,
&formatter,
);
assert_eq!(hints.len(), 1);
match &hints[0].label {
InlayHintLabel::String(text) => {
assert_eq!(text, "❌ 2.1.1");
}
_ => panic!("Expected string label"),
}
}
#[test]
fn test_inlay_hint_caret_version_up_to_date() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let config = EcosystemConfig {
show_up_to_date_hints: true,
up_to_date_text: "✅".to_string(),
needs_update_text: "❌ {}".to_string(),
loading_text: "⏳".to_string(),
show_loading_hints: true,
};
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "^2.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("serde".into(), PackageVersions::latest_only("2.1.1"));
let mut resolved_versions = HashMap::new();
resolved_versions.insert("serde".into(), "2.1.1".to_string());
let hints = generate_inlay_hints(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
crate::LoadingState::Loaded,
&config,
&formatter,
);
assert_eq!(hints.len(), 1);
match &hints[0].label {
InlayHintLabel::String(text) => {
assert!(
text.starts_with("✅"),
"Expected up-to-date hint, got: {}",
text
);
}
_ => panic!("Expected string label"),
}
}
#[test]
fn test_inlay_hint_go_prefers_manifest_requirement_over_stale_resolved_version() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockGoFormatter;
let config = EcosystemConfig {
show_up_to_date_hints: true,
up_to_date_text: "✅".to_string(),
needs_update_text: "❌ {}".to_string(),
loading_text: "⏳".to_string(),
show_loading_hints: true,
};
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "example.com/mod".into(),
version_req: "v0.8.1".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/go.mod"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"example.com/mod".into(),
PackageVersions::latest_only("v0.9.1"),
);
let mut resolved_versions = HashMap::new();
resolved_versions.insert("example.com/mod".into(), "v0.9.1".to_string());
let hints = generate_inlay_hints(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
crate::LoadingState::Loaded,
&config,
&formatter,
);
assert_eq!(hints.len(), 1);
match &hints[0].label {
InlayHintLabel::String(text) => {
assert!(
text.starts_with("❌"),
"expected outdated hint driven by go.mod pin, got: {text}"
);
}
_ => panic!("Expected string label"),
}
}
#[test]
fn test_inlay_hint_non_go_formatter_uses_resolved_lockfile_version() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let config = EcosystemConfig {
show_up_to_date_hints: true,
up_to_date_text: "✅".to_string(),
needs_update_text: "❌ {}".to_string(),
loading_text: "⏳".to_string(),
show_loading_hints: true,
};
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("serde".into(), PackageVersions::latest_only("1.2.0"));
let mut resolved_versions = HashMap::new();
resolved_versions.insert("serde".into(), "1.2.0".to_string());
let hints = generate_inlay_hints(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
crate::LoadingState::Loaded,
&config,
&formatter,
);
assert_eq!(hints.len(), 1);
match &hints[0].label {
InlayHintLabel::String(text) => {
assert!(
text.starts_with("✅"),
"expected up-to-date hint from resolved lockfile version, got: {text}"
);
}
_ => panic!("Expected string label"),
}
}
#[test]
fn test_loading_hint_shows_when_no_cached_version() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let config = EcosystemConfig {
show_up_to_date_hints: true,
up_to_date_text: "✅".to_string(),
needs_update_text: "❌ {}".to_string(),
loading_text: "⏳".to_string(),
show_loading_hints: true,
};
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "tokio".into(),
version_req: "1.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let hints = generate_inlay_hints(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
crate::LoadingState::Loading,
&config,
&formatter,
);
assert_eq!(hints.len(), 1);
match &hints[0].label {
InlayHintLabel::String(text) => {
assert_eq!(text, "⏳", "Expected loading hint");
}
_ => panic!("Expected string label"),
}
if let Some(InlayHintTooltip::String(tooltip)) = &hints[0].tooltip {
assert_eq!(tooltip, "Fetching latest version...");
} else {
panic!("Expected tooltip");
}
}
#[test]
fn test_loading_hint_disabled_when_config_false() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let config = EcosystemConfig {
show_up_to_date_hints: true,
up_to_date_text: "✅".to_string(),
needs_update_text: "❌ {}".to_string(),
loading_text: "⏳".to_string(),
show_loading_hints: false,
};
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "tokio".into(),
version_req: "1.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let hints = generate_inlay_hints(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
crate::LoadingState::Loading,
&config,
&formatter,
);
assert_eq!(
hints.len(),
0,
"Expected no hints when loading hints disabled"
);
}
#[test]
fn test_caret_version_0x_edge_cases() {
let formatter = MockFormatter;
assert!(formatter.version_satisfies_requirement("0.2.0", "^0.2"));
assert!(formatter.version_satisfies_requirement("0.2.5", "^0.2"));
assert!(formatter.version_satisfies_requirement("0.2.99", "^0.2"));
assert!(!formatter.version_satisfies_requirement("0.3.0", "^0.2"));
assert!(!formatter.version_satisfies_requirement("0.1.0", "^0.2"));
assert!(!formatter.version_satisfies_requirement("1.0.0", "^0.2"));
assert!(formatter.version_satisfies_requirement("0.0.3", "^0.0.3"));
assert!(formatter.version_satisfies_requirement("0.0.3", "^0.0"));
assert!(formatter.version_satisfies_requirement("0.0.0", "^0"));
assert!(formatter.version_satisfies_requirement("0.5.0", "^0"));
assert!(!formatter.version_satisfies_requirement("1.0.0", "^0"));
}
#[test]
fn test_caret_version_non_zero_major() {
let formatter = MockFormatter;
assert!(formatter.version_satisfies_requirement("1.0.0", "^1.2"));
assert!(formatter.version_satisfies_requirement("1.2.0", "^1.2"));
assert!(formatter.version_satisfies_requirement("1.9.9", "^1.2"));
assert!(!formatter.version_satisfies_requirement("2.0.0", "^1.2"));
assert!(!formatter.version_satisfies_requirement("0.9.0", "^1.2"));
}
#[test]
fn test_loading_hint_not_shown_when_cached_version_exists() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let config = EcosystemConfig {
show_up_to_date_hints: true,
up_to_date_text: "✅".to_string(),
needs_update_text: "❌ {}".to_string(),
loading_text: "⏳".to_string(),
show_loading_hints: true,
};
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "1.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("serde".into(), PackageVersions::latest_only("1.0.214"));
let mut resolved_versions = HashMap::new();
resolved_versions.insert("serde".into(), "1.0.214".to_string());
let hints = generate_inlay_hints(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
crate::LoadingState::Loading,
&config,
&formatter,
);
assert_eq!(hints.len(), 1);
match &hints[0].label {
InlayHintLabel::String(text) => {
assert_eq!(
text, "✅ 1.0.214",
"Expected up-to-date hint, not loading hint, got: {}",
text
);
}
_ => panic!("Expected string label"),
}
}
#[test]
fn test_inlay_hint_not_in_lockfile_but_satisfies_requirement() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let config = EcosystemConfig {
show_up_to_date_hints: true,
up_to_date_text: "✅".to_string(),
needs_update_text: "❌ {}".to_string(),
loading_text: "⏳".to_string(),
show_loading_hints: true,
};
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "criterion".into(),
version_req: "0.5".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 9)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("criterion".into(), PackageVersions::latest_only("0.5.1"));
let resolved_versions = HashMap::new();
let hints = generate_inlay_hints(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
crate::LoadingState::Loaded,
&config,
&formatter,
);
assert_eq!(hints.len(), 1);
match &hints[0].label {
InlayHintLabel::String(text) => {
assert!(
text.starts_with("✅"),
"Expected up-to-date hint for satisfied requirement, got: {}",
text
);
}
_ => panic!("Expected string label"),
}
}
#[test]
fn test_inlay_hint_not_in_lockfile_and_outdated() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let formatter = MockFormatter;
let config = EcosystemConfig {
show_up_to_date_hints: true,
up_to_date_text: "✅".to_string(),
needs_update_text: "❌ {}".to_string(),
loading_text: "⏳".to_string(),
show_loading_hints: true,
};
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "criterion".into(),
version_req: "0.4".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 9)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert("criterion".into(), PackageVersions::latest_only("0.5.1"));
let resolved_versions = HashMap::new();
let hints = generate_inlay_hints(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
crate::LoadingState::Loaded,
&config,
&formatter,
);
assert_eq!(hints.len(), 1);
match &hints[0].label {
InlayHintLabel::String(text) => {
assert!(
text.starts_with("❌"),
"Expected needs-update hint for unsatisfied requirement, got: {}",
text
);
assert!(text.contains("0.5.1"), "Expected latest version in hint");
}
_ => panic!("Expected string label"),
}
}
#[test]
fn test_inlay_hint_unresolved_requirement_emits_no_hint() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let config = EcosystemConfig {
show_up_to_date_hints: true,
up_to_date_text: "✅".to_string(),
needs_update_text: "❌ {}".to_string(),
loading_text: "⏳".to_string(),
show_loading_hints: true,
};
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "spring-boot-starter".into(),
version_req: "$missing".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/libs.versions.toml"),
};
let mut cached_versions = HashMap::new();
cached_versions.insert(
"spring-boot-starter".into(),
PackageVersions::latest_only("3.2.0"),
);
let resolved_versions = HashMap::new();
let hints = generate_inlay_hints(
&parse_result,
VersionData::new(&cached_versions, &resolved_versions),
crate::LoadingState::Loaded,
&config,
&MockUnresolvedFormatter,
);
assert!(
hints.is_empty(),
"Expected no inlay hint at all for an unresolved requirement (not even 'up to date'), got: {hints:?}"
);
}
}