use std::any::Any;
use std::pin::Pin;
use std::sync::Arc;
use tower_lsp_server::ls_types::{
CodeAction, CodeLens, Diagnostic, Hover, InlayHint, Position, Uri,
};
use crate::{
Registry,
completion::Completions,
lsp_helpers::{EcosystemFormatter, VersionData},
};
pub mod private {
pub trait Sealed {}
}
pub type BoxFuture<'a, T> = Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EcosystemId {
Cargo,
Npm,
Pypi,
Go,
Bundler,
Dart,
Maven,
Composer,
Gradle,
Swift,
NuGet,
Deno,
}
impl EcosystemId {
#[must_use]
pub const fn id(self) -> &'static str {
match self {
Self::Cargo => "cargo",
Self::Npm => "npm",
Self::Pypi => "pypi",
Self::Go => "go",
Self::Bundler => "bundler",
Self::Dart => "dart",
Self::Maven => "maven",
Self::Composer => "composer",
Self::Gradle => "gradle",
Self::Swift => "swift",
Self::NuGet => "nuget",
Self::Deno => "deno",
}
}
#[must_use]
pub const fn osv_ecosystem(self) -> Option<&'static str> {
Some(match self {
Self::Cargo => "crates.io",
Self::Npm | Self::Deno => "npm",
Self::Pypi => "PyPI",
Self::Go => "Go",
Self::Bundler => "RubyGems",
Self::Dart => "Pub",
Self::Maven | Self::Gradle => "Maven",
Self::Composer => "Packagist",
Self::Swift => "SwiftURL",
Self::NuGet => "NuGet",
})
}
}
impl std::fmt::Display for EcosystemId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.id())
}
}
impl std::str::FromStr for EcosystemId {
type Err = crate::error::DepsError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"cargo" => Ok(Self::Cargo),
"npm" => Ok(Self::Npm),
"pypi" => Ok(Self::Pypi),
"go" => Ok(Self::Go),
"bundler" => Ok(Self::Bundler),
"dart" => Ok(Self::Dart),
"maven" => Ok(Self::Maven),
"composer" => Ok(Self::Composer),
"gradle" => Ok(Self::Gradle),
"swift" => Ok(Self::Swift),
"nuget" => Ok(Self::NuGet),
"deno" => Ok(Self::Deno),
_ => Err(crate::error::DepsError::UnsupportedEcosystem(s.to_string())),
}
}
}
pub trait ParseResult: Send + Sync {
fn dependencies(&self) -> Vec<&dyn Dependency>;
fn workspace_root(&self) -> Option<&std::path::Path>;
fn uri(&self) -> &Uri;
fn as_any(&self) -> &dyn Any;
}
pub trait Dependency: Send + Sync {
fn name(&self) -> &crate::PackageName;
fn name_range(&self) -> tower_lsp_server::ls_types::Range;
fn version_requirement(&self) -> Option<&crate::VersionReq>;
fn version_range(&self) -> Option<tower_lsp_server::ls_types::Range>;
fn source(&self) -> crate::parser::DependencySource;
fn features(&self) -> &[String] {
&[]
}
fn features_range(&self) -> Option<tower_lsp_server::ls_types::Range> {
None
}
fn markers(&self) -> Option<&str> {
None
}
fn markers_range(&self) -> Option<tower_lsp_server::ls_types::Range> {
None
}
fn version_literal(&self) -> Option<&str> {
None
}
fn as_any(&self) -> &dyn Any;
}
#[derive(Debug, Clone)]
pub struct EcosystemConfig {
pub show_up_to_date_hints: bool,
pub up_to_date_text: String,
pub needs_update_text: String,
pub loading_text: String,
pub show_loading_hints: bool,
}
impl Default for EcosystemConfig {
fn default() -> Self {
Self {
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,
}
}
}
pub trait Ecosystem: Send + Sync + private::Sealed {
fn id(&self) -> &'static str;
fn display_name(&self) -> &'static str;
fn manifest_filenames(&self) -> &[&'static str];
fn manifest_extensions(&self) -> &[&'static str] {
&[]
}
fn manifest_patterns(&self) -> &[&'static str] {
&[]
}
fn lockfile_filenames(&self) -> &[&'static str] {
&[]
}
fn parse_manifest<'a>(
&'a self,
content: &'a str,
uri: &'a Uri,
) -> BoxFuture<'a, crate::error::Result<Box<dyn ParseResult>>>;
fn registry(&self) -> Arc<dyn Registry>;
fn lockfile_provider(&self) -> Option<Arc<dyn crate::lockfile::LockFileProvider>> {
None
}
fn formatter(&self) -> &dyn EcosystemFormatter;
fn generate_inlay_hints<'a>(
&'a self,
parse_result: &'a dyn ParseResult,
versions: VersionData<'a>,
loading_state: crate::LoadingState,
config: &'a EcosystemConfig,
) -> BoxFuture<'a, Vec<InlayHint>> {
Box::pin(async move {
crate::lsp_helpers::generate_inlay_hints(
parse_result,
versions,
loading_state,
config,
self.formatter(),
)
})
}
fn generate_hover<'a>(
&'a self,
parse_result: &'a dyn ParseResult,
position: Position,
versions: VersionData<'a>,
freshness: crate::freshness::FreshnessSettings,
) -> BoxFuture<'a, Option<Hover>> {
Box::pin(async move {
let registry = self.registry();
crate::lsp_helpers::generate_hover(
parse_result,
position,
versions,
registry.as_ref(),
self.formatter(),
freshness,
crate::freshness::PublishTime::now(),
)
.await
})
}
fn generate_code_actions<'a>(
&'a self,
parse_result: &'a dyn ParseResult,
position: Position,
uri: &'a Uri,
versions: VersionData<'a>,
content: &'a str,
) -> BoxFuture<'a, Vec<CodeAction>> {
Box::pin(async move {
let registry = self.registry();
crate::lsp_helpers::generate_code_actions(
parse_result,
position,
uri,
versions,
content,
registry.as_ref(),
self.formatter(),
)
.await
})
}
fn generate_diagnostics<'a>(
&'a self,
parse_result: &'a dyn ParseResult,
versions: VersionData<'a>,
_uri: &'a Uri,
freshness: crate::freshness::FreshnessSettings,
severities: crate::lsp_helpers::DiagnosticSeverities,
) -> BoxFuture<'a, Vec<Diagnostic>> {
Box::pin(async move {
crate::lsp_helpers::generate_diagnostics_from_cache(
parse_result,
versions,
self.formatter(),
freshness,
severities,
crate::freshness::PublishTime::now(),
)
})
}
fn generate_code_lenses<'a>(
&'a self,
parse_result: &'a dyn ParseResult,
content: &'a str,
versions: VersionData<'a>,
uri: &'a Uri,
command_id: &'a str,
) -> BoxFuture<'a, Vec<CodeLens>> {
Box::pin(async move {
crate::lsp_helpers::generate_code_lenses(
parse_result,
content,
versions,
self.formatter(),
uri,
command_id,
)
})
}
fn generate_completions<'a>(
&'a self,
parse_result: &'a dyn ParseResult,
position: Position,
content: &'a str,
freshness: crate::FreshnessSettings,
) -> BoxFuture<'a, Completions>;
fn package_search_is_incomplete(&self) -> bool {
false
}
fn as_any(&self) -> &dyn Any;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ecosystem_id_roundtrip() {
const ALL: &[EcosystemId] = &[
EcosystemId::Cargo,
EcosystemId::Npm,
EcosystemId::Pypi,
EcosystemId::Go,
EcosystemId::Bundler,
EcosystemId::Dart,
EcosystemId::Maven,
EcosystemId::Composer,
EcosystemId::Gradle,
EcosystemId::Swift,
EcosystemId::NuGet,
EcosystemId::Deno,
];
for id in ALL {
let parsed: EcosystemId = id.id().parse().unwrap();
assert_eq!(parsed, *id);
assert_eq!(id.to_string(), id.id());
}
}
#[test]
fn test_osv_ecosystem_mapping_pinned() {
let expected: &[(EcosystemId, &str)] = &[
(EcosystemId::Cargo, "crates.io"),
(EcosystemId::Npm, "npm"),
(EcosystemId::Pypi, "PyPI"),
(EcosystemId::Go, "Go"),
(EcosystemId::Bundler, "RubyGems"),
(EcosystemId::Dart, "Pub"),
(EcosystemId::Maven, "Maven"),
(EcosystemId::Composer, "Packagist"),
(EcosystemId::Gradle, "Maven"),
(EcosystemId::Swift, "SwiftURL"),
(EcosystemId::NuGet, "NuGet"),
(EcosystemId::Deno, "npm"),
];
for (id, expected_str) in expected {
assert_eq!(
id.osv_ecosystem(),
Some(*expected_str),
"unexpected OSV ecosystem string for {id:?}"
);
}
}
#[test]
fn test_ecosystem_id_from_str_unknown() {
let err = "unknown".parse::<EcosystemId>().unwrap_err();
assert!(matches!(err, crate::error::DepsError::UnsupportedEcosystem(s) if s == "unknown"));
}
#[test]
fn test_ecosystem_config_default() {
let config = EcosystemConfig::default();
assert!(config.show_up_to_date_hints);
assert_eq!(config.up_to_date_text, "✅");
assert_eq!(config.needs_update_text, "❌ {}");
}
#[test]
fn test_ecosystem_config_custom() {
let config = EcosystemConfig {
show_up_to_date_hints: false,
up_to_date_text: "OK".to_string(),
needs_update_text: "Update to {}".to_string(),
loading_text: "Loading...".to_string(),
show_loading_hints: false,
};
assert!(!config.show_up_to_date_hints);
assert_eq!(config.up_to_date_text, "OK");
assert_eq!(config.needs_update_text, "Update to {}");
}
#[test]
fn test_ecosystem_config_clone() {
let config1 = EcosystemConfig::default();
let config2 = config1.clone();
assert_eq!(config1.up_to_date_text, config2.up_to_date_text);
assert_eq!(config1.show_up_to_date_hints, config2.show_up_to_date_hints);
assert_eq!(config1.needs_update_text, config2.needs_update_text);
}
#[test]
fn test_dependency_default_features() {
struct MockDep;
impl Dependency for MockDep {
fn name(&self) -> &crate::PackageName {
static NAME: std::sync::LazyLock<crate::PackageName> =
std::sync::LazyLock::new(|| crate::PackageName::new("test"));
&NAME
}
fn name_range(&self) -> tower_lsp_server::ls_types::Range {
tower_lsp_server::ls_types::Range::default()
}
fn version_requirement(&self) -> Option<&crate::VersionReq> {
None
}
fn version_range(&self) -> Option<tower_lsp_server::ls_types::Range> {
None
}
fn source(&self) -> crate::parser::DependencySource {
crate::parser::DependencySource::Registry
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
let dep = MockDep;
assert_eq!(dep.features(), &[] as &[String]);
}
}