use std::str::FromStr;
use std::sync::LazyLock;
use owo_colors::OwoColorize;
use rustc_hash::FxHashMap;
use version_ranges::Ranges;
use uv_distribution_types::{DerivationChain, DerivationStep};
use uv_errors::Hints;
use uv_normalize::PackageName;
use uv_pep440::{Version, strip_local_version_sentinels};
static SUGGESTIONS: LazyLock<FxHashMap<PackageName, PackageName>> = LazyLock::new(|| {
let suggestions: Vec<(String, String)> =
serde_json::from_str(include_str!("suggestions.json")).unwrap();
suggestions
.iter()
.map(|(k, v)| {
(
PackageName::from_str(k).unwrap(),
PackageName::from_str(v).unwrap(),
)
})
.collect()
});
pub fn dist_hints(
name: &PackageName,
version: Option<&Version>,
chain: &DerivationChain,
cause_hints: Hints<'_>,
) -> Hints<'static> {
let mut hints = Hints::none();
if let Some(suggestion) = SUGGESTIONS.get(name) {
hints.push(format!(
"`{}` is often confused for `{}`. Did you mean to install `{}` instead?",
name.cyan(),
suggestion.cyan(),
suggestion.cyan(),
));
} else if !chain.is_empty() {
hints.push(format_chain(name, version, chain));
}
hints.extend(cause_hints);
hints.into_owned()
}
fn format_chain(name: &PackageName, version: Option<&Version>, chain: &DerivationChain) -> String {
fn format_step(step: &DerivationStep, range: Option<Ranges<Version>>) -> String {
if let Some(range) =
range.filter(|range| *range != Ranges::empty() && *range != Ranges::full())
{
if let Some(extra) = &step.extra {
if let Some(version) = step.version.as_ref() {
format!(
"`{}{}` ({})",
format!("{}[{}]", step.name, extra).cyan(),
range.cyan(),
format!("v{version}").cyan(),
)
} else {
format!(
"`{}{}`",
format!("{}[{}]", step.name, extra).cyan(),
range.cyan(),
)
}
} else if let Some(group) = &step.group {
if let Some(version) = step.version.as_ref() {
format!(
"`{}{}` ({})",
format!("{}:{}", step.name, group).cyan(),
range.cyan(),
format!("v{version}").cyan(),
)
} else {
format!(
"`{}{}`",
format!("{}:{}", step.name, group).cyan(),
range.cyan(),
)
}
} else {
if let Some(version) = step.version.as_ref() {
format!(
"`{}{}` ({})",
step.name.cyan(),
range.cyan(),
format!("v{version}").cyan(),
)
} else {
format!("`{}{}`", step.name.cyan(), range.cyan())
}
}
} else {
if let Some(extra) = &step.extra {
if let Some(version) = step.version.as_ref() {
format!(
"`{}` ({})",
format!("{}[{}]", step.name, extra).cyan(),
format!("v{version}").cyan(),
)
} else {
format!("`{}`", format!("{}[{}]", step.name, extra).cyan())
}
} else if let Some(group) = &step.group {
if let Some(version) = step.version.as_ref() {
format!(
"`{}` ({})",
format!("{}:{}", step.name, group).cyan(),
format!("v{version}").cyan(),
)
} else {
format!("`{}`", format!("{}:{}", step.name, group).cyan())
}
} else {
if let Some(version) = step.version.as_ref() {
format!("`{}` ({})", step.name.cyan(), format!("v{version}").cyan())
} else {
format!("`{}`", step.name.cyan())
}
}
}
}
let mut message = if let Some(version) = version {
format!(
"`{}` ({}) was included because",
name.cyan(),
format!("v{version}").cyan()
)
} else {
format!("`{}` was included because", name.cyan())
};
let mut range: Option<Ranges<Version>> = None;
for (i, step) in chain.iter().enumerate() {
if i > 0 {
message = format!("{message} {} which depends on", format_step(step, range));
} else {
message = format!("{message} {} depends on", format_step(step, range));
}
range = Some(strip_local_version_sentinels(&step.range));
}
if let Some(range) = range.filter(|range| *range != Ranges::empty() && *range != Ranges::full())
{
message = format!("{message} `{}{}`", name.cyan(), range.cyan());
} else {
message = format!("{message} `{}`", name.cyan());
}
message
}