use tower_lsp_server::ls_types::{CodeLens, Command, Position, Range, TextEdit, Uri};
use crate::ParseResult;
#[cfg(test)]
use crate::PublishTime;
use super::{
EcosystemFormatter, LineOffsetTable, RequirementStatus, VersionData, is_safe_version_string,
literal_span_matches, slice_for_range, strip_whitespace, warn_rejected_value,
};
pub const PIN_ALL_TO_SHA_COMMAND_ID: &str = "deps-lsp.pinAllToSha";
#[derive(Debug, Clone, Copy)]
pub struct PinNoun {
pub singular: &'static str,
pub plural: &'static str,
}
pub fn collect_update_all_edits(
parse_result: &dyn ParseResult,
content: &str,
versions: VersionData<'_>,
formatter: &dyn EcosystemFormatter,
) -> Vec<TextEdit> {
let deps = parse_result.dependencies();
let mut edits: Vec<TextEdit> = Vec::with_capacity(deps.len());
let line_offsets = LineOffsetTable::new(content);
for dep in deps {
let Some(version_range) = dep.version_range() else {
continue;
};
let normalized_name = formatter.normalize_package_name(dep.name());
let Some(latest) = versions
.cached
.get(normalized_name.as_str())
.or_else(|| versions.cached.get(dep.name()))
.map(|v| &v.latest)
else {
continue;
};
if !is_safe_version_string(latest.as_str()) {
warn_rejected_value(
"is_safe_version_string",
"update-all code lens edit",
latest.as_str(),
);
continue;
}
let Some(version_req) = dep.version_requirement() else {
continue;
};
if version_req.as_str().is_empty() {
continue;
}
if formatter.requirement_status_for(dep, version_req, latest) != RequirementStatus::Outdated
{
continue;
}
let slice = slice_for_range(content, &line_offsets, version_range);
let literal_target = dep
.version_literal()
.unwrap_or_else(|| version_req.as_str());
if !literal_span_matches(slice, literal_target) {
continue;
}
let new_text = formatter.format_version_replacing_for(dep, latest, version_req.as_str());
if strip_whitespace(&new_text) == strip_whitespace(literal_target) {
continue;
}
edits.push(TextEdit {
range: version_range,
new_text,
});
}
dedup_overlapping_edits(edits, "collect_update_all_edits")
}
pub fn dedup_overlapping_edits(mut edits: Vec<TextEdit>, caller: &str) -> Vec<TextEdit> {
edits.sort_by_key(|edit| (edit.range.start.line, edit.range.start.character));
let mut non_overlapping: Vec<TextEdit> = Vec::with_capacity(edits.len());
for edit in edits {
let overlaps_prev = non_overlapping.last().is_some_and(|prev: &TextEdit| {
(edit.range.start.line, edit.range.start.character)
< (prev.range.end.line, prev.range.end.character)
});
if overlaps_prev {
tracing::warn!(
range = ?edit.range,
caller,
"dropping overlapping TextEdit"
);
continue;
}
non_overlapping.push(edit);
}
non_overlapping
}
pub fn generate_code_lenses(
parse_result: &dyn ParseResult,
content: &str,
versions: VersionData<'_>,
formatter: &dyn EcosystemFormatter,
uri: &Uri,
command_id: &str,
) -> Vec<CodeLens> {
let edits = collect_update_all_edits(parse_result, content, versions, formatter);
if edits.is_empty() {
return Vec::new();
}
let count = edits.len();
let title = if count == 1 {
"Update 1 outdated dependency".to_string()
} else {
format!("Update {count} outdated dependencies")
};
vec![CodeLens {
range: Range::new(Position::new(0, 0), Position::new(0, 0)),
command: Some(Command {
title,
command: command_id.to_string(),
arguments: Some(vec![serde_json::json!({ "uri": uri })]),
}),
data: None,
}]
}
#[must_use]
pub fn build_pin_all_to_sha_lens(count: usize, noun: PinNoun, uri: &Uri) -> Option<CodeLens> {
if count == 0 {
return None;
}
let title = if count == 1 {
format!("Pin 1 {} to commit SHA", noun.singular)
} else {
format!("Pin {count} {} to commit SHA", noun.plural)
};
Some(CodeLens {
range: Range::new(Position::new(0, 0), Position::new(0, 0)),
command: Some(Command {
title,
command: PIN_ALL_TO_SHA_COMMAND_ID.to_string(),
arguments: Some(vec![serde_json::json!({ "uri": uri })]),
}),
data: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lsp_helpers::test_support::*;
use crate::lsp_helpers::*;
use crate::{ConcreteVersion, Dependency, PackageName, VersionReq};
use std::any::Any;
use std::collections::HashMap;
mod update_all_edits_tests {
use super::*;
use tower_lsp_server::ls_types::{Position, Range};
struct UaeDep {
name: PackageName,
version_req: Option<VersionReq>,
version_range: Option<Range>,
}
impl Dependency for UaeDep {
fn name(&self) -> &PackageName {
&self.name
}
fn name_range(&self) -> Range {
Range::default()
}
fn version_requirement(&self) -> Option<&VersionReq> {
self.version_req.as_ref()
}
fn version_range(&self) -> Option<Range> {
self.version_range
}
fn source(&self) -> crate::parser::DependencySource {
crate::parser::DependencySource::Registry
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct UaeParseResult {
deps: Vec<UaeDep>,
uri: Uri,
}
impl ParseResult for UaeParseResult {
fn dependencies(&self) -> Vec<&dyn Dependency> {
self.deps.iter().map(|d| d as &dyn Dependency).collect()
}
fn workspace_root(&self) -> Option<&std::path::Path> {
None
}
fn uri(&self) -> &Uri {
&self.uri
}
fn as_any(&self) -> &dyn Any {
self
}
}
fn range(sl: u32, sc: u32, el: u32, ec: u32) -> Range {
Range::new(Position::new(sl, sc), Position::new(el, ec))
}
fn dep(name: &str, req: Option<&str>, vr: Option<Range>) -> UaeDep {
UaeDep {
name: PackageName::new(name),
version_req: req.map(VersionReq::new),
version_range: vr,
}
}
fn parse_result(deps: Vec<UaeDep>) -> UaeParseResult {
UaeParseResult {
deps,
uri: crate::test_util::test_uri("/test/Cargo.toml"),
}
}
struct FloorFormatter;
impl PackageNaming for FloorFormatter {}
impl PackageRendering for FloorFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
format!("{version}-forced")
}
fn package_url(&self, name: &PackageName) -> String {
format!("https://example.com/{name}")
}
}
impl RequirementResolution for FloorFormatter {
fn is_requirement_up_to_date(
&self,
_requirement: &VersionReq,
_latest: &ConcreteVersion,
) -> bool {
false
}
}
impl DiagnosticMessages for FloorFormatter {}
impl DiagnosticPolicy for FloorFormatter {}
impl SourcePolicy for FloorFormatter {}
impl OsvNaming for FloorFormatter {}
#[test]
fn test_zero_outdated_returns_empty_edits_and_no_lens() {
let content = r#"serde = "1.0.0""#;
let pr = parse_result(vec![dep("serde", Some("1.0.0"), Some(range(0, 9, 0, 14)))]);
let mut cached = HashMap::new();
cached.insert("serde".into(), PackageVersions::latest_only("1.0.0"));
let resolved = HashMap::new();
let versions = VersionData::new(&cached, &resolved);
let edits = collect_update_all_edits(&pr, content, versions, &MockFormatter);
assert!(edits.is_empty());
let lenses = generate_code_lenses(
&pr,
content,
versions,
&MockFormatter,
pr.uri(),
"deps-lsp.updateAllOutdated",
);
assert!(lenses.is_empty());
}
#[test]
fn test_n_outdated_produces_n_edits_with_expected_range_and_text() {
let content = "serde = \"1.0.0\"\ntokio = \"1.0.0\"\n";
let pr = parse_result(vec![
dep("serde", Some("1.0.0"), Some(range(0, 9, 0, 14))),
dep("tokio", Some("1.0.0"), Some(range(1, 9, 1, 14))),
]);
let mut cached = HashMap::new();
cached.insert("serde".into(), PackageVersions::latest_only("1.2.0"));
cached.insert("tokio".into(), PackageVersions::latest_only("1.3.0"));
let resolved = HashMap::new();
let versions = VersionData::new(&cached, &resolved);
let edits = collect_update_all_edits(&pr, content, versions, &MockFormatter);
assert_eq!(edits.len(), 2);
assert_eq!(edits[0].range, range(0, 9, 0, 14));
assert_eq!(
edits[0].new_text,
MockFormatter.format_version_for_text_edit(&ConcreteVersion::new("1.2.0"))
);
assert_eq!(edits[1].range, range(1, 9, 1, 14));
assert_eq!(
edits[1].new_text,
MockFormatter.format_version_for_text_edit(&ConcreteVersion::new("1.3.0"))
);
let lenses = generate_code_lenses(
&pr,
content,
versions,
&MockFormatter,
pr.uri(),
"deps-lsp.updateAllOutdated",
);
assert_eq!(lenses.len(), 1);
let command = lenses[0].command.as_ref().expect("lens has a command");
assert_eq!(command.title, "Update 2 outdated dependencies");
assert_eq!(command.command, "deps-lsp.updateAllOutdated");
}
#[test]
fn test_singular_title_for_one_outdated_dependency() {
let content = r#"serde = "1.0.0""#;
let pr = parse_result(vec![dep("serde", Some("1.0.0"), Some(range(0, 9, 0, 14)))]);
let mut cached = HashMap::new();
cached.insert("serde".into(), PackageVersions::latest_only("1.2.0"));
let resolved = HashMap::new();
let versions = VersionData::new(&cached, &resolved);
let lenses = generate_code_lenses(
&pr,
content,
versions,
&MockFormatter,
pr.uri(),
"deps-lsp.updateAllOutdated",
);
assert_eq!(lenses.len(), 1);
assert_eq!(
lenses[0].command.as_ref().unwrap().title,
"Update 1 outdated dependency"
);
}
#[test]
fn test_missing_version_range_is_skipped() {
let content = "serde = \"1.0.0\"\n";
let pr = parse_result(vec![dep("serde", Some("1.0.0"), None)]);
let mut cached = HashMap::new();
cached.insert("serde".into(), PackageVersions::latest_only("1.2.0"));
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&MockFormatter,
);
assert!(edits.is_empty());
}
#[test]
fn test_empty_version_requirement_is_skipped() {
let content = "pkg = \"\"\n";
let pr = parse_result(vec![dep("pkg", Some(""), Some(range(0, 6, 0, 6)))]);
let mut cached = HashMap::new();
cached.insert("pkg".into(), PackageVersions::latest_only("1.0.0"));
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&MockFormatter,
);
assert!(
edits.is_empty(),
"an empty version requirement must never produce an edit"
);
}
#[test]
fn test_dependency_absent_from_cache_is_skipped() {
let content = "git-dep = \"1.0.0\"\n";
let pr = parse_result(vec![dep(
"git-dep",
Some("1.0.0"),
Some(range(0, 11, 0, 16)),
)]);
let cached = HashMap::new();
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&MockFormatter,
);
assert!(edits.is_empty());
}
#[test]
fn test_empty_cached_latest_is_skipped() {
let content = "serde = \"1.0.0\"\n";
let pr = parse_result(vec![dep("serde", Some("1.0.0"), Some(range(0, 9, 0, 14)))]);
let mut cached = HashMap::new();
cached.insert("serde".into(), PackageVersions::latest_only(""));
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&MockFormatter,
);
assert!(
edits.is_empty(),
"an empty cached latest must never produce a requirement-erasing edit"
);
}
#[test]
fn test_whitespace_only_cached_latest_is_skipped() {
let content = "serde = \"1.0.0\"\n";
let pr = parse_result(vec![dep("serde", Some("1.0.0"), Some(range(0, 9, 0, 14)))]);
let mut cached = HashMap::new();
cached.insert("serde".into(), PackageVersions::latest_only(" "));
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&MockFormatter,
);
assert!(edits.is_empty());
}
#[test]
fn test_cached_latest_with_unsafe_characters_is_skipped() {
let content = "serde = \"1.0.0\"\n";
let pr = parse_result(vec![dep("serde", Some("1.0.0"), Some(range(0, 9, 0, 14)))]);
let mut cached = HashMap::new();
cached.insert(
"serde".into(),
PackageVersions::latest_only("1.2.0\", \"evil\": \"true"),
);
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&MockFormatter,
);
assert!(edits.is_empty());
}
#[test]
fn test_requirement_already_accepts_latest_is_not_counted() {
let content = "serde = \"^1.0\"\n";
let pr = parse_result(vec![dep("serde", Some("^1.0"), Some(range(0, 9, 0, 13)))]);
let mut cached = HashMap::new();
cached.insert("serde".into(), PackageVersions::latest_only("1.2.0"));
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&MockFormatter,
);
assert!(edits.is_empty());
}
#[test]
fn test_formatter_is_requirement_up_to_date_override_is_honored() {
let content = r#"pkg = "1.0.0""#;
let pr = parse_result(vec![dep("pkg", Some("1.0.0"), Some(range(0, 7, 0, 12)))]);
let mut cached = HashMap::new();
cached.insert("pkg".into(), PackageVersions::latest_only("1.0.0"));
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&FloorFormatter,
);
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].new_text, "1.0.0-forced");
}
#[test]
fn test_no_op_edit_is_excluded() {
struct NoOpFormatter;
impl PackageNaming for NoOpFormatter {}
impl PackageRendering for NoOpFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
format!("https://example.com/{name}")
}
fn format_version_replacing(
&self,
_version: &ConcreteVersion,
current: &str,
) -> String {
current.to_string()
}
}
impl RequirementResolution for NoOpFormatter {
fn is_requirement_up_to_date(
&self,
_requirement: &VersionReq,
_latest: &ConcreteVersion,
) -> bool {
false
}
}
impl DiagnosticMessages for NoOpFormatter {}
impl DiagnosticPolicy for NoOpFormatter {}
impl SourcePolicy for NoOpFormatter {}
impl OsvNaming for NoOpFormatter {}
let content = r#"pkg = "1.0.0""#;
let pr = parse_result(vec![dep("pkg", Some("1.0.0"), Some(range(0, 7, 0, 12)))]);
let mut cached = HashMap::new();
cached.insert("pkg".into(), PackageVersions::latest_only("1.2.0"));
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&NoOpFormatter,
);
assert!(edits.is_empty());
}
#[test]
fn test_guard_rejects_span_that_is_not_the_requirement() {
let content = "<version>${slf4j.version}</version>";
let pr = parse_result(vec![dep(
"slf4j-api",
Some("2.0.16"),
Some(range(0, 9, 0, 25)),
)]);
let mut cached = HashMap::new();
cached.insert("slf4j-api".into(), PackageVersions::latest_only("2.1.0"));
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&MockFormatter,
);
assert!(
edits.is_empty(),
"a version_range spanning a property reference must not be edited"
);
}
#[test]
fn test_guard_accepts_whitespace_only_difference() {
let content = "pkg>=1.7,<2.0";
let pr = parse_result(vec![dep(
"pkg",
Some(">=1.7, <2.0"),
Some(range(0, 3, 0, 13)),
)]);
let mut cached = HashMap::new();
cached.insert("pkg".into(), PackageVersions::latest_only("3.0.0"));
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&MockFormatter,
);
assert_eq!(edits.len(), 1, "whitespace-only divergence must not skip");
}
#[test]
fn test_guard_accepts_nuget_bracket_wrap() {
let content = r#"<PackageReference Include="Newtonsoft.Json" Version="1.0.0" />"#;
let pr = parse_result(vec![dep(
"Newtonsoft.Json",
Some("[1.0.0]"),
Some(range(0, 53, 0, 58)),
)]);
let mut cached = HashMap::new();
cached.insert(
"Newtonsoft.Json".into(),
PackageVersions::latest_only("13.0.3"),
);
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&MockFormatter,
);
assert_eq!(
edits.len(),
1,
"NuGet's bracket-wrapped requirement must be kept"
);
}
#[test]
fn test_guard_accepts_nuget_already_bracketed_source() {
let content = r#"<PackageReference Include="Newtonsoft.Json" Version="[1.0.0]" />"#;
let pr = parse_result(vec![dep(
"Newtonsoft.Json",
Some("[[1.0.0]]"),
Some(range(0, 53, 0, 60)),
)]);
let mut cached = HashMap::new();
cached.insert(
"Newtonsoft.Json".into(),
PackageVersions::latest_only("13.0.3"),
);
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&MockFormatter,
);
assert_eq!(
edits.len(),
1,
"an already-bracketed NuGet source must not be falsely rejected"
);
}
#[test]
fn test_guard_accepts_nuget_open_ended_lower_bound_spelling() {
let content = r#"<PackageReference Include="Newtonsoft.Json" Version="[1.0.0,]" />"#;
let pr = parse_result(vec![dep(
"Newtonsoft.Json",
Some("[[1.0.0,]]"),
Some(range(0, 53, 0, 61)),
)]);
let mut cached = HashMap::new();
cached.insert(
"Newtonsoft.Json".into(),
PackageVersions::latest_only("13.0.3"),
);
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&MockFormatter,
);
assert_eq!(
edits.len(),
1,
"the open-ended-lower-bound NuGet spelling must not be falsely rejected"
);
}
#[test]
fn test_guard_accepts_nuget_exclusive_upper_bound_spelling() {
let content = r#"<PackageReference Include="Newtonsoft.Json" Version="[1.0,2.0)" />"#;
let pr = parse_result(vec![dep(
"Newtonsoft.Json",
Some("[[1.0,2.0)]"),
Some(range(0, 53, 0, 62)),
)]);
let mut cached = HashMap::new();
cached.insert(
"Newtonsoft.Json".into(),
PackageVersions::latest_only("13.0.3"),
);
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&MockFormatter,
);
assert_eq!(
edits.len(),
1,
"the exclusive-upper-bound NuGet spelling must not be falsely rejected"
);
}
#[test]
fn test_guard_rejects_bracketed_interval_against_unbracketed_requirement() {
let content = "<version>[1.0,2.0]</version>";
let pr = parse_result(vec![dep(
"interval-dep",
Some("1.0,2.0"),
Some(range(0, 9, 0, 18)),
)]);
let mut cached = HashMap::new();
cached.insert("interval-dep".into(), PackageVersions::latest_only("3.0.0"));
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&MockFormatter,
);
assert!(
edits.is_empty(),
"a bracketed interval span must not match an unbracketed requirement"
);
}
#[test]
fn test_invariant_edit_count_matches_diagnostic_count_when_guard_is_noop() {
let content = "serde = \"1.0.0\"\ntokio = \"^1.5\"\nunknown = \"1.0.0\"\n";
let pr = parse_result(vec![
dep("serde", Some("1.0.0"), Some(range(0, 9, 0, 14))),
dep("tokio", Some("^1.5"), Some(range(1, 9, 1, 13))),
dep("unknown", Some("1.0.0"), Some(range(2, 11, 2, 16))),
]);
let mut cached = HashMap::new();
cached.insert("serde".into(), PackageVersions::latest_only("2.0.0"));
cached.insert("tokio".into(), PackageVersions::latest_only("1.9.0"));
let resolved = HashMap::new();
let versions = VersionData::new(&cached, &resolved);
let edits = collect_update_all_edits(&pr, content, versions, &MockFormatter);
let diagnostics = generate_diagnostics_from_cache(
&pr,
versions,
&MockFormatter,
pr.uri(),
crate::FreshnessSettings::default(),
DiagnosticSeverities::default(),
PublishTime::now(),
);
let newer_version_diagnostics = diagnostics
.iter()
.filter(|d| d.message.contains("Newer version available"))
.count();
assert_eq!(edits.len(), newer_version_diagnostics);
assert_eq!(edits.len(), 1);
}
#[test]
fn test_overlapping_edits_are_dropped_keeping_the_first() {
let content = "aaaa = \"1.0.0\"\n";
let pr = parse_result(vec![
dep("aaaa", Some("1.0.0"), Some(range(0, 8, 0, 13))),
dep("aaaa-dup", Some("1.0.0"), Some(range(0, 8, 0, 13))),
]);
let mut cached = HashMap::new();
cached.insert("aaaa".into(), PackageVersions::latest_only("2.0.0"));
cached.insert("aaaa-dup".into(), PackageVersions::latest_only("3.0.0"));
let resolved = HashMap::new();
let edits = collect_update_all_edits(
&pr,
content,
VersionData::new(&cached, &resolved),
&MockFormatter,
);
assert_eq!(edits.len(), 1, "the overlapping later edit must be dropped");
}
}
mod pin_all_to_sha_lens_tests {
use super::*;
fn noun() -> PinNoun {
PinNoun {
singular: "action",
plural: "actions",
}
}
#[test]
fn test_zero_count_returns_no_lens() {
let uri = crate::test_util::test_uri("/repo/.github/workflows/ci.yml");
assert!(build_pin_all_to_sha_lens(0, noun(), &uri).is_none());
}
#[test]
fn test_singular_title_for_one() {
let uri = crate::test_util::test_uri("/repo/.github/workflows/ci.yml");
let lens = build_pin_all_to_sha_lens(1, noun(), &uri).unwrap();
let command = lens.command.unwrap();
assert_eq!(command.title, "Pin 1 action to commit SHA");
assert_eq!(command.command, PIN_ALL_TO_SHA_COMMAND_ID);
}
#[test]
fn test_plural_title_for_n() {
let uri = crate::test_util::test_uri("/repo/.github/workflows/ci.yml");
let lens = build_pin_all_to_sha_lens(3, noun(), &uri).unwrap();
assert_eq!(lens.command.unwrap().title, "Pin 3 actions to commit SHA");
}
#[test]
fn test_command_arguments_carry_uri() {
let uri = crate::test_util::test_uri("/repo/.github/workflows/ci.yml");
let lens = build_pin_all_to_sha_lens(1, noun(), &uri).unwrap();
let command = lens.command.unwrap();
let args = command.arguments.expect("command has arguments");
assert_eq!(args[0]["uri"], uri.as_str());
}
#[test]
fn test_trait_default_returns_empty() {
use crate::Ecosystem as _;
struct DefaultEcosystem;
impl crate::ecosystem::private::Sealed for DefaultEcosystem {}
impl crate::Ecosystem for DefaultEcosystem {
fn ecosystem_id(&self) -> crate::EcosystemId {
crate::EcosystemId::Cargo
}
fn display_name(&self) -> &'static str {
"Default"
}
fn manifest_filenames(&self) -> &[&'static str] {
&[]
}
fn parse_manifest<'a>(
&'a self,
_content: &'a str,
_uri: &'a Uri,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Box<dyn ParseResult>>>
{
unimplemented!()
}
fn registry(&self) -> std::sync::Arc<dyn crate::Registry> {
unimplemented!()
}
fn formatter(&self) -> &dyn EcosystemFormatter {
unimplemented!()
}
fn generate_completions<'a>(
&'a self,
_parse_result: &'a dyn ParseResult,
_position: Position,
_content: &'a str,
_freshness: crate::FreshnessSettings,
) -> crate::ecosystem::BoxFuture<'a, crate::completion::Completions>
{
unimplemented!()
}
fn complete_version<'a>(
&'a self,
_request: crate::completion::CompletionRequest<'a>,
_package_name: crate::PackageName,
_prefix: String,
) -> crate::ecosystem::BoxFuture<'a, crate::completion::Completions>
{
unimplemented!()
}
fn completion_insert_text(
&self,
_metadata: &dyn crate::Metadata,
) -> Option<String> {
unimplemented!()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
let eco = DefaultEcosystem;
let uri = crate::test_util::test_uri("/test/manifest");
let cached = std::collections::HashMap::new();
let resolved = std::collections::HashMap::new();
struct EmptyParseResult {
uri: Uri,
}
impl ParseResult for EmptyParseResult {
fn dependencies(&self) -> Vec<&dyn crate::Dependency> {
vec![]
}
fn workspace_root(&self) -> Option<&std::path::Path> {
None
}
fn uri(&self) -> &Uri {
&self.uri
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
let pr = EmptyParseResult { uri };
let edits = eco.collect_pin_all_to_sha_edits(&pr, VersionData::new(&cached, &resolved));
assert!(edits.is_empty());
assert_eq!(eco.pin_all_to_sha_noun().singular, "ref");
assert_eq!(eco.pin_all_to_sha_noun().plural, "refs");
}
}
}