use crate::lsp_helpers::{
CooldownPrecedence, MAX_VERSION_DIAGNOSTIC_CHARS, escape_markdown, is_safe_registry_url,
is_safe_version_string, local_cooldown_precedence, replace_markdown_unsafe_chars,
truncate_for_diagnostic, warn_rejected_value,
};
use crate::{
ConcreteVersion, FreshnessSettings, Metadata, PackageName, ParseResult, PublishTime, Version,
format_relative_age,
};
use tower_lsp_server::ls_types::{
CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionTextEdit,
Documentation, InsertTextFormat, MarkupContent, MarkupKind, Position, Range, TextEdit,
};
pub use crate::lsp_helpers::COMPLETION_SEARCH_TIMEOUT;
#[non_exhaustive]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Completions {
pub items: Vec<CompletionItem>,
pub is_incomplete: bool,
pub origin: CompletionOrigin,
}
impl Completions {
#[must_use]
pub fn new(items: Vec<CompletionItem>) -> Self {
Self {
items,
is_incomplete: false,
origin: CompletionOrigin::Unresolved,
}
}
#[must_use]
pub const fn with_incomplete(mut self, is_incomplete: bool) -> Self {
self.is_incomplete = is_incomplete;
self
}
#[must_use]
pub const fn with_origin(mut self, origin: CompletionOrigin) -> Self {
self.origin = origin;
self
}
}
impl From<Vec<CompletionItem>> for Completions {
fn from(items: Vec<CompletionItem>) -> Self {
Self::new(items)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum CompletionOrigin {
#[default]
Unresolved,
PackageName,
Version,
Feature,
}
impl CompletionOrigin {
#[must_use]
pub const fn allows_package_name_fallback(self) -> bool {
match self {
Self::Unresolved | Self::PackageName => true,
Self::Version | Self::Feature => false,
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy)]
pub struct CompletionRequest<'a> {
pub parse_result: &'a dyn ParseResult,
pub position: Position,
pub freshness: FreshnessSettings,
}
impl<'a> CompletionRequest<'a> {
#[must_use]
pub const fn new(
parse_result: &'a dyn ParseResult,
position: Position,
freshness: FreshnessSettings,
) -> Self {
Self {
parse_result,
position,
freshness,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompletionContext {
PackageName {
prefix: String,
range: Range,
},
Version {
package_name: PackageName,
prefix: String,
},
Feature {
package_name: PackageName,
prefix: String,
},
None,
}
pub fn detect_completion_context(
parse_result: &dyn ParseResult,
position: Position,
content: &str,
) -> CompletionContext {
let dependencies = parse_result.dependencies();
for dep in dependencies {
if dep.name_range_is_synthetic() {
continue;
}
let name_range: Range = dep.name_range().into();
if crate::lsp_helpers::position_in_range(position.into(), dep.name_range()) {
let prefix = extract_prefix(content, position, name_range);
return CompletionContext::PackageName {
prefix,
range: name_range,
};
}
if let Some(version_range) = dep.version_range().map(Into::into)
&& position_in_range(position, version_range)
{
if crate::lsp_helpers::dependency_version_range_is_literal(
dep,
content,
version_range.into(),
) {
let prefix = extract_prefix(content, position, version_range);
return CompletionContext::Version {
package_name: dep.name().clone(),
prefix,
};
}
}
if let Some(features_range) = dep.features_range().map(Into::into)
&& position_in_range(position, features_range)
{
let prefix = extract_feature_prefix(content, position);
return CompletionContext::Feature {
package_name: dep.name().clone(),
prefix,
};
}
}
CompletionContext::None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeclarationScope {
Unchecked,
Outside,
Within(crate::position::Range),
}
#[must_use]
pub fn literal_version_dependency_in_scope<'a>(
parse_result: &'a dyn ParseResult,
position: Position,
content: &str,
value_range: Range,
scope: DeclarationScope,
) -> Option<&'a dyn crate::ecosystem::Dependency> {
let dep = version_dependency_at_position(parse_result, position, scope)?;
crate::lsp_helpers::dependency_version_range_is_literal(dep, content, value_range.into())
.then_some(dep)
}
#[must_use]
pub fn version_dependency_at_position(
parse_result: &dyn ParseResult,
position: Position,
scope: DeclarationScope,
) -> Option<&dyn crate::ecosystem::Dependency> {
let deps = parse_result.dependencies();
deps.iter()
.copied()
.find(|d| {
d.version_range()
.is_some_and(|r| crate::lsp_helpers::position_in_range(position.into(), r))
})
.or_else(|| {
if matches!(scope, DeclarationScope::Outside) {
return None;
}
deps.iter()
.copied()
.filter(|d| {
d.version_range().map_or_else(
|| d.name_range().start.line == position.line,
|r| r.start.line == position.line,
)
})
.filter(|d| {
let DeclarationScope::Within(span) = scope else {
return true;
};
let anchor = d
.version_range()
.map_or_else(|| d.name_range().start, |r| r.start);
crate::lsp_helpers::position_in_range(anchor, span)
})
.min_by_key(|d| version_range_distance(d.version_range(), position.character))
})
}
#[must_use]
pub fn literal_version_dependency<'a>(
parse_result: &'a dyn ParseResult,
position: Position,
content: &str,
value_range: Range,
) -> Option<&'a dyn crate::ecosystem::Dependency> {
literal_version_dependency_in_scope(
parse_result,
position,
content,
value_range,
DeclarationScope::Unchecked,
)
}
fn version_range_distance(range: Option<crate::position::Range>, character: u32) -> u32 {
range.map_or(u32::MAX, |r| {
character
.abs_diff(r.start.character)
.min(character.abs_diff(r.end.character))
})
}
const fn position_in_range(position: Position, range: Range) -> bool {
if position.line < range.start.line {
return false;
}
if position.line == range.start.line && position.character < range.start.character {
return false;
}
if position.line > range.end.line {
return false;
}
if position.line == range.end.line && position.character > range.end.character + 1 {
return false;
}
true
}
pub use crate::lsp_helpers::byte_to_utf16_offset;
pub use crate::lsp_helpers::utf16_to_byte_offset;
#[expect(
clippy::string_slice,
reason = "start_byte/cursor_byte come from utf16_to_byte_offset (char_indices-based) and \
are bounds/ordering-checked above, so both are verified char boundaries"
)]
pub fn extract_prefix(content: &str, position: Position, range: Range) -> String {
let line = match content.lines().nth(position.line as usize) {
Some(l) => l,
None => return String::new(),
};
let start_byte = if position.line == range.start.line {
match utf16_to_byte_offset(line, range.start.character) {
Some(offset) => offset,
None => return String::new(),
}
} else {
0
};
let cursor_byte = match utf16_to_byte_offset(line, position.character) {
Some(offset) => offset,
None => return String::new(),
};
if start_byte > line.len() || cursor_byte > line.len() || start_byte > cursor_byte {
return String::new();
}
let prefix = &line[start_byte..cursor_byte];
prefix
.trim()
.trim_matches('"')
.trim_matches('\'')
.trim()
.to_string()
}
#[expect(
clippy::string_slice,
reason = "cursor_byte comes from utf16_to_byte_offset (char_indices-based) and is clamped \
to line.len(); segment_start is an ASCII-char ([) index"
)]
pub fn extract_feature_prefix(content: &str, position: Position) -> String {
let line = match content.lines().nth(position.line as usize) {
Some(l) => l,
None => return String::new(),
};
let cursor_byte = match utf16_to_byte_offset(line, position.character) {
Some(offset) => offset.min(line.len()),
None => return String::new(),
};
let before_cursor = &line[..cursor_byte];
let segment_start = before_cursor.rfind('[').map_or(0, |i| i + 1);
let segment = &before_cursor[segment_start..];
crate::fallback_completion::open_quoted_tail(segment)
.unwrap_or_default()
.to_string()
}
#[must_use]
pub fn starts_with_ascii_case_insensitive(candidate: &str, prefix: &str) -> bool {
fn significant(c: char) -> Option<char> {
(!matches!(c, '-' | '_' | '.')).then(|| c.to_ascii_lowercase())
}
let mut candidate_chars = candidate.chars().filter_map(significant);
prefix
.chars()
.filter_map(significant)
.all(|p| candidate_chars.next().is_some_and(|c| c == p))
}
#[must_use]
pub fn build_completion_sort_text(index: usize, prefix: &str, candidate: &str) -> String {
let tier = u8::from(!starts_with_ascii_case_insensitive(candidate, prefix));
format!("{tier}{index:010}")
}
pub fn build_package_completion(
metadata: &dyn Metadata,
insert_range: Range,
index: usize,
prefix: &str,
) -> Option<CompletionItem> {
let mut item = build_package_completion_fields(metadata, index, prefix)?;
let name = metadata.name();
item.insert_text = Some(name.as_str().to_string());
item.text_edit = Some(CompletionTextEdit::Edit(TextEdit {
range: insert_range,
new_text: name.as_str().to_string(),
}));
Some(item)
}
#[expect(
clippy::string_slice,
reason = "end is floor_char_boundary-clamped just below before slicing desc"
)]
pub fn build_package_completion_fields(
metadata: &dyn Metadata,
index: usize,
prefix: &str,
) -> Option<CompletionItem> {
let name = metadata.name();
if !crate::is_safe_package_name(name.as_str()) {
warn_rejected_value(
"is_safe_package_name",
"primary completion path package name",
name.as_str(),
);
return None;
}
let latest = metadata.latest_version().as_str();
if !latest.is_empty() && !is_safe_version_string(latest) {
warn_rejected_value(
"is_safe_version_string",
"primary completion path latest version",
latest,
);
return None;
}
let capped_latest = truncate_for_diagnostic(latest, MAX_VERSION_DIAGNOSTIC_CHARS);
let header = if latest.is_empty() {
format!("**{}**", escape_markdown(name.as_str()))
} else {
format!(
"**{}** v{}",
escape_markdown(name.as_str()),
escape_markdown(&capped_latest)
)
};
let mut doc_parts = vec![header];
if let Some(desc) = metadata.description() {
doc_parts.push(String::new());
let truncated = if desc.len() > 200 {
let end = desc.floor_char_boundary(200);
format!("{}...", escape_markdown(&desc[..end]))
} else {
escape_markdown(desc)
};
doc_parts.push(truncated);
}
let mut links = Vec::new();
if let Some(repo) = metadata.repository() {
if is_safe_registry_url(repo) {
links.push(format!("[Repository]({})", escape_markdown(repo)));
} else {
warn_rejected_value(
"is_safe_registry_url",
"package completion repository link",
repo,
);
}
}
if let Some(docs) = metadata.documentation() {
if is_safe_registry_url(docs) {
links.push(format!("[Documentation]({})", escape_markdown(docs)));
} else {
warn_rejected_value(
"is_safe_registry_url",
"package completion documentation link",
docs,
);
}
}
if !links.is_empty() {
doc_parts.push(String::new());
doc_parts.push(links.join(" | "));
}
let sort_text = build_completion_sort_text(index, prefix, name.as_str());
Some(CompletionItem {
label: name.as_str().to_string(),
kind: Some(CompletionItemKind::MODULE),
detail: if latest.is_empty() {
None
} else {
Some(format!(
"v{}",
replace_markdown_unsafe_chars(&capped_latest)
))
},
documentation: Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: doc_parts.join("\n"),
})),
insert_text: None,
text_edit: None,
sort_text: Some(sort_text),
filter_text: Some(name.as_str().to_string()),
..Default::default()
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionReplacement {
pub range: Range,
pub lead: String,
pub trail: String,
pub replaced_text: String,
}
impl VersionReplacement {
fn render(&self, version: &str) -> String {
format!("{}{version}{}", self.lead, self.trail)
}
}
pub fn build_version_completion(
display_item: &VersionDisplayItem,
replacement: Option<&VersionReplacement>,
now: PublishTime,
freshness: FreshnessSettings,
) -> CompletionItem {
let sort_text = format!("{:05}", display_item.index);
let within_cooldown = freshness.cooldown().is_some_and(|window| {
matches!(
local_cooldown_precedence(display_item.published_at, window, now),
CooldownPrecedence::Blocked(_)
)
});
let label_details = freshness
.is_enabled()
.then_some(display_item.published_at)
.flatten()
.map(|published_at| CompletionItemLabelDetails {
detail: Some(format!(
" {}{}",
if within_cooldown { "⏳ " } else { "" },
format_relative_age(published_at.age_secs_from(now))
)),
description: None,
});
let (text_edit, filter_text, insert_text_format) = match replacement {
None => (None, None, None),
Some(replacement) => (
Some(CompletionTextEdit::Edit(TextEdit {
range: replacement.range,
new_text: replacement.render(display_item.version.as_str()),
})),
Some(replacement.replaced_text.clone()),
Some(InsertTextFormat::PLAIN_TEXT),
),
};
CompletionItem {
label: display_item.label.clone(),
kind: Some(CompletionItemKind::VALUE),
detail: Some(display_item.description.clone()),
documentation: None,
insert_text: Some(display_item.version.to_string()),
text_edit,
filter_text,
insert_text_format,
sort_text: Some(sort_text),
preselect: Some(display_item.is_latest),
label_details,
..Default::default()
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct VersionDisplayItem {
pub version: ConcreteVersion,
pub label: String,
pub description: String,
pub index: usize,
pub is_latest: bool,
pub published_at: Option<PublishTime>,
}
impl VersionDisplayItem {
#[must_use]
pub fn new(
version: &dyn Version,
package_name: &PackageName,
index: usize,
is_latest: bool,
) -> Self {
let version_str = version.version_string();
let label = if is_latest {
format!("{} (latest)", version_str)
} else {
version_str.to_string()
};
let description = format!("Update {} to {}", package_name.as_str(), version_str);
Self {
version: version_str.clone(),
label,
description,
index,
is_latest,
published_at: version.published_at(),
}
}
}
pub fn prepare_version_display_items<V: AsRef<dyn Version>>(
versions: &[V],
package_name: &PackageName,
latest_idx: Option<usize>,
) -> Vec<VersionDisplayItem> {
let mut survivors = versions
.iter()
.map(AsRef::as_ref)
.enumerate()
.filter(|(_, v)| !v.removal_status().blocks_resolution());
let mut head: Vec<(usize, &dyn Version)> =
survivors.by_ref().take(MAX_COMPLETION_VERSIONS).collect();
if let Some(idx) = latest_idx
&& !head.iter().any(|(i, _)| *i == idx)
&& let Some(pick) = survivors.find(|(i, _)| *i == idx)
{
head.truncate(MAX_COMPLETION_VERSIONS - 1);
head.push(pick);
}
head.into_iter()
.enumerate()
.map(|(display_index, (orig_index, version))| {
VersionDisplayItem::new(
version,
package_name,
display_index,
Some(orig_index) == latest_idx,
)
})
.collect()
}
pub fn build_feature_completion(
feature_name: &str,
package_name: &PackageName,
insert_range: Option<Range>,
) -> Option<CompletionItem> {
if !crate::is_safe_feature_name(feature_name) {
warn_rejected_value(
"is_safe_feature_name",
"feature completion feature name",
feature_name,
);
return None;
}
Some(CompletionItem {
label: feature_name.to_string(),
kind: Some(CompletionItemKind::PROPERTY),
detail: Some(format!("Feature of {}", package_name.as_str())),
documentation: None,
insert_text: Some(feature_name.to_string()),
text_edit: insert_range.map(|range| {
CompletionTextEdit::Edit(TextEdit {
range,
new_text: feature_name.to_string(),
})
}),
sort_text: Some(feature_name.to_string()),
..Default::default()
})
}
const MAX_COMPLETION_VERSIONS: usize = 5;
#[must_use]
pub fn is_valid_completion_prefix_len(prefix: &str) -> bool {
(2..=200).contains(&prefix.chars().count())
}
#[must_use]
pub fn reject_credential_bearing_value(value: &str, context: &str) -> Option<Vec<CompletionItem>> {
if crate::redact::is_credential_or_query_bearing(value) {
warn_rejected_value("credential_bearing", context, value);
return Some(vec![]);
}
None
}
pub fn apply_raw_prefix_filter_text(
items: &mut [CompletionItem],
registry: &dyn crate::Registry,
query: &str,
) {
if !registry.search_normalizes_query() {
return;
}
for item in items {
item.filter_text = Some(query.to_string());
}
}
pub async fn complete_package_names_generic(
registry: &dyn crate::Registry,
prefix: &str,
limit: usize,
insert_range: Range,
) -> Vec<CompletionItem> {
if !is_valid_completion_prefix_len(prefix) {
return vec![];
}
if let Some(rejected) =
reject_credential_bearing_value(prefix, "package-name completion prefix")
{
return rejected;
}
let results = match registry.search(prefix, limit).await {
Ok(r) => r,
Err(e) => {
tracing::warn!(
"Registry search failed for '{}': {}",
crate::redact::url_for_tracing(prefix),
e
);
return vec![];
}
};
let mut items: Vec<CompletionItem> = results
.into_iter()
.enumerate()
.filter_map(|(index, metadata)| {
build_package_completion(metadata.as_ref(), insert_range, index, prefix)
})
.collect();
apply_raw_prefix_filter_text(&mut items, registry, prefix);
items
}
#[allow(
clippy::too_many_arguments,
reason = "mirrors complete_versions_generic_from's own 7 parameters plus the two \
(`replacement`, `selection_context`) this function adds; wrapping the existing \
7 in a request struct now would break every other ecosystem's already-stable \
call pattern"
)]
pub async fn complete_versions_generic_replacing(
registry: &dyn crate::Registry,
formatter: &dyn crate::lsp_helpers::EcosystemFormatter,
package_name: &PackageName,
source: &crate::parser::DependencySource,
prefix: &str,
operator_chars: &[char],
freshness: FreshnessSettings,
replacement: Option<&VersionReplacement>,
selection_context: &crate::SelectionContext,
) -> Vec<CompletionItem> {
if !formatter.can_resolve_source(source) {
return vec![];
}
let versions = match registry
.get_versions_from(package_name, source, freshness)
.await
{
Ok(v) => v,
Err(e) => {
tracing::warn!(
"Failed to fetch versions for '{}': {}",
package_name.for_tracing(),
e
);
return vec![];
}
};
let clean_prefix = prefix.trim_start_matches(operator_chars).trim();
let version_matches_prefix = |version: &str| {
version.starts_with(clean_prefix)
|| version
.trim_start_matches(['v', 'V'])
.starts_with(clean_prefix)
};
let has_prefix_match = versions
.iter()
.any(|v| version_matches_prefix(v.version_string().as_str()));
let wildcard_req = crate::existence_wildcard_req();
let display_items = if has_prefix_match {
let filtered_versions: Vec<Box<dyn Version>> = versions
.into_iter()
.filter(|v| version_matches_prefix(v.version_string().as_str()))
.collect();
let latest_idx =
registry.select_latest_matching(&filtered_versions, &wildcard_req, selection_context);
prepare_version_display_items(&filtered_versions, package_name, latest_idx)
} else {
let latest_idx =
registry.select_latest_matching(&versions, &wildcard_req, selection_context);
prepare_version_display_items(&versions, package_name, latest_idx)
};
let now = PublishTime::now();
display_items
.iter()
.filter(|item| {
let safe = is_safe_version_string(item.version.as_str());
if !safe {
warn_rejected_value(
"is_safe_version_string",
"version completion item",
item.version.as_str(),
);
}
safe
})
.map(|item| {
let mut completion_item = build_version_completion(item, replacement, now, freshness);
let styled = formatter.format_version_for_completion(&item.version, clean_prefix);
if styled != item.version.as_str() {
completion_item.insert_text = Some(styled.clone());
if let (Some(CompletionTextEdit::Edit(edit)), Some(r)) =
(completion_item.text_edit.as_mut(), replacement)
{
edit.new_text = r.render(&styled);
}
}
completion_item
})
.collect()
}
#[allow(
clippy::too_many_arguments,
reason = "mirrors complete_versions_generic_replacing's own 9 parameters minus \
`replacement`; wrapping the existing 8 in a request struct now would break \
every other ecosystem's already-stable call pattern"
)]
pub async fn complete_versions_generic_from(
registry: &dyn crate::Registry,
formatter: &dyn crate::lsp_helpers::EcosystemFormatter,
package_name: &PackageName,
source: &crate::parser::DependencySource,
prefix: &str,
operator_chars: &[char],
freshness: FreshnessSettings,
selection_context: &crate::SelectionContext,
) -> Vec<CompletionItem> {
complete_versions_generic_replacing(
registry,
formatter,
package_name,
source,
prefix,
operator_chars,
freshness,
None,
selection_context,
)
.await
}
pub async fn complete_versions_at_position(
registry: &dyn crate::Registry,
formatter: &dyn crate::lsp_helpers::EcosystemFormatter,
parse_result: &dyn ParseResult,
position: Position,
prefix: &str,
operator_chars: &[char],
freshness: FreshnessSettings,
) -> Vec<CompletionItem> {
let Some(dep) = parse_result.dependencies().into_iter().find(|d| {
d.version_range()
.is_some_and(|r| position_in_range(position, r.into()))
}) else {
return vec![];
};
complete_versions_generic_from(
registry,
formatter,
dep.name(),
&dep.source(),
prefix,
operator_chars,
freshness,
&parse_result.selection_context(),
)
.await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lsp_helpers::test_support::MOCK_FORMATTER;
use std::any::Any;
use std::assert_matches;
fn pkg(s: &str) -> PackageName {
PackageName::new(s)
}
struct MockDependency {
name: crate::PackageName,
name_range: crate::position::Range,
version_range: Option<crate::position::Range>,
features_range: Option<crate::position::Range>,
}
impl crate::ecosystem::Dependency for MockDependency {
fn name(&self) -> &crate::PackageName {
&self.name
}
fn name_range(&self) -> crate::position::Range {
self.name_range
}
fn version_requirement(&self) -> Option<&crate::VersionReq> {
static VERSION_REQ: std::sync::LazyLock<crate::VersionReq> =
std::sync::LazyLock::new(|| crate::VersionReq::new("1.0"));
Some(&VERSION_REQ)
}
fn version_range(&self) -> Option<crate::position::Range> {
self.version_range
}
fn features_range(&self) -> Option<crate::position::Range> {
self.features_range
}
fn source(&self) -> crate::parser::DependencySource {
crate::parser::DependencySource::Registry
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct MockParseResult {
dependencies: Vec<MockDependency>,
}
impl ParseResult for MockParseResult {
fn dependencies(&self) -> Vec<&dyn crate::ecosystem::Dependency> {
self.dependencies
.iter()
.map(|d| d as &dyn crate::ecosystem::Dependency)
.collect()
}
fn workspace_root(&self) -> Option<&std::path::Path> {
None
}
fn uri(&self) -> &url::Url {
static URL: std::sync::LazyLock<url::Url> =
std::sync::LazyLock::new(|| "file:///test/Cargo.toml".parse().unwrap());
&URL
}
fn as_any(&self) -> &dyn Any {
self
}
}
use crate::test_util::MockVersion;
#[derive(Clone)]
struct MockMetadata {
name: crate::PackageName,
description: Option<String>,
repository: Option<String>,
documentation: Option<String>,
latest_version: ConcreteVersion,
}
impl crate::registry::Metadata for MockMetadata {
fn name(&self) -> &crate::PackageName {
&self.name
}
fn description(&self) -> Option<&str> {
self.description.as_deref()
}
fn repository(&self) -> Option<&str> {
self.repository.as_deref()
}
fn documentation(&self) -> Option<&str> {
self.documentation.as_deref()
}
fn latest_version(&self) -> &ConcreteVersion {
&self.latest_version
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct MockRegistry {
versions: Vec<MockVersion>,
}
impl crate::Registry for MockRegistry {
fn get_versions<'a>(
&'a self,
_package_name: &'a crate::PackageName,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Version>>>>
{
let versions: Vec<Box<dyn crate::Version>> = self
.versions
.iter()
.cloned()
.map(|v| Box::new(v) as Box<dyn crate::Version>)
.collect();
Box::pin(async move { Ok(versions) })
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a crate::PackageName,
_req: &'a crate::VersionReq,
_selection_context: &'a crate::SelectionContext,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Option<Box<dyn crate::Version>>>>
{
Box::pin(async move { Ok(None) })
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn select_latest_matching(
&self,
versions: &[Box<dyn crate::Version>],
_req: &crate::VersionReq,
_selection_context: &crate::SelectionContext,
) -> Option<usize> {
crate::select_latest_for_existence(versions, |v| v.as_ref())
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct MockSearchRegistry {
results: Vec<MockMetadata>,
}
impl crate::Registry for MockSearchRegistry {
fn get_versions<'a>(
&'a self,
_package_name: &'a crate::PackageName,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Version>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a crate::PackageName,
_req: &'a crate::VersionReq,
_selection_context: &'a crate::SelectionContext,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Option<Box<dyn crate::Version>>>>
{
Box::pin(async move { Ok(None) })
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Metadata>>>>
{
let results: Vec<Box<dyn crate::Metadata>> = self
.results
.iter()
.cloned()
.map(|m| Box::new(m) as Box<dyn crate::Metadata>)
.collect();
Box::pin(async move { Ok(results) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[tokio::test]
async fn test_complete_package_names_generic_uses_insert_range() {
let registry = MockSearchRegistry {
results: vec![MockMetadata {
name: pkg("serde"),
description: None,
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
}],
};
let insert_range = Range {
start: Position {
line: 3,
character: 4,
},
end: Position {
line: 3,
character: 7,
},
};
let items = complete_package_names_generic(®istry, "ser", 5, insert_range).await;
assert_eq!(items.len(), 1);
assert_ne!(insert_range, Range::default());
match &items[0].text_edit {
Some(CompletionTextEdit::Edit(edit)) => {
assert_eq!(edit.range, insert_range);
assert_eq!(edit.new_text, "serde");
}
other => panic!("Expected a textEdit::Edit, got {other:?}"),
}
}
#[tokio::test]
async fn test_complete_package_names_generic_short_prefix_empty() {
let registry = MockSearchRegistry {
results: vec![MockMetadata {
name: pkg("serde"),
description: None,
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
}],
};
let items = complete_package_names_generic(®istry, "s", 5, Range::default()).await;
assert!(items.is_empty());
}
#[tokio::test]
async fn test_complete_package_names_generic_drops_unsafe_names() {
let registry = MockSearchRegistry {
results: vec![
MockMetadata {
name: pkg("serde"),
description: None,
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
},
MockMetadata {
name: pkg("guava'); System.exit(1); //"),
description: None,
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
},
],
};
let items = complete_package_names_generic(®istry, "gua", 5, Range::default()).await;
assert_eq!(items.len(), 1);
assert_eq!(items[0].label, "serde");
}
#[test]
fn test_apply_raw_prefix_filter_text_noop_for_non_normalizing_registry() {
let registry = MockSearchRegistry { results: vec![] };
let mut items = vec![CompletionItem {
filter_text: Some("serde".to_string()),
..Default::default()
}];
apply_raw_prefix_filter_text(&mut items, ®istry, "ser");
assert_eq!(items[0].filter_text.as_deref(), Some("serde"));
}
#[test]
fn test_apply_raw_prefix_filter_text_rewrites_for_normalizing_registry() {
struct NormalizingRegistry;
impl crate::Registry for NormalizingRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a crate::PackageName,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Version>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a crate::PackageName,
_req: &'a crate::VersionReq,
_selection_context: &'a crate::SelectionContext,
) -> crate::ecosystem::BoxFuture<
'a,
crate::error::Result<Option<Box<dyn crate::Version>>>,
> {
Box::pin(async move { Ok(None) })
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn search_normalizes_query(&self) -> bool {
true
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry = NormalizingRegistry;
let mut items = vec![
CompletionItem {
filter_text: Some("zope-interface".to_string()),
..Default::default()
},
CompletionItem {
filter_text: None,
..Default::default()
},
];
apply_raw_prefix_filter_text(&mut items, ®istry, "zope.int");
assert_eq!(items[0].filter_text.as_deref(), Some("zope.int"));
assert_eq!(items[1].filter_text.as_deref(), Some("zope.int"));
}
#[test]
fn test_is_valid_completion_prefix_len_ascii_short_rejected() {
assert!(!is_valid_completion_prefix_len("a"));
}
#[test]
fn test_is_valid_completion_prefix_len_one_char_cjk_rejected() {
assert!(!is_valid_completion_prefix_len("日"));
}
#[test]
fn test_is_valid_completion_prefix_len_two_char_cjk_accepted() {
assert!(is_valid_completion_prefix_len("日本"));
}
#[test]
fn test_is_valid_completion_prefix_len_200_chars_accepted() {
assert!(is_valid_completion_prefix_len(&"a".repeat(200)));
}
#[test]
fn test_is_valid_completion_prefix_len_201_chars_rejected() {
assert!(!is_valid_completion_prefix_len(&"a".repeat(201)));
}
#[tokio::test]
async fn test_complete_package_names_generic_one_char_cjk_prefix_empty() {
let registry = MockSearchRegistry {
results: vec![MockMetadata {
name: pkg("serde"),
description: None,
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
}],
};
let items = complete_package_names_generic(®istry, "日", 5, Range::default()).await;
assert!(items.is_empty());
}
#[tokio::test]
async fn test_complete_package_names_generic_two_char_cjk_prefix_accepted() {
let registry = MockSearchRegistry {
results: vec![MockMetadata {
name: pkg("serde"),
description: None,
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
}],
};
let items = complete_package_names_generic(®istry, "日本", 5, Range::default()).await;
assert_eq!(items.len(), 1);
}
#[test]
fn test_detect_package_name_context_at_start() {
let parse_result = MockParseResult {
dependencies: vec![MockDependency {
name: "serde".into(),
name_range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
}
.into(),
version_range: None,
features_range: None,
}],
};
let content = "serde";
let position = Position {
line: 0,
character: 0,
};
let context = detect_completion_context(&parse_result, position, content);
match context {
CompletionContext::PackageName { prefix, range } => {
assert_eq!(prefix, "");
assert_eq!(
range,
Range {
start: Position {
line: 0,
character: 0
},
end: Position {
line: 0,
character: 5
},
}
);
}
_ => panic!("Expected PackageName context, got {:?}", context),
}
}
#[test]
fn test_detect_completion_context_skips_synthetic_range_dependency() {
struct SyntheticDep {
name: crate::PackageName,
}
impl crate::ecosystem::Dependency for SyntheticDep {
fn name(&self) -> &crate::PackageName {
&self.name
}
fn name_range(&self) -> crate::position::Range {
crate::position::Range::default()
}
fn version_requirement(&self) -> Option<&crate::VersionReq> {
None
}
fn version_range(&self) -> Option<crate::position::Range> {
None
}
fn source(&self) -> crate::parser::DependencySource {
crate::parser::DependencySource::Registry
}
fn name_range_is_synthetic(&self) -> bool {
true
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct SyntheticParseResult {
dep: SyntheticDep,
}
impl ParseResult for SyntheticParseResult {
fn dependencies(&self) -> Vec<&dyn crate::ecosystem::Dependency> {
vec![&self.dep as &dyn crate::ecosystem::Dependency]
}
fn workspace_root(&self) -> Option<&std::path::Path> {
None
}
fn uri(&self) -> &url::Url {
static URI: std::sync::LazyLock<url::Url> =
std::sync::LazyLock::new(|| crate::test_util::test_uri("/test/pubspec.yaml"));
&URI
}
fn as_any(&self) -> &dyn Any {
self
}
}
let parse_result = SyntheticParseResult {
dep: SyntheticDep {
name: "synthetic-pkg".into(),
},
};
let context = detect_completion_context(
&parse_result,
Position {
line: 0,
character: 0,
},
"",
);
assert_matches!(context, CompletionContext::None);
}
fn char_range(line: u32, start: u32, end: u32) -> Range {
Range {
start: Position {
line,
character: start,
},
end: Position {
line,
character: end,
},
}
}
fn make_dep_with_version_range(
name: &str,
name_start: u32,
name_end: u32,
version_start: u32,
version_end: u32,
) -> MockDependency {
MockDependency {
name: name.into(),
name_range: char_range(0, name_start, name_end).into(),
version_range: Some(char_range(0, version_start, version_end).into()),
features_range: None,
}
}
#[test]
fn test_literal_version_dependency_prefers_version_range_match_over_earlier_same_line_dependency()
{
let content = "dep-one:1.0 dep-two:1.0\n";
let dep_one = make_dep_with_version_range("dep-one", 0, 7, 8, 11);
let dep_two_version_range = char_range(0, 20, 23);
let dep_two = make_dep_with_version_range("dep-two", 12, 19, 20, 23);
let parse_result = MockParseResult {
dependencies: vec![dep_one, dep_two],
};
let position = Position {
line: 0,
character: 21,
};
let dep =
literal_version_dependency(&parse_result, position, content, dep_two_version_range)
.expect("dep-two's literal version must be found");
assert_eq!(dep.name().as_str(), "dep-two");
}
#[test]
fn test_literal_version_dependency_same_line_fallback_prefers_nearest_version_range_on_minified_line()
{
let content = "dep-one:1.0 dep-two:1.0\n";
let dep_one_version_range = char_range(0, 8, 11);
let dep_two_version_range = char_range(0, 20, 23);
let dep_one = make_dep_with_version_range("dep-one", 0, 7, 8, 11);
let dep_two = make_dep_with_version_range("dep-two", 12, 19, 20, 23);
let parse_result = MockParseResult {
dependencies: vec![dep_one, dep_two],
};
let before_dep_one_vr = Position {
line: 0,
character: 7,
};
let dep = literal_version_dependency(
&parse_result,
before_dep_one_vr,
content,
dep_one_version_range,
)
.expect("dep-one must be rescued at its own version_range boundary");
assert_eq!(dep.name().as_str(), "dep-one");
let before_dep_two_vr = Position {
line: 0,
character: 19,
};
let dep = literal_version_dependency(
&parse_result,
before_dep_two_vr,
content,
dep_two_version_range,
)
.expect("dep-two must be rescued at its own version_range boundary");
assert_eq!(dep.name().as_str(), "dep-two");
}
#[test]
fn test_literal_version_dependency_same_line_fallback_ignores_name_range_order() {
let content = "v:1.0 n:dep-one\n";
let dep_one = make_dep_with_version_range("dep-one", 8, 15, 2, 5);
let parse_result = MockParseResult {
dependencies: vec![dep_one],
};
let value_range = char_range(0, 2, 5);
let just_before_vr = Position {
line: 0,
character: 1,
};
let dep = literal_version_dependency(&parse_result, just_before_vr, content, value_range)
.expect("must resolve despite name_range starting after version_range");
assert_eq!(dep.name().as_str(), "dep-one");
}
#[test]
fn test_literal_version_dependency_same_line_fallback_returns_none_without_same_line_dependency()
{
let content = "dep-one:1.0\n\n";
let dep_one = make_dep_with_version_range("dep-one", 0, 7, 8, 11);
let parse_result = MockParseResult {
dependencies: vec![dep_one],
};
let position = Position {
line: 1,
character: 0,
};
let value_range = char_range(1, 0, 0);
assert!(
literal_version_dependency(&parse_result, position, content, value_range).is_none()
);
}
#[test]
fn test_literal_version_dependency_same_line_fallback_rescues_single_dependency_at_version_range_boundary()
{
let content = "dep-one:1.0\n";
let dep_one = make_dep_with_version_range("dep-one", 0, 7, 8, 11);
let parse_result = MockParseResult {
dependencies: vec![dep_one],
};
let value_range = char_range(0, 8, 11);
let just_before_vr = Position {
line: 0,
character: 7,
};
let dep = literal_version_dependency(&parse_result, just_before_vr, content, value_range)
.expect("must be rescued just before version_range start");
assert_eq!(dep.name().as_str(), "dep-one");
}
#[test]
fn test_literal_version_dependency_in_scope_outside_does_not_affect_pass_one() {
let content = "dep-one:1.0\n";
let dep_one = make_dep_with_version_range("dep-one", 0, 7, 8, 11);
let parse_result = MockParseResult {
dependencies: vec![dep_one],
};
let value_range = char_range(0, 8, 11);
let inside_version_range = Position {
line: 0,
character: 9,
};
let dep = literal_version_dependency_in_scope(
&parse_result,
inside_version_range,
content,
value_range,
DeclarationScope::Outside,
)
.expect("pass 1 must resolve regardless of DeclarationScope::Outside");
assert_eq!(dep.name().as_str(), "dep-one");
}
#[test]
fn test_literal_version_dependency_in_scope_outside_suppresses_pass_two() {
let content = "dep-one:1.0\n";
let dep_one = make_dep_with_version_range("dep-one", 0, 7, 8, 11);
let parse_result = MockParseResult {
dependencies: vec![dep_one],
};
let value_range = char_range(0, 8, 11);
let just_before_vr = Position {
line: 0,
character: 7,
};
assert!(
literal_version_dependency_in_scope(
&parse_result,
just_before_vr,
content,
value_range,
DeclarationScope::Outside,
)
.is_none(),
"DeclarationScope::Outside must skip pass 2's same-line fallback entirely"
);
}
#[test]
fn test_literal_version_dependency_in_scope_within_excludes_anchor_outside_span() {
let content = "dep-one:1.0 dep-two:1.0\n";
let dep_one_version_range = char_range(0, 8, 11);
let dep_two_version_range = char_range(0, 20, 23);
let dep_one = make_dep_with_version_range("dep-one", 0, 7, 8, 11);
let dep_two = make_dep_with_version_range("dep-two", 12, 19, 20, 23);
let parse_result = MockParseResult {
dependencies: vec![dep_one, dep_two],
};
let before_dep_one_vr = Position {
line: 0,
character: 7,
};
let dep_two_scope = DeclarationScope::Within(dep_two_version_range.into());
let dep = literal_version_dependency_in_scope(
&parse_result,
before_dep_one_vr,
content,
dep_one_version_range,
dep_two_scope,
);
assert_eq!(
dep.map(|d| d.name().as_str()),
Some("dep-two"),
"dep-one is nearer by distance but its anchor falls outside dep-two's span"
);
}
#[test]
fn test_detect_package_name_context_partial() {
let parse_result = MockParseResult {
dependencies: vec![MockDependency {
name: "serde".into(),
name_range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
}
.into(),
version_range: None,
features_range: None,
}],
};
let content = "serde";
let position = Position {
line: 0,
character: 3,
};
let context = detect_completion_context(&parse_result, position, content);
match context {
CompletionContext::PackageName { prefix, range } => {
assert_eq!(prefix, "ser");
assert_eq!(
range,
Range {
start: Position {
line: 0,
character: 0
},
end: Position {
line: 0,
character: 5
},
}
);
}
_ => panic!("Expected PackageName context, got {:?}", context),
}
}
#[test]
fn test_detect_package_name_context_one_past_end_does_not_widen_range() {
let parse_result = MockParseResult {
dependencies: vec![MockDependency {
name: "serde".into(),
name_range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
}
.into(),
version_range: None,
features_range: None,
}],
};
let content = "serde";
let position = Position {
line: 0,
character: 6,
};
let context = detect_completion_context(&parse_result, position, content);
assert_eq!(context, CompletionContext::None);
}
#[test]
fn test_detect_package_name_context_exactly_at_end_matches_unwidened_range() {
let parse_result = MockParseResult {
dependencies: vec![MockDependency {
name: "serde".into(),
name_range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
}
.into(),
version_range: None,
features_range: None,
}],
};
let content = "serde";
let position = Position {
line: 0,
character: 5,
};
let context = detect_completion_context(&parse_result, position, content);
match context {
CompletionContext::PackageName { prefix, range } => {
assert_eq!(prefix, "serde");
assert_eq!(
range,
Range {
start: Position {
line: 0,
character: 0
},
end: Position {
line: 0,
character: 5
},
}
);
}
_ => panic!("Expected PackageName context, got {:?}", context),
}
}
#[test]
fn test_detect_package_name_context_fires_for_ecosystem_supplied_partial_name() {
let parse_result = MockParseResult {
dependencies: vec![MockDependency {
name: "jsr:@std/".into(),
name_range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 9,
},
}
.into(),
version_range: None,
features_range: None,
}],
};
let content = "jsr:@std/";
let position = Position {
line: 0,
character: 9,
};
let context = detect_completion_context(&parse_result, position, content);
match context {
CompletionContext::PackageName { prefix, range } => {
assert_eq!(prefix, "jsr:@std/");
assert_eq!(range, parse_result.dependencies[0].name_range.into());
}
other => panic!("Expected PackageName context, got {other:?}"),
}
}
#[test]
fn test_detect_version_context() {
let parse_result = MockParseResult {
dependencies: vec![MockDependency {
name: "serde".into(),
name_range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
}
.into(),
version_range: Some(
Range {
start: Position {
line: 0,
character: 9,
},
end: Position {
line: 0,
character: 12,
},
}
.into(),
),
features_range: None,
}],
};
let content = r#"serde = "1.0""#;
let position = Position {
line: 0,
character: 11,
};
let context = detect_completion_context(&parse_result, position, content);
match context {
CompletionContext::Version {
package_name,
prefix,
} => {
assert_eq!(package_name, "serde");
assert_eq!(prefix, "1.");
}
_ => panic!("Expected Version context, got {:?}", context),
}
}
#[test]
fn test_detect_version_context_withheld_for_property_interpolation() {
let parse_result = MockParseResult {
dependencies: vec![MockDependency {
name: "slf4j-api".into(),
name_range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 9,
},
}
.into(),
version_range: Some(
Range {
start: Position {
line: 0,
character: 13,
},
end: Position {
line: 0,
character: 29,
},
}
.into(),
),
features_range: None,
}],
};
let content = r#"slf4j-api = "${slf4j.version}""#;
let position = Position {
line: 0,
character: 20,
};
let context = detect_completion_context(&parse_result, position, content);
assert_eq!(context, CompletionContext::None);
}
#[test]
fn test_detect_version_context_withheld_for_yaml_alias() {
let parse_result = MockParseResult {
dependencies: vec![MockDependency {
name: "my-job".into(),
name_range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 6,
},
}
.into(),
version_range: Some(
Range {
start: Position {
line: 0,
character: 7,
},
end: Position {
line: 0,
character: 11,
},
}
.into(),
),
features_range: None,
}],
};
let content = "my-job *pin";
let position = Position {
line: 0,
character: 9,
};
let context = detect_completion_context(&parse_result, position, content);
assert_eq!(context, CompletionContext::None);
}
#[test]
fn test_detect_no_context_before_dependencies() {
let parse_result = MockParseResult {
dependencies: vec![MockDependency {
name: "serde".into(),
name_range: Range {
start: Position {
line: 5,
character: 0,
},
end: Position {
line: 5,
character: 5,
},
}
.into(),
version_range: None,
features_range: None,
}],
};
let content = "[dependencies]\nserde";
let position = Position {
line: 0,
character: 10,
};
let context = detect_completion_context(&parse_result, position, content);
assert_eq!(context, CompletionContext::None);
}
#[test]
fn test_detect_no_context_invalid_position() {
let parse_result = MockParseResult {
dependencies: vec![],
};
let content = "";
let position = Position {
line: 100,
character: 100,
};
let context = detect_completion_context(&parse_result, position, content);
assert_eq!(context, CompletionContext::None);
}
#[test]
fn test_extract_prefix_at_start() {
let content = "serde";
let position = Position {
line: 0,
character: 0,
};
let range = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
};
let prefix = extract_prefix(content, position, range);
assert_eq!(prefix, "");
}
#[test]
fn test_extract_prefix_partial() {
let content = "serde";
let position = Position {
line: 0,
character: 3,
};
let range = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
};
let prefix = extract_prefix(content, position, range);
assert_eq!(prefix, "ser");
}
#[test]
fn test_extract_prefix_with_quotes() {
let content = r#"serde = "1.0""#;
let position = Position {
line: 0,
character: 11,
};
let range = Range {
start: Position {
line: 0,
character: 9,
},
end: Position {
line: 0,
character: 13,
},
};
let prefix = extract_prefix(content, position, range);
assert_eq!(prefix, "1.");
}
#[test]
fn test_extract_prefix_empty() {
let content = r#"serde = """#;
let position = Position {
line: 0,
character: 9,
};
let range = Range {
start: Position {
line: 0,
character: 9,
},
end: Position {
line: 0,
character: 11,
},
};
let prefix = extract_prefix(content, position, range);
assert_eq!(prefix, "");
}
#[test]
fn test_extract_prefix_version_with_operator() {
let content = r#"serde = "^1.0""#;
let position = Position {
line: 0,
character: 12,
};
let range = Range {
start: Position {
line: 0,
character: 9,
},
end: Position {
line: 0,
character: 14,
},
};
let prefix = extract_prefix(content, position, range);
assert_eq!(prefix, "^1.");
}
#[test]
fn test_build_package_completion_full() {
let metadata = MockMetadata {
name: "serde".into(),
description: Some("Serialization framework".to_string()),
repository: Some("https://github.com/serde-rs/serde".to_string()),
documentation: Some("https://docs.rs/serde".to_string()),
latest_version: "1.0.214".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 3, "ser").unwrap();
assert_eq!(item.label, "serde");
assert_eq!(item.kind, Some(CompletionItemKind::MODULE));
assert_eq!(item.detail, Some("v1.0.214".to_string()));
assert_matches!(item.documentation, Some(Documentation::MarkupContent(_)));
assert_eq!(item.insert_text, Some("serde".to_string()));
assert_eq!(item.filter_text, Some("serde".to_string()));
assert_eq!(
item.text_edit,
Some(CompletionTextEdit::Edit(TextEdit {
range,
new_text: "serde".to_string(),
}))
);
assert_eq!(item.sort_text, Some("00000000003".to_string()));
if let Some(Documentation::MarkupContent(content)) = item.documentation {
assert!(content.value.contains("**serde** v1\\.0\\.214"));
assert!(content.value.contains("Serialization framework"));
assert!(content.value.contains("Repository"));
assert!(content.value.contains("Documentation"));
}
}
#[test]
fn test_build_package_completion_sort_text_tiers_exact_prefix_match() {
let exact_match = MockMetadata {
name: "serde".into(),
description: None,
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
};
let fuzzy_match = MockMetadata {
name: "actix-serde".into(),
description: None,
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
};
let range = Range::default();
let exact_item = build_package_completion(&exact_match, range, 1, "ser").unwrap();
let fuzzy_item = build_package_completion(&fuzzy_match, range, 0, "ser").unwrap();
assert_eq!(exact_item.sort_text, Some("00000000001".to_string()));
assert_eq!(fuzzy_item.sort_text, Some("10000000000".to_string()));
assert!(exact_item.sort_text < fuzzy_item.sort_text);
}
#[test]
fn test_starts_with_ascii_case_insensitive_ignores_pep503_separators() {
assert!(starts_with_ascii_case_insensitive(
"zope-interface",
"zope.int"
));
assert!(starts_with_ascii_case_insensitive(
"zope_interface",
"ZOPE-INT"
));
assert!(!starts_with_ascii_case_insensitive(
"zope-interface",
"interface"
));
}
#[test]
fn test_build_completion_sort_text_index_padding_preserves_order_past_5_digits() {
let lower = build_completion_sort_text(99_999, "", "pkg");
let higher = build_completion_sort_text(100_000, "", "pkg");
assert_eq!(lower.len(), higher.len());
assert!(lower < higher);
}
#[test]
fn test_build_package_completion_minimal() {
let metadata = MockMetadata {
name: "test-pkg".into(),
description: None,
repository: None,
documentation: None,
latest_version: "0.1.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
assert_eq!(item.label, "test-pkg");
assert_eq!(item.detail, Some("v0.1.0".to_string()));
if let Some(Documentation::MarkupContent(content)) = item.documentation {
assert!(content.value.contains("**test\\-pkg** v0\\.1\\.0"));
assert!(!content.value.contains("Repository"));
}
}
#[test]
fn test_build_package_completion_empty_latest_version() {
let metadata = MockMetadata {
name: "swift-nio".into(),
description: None,
repository: None,
documentation: None,
latest_version: String::new().into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
assert_eq!(item.detail, None);
if let Some(Documentation::MarkupContent(content)) = item.documentation {
assert!(content.value.contains("**swift\\-nio**"));
assert!(!content.value.trim_end().ends_with('v'));
}
}
#[test]
fn test_build_package_completion_escapes_description_markdown() {
let metadata = MockMetadata {
name: "test-pkg".into(),
description: Some("Fast *bold* _italic_ [link](evil) `code`".to_string()),
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
assert!(!content.value.contains("*bold*"));
assert!(!content.value.contains("_italic_"));
assert!(!content.value.contains("[link](evil)"));
assert!(content.value.contains(r"\*bold\*"));
assert!(content.value.contains(r"\[link\]\(evil\)"));
} else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_build_package_completion_escapes_repository_link_breakout() {
let malicious_repo = "https://legit.example)[ClickHere](https://evil.example";
let metadata = MockMetadata {
name: "test-pkg".into(),
description: None,
repository: Some(malicious_repo.to_string()),
documentation: None,
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
assert!(!content.value.contains(")[ClickHere]("));
assert!(content.value.contains(r"\)\[ClickHere\]\("));
} else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_build_package_completion_rejects_javascript_scheme_repository_link() {
let metadata = MockMetadata {
name: "test-pkg".into(),
description: None,
repository: Some("javascript:alert(document.cookie)".to_string()),
documentation: None,
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
assert!(!content.value.contains("javascript:"));
assert!(!content.value.contains("Repository"));
} else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_build_package_completion_rejects_data_scheme_repository_link() {
let metadata = MockMetadata {
name: "test-pkg".into(),
description: None,
repository: Some("data:text/html,<script>alert(1)</script>".to_string()),
documentation: None,
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
assert!(!content.value.contains("data:"));
assert!(!content.value.contains("Repository"));
} else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_build_package_completion_rejects_whitespace_bearing_repository_url() {
let metadata = MockMetadata {
name: "test-pkg".into(),
description: None,
repository: Some("https://legit.example)[Click here](https://evil.example".to_string()),
documentation: None,
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
assert!(!content.value.contains("Click here"));
assert!(!content.value.contains("Repository"));
} else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_build_package_completion_escapes_documentation_link_breakout() {
let malicious_docs = "https://legit.example)[ClickHere](https://evil.example";
let metadata = MockMetadata {
name: "test-pkg".into(),
description: None,
repository: None,
documentation: Some(malicious_docs.to_string()),
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
assert!(!content.value.contains(")[ClickHere]("));
assert!(content.value.contains(r"\)\[ClickHere\]\("));
} else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_build_package_completion_rejects_javascript_scheme_documentation_link() {
let metadata = MockMetadata {
name: "test-pkg".into(),
description: None,
repository: None,
documentation: Some("javascript:alert(document.cookie)".to_string()),
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
assert!(!content.value.contains("javascript:"));
assert!(!content.value.contains("Documentation"));
} else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_build_package_completion_rejects_data_scheme_documentation_link() {
let metadata = MockMetadata {
name: "test-pkg".into(),
description: None,
repository: None,
documentation: Some("data:text/html,<script>alert(1)</script>".to_string()),
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
assert!(!content.value.contains("data:"));
assert!(!content.value.contains("Documentation"));
} else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_build_package_completion_truncate_then_escape_no_dangling_backslash() {
let mut desc = "a".repeat(199);
desc.push('*');
desc.push_str(&"b".repeat(50));
let metadata = MockMetadata {
name: "test-pkg".into(),
description: Some(desc),
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
let lines: Vec<_> = content.value.lines().collect();
let desc_line = lines[2];
assert!(desc_line.ends_with(r"\*..."), "got: {desc_line}");
assert!(
!desc_line.ends_with(r"\..."),
"dangling backslash: {desc_line}"
);
} else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_build_package_completion_rejects_unsafe_latest_version() {
let malicious_latest = "1.0.0)[click](https://evil.example";
let metadata = MockMetadata {
name: "test-pkg".into(),
description: None,
repository: None,
documentation: None,
latest_version: malicious_latest.into(),
};
let range = Range::default();
assert!(build_package_completion(&metadata, range, 0, "").is_none());
}
#[test]
fn test_build_package_completion_escapes_markdown_active_chars_in_safe_version() {
let latest = "1.0.0-alpha*evil*";
let metadata = MockMetadata {
name: "test-pkg".into(),
description: None,
repository: None,
documentation: None,
latest_version: latest.into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
assert!(!content.value.contains("*evil*"));
assert!(content.value.contains(r"\*evil\*"));
} else {
panic!("Expected MarkupContent documentation");
}
assert_eq!(item.detail, Some(format!("v{latest}")));
}
#[test]
fn test_build_package_completion_rejects_unsafe_name() {
let malicious_name = "a** [Official Download](https://evil.example) **b";
let metadata = MockMetadata {
name: malicious_name.into(),
description: None,
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
};
let range = Range::default();
assert!(build_package_completion(&metadata, range, 0, "").is_none());
}
#[test]
fn test_build_package_completion_benign_repository_url_round_trips() {
let metadata = MockMetadata {
name: "test-pkg".into(),
description: None,
repository: Some("https://github.com/owner/repo".to_string()),
documentation: None,
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
let unescaped: String = content.value.chars().filter(|&c| c != '\\').collect();
assert!(unescaped.contains("[Repository](https://github.com/owner/repo)"));
} else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_build_package_completion_escapes_html_in_description() {
let metadata = MockMetadata {
name: "test-pkg".into(),
description: Some("<img src=x onerror=alert(1)>".to_string()),
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
assert!(!content.value.contains("<img src=x onerror=alert(1)>"));
assert!(
content
.value
.contains(r"\<img src\=x onerror\=alert\(1\)\>")
);
} else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_build_package_completion_empty_description() {
let metadata = MockMetadata {
name: "test-pkg".into(),
description: Some(String::new()),
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
assert!(content.value.starts_with(r"**test\-pkg** v1\.0\.0"));
} else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_build_package_completion_truncate_snaps_multibyte_boundary() {
let mut desc = "a".repeat(199);
desc.push('日'); desc.push('*');
desc.push_str(&"b".repeat(50));
let metadata = MockMetadata {
name: "test-pkg".into(),
description: Some(desc),
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
let lines: Vec<_> = content.value.lines().collect();
let desc_line = lines[2];
assert!(!desc_line.contains('日'));
assert!(desc_line.ends_with("..."));
} else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_build_version_completion_stable() {
let version = MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false);
let now = PublishTime::now();
let display_item = VersionDisplayItem::new(&version, &pkg("serde"), 0, false);
let item = build_version_completion(&display_item, None, now, FreshnessSettings::default());
assert_eq!(item.label, "1.0.0");
assert_eq!(item.kind, Some(CompletionItemKind::VALUE));
assert_eq!(item.detail, Some("Update serde to 1.0.0".to_string()));
assert_eq!(item.documentation, None);
assert_eq!(item.preselect, Some(false));
assert_eq!(item.sort_text, Some("00000".to_string()));
assert_eq!(item.text_edit, None); }
#[test]
fn test_build_version_completion_with_replacement_sets_text_edit_and_filter_text() {
let version = MockVersion::new("1.2.3")
.yanked(false)
.with_prerelease(false);
let range = Range {
start: Position::new(3, 6),
end: Position::new(3, 17),
};
let replacement = VersionReplacement {
range,
lead: "<version>".to_string(),
trail: "</version>".to_string(),
replaced_text: "<version/>".to_string(),
};
let now = PublishTime::now();
let display_item = VersionDisplayItem::new(&version, &pkg("junit"), 0, true);
let item = build_version_completion(
&display_item,
Some(&replacement),
now,
FreshnessSettings::default(),
);
assert_eq!(
item.text_edit,
Some(CompletionTextEdit::Edit(TextEdit {
range,
new_text: "<version>1.2.3</version>".to_string(),
}))
);
assert_eq!(item.filter_text, Some("<version/>".to_string()));
assert_eq!(item.insert_text, Some("1.2.3".to_string()));
assert_eq!(item.insert_text_format, Some(InsertTextFormat::PLAIN_TEXT));
}
#[test]
fn test_build_version_completion_without_replacement_omits_filter_text_and_format() {
let version = MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false);
let now = PublishTime::now();
let display_item = VersionDisplayItem::new(&version, &pkg("serde"), 0, false);
let item = build_version_completion(&display_item, None, now, FreshnessSettings::default());
assert_eq!(item.filter_text, None);
assert_eq!(item.insert_text_format, None);
}
#[test]
fn test_build_version_completion_latest() {
let version = MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false);
let now = PublishTime::now();
let display_item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
let item = build_version_completion(&display_item, None, now, FreshnessSettings::default());
assert_eq!(item.label, "1.0.0 (latest)");
assert_eq!(item.kind, Some(CompletionItemKind::VALUE));
assert_eq!(item.detail, Some("Update serde to 1.0.0".to_string()));
assert_eq!(item.documentation, None);
assert_eq!(item.preselect, Some(true));
assert_eq!(item.sort_text, Some("00000".to_string()));
assert_eq!(item.text_edit, None); }
#[test]
fn test_build_version_completion_not_latest() {
let version = MockVersion::new("0.9.0")
.yanked(false)
.with_prerelease(false);
let now = PublishTime::now();
let display_item = VersionDisplayItem::new(&version, &pkg("tokio"), 1, false);
let item = build_version_completion(&display_item, None, now, FreshnessSettings::default());
assert_eq!(item.label, "0.9.0");
assert_eq!(item.detail, Some("Update tokio to 0.9.0".to_string()));
assert_eq!(item.documentation, None);
assert_eq!(item.preselect, Some(false));
assert_eq!(item.sort_text, Some("00001".to_string()));
assert_eq!(item.text_edit, None); }
#[test]
fn test_build_version_completion_sort_order() {
let v1 = MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false);
let v2 = MockVersion::new("0.9.0")
.yanked(false)
.with_prerelease(false);
let v3 = MockVersion::new("0.8.0")
.yanked(false)
.with_prerelease(false);
let display_item1 = VersionDisplayItem::new(&v1, &pkg("test"), 0, true);
let display_item2 = VersionDisplayItem::new(&v2, &pkg("test"), 1, false);
let display_item3 = VersionDisplayItem::new(&v3, &pkg("test"), 2, false);
let now = PublishTime::now();
let item1 =
build_version_completion(&display_item1, None, now, FreshnessSettings::default());
let item2 =
build_version_completion(&display_item2, None, now, FreshnessSettings::default());
let item3 =
build_version_completion(&display_item3, None, now, FreshnessSettings::default());
assert_eq!(item1.sort_text.as_ref().unwrap(), "00000");
assert_eq!(item2.sort_text.as_ref().unwrap(), "00001");
assert_eq!(item3.sort_text.as_ref().unwrap(), "00002");
assert_eq!(item1.preselect, Some(true));
assert_eq!(item2.preselect, Some(false));
assert_eq!(item3.preselect, Some(false));
}
#[test]
fn test_version_completion_semantic_ordering() {
let versions = [
MockVersion::new("0.14.0")
.yanked(false)
.with_prerelease(false),
MockVersion::new("0.8.0")
.yanked(false)
.with_prerelease(false),
MockVersion::new("0.2.0")
.yanked(false)
.with_prerelease(false),
];
let now = PublishTime::now();
let items: Vec<_> = versions
.iter()
.enumerate()
.map(|(idx, v)| {
let display_item = VersionDisplayItem::new(v, &pkg("test"), idx, idx == 0);
build_version_completion(&display_item, None, now, FreshnessSettings::default())
})
.collect();
assert_eq!(items[0].sort_text.as_ref().unwrap(), "00000");
assert_eq!(items[1].sort_text.as_ref().unwrap(), "00001");
assert_eq!(items[2].sort_text.as_ref().unwrap(), "00002");
let mut sorted_items = items;
sorted_items.sort_by(|a, b| {
a.sort_text
.as_ref()
.unwrap()
.cmp(b.sort_text.as_ref().unwrap())
});
assert_eq!(sorted_items[0].label, "0.14.0 (latest)");
assert_eq!(sorted_items[1].label, "0.8.0");
assert_eq!(sorted_items[2].label, "0.2.0");
}
#[test]
fn test_version_completion_index_ordering() {
let versions = ["1.20.0", "1.9.0", "1.2.0", "0.99.0", "0.50.0"];
let now = PublishTime::now();
let items: Vec<_> = versions
.iter()
.enumerate()
.map(|(idx, ver)| {
let v = MockVersion::new(*ver).yanked(false).with_prerelease(false);
let display_item = VersionDisplayItem::new(&v, &pkg("test"), idx, idx == 0);
build_version_completion(&display_item, None, now, FreshnessSettings::default())
})
.collect();
assert_eq!(items[0].sort_text.as_ref().unwrap(), "00000");
assert_eq!(items[1].sort_text.as_ref().unwrap(), "00001");
assert_eq!(items[2].sort_text.as_ref().unwrap(), "00002");
assert_eq!(items[3].sort_text.as_ref().unwrap(), "00003");
assert_eq!(items[4].sort_text.as_ref().unwrap(), "00004");
let mut sorted_items = items;
sorted_items.sort_by(|a, b| {
a.sort_text
.as_ref()
.unwrap()
.cmp(b.sort_text.as_ref().unwrap())
});
assert_eq!(sorted_items[0].label, "1.20.0 (latest)");
assert_eq!(sorted_items[1].label, "1.9.0");
assert_eq!(sorted_items[2].label, "1.2.0");
assert_eq!(sorted_items[3].label, "0.99.0");
assert_eq!(sorted_items[4].label, "0.50.0");
}
#[test]
fn test_version_display_item_latest() {
let version = MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false);
let item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
assert_eq!(item.version, "1.0.0");
assert_eq!(item.label, "1.0.0 (latest)");
assert_eq!(item.description, "Update serde to 1.0.0");
assert_eq!(item.index, 0);
assert!(item.is_latest);
}
#[test]
fn test_version_display_item_not_latest() {
let version = MockVersion::new("0.9.0")
.yanked(false)
.with_prerelease(false);
let item = VersionDisplayItem::new(&version, &pkg("tokio"), 1, false);
assert_eq!(item.version, "0.9.0");
assert_eq!(item.label, "0.9.0");
assert_eq!(item.description, "Update tokio to 0.9.0");
assert_eq!(item.index, 1);
assert!(!item.is_latest);
}
#[test]
fn test_prepare_version_display_items_filters_yanked() {
let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![
std::sync::Arc::new(
MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false),
),
std::sync::Arc::new(
MockVersion::new("0.9.0")
.yanked(true)
.with_prerelease(false),
),
std::sync::Arc::new(
MockVersion::new("0.8.0")
.yanked(false)
.with_prerelease(false),
),
];
let items = prepare_version_display_items(&versions, &pkg("test"), Some(0));
assert_eq!(items.len(), 2);
assert_eq!(items[0].version, "1.0.0");
assert_eq!(items[0].label, "1.0.0 (latest)");
assert!(items[0].is_latest);
assert_eq!(items[1].version, "0.8.0");
assert_eq!(items[1].label, "0.8.0");
assert!(!items[1].is_latest);
}
#[test]
fn test_prepare_version_display_items_limits_to_5() {
let versions: Vec<std::sync::Arc<dyn crate::Version>> = (0..10)
.map(|i| {
std::sync::Arc::new(
MockVersion::new(format!("1.0.{}", i))
.yanked(false)
.with_prerelease(false),
) as std::sync::Arc<dyn crate::Version>
})
.collect();
let items = prepare_version_display_items(&versions, &pkg("test"), Some(0));
assert_eq!(items.len(), 5);
assert_eq!(items[0].version, "1.0.0");
assert_eq!(items[0].label, "1.0.0 (latest)");
assert_eq!(items[4].version, "1.0.4");
assert_eq!(items[4].label, "1.0.4");
}
#[test]
fn test_prepare_version_display_items_empty() {
let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![];
let items = prepare_version_display_items(&versions, &pkg("test"), None);
assert_eq!(items.len(), 0);
}
#[test]
fn test_prepare_version_display_items_all_yanked() {
let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![
std::sync::Arc::new(
MockVersion::new("1.0.0")
.yanked(true)
.with_prerelease(false),
),
std::sync::Arc::new(
MockVersion::new("0.9.0")
.yanked(true)
.with_prerelease(false),
),
];
let items = prepare_version_display_items(&versions, &pkg("test"), Some(0));
assert_eq!(items.len(), 0);
}
#[test]
fn test_prepare_version_display_items_tags_caller_supplied_index_not_raw_zero() {
let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![
std::sync::Arc::new(
MockVersion::new("13.0.5-beta1")
.yanked(false)
.with_prerelease(true),
),
std::sync::Arc::new(
MockVersion::new("13.0.4")
.yanked(false)
.with_prerelease(false),
),
std::sync::Arc::new(
MockVersion::new("13.0.3")
.yanked(false)
.with_prerelease(false),
),
];
let items = prepare_version_display_items(&versions, &pkg("test"), Some(1));
assert_eq!(items.len(), 3);
assert_eq!(items[0].version, "13.0.5-beta1");
assert!(!items[0].is_latest, "index 0 was not the supplied pick");
assert_eq!(items[1].version, "13.0.4");
assert_eq!(items[1].label, "13.0.4 (latest)");
assert!(items[1].is_latest);
assert_eq!(items[2].version, "13.0.3");
assert!(!items[2].is_latest);
}
#[test]
fn test_prepare_version_display_items_none_latest_idx_tags_nothing() {
let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![std::sync::Arc::new(
MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false),
)];
let items = prepare_version_display_items(&versions, &pkg("test"), None);
assert_eq!(items.len(), 1);
assert_eq!(items[0].label, "1.0.0");
assert!(!items[0].is_latest);
}
#[test]
fn test_prepare_version_display_items_latest_idx_maps_through_internal_yanked_filter() {
let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![
std::sync::Arc::new(
MockVersion::new("2.0.0")
.yanked(true)
.with_prerelease(false),
),
std::sync::Arc::new(
MockVersion::new("1.9.0")
.yanked(false)
.with_prerelease(false),
),
std::sync::Arc::new(
MockVersion::new("1.8.0")
.yanked(false)
.with_prerelease(false),
),
];
let items = prepare_version_display_items(&versions, &pkg("test"), Some(2));
assert_eq!(items.len(), 2, "the yanked entry is still filtered out");
assert_eq!(items[0].version, "1.9.0");
assert!(!items[0].is_latest);
assert_eq!(items[1].version, "1.8.0");
assert_eq!(items[1].label, "1.8.0 (latest)");
assert!(items[1].is_latest);
}
#[test]
fn test_prepare_version_display_items_bumps_pick_outside_raw_order_window() {
let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![
std::sync::Arc::new(
MockVersion::new("2.0.0-rc5")
.yanked(false)
.with_prerelease(true),
),
std::sync::Arc::new(
MockVersion::new("2.0.0-rc4")
.yanked(false)
.with_prerelease(true),
),
std::sync::Arc::new(
MockVersion::new("2.0.0-rc3")
.yanked(false)
.with_prerelease(true),
),
std::sync::Arc::new(
MockVersion::new("2.0.0-rc2")
.yanked(false)
.with_prerelease(true),
),
std::sync::Arc::new(
MockVersion::new("2.0.0-rc1")
.yanked(false)
.with_prerelease(true),
),
std::sync::Arc::new(
MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false),
),
];
let items = prepare_version_display_items(&versions, &pkg("test"), Some(5));
assert_eq!(items.len(), 5, "still capped at MAX_COMPLETION_VERSIONS");
assert_eq!(
items[3].version, "2.0.0-rc2",
"raw-order window kept otherwise"
);
assert_eq!(
items[4].version, "1.0.0",
"the pick is appended rather than dropped"
);
assert_eq!(items[4].label, "1.0.0 (latest)");
assert!(items[4].is_latest);
assert!(
items.iter().take(4).all(|item| !item.is_latest),
"no pre-release entry is mislabeled as latest"
);
}
#[test]
fn test_prepare_version_display_items_pick_at_last_window_slot_not_bumped() {
let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![
std::sync::Arc::new(
MockVersion::new("2.0.0-rc4")
.yanked(false)
.with_prerelease(true),
),
std::sync::Arc::new(
MockVersion::new("2.0.0-rc3")
.yanked(false)
.with_prerelease(true),
),
std::sync::Arc::new(
MockVersion::new("2.0.0-rc2")
.yanked(false)
.with_prerelease(true),
),
std::sync::Arc::new(
MockVersion::new("2.0.0-rc1")
.yanked(false)
.with_prerelease(true),
),
std::sync::Arc::new(
MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false),
),
];
let items = prepare_version_display_items(&versions, &pkg("test"), Some(4));
assert_eq!(
items.len(),
5,
"all 5 raw-order entries returned, nothing bumped"
);
assert_eq!(items[4].version, "1.0.0");
assert_eq!(items[4].label, "1.0.0 (latest)");
assert!(items[4].is_latest);
assert!(items.iter().take(4).all(|item| !item.is_latest));
}
#[test]
fn test_prepare_version_display_items_pick_filtered_out_with_many_survivors() {
let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![
std::sync::Arc::new(
MockVersion::new("9.9.9")
.yanked(true)
.with_prerelease(false),
),
std::sync::Arc::new(
MockVersion::new("1.6.0")
.yanked(false)
.with_prerelease(false),
),
std::sync::Arc::new(
MockVersion::new("1.5.0")
.yanked(false)
.with_prerelease(false),
),
std::sync::Arc::new(
MockVersion::new("1.4.0")
.yanked(false)
.with_prerelease(false),
),
std::sync::Arc::new(
MockVersion::new("1.3.0")
.yanked(false)
.with_prerelease(false),
),
std::sync::Arc::new(
MockVersion::new("1.2.0")
.yanked(false)
.with_prerelease(false),
),
std::sync::Arc::new(
MockVersion::new("1.1.0")
.yanked(false)
.with_prerelease(false),
),
];
let items = prepare_version_display_items(&versions, &pkg("test"), Some(0));
assert_eq!(
items.len(),
5,
"capped at MAX_COMPLETION_VERSIONS, no bump since the pick never survives"
);
assert_eq!(items[0].version, "1.6.0");
assert_eq!(items[4].version, "1.2.0");
assert!(
items.iter().all(|item| !item.is_latest),
"the yanked pick is never tagged"
);
}
#[test]
fn test_prepare_version_display_items_bump_survives_interleaved_yanked_at_scale() {
let specs: [(&str, bool, bool); 12] = [
("3.0.0-rc1", false, true),
("2.9.0", true, false),
("2.8.0-rc1", false, true),
("2.7.0", true, false),
("2.6.0-rc1", false, true),
("2.5.0", true, false),
("2.4.0-rc1", false, true),
("2.3.0", true, false),
("2.2.0-rc1", false, true),
("2.1.0", true, false),
("2.0.0-rc1", false, true),
("1.0.0", false, false),
];
let versions: Vec<std::sync::Arc<dyn crate::Version>> = specs
.into_iter()
.map(|(version, yanked, prerelease)| {
std::sync::Arc::new(
MockVersion::new(version)
.yanked(yanked)
.with_prerelease(prerelease),
) as std::sync::Arc<dyn crate::Version>
})
.collect();
let items = prepare_version_display_items(&versions, &pkg("test"), Some(11));
assert_eq!(
items.len(),
5,
"capped at MAX_COMPLETION_VERSIONS despite 7 survivors"
);
assert_eq!(items[0].version, "3.0.0-rc1");
assert_eq!(
items[3].version, "2.4.0-rc1",
"4th surviving entry in raw order, yanked ones skipped"
);
assert_eq!(
items[4].version, "1.0.0",
"the pick is bumped in from post-filter position 6"
);
assert_eq!(items[4].label, "1.0.0 (latest)");
assert!(items[4].is_latest);
assert!(items.iter().take(4).all(|item| !item.is_latest));
}
#[test]
fn test_build_feature_completion() {
let item = build_feature_completion("derive", &pkg("serde"), None).unwrap();
assert_eq!(item.label, "derive");
assert_eq!(item.kind, Some(CompletionItemKind::PROPERTY));
assert_eq!(item.detail, Some("Feature of serde".to_string()));
assert!(item.documentation.is_none());
assert!(item.text_edit.is_none());
assert_eq!(item.sort_text, Some("derive".to_string()));
}
#[test]
fn test_build_feature_completion_with_range() {
let range = Range::default();
let item = build_feature_completion("derive", &pkg("serde"), Some(range)).unwrap();
assert_eq!(item.label, "derive");
assert!(item.text_edit.is_some());
}
#[test]
fn test_build_feature_completion_accepts_legitimate_names() {
for name in ["derive", "std_alloc-v2+extra", "full.1"] {
assert!(
build_feature_completion(name, &pkg("serde"), None).is_some(),
"expected {name:?} to be accepted"
);
}
}
#[test]
fn test_build_feature_completion_rejects_unsafe_names() {
for name in [
"dep:ravif",
"rgb?/serde",
"jpeg-decoder/rayon",
"evil\"\nbackdoor = \"9.9.9",
] {
assert!(
build_feature_completion(name, &pkg("serde"), None).is_none(),
"expected {name:?} to be rejected"
);
}
}
#[test]
fn test_position_in_range_within() {
let range = Range {
start: Position {
line: 0,
character: 5,
},
end: Position {
line: 0,
character: 10,
},
};
let position = Position {
line: 0,
character: 7,
};
assert!(position_in_range(position, range));
}
#[test]
fn test_position_in_range_at_start() {
let range = Range {
start: Position {
line: 0,
character: 5,
},
end: Position {
line: 0,
character: 10,
},
};
let position = Position {
line: 0,
character: 5,
};
assert!(position_in_range(position, range));
}
#[test]
fn test_position_in_range_at_end() {
let range = Range {
start: Position {
line: 0,
character: 5,
},
end: Position {
line: 0,
character: 10,
},
};
let position = Position {
line: 0,
character: 10,
};
assert!(position_in_range(position, range));
}
#[test]
fn test_position_in_range_one_past_end() {
let range = Range {
start: Position {
line: 0,
character: 5,
},
end: Position {
line: 0,
character: 10,
},
};
let position = Position {
line: 0,
character: 11,
};
assert!(position_in_range(position, range));
}
#[test]
fn test_position_in_range_before() {
let range = Range {
start: Position {
line: 0,
character: 5,
},
end: Position {
line: 0,
character: 10,
},
};
let position = Position {
line: 0,
character: 4,
};
assert!(!position_in_range(position, range));
}
#[test]
fn test_position_in_range_after() {
let range = Range {
start: Position {
line: 0,
character: 5,
},
end: Position {
line: 0,
character: 10,
},
};
let position = Position {
line: 0,
character: 12,
};
assert!(!position_in_range(position, range));
}
#[test]
fn test_utf16_to_byte_offset_ascii() {
let s = "hello";
assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
assert_eq!(utf16_to_byte_offset(s, 2), Some(2));
assert_eq!(utf16_to_byte_offset(s, 5), Some(5));
}
#[test]
fn test_utf16_to_byte_offset_multibyte() {
let s = "日本語";
assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
assert_eq!(utf16_to_byte_offset(s, 1), Some(3));
assert_eq!(utf16_to_byte_offset(s, 2), Some(6));
assert_eq!(utf16_to_byte_offset(s, 3), Some(9));
}
#[test]
fn test_utf16_to_byte_offset_emoji() {
let s = "😀test";
assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
assert_eq!(utf16_to_byte_offset(s, 2), Some(4)); assert_eq!(utf16_to_byte_offset(s, 3), Some(5)); }
#[test]
fn test_utf16_to_byte_offset_mixed() {
let s = "hello 世界 😀!";
assert_eq!(utf16_to_byte_offset(s, 0), Some(0)); assert_eq!(utf16_to_byte_offset(s, 6), Some(6)); assert_eq!(utf16_to_byte_offset(s, 7), Some(9)); assert_eq!(utf16_to_byte_offset(s, 9), Some(13)); assert_eq!(utf16_to_byte_offset(s, 11), Some(17)); }
#[test]
fn test_utf16_to_byte_offset_out_of_bounds() {
let s = "hello";
assert_eq!(utf16_to_byte_offset(s, 100), None);
}
#[test]
fn test_utf16_to_byte_offset_empty() {
let s = "";
assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
assert_eq!(utf16_to_byte_offset(s, 1), None);
}
#[test]
fn test_byte_to_utf16_offset_ascii() {
let s = "hello";
assert_eq!(byte_to_utf16_offset(s, 0), 0);
assert_eq!(byte_to_utf16_offset(s, 2), 2);
assert_eq!(byte_to_utf16_offset(s, 5), 5);
}
#[test]
fn test_byte_to_utf16_offset_multibyte() {
let s = "日本語";
assert_eq!(byte_to_utf16_offset(s, 0), 0);
assert_eq!(byte_to_utf16_offset(s, 3), 1);
assert_eq!(byte_to_utf16_offset(s, 6), 2);
assert_eq!(byte_to_utf16_offset(s, 9), 3);
}
#[test]
fn test_byte_to_utf16_offset_emoji() {
let s = "😀test";
assert_eq!(byte_to_utf16_offset(s, 0), 0);
assert_eq!(byte_to_utf16_offset(s, 4), 2); assert_eq!(byte_to_utf16_offset(s, 5), 3); }
#[test]
fn test_byte_to_utf16_offset_mixed() {
let s = "hello 世界 😀!";
assert_eq!(byte_to_utf16_offset(s, 0), 0); assert_eq!(byte_to_utf16_offset(s, 6), 6); assert_eq!(byte_to_utf16_offset(s, 9), 7); assert_eq!(byte_to_utf16_offset(s, 13), 9); assert_eq!(byte_to_utf16_offset(s, 17), 11); }
#[test]
fn test_byte_to_utf16_offset_empty() {
let s = "";
assert_eq!(byte_to_utf16_offset(s, 0), 0);
}
#[test]
fn test_byte_to_utf16_offset_never_panics_on_bad_offsets() {
let s = "日本語";
assert_eq!(byte_to_utf16_offset(s, 1), 0);
assert_eq!(byte_to_utf16_offset(s, 2), 0);
assert_eq!(byte_to_utf16_offset(s, 9), 3);
assert_eq!(byte_to_utf16_offset(s, 999), 3);
}
#[test]
fn test_build_package_completion_long_description_ascii() {
let long_desc = "a".repeat(250);
let metadata = MockMetadata {
name: "test-pkg".into(),
description: Some(long_desc),
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
let lines: Vec<_> = content.value.lines().collect();
assert!(lines[2].ends_with("..."));
assert!(lines[2].len() <= 203); } else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_build_package_completion_long_description_unicode() {
let mut long_desc = String::new();
for _ in 0..67 {
long_desc.push('日');
}
let metadata = MockMetadata {
name: "test-pkg".into(),
description: Some(long_desc),
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
let lines: Vec<_> = content.value.lines().collect();
assert!(lines[2].ends_with("..."));
assert!(lines[2].is_char_boundary(lines[2].len()));
} else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_build_package_completion_long_description_emoji() {
let long_desc = "😀".repeat(51);
let metadata = MockMetadata {
name: "test-pkg".into(),
description: Some(long_desc),
repository: None,
documentation: None,
latest_version: "1.0.0".into(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range, 0, "").unwrap();
if let Some(Documentation::MarkupContent(content)) = item.documentation {
let lines: Vec<_> = content.value.lines().collect();
assert!(lines[2].ends_with("..."));
assert!(lines[2].is_char_boundary(lines[2].len()));
} else {
panic!("Expected MarkupContent documentation");
}
}
#[test]
fn test_extract_prefix_unicode_package_name() {
let content = "日本語-crate = \"1.0\"";
let position = Position {
line: 0,
character: 3, };
let range = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 10,
},
};
let prefix = extract_prefix(content, position, range);
assert_eq!(prefix, "日本語");
}
#[test]
fn test_extract_prefix_emoji_in_content() {
let content = "emoji-😀-crate = \"1.0\"";
let position = Position {
line: 0,
character: 8, };
let range = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 14,
},
};
let prefix = extract_prefix(content, position, range);
assert_eq!(prefix, "emoji-😀");
}
#[tokio::test]
async fn test_complete_versions_generic_operator_stripping() {
let registry = MockRegistry {
versions: vec![
MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false),
MockVersion::new("1.0.1")
.yanked(false)
.with_prerelease(false),
MockVersion::new("1.1.0")
.yanked(false)
.with_prerelease(false),
MockVersion::new("2.0.0")
.yanked(false)
.with_prerelease(false),
],
};
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"^1.0",
&['^', '~', '=', '<', '>'],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 2);
assert_eq!(items[0].label, "1.0.0 (latest)");
assert_eq!(items[1].label, "1.0.1");
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"~1.1",
&['^', '~', '=', '<', '>'],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 1);
assert_eq!(items[0].label, "1.1.0 (latest)");
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"=2.0",
&['^', '~', '=', '<', '>'],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 1);
assert_eq!(items[0].label, "2.0.0 (latest)");
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"1.0",
&['^', '~', '=', '<', '>'],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 2);
assert_eq!(items[0].label, "1.0.0 (latest)");
assert_eq!(items[1].label, "1.0.1");
}
#[tokio::test]
async fn test_complete_versions_generic_prefix_matches_v_tagged_version() {
let registry = MockRegistry {
versions: vec![
MockVersion::new("v4.0.0")
.yanked(false)
.with_prerelease(false),
MockVersion::new("v3.0.0")
.yanked(false)
.with_prerelease(false),
],
};
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"4",
&[],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(
items.len(),
1,
"an unprefixed prefix must narrow a v-tagged version list, not fall back to \
every version: {items:?}"
);
assert_eq!(items[0].label, "v4.0.0 (latest)");
}
struct VStyleFormatter;
impl crate::lsp_helpers::PackageNaming for VStyleFormatter {}
impl crate::lsp_helpers::PackageRendering for VStyleFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
name.as_str().to_string()
}
fn format_version_for_completion(
&self,
version: &ConcreteVersion,
typed_prefix: &str,
) -> String {
crate::lsp_helpers::match_v_prefix_style(typed_prefix, version.as_str())
}
}
impl crate::lsp_helpers::RequirementResolution for VStyleFormatter {}
impl crate::lsp_helpers::DiagnosticMessages for VStyleFormatter {}
impl crate::lsp_helpers::DiagnosticPolicy for VStyleFormatter {}
impl crate::lsp_helpers::SourcePolicy for VStyleFormatter {}
impl crate::lsp_helpers::OsvNaming for VStyleFormatter {}
#[tokio::test]
async fn test_complete_versions_generic_preserves_prefix_v_style_on_insert() {
let registry = MockRegistry {
versions: vec![
MockVersion::new("v4.0.0")
.yanked(false)
.with_prerelease(false),
],
};
let items = complete_versions_generic_from(
®istry,
&VStyleFormatter,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"3.2",
&[],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 1);
assert_eq!(
items[0].label, "v4.0.0 (latest)",
"label stays the real registry tag text"
);
assert_eq!(
items[0].insert_text.as_deref(),
Some("4.0.0"),
"insert text must match the unprefixed style already typed, not the raw v-tag"
);
}
#[tokio::test]
async fn test_complete_versions_generic_preserves_prefix_v_style_with_leading_operator() {
let registry = MockRegistry {
versions: vec![
MockVersion::new("v4.0.0")
.yanked(false)
.with_prerelease(false),
],
};
let items = complete_versions_generic_from(
®istry,
&VStyleFormatter,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"^v4.0",
&['^'],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 1);
assert_eq!(
items[0].insert_text.as_deref(),
Some("v4.0.0"),
"the v-style already typed after the `^` operator must be preserved, not \
stripped because the raw prefix (before operator-stripping) has no leading v"
);
}
#[tokio::test]
async fn test_complete_versions_generic_default_completion_style_hook_is_identity() {
let registry = MockRegistry {
versions: vec![
MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false),
],
};
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"",
&[],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 1);
assert_eq!(
items[0].insert_text.as_deref(),
Some("1.0.0"),
"the default format_version_for_completion hook must not quote the insert text \
even though MOCK_FORMATTER's format_version_for_text_edit does"
);
}
#[tokio::test]
async fn test_complete_versions_generic_operator_stripping_composer_not_equal() {
let registry = MockRegistry {
versions: vec![
MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false),
MockVersion::new("2.0.0")
.yanked(false)
.with_prerelease(false),
],
};
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"!=2.0",
&['^', '~', '=', '<', '>', '*', '!'],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 1);
assert_eq!(items[0].label, "2.0.0 (latest)");
}
#[tokio::test]
async fn test_complete_versions_generic_replacing_threads_replacement_into_every_item() {
let registry = MockRegistry {
versions: vec![
MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false),
MockVersion::new("1.0.1")
.yanked(false)
.with_prerelease(false),
],
};
let range = Range {
start: Position::new(2, 4),
end: Position::new(2, 15),
};
let replacement = VersionReplacement {
range,
lead: "<version>".to_string(),
trail: "</version>".to_string(),
replaced_text: "<version/>".to_string(),
};
let items = complete_versions_generic_replacing(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"",
&[],
FreshnessSettings::default(),
Some(&replacement),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 2);
for item in &items {
assert_eq!(item.filter_text, Some("<version/>".to_string()));
let Some(CompletionTextEdit::Edit(edit)) = &item.text_edit else {
panic!("expected a textEdit::Edit for {item:?}");
};
assert_eq!(edit.range, range);
assert!(edit.new_text.starts_with("<version>1.0."));
assert!(edit.new_text.ends_with("</version>"));
}
}
#[tokio::test]
async fn test_complete_versions_generic_replacing_drops_unsafe_version_before_render() {
let registry = MockRegistry {
versions: vec![
MockVersion::new("1.0.0</version><parent><groupId>evil")
.yanked(false)
.with_prerelease(false),
],
};
let replacement = VersionReplacement {
range: Range::default(),
lead: "<version>".to_string(),
trail: "</version>".to_string(),
replaced_text: "<version/>".to_string(),
};
let items = complete_versions_generic_replacing(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"",
&[],
FreshnessSettings::default(),
Some(&replacement),
&crate::SelectionContext::none(),
)
.await;
assert!(items.is_empty());
}
#[tokio::test]
async fn test_complete_versions_generic_from_gates_on_can_resolve_source() {
struct PanicsIfQueriedRegistry;
impl crate::Registry for PanicsIfQueriedRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a crate::PackageName,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Version>>>>
{
panic!("registry must not be queried for a non-resolvable source");
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a crate::PackageName,
_req: &'a crate::VersionReq,
_selection_context: &'a crate::SelectionContext,
) -> crate::ecosystem::BoxFuture<
'a,
crate::error::Result<Option<Box<dyn crate::Version>>>,
> {
panic!("registry must not be queried for a non-resolvable source");
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Metadata>>>>
{
panic!("registry must not be queried for a non-resolvable source");
}
fn as_any(&self) -> &dyn Any {
self
}
}
let git_source = crate::parser::DependencySource::Git {
url: "https://example.com/private/repo.git".into(),
rev: None,
};
let items = complete_versions_generic_from(
&PanicsIfQueriedRegistry,
&MOCK_FORMATTER,
&pkg("private-pkg"),
&git_source,
"1.0",
&[],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert!(items.is_empty());
}
struct RoutingMockRegistry;
impl crate::Registry for RoutingMockRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a crate::PackageName,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Version>>>>
{
Box::pin(async move {
Ok(vec![Box::new(
MockVersion::new("9.9.9")
.yanked(false)
.with_prerelease(false),
) as Box<dyn crate::Version>])
})
}
fn get_versions_from<'a>(
&'a self,
_name: &'a crate::PackageName,
source: &'a crate::parser::DependencySource,
_freshness: FreshnessSettings,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Version>>>>
{
let version = if matches!(
source,
crate::parser::DependencySource::AlternateRegistry { .. }
) {
"2.0.0"
} else {
"1.0.0"
};
Box::pin(async move {
Ok(vec![Box::new(
MockVersion::new(version)
.yanked(false)
.with_prerelease(false),
) as Box<dyn crate::Version>])
})
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a crate::PackageName,
_req: &'a crate::VersionReq,
_selection_context: &'a crate::SelectionContext,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Option<Box<dyn crate::Version>>>>
{
Box::pin(async move { Ok(None) })
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn select_latest_matching(
&self,
versions: &[Box<dyn crate::Version>],
_req: &crate::VersionReq,
_selection_context: &crate::SelectionContext,
) -> Option<usize> {
crate::select_latest_for_existence(versions, |v| v.as_ref())
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[tokio::test]
async fn test_complete_versions_generic_from_routes_source_to_get_versions_from() {
use crate::lsp_helpers::test_support_lsp::MOCK_WIDENED_RESOLVE_FORMATTER;
let registry = RoutingMockRegistry;
let alternate = crate::parser::DependencySource::AlternateRegistry {
index: "gitlab-ci:deadbeef".into(),
mirrors_crates_io: false,
};
let items = complete_versions_generic_from(
®istry,
&MOCK_WIDENED_RESOLVE_FORMATTER,
&pkg("test-pkg"),
&alternate,
"",
&[],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items[0].label, "2.0.0 (latest)");
assert_ne!(items[0].label, "9.9.9 (latest)");
}
#[tokio::test]
async fn test_complete_versions_generic_fallback_when_no_prefix_match() {
let registry = MockRegistry {
versions: vec![
MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false),
MockVersion::new("1.1.0")
.yanked(false)
.with_prerelease(false),
MockVersion::new("2.0.0")
.yanked(false)
.with_prerelease(false),
MockVersion::new("2.1.0")
.yanked(true)
.with_prerelease(false),
],
};
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"3.0",
&['^', '~', '=', '<', '>'],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 3);
assert_eq!(items[0].label, "1.0.0 (latest)");
assert_eq!(items[1].label, "1.1.0");
assert_eq!(items[2].label, "2.0.0");
assert!(!items.iter().any(|item| item.label == "2.1.0"));
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"",
&[],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 3);
assert_eq!(items[0].label, "1.0.0 (latest)");
assert_eq!(items[1].label, "1.1.0");
assert_eq!(items[2].label, "2.0.0");
}
#[tokio::test]
async fn test_complete_versions_generic_filters_yanked_in_prefix_match() {
let registry = MockRegistry {
versions: vec![
MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false),
MockVersion::new("1.0.1")
.yanked(true)
.with_prerelease(false),
MockVersion::new("1.0.2")
.yanked(false)
.with_prerelease(false),
],
};
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"1.0",
&[],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 2);
assert_eq!(items[0].label, "1.0.0 (latest)");
assert_eq!(items[1].label, "1.0.2");
assert!(!items.iter().any(|item| item.label == "1.0.1"));
}
#[tokio::test]
async fn test_complete_versions_generic_filters_unsafe_version_string() {
let registry = MockRegistry {
versions: vec![
MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false),
MockVersion::new("1.0.1\", \"evil\": \"true")
.yanked(false)
.with_prerelease(false),
],
};
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"1.0",
&[],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert!(
!items.iter().any(|item| item.label.contains("evil")),
"an unsafe version string must never be offered as a completion item: {items:?}"
);
assert!(
items.iter().any(|item| item.label.starts_with("1.0.0")),
"a safe version must still be offered: {items:?}"
);
}
#[tokio::test]
async fn test_complete_versions_generic_limit_5() {
let versions: Vec<_> = (0..10)
.map(|i| {
MockVersion::new(format!("1.0.{}", i))
.yanked(false)
.with_prerelease(false)
})
.collect();
let registry = MockRegistry { versions };
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"1.0",
&[],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 5);
assert_eq!(items[0].label, "1.0.0 (latest)");
assert_eq!(items[4].label, "1.0.4");
}
#[tokio::test]
async fn test_complete_versions_generic_go_no_operators() {
let registry = MockRegistry {
versions: vec![
MockVersion::new("v1.9.0")
.yanked(false)
.with_prerelease(false),
MockVersion::new("v1.9.1")
.yanked(false)
.with_prerelease(false),
MockVersion::new("v1.10.0")
.yanked(false)
.with_prerelease(false),
],
};
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("github.com/gin-gonic/gin"),
&crate::parser::DependencySource::Registry,
"v1.9",
&[],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 2);
assert_eq!(items[0].label, "v1.9.0 (latest)");
assert_eq!(items[1].label, "v1.9.1");
}
#[tokio::test]
async fn test_complete_versions_generic_from_skips_prerelease_at_raw_top() {
let registry = MockRegistry {
versions: vec![
MockVersion::new("13.0.5-beta1")
.yanked(false)
.with_prerelease(true),
MockVersion::new("13.0.4")
.yanked(false)
.with_prerelease(false),
MockVersion::new("13.0.3")
.yanked(false)
.with_prerelease(false),
],
};
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("Newtonsoft.Json"),
&crate::parser::DependencySource::Registry,
"",
&[],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 3);
assert_eq!(items[0].label, "13.0.5-beta1");
assert_eq!(
items[0].preselect,
Some(false),
"the pre-release must not be preselected"
);
assert_eq!(items[1].label, "13.0.4 (latest)");
assert_eq!(
items[1].preselect,
Some(true),
"the first stable entry must be preselected instead"
);
assert_eq!(items[2].label, "13.0.3");
assert_eq!(items[2].preselect, Some(false));
}
#[tokio::test]
async fn test_complete_versions_generic_from_all_prerelease_still_marks_a_latest() {
let registry = MockRegistry {
versions: vec![
MockVersion::new("2.0.0-beta2")
.yanked(false)
.with_prerelease(true),
MockVersion::new("2.0.0-beta1")
.yanked(false)
.with_prerelease(true),
],
};
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"",
&[],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 2);
assert_eq!(items[0].label, "2.0.0-beta2 (latest)");
assert_eq!(items[0].preselect, Some(true));
assert_eq!(items[1].label, "2.0.0-beta1");
assert_eq!(items[1].preselect, Some(false));
}
#[tokio::test]
async fn test_complete_versions_generic_from_prefers_non_deprecated_over_newer_deprecated() {
use crate::lsp_helpers::test_support_lsp::{
MockRegistryPreferringUnflagged, MockVersionWithStatus,
};
let registry = MockRegistryPreferringUnflagged {
versions: vec![
MockVersionWithStatus {
version: "2.0.0".into(),
status: crate::RemovalStatus::AdvisoryDeprecated,
},
MockVersionWithStatus {
version: "1.9.0".into(),
status: crate::RemovalStatus::Available,
},
],
};
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"",
&[],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(items.len(), 2);
assert_eq!(
items[0].label, "2.0.0",
"the deprecated entry is still offered"
);
assert_eq!(
items[0].preselect,
Some(false),
"but must not be labeled/preselected as latest"
);
assert_eq!(items[1].label, "1.9.0 (latest)");
assert_eq!(items[1].preselect, Some(true));
}
#[tokio::test]
async fn test_complete_versions_generic_from_bump_survives_prefix_filtering() {
let registry = MockRegistry {
versions: vec![
MockVersion::new("2.0.0-rc5")
.yanked(false)
.with_prerelease(true),
MockVersion::new("2.0.0-rc4")
.yanked(false)
.with_prerelease(true),
MockVersion::new("2.0.0-rc3")
.yanked(false)
.with_prerelease(true),
MockVersion::new("2.0.0-rc2")
.yanked(false)
.with_prerelease(true),
MockVersion::new("2.0.0-rc1")
.yanked(false)
.with_prerelease(true),
MockVersion::new("2.0.0")
.yanked(false)
.with_prerelease(false),
MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false),
],
};
let items = complete_versions_generic_from(
®istry,
&MOCK_FORMATTER,
&pkg("test-pkg"),
&crate::parser::DependencySource::Registry,
"2.",
&[],
FreshnessSettings::default(),
&crate::SelectionContext::none(),
)
.await;
assert_eq!(
items.len(),
5,
"capped, bumped via the prefix-filtered slice"
);
assert_eq!(items[3].label, "2.0.0-rc2");
assert_eq!(items[4].label, "2.0.0 (latest)");
assert_eq!(items[4].preselect, Some(true));
assert!(
items
.iter()
.take(4)
.all(|item| item.preselect != Some(true))
);
assert!(
!items.iter().any(|item| item.label == "2.0.0-rc1"),
"the 5th raw-order pre-release is displaced by the bumped pick"
);
}
fn make_dep_with_features_range(
name: &str,
name_range: Range,
features_range: Range,
) -> MockDependency {
MockDependency {
name: name.into(),
name_range: name_range.into(),
version_range: None,
features_range: Some(features_range.into()),
}
}
#[test]
fn test_detect_feature_context_inline() {
let features_range = Range {
start: Position {
line: 0,
character: 36,
},
end: Position {
line: 0,
character: 52,
},
};
let dep = make_dep_with_features_range(
"serde",
Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
features_range,
);
let parse_result = MockParseResult {
dependencies: vec![dep],
};
let content = r#"serde = { version = "1", features = ["derive", "std"] }"#;
let position = Position {
line: 0,
character: 41,
};
let context = detect_completion_context(&parse_result, position, content);
assert!(
matches!(context, CompletionContext::Feature { ref package_name, ref prefix }
if package_name == "serde" && prefix == "der"),
"Expected Feature context with prefix 'der', got {context:?}"
);
}
#[test]
fn test_detect_feature_context_empty_prefix() {
let features_range = Range {
start: Position {
line: 0,
character: 11,
},
end: Position {
line: 0,
character: 15,
},
};
let dep = make_dep_with_features_range(
"tokio",
Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
features_range,
);
let parse_result = MockParseResult {
dependencies: vec![dep],
};
let content = r#"features = [""]"#;
let position = Position {
line: 0,
character: 13,
};
let context = detect_completion_context(&parse_result, position, content);
assert!(
matches!(context, CompletionContext::Feature { ref package_name, ref prefix }
if package_name == "tokio" && prefix.is_empty()),
"Expected Feature context with empty prefix, got {context:?}"
);
}
#[test]
fn test_detect_feature_context_second_item() {
let features_range = Range {
start: Position {
line: 0,
character: 11,
},
end: Position {
line: 0,
character: 28,
},
};
let dep = make_dep_with_features_range(
"tokio",
Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
features_range,
);
let parse_result = MockParseResult {
dependencies: vec![dep],
};
let content = r#"features = ["full", "rt-"]"#;
let position = Position {
line: 0,
character: 24,
};
let context = detect_completion_context(&parse_result, position, content);
assert!(
matches!(context, CompletionContext::Feature { ref package_name, ref prefix }
if package_name == "tokio" && prefix == "rt-"),
"Expected Feature context with prefix 'rt-', got {context:?}"
);
}
#[test]
fn test_detect_no_feature_context_outside_range() {
let features_range = Range {
start: Position {
line: 2,
character: 11,
},
end: Position {
line: 2,
character: 20,
},
};
let dep = make_dep_with_features_range(
"serde",
Range {
start: Position {
line: 2,
character: 0,
},
end: Position {
line: 2,
character: 5,
},
},
features_range,
);
let parse_result = MockParseResult {
dependencies: vec![dep],
};
let content = "[package]\nname = \"test\"\nfeatures = [\"full\"]";
let position = Position {
line: 0,
character: 5,
};
let context = detect_completion_context(&parse_result, position, content);
assert_eq!(context, CompletionContext::None);
}
#[test]
fn test_extract_feature_prefix_basic() {
let content = r#"serde = { features = ["derive"] }"#;
let position = Position {
line: 0,
character: 27,
};
let prefix = extract_feature_prefix(content, position);
assert_eq!(prefix, "deri");
}
#[test]
fn test_extract_feature_prefix_empty() {
let content = r#"features = [""]"#;
let position = Position {
line: 0,
character: 13,
};
let prefix = extract_feature_prefix(content, position);
assert_eq!(prefix, "");
}
#[test]
fn test_extract_feature_prefix_multiline() {
let content = "features = [\n \"rt-multi-thread\",\n \"mac\"\n]";
let position = Position {
line: 2,
character: 8,
};
let prefix = extract_feature_prefix(content, position);
assert_eq!(prefix, "mac");
}
#[test]
fn test_extract_feature_prefix_no_quote() {
let content = "features = [\n \n]";
let position = Position {
line: 1,
character: 4,
};
let prefix = extract_feature_prefix(content, position);
assert_eq!(prefix, "");
}
#[test]
fn test_extract_feature_prefix_between_items_no_quote() {
let content = r#"features = ["full", ]"#;
let position = Position {
line: 0,
character: 19,
};
let prefix = extract_feature_prefix(content, position);
assert_eq!(prefix, "");
}
#[test]
fn test_extract_feature_prefix_cursor_after_opening_bracket() {
let content = "features = []";
let position = Position {
line: 0,
character: 12,
};
let prefix = extract_feature_prefix(content, position);
assert_eq!(prefix, "");
}
#[expect(
clippy::cast_possible_truncation,
reason = "a short ASCII test line can never overflow u32"
)]
#[test]
fn test_extract_feature_prefix_skips_escaped_quote() {
let content = r#"features = ["a\"b"#;
let position = Position {
line: 0,
character: content.chars().count() as u32,
};
let prefix = extract_feature_prefix(content, position);
assert_eq!(prefix, r#"a\"b"#);
}
#[test]
fn test_version_display_item_captures_published_at() {
let version =
MockVersion::new("1.0.0").with_published_at(PublishTime::from_unix_secs(1_000));
let item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
assert_eq!(item.published_at, Some(PublishTime::from_unix_secs(1_000)));
}
#[test]
fn test_version_display_item_published_at_none_when_unavailable() {
let version = MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false);
let item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
assert_eq!(item.published_at, None);
}
#[test]
fn test_build_version_completion_label_details_present_when_published_at_known() {
let now = PublishTime::from_unix_secs(10_000);
let published_two_hours_ago = PublishTime::from_unix_secs(10_000 - 2 * 3600);
let version = MockVersion::new("1.2.3").with_published_at(published_two_hours_ago);
let display_item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
let item = build_version_completion(&display_item, None, now, FreshnessSettings::default());
let details = item
.label_details
.expect("label_details must be set when published_at is known");
assert_eq!(details.detail, Some(" ⏳ 2 hours ago".to_string()));
assert_eq!(details.description, None);
}
#[test]
fn test_build_version_completion_label_details_absent_when_freshness_disabled() {
let now = PublishTime::from_unix_secs(10_000);
let published_two_hours_ago = PublishTime::from_unix_secs(10_000 - 2 * 3600);
let version = MockVersion::new("1.2.3").with_published_at(published_two_hours_ago);
let display_item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
let item = build_version_completion(&display_item, None, now, FreshnessSettings::Disabled);
assert!(item.label_details.is_none());
}
#[test]
fn test_build_version_completion_label_details_absent_when_published_at_unknown() {
let version = MockVersion::new("1.2.3")
.yanked(false)
.with_prerelease(false);
let display_item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
let item = build_version_completion(
&display_item,
None,
PublishTime::now(),
FreshnessSettings::default(),
);
assert!(item.label_details.is_none());
}
#[test]
fn test_build_version_completion_no_cooldown_badge_outside_window() {
let now = PublishTime::from_unix_secs(10_000);
let published = PublishTime::from_unix_secs(10_000 - 4 * 24 * 3600);
let version = MockVersion::new("1.2.3").with_published_at(published);
let display_item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
let item = build_version_completion(&display_item, None, now, FreshnessSettings::default());
let details = item.label_details.expect("label_details must be set");
assert!(!details.detail.unwrap().contains('⏳'));
}
#[test]
fn test_build_version_completion_respects_custom_cooldown_window() {
let now = PublishTime::from_unix_secs(10_000);
let published_two_hours_ago = PublishTime::from_unix_secs(10_000 - 2 * 3600);
let version = MockVersion::new("1.2.3").with_published_at(published_two_hours_ago);
let display_item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
let item = build_version_completion(
&display_item,
None,
now,
FreshnessSettings::Enabled {
cooldown: crate::CooldownWindow::from_secs(3600),
},
);
let details = item.label_details.expect("label_details must be set");
assert!(!details.detail.unwrap().contains('⏳'));
}
#[test]
fn test_build_version_completion_byte_identical_output_without_freshness_data() {
let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![
std::sync::Arc::new(
MockVersion::new("1.0.0")
.yanked(false)
.with_prerelease(false),
),
std::sync::Arc::new(
MockVersion::new("0.9.0")
.yanked(true)
.with_prerelease(false),
),
std::sync::Arc::new(
MockVersion::new("0.8.0")
.yanked(false)
.with_prerelease(false),
),
];
let display_items = prepare_version_display_items(&versions, &pkg("test"), Some(0));
assert_eq!(display_items.len(), 2, "yanked filtering must be unchanged");
let now = PublishTime::now();
let items: Vec<_> = display_items
.iter()
.map(|item| build_version_completion(item, None, now, FreshnessSettings::default()))
.collect();
assert_eq!(items[0].label, "1.0.0 (latest)");
assert_eq!(items[0].sort_text, Some("00000".to_string()));
assert_eq!(items[0].preselect, Some(true));
assert_eq!(items[0].label_details, None);
assert_eq!(items[1].label, "0.8.0");
assert_eq!(items[1].sort_text, Some("00001".to_string()));
assert_eq!(items[1].preselect, Some(false));
assert_eq!(items[1].label_details, None);
}
}