use crate::{Metadata, ParseResult, Version};
use tower_lsp_server::ls_types::{
CompletionItem, CompletionItemKind, CompletionTextEdit, Documentation, MarkupContent,
MarkupKind, Position, Range, TextEdit,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompletionContext {
PackageName {
prefix: String,
},
Version {
package_name: String,
prefix: String,
},
Feature {
package_name: String,
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 {
let name_range = dep.name_range();
if position_in_range(position, name_range) {
let prefix = extract_prefix(content, position, name_range);
return CompletionContext::PackageName { prefix };
}
if let Some(version_range) = dep.version_range()
&& position_in_range(position, version_range)
{
let prefix = extract_prefix(content, position, version_range);
return CompletionContext::Version {
package_name: dep.name().to_string(),
prefix,
};
}
if let Some(features_range) = dep.features_range()
&& position_in_range(position, features_range)
{
let prefix = extract_feature_prefix(content, position);
return CompletionContext::Feature {
package_name: dep.name().to_string(),
prefix,
};
}
}
CompletionContext::None
}
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 fn utf16_to_byte_offset(s: &str, utf16_offset: u32) -> Option<usize> {
let mut utf16_count = 0u32;
for (byte_idx, ch) in s.char_indices() {
if utf16_count >= utf16_offset {
return Some(byte_idx);
}
utf16_count += ch.len_utf16() as u32;
}
if utf16_count == utf16_offset {
return Some(s.len());
}
None
}
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()
}
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..];
let quote_count = segment.chars().filter(|&c| c == '"').count();
if quote_count % 2 == 0 {
return String::new();
}
match segment.rfind('"') {
Some(pos) => segment[pos + 1..].to_string(),
None => String::new(),
}
}
pub fn build_package_completion(metadata: &dyn Metadata, insert_range: Range) -> CompletionItem {
let name = metadata.name();
let latest = metadata.latest_version();
let mut doc_parts = vec![format!("**{}** v{}", name, latest)];
if let Some(desc) = metadata.description() {
doc_parts.push(String::new()); let truncated = if desc.len() > 200 {
let mut end = 200;
while end > 0 && !desc.is_char_boundary(end) {
end -= 1;
}
format!("{}...", &desc[..end])
} else {
desc.to_string()
};
doc_parts.push(truncated);
}
let mut links = Vec::new();
if let Some(repo) = metadata.repository() {
links.push(format!("[Repository]({})", repo));
}
if let Some(docs) = metadata.documentation() {
links.push(format!("[Documentation]({})", docs));
}
if !links.is_empty() {
doc_parts.push(String::new()); doc_parts.push(links.join(" | "));
}
CompletionItem {
label: name.to_string(),
kind: Some(CompletionItemKind::MODULE),
detail: Some(format!("v{}", latest)),
documentation: Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: doc_parts.join("\n"),
})),
insert_text: Some(name.to_string()),
text_edit: Some(CompletionTextEdit::Edit(TextEdit {
range: insert_range,
new_text: name.to_string(),
})),
sort_text: Some(name.to_string()),
filter_text: Some(name.to_string()),
..Default::default()
}
}
pub fn build_version_completion(
display_item: &VersionDisplayItem,
insert_range: Option<Range>,
) -> CompletionItem {
let sort_text = format!("{:05}", display_item.index);
CompletionItem {
label: display_item.label.clone(),
kind: Some(CompletionItemKind::VALUE),
detail: Some(display_item.description.clone()),
documentation: None,
insert_text: Some(display_item.version.clone()),
text_edit: insert_range.map(|range| {
CompletionTextEdit::Edit(TextEdit {
range,
new_text: display_item.version.clone(),
})
}),
sort_text: Some(sort_text),
preselect: Some(display_item.is_latest),
..Default::default()
}
}
#[derive(Debug, Clone)]
pub struct VersionDisplayItem {
pub version: String,
pub label: String,
pub description: String,
pub index: usize,
pub is_latest: bool,
}
impl VersionDisplayItem {
pub fn new(version: &dyn Version, package_name: &str, 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, version_str);
Self {
version: version_str.to_string(),
label,
description,
index,
is_latest,
}
}
}
pub fn prepare_version_display_items<V: AsRef<dyn Version>>(
versions: &[V],
package_name: &str,
) -> Vec<VersionDisplayItem> {
versions
.iter()
.map(|v| v.as_ref())
.filter(|v| !v.is_yanked())
.take(MAX_COMPLETION_VERSIONS)
.enumerate()
.map(|(index, version)| VersionDisplayItem::new(version, package_name, index, index == 0))
.collect()
}
pub fn build_feature_completion(
feature_name: &str,
package_name: &str,
insert_range: Option<Range>,
) -> CompletionItem {
CompletionItem {
label: feature_name.to_string(),
kind: Some(CompletionItemKind::PROPERTY),
detail: Some(format!("Feature of {}", package_name)),
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;
pub async fn complete_package_names_generic(
registry: &dyn crate::Registry,
prefix: &str,
limit: usize,
) -> Vec<CompletionItem> {
if prefix.len() < 2 || prefix.len() > 200 {
return vec![];
}
let results = match registry.search(prefix, limit).await {
Ok(r) => r,
Err(e) => {
tracing::warn!("Registry search failed for '{}': {}", prefix, e);
return vec![];
}
};
let insert_range = tower_lsp_server::ls_types::Range::default();
results
.into_iter()
.map(|metadata| build_package_completion(metadata.as_ref(), insert_range))
.collect()
}
pub async fn complete_versions_generic(
registry: &dyn crate::Registry,
package_name: &str,
prefix: &str,
operator_chars: &[char],
) -> Vec<CompletionItem> {
let versions = match registry.get_versions(package_name).await {
Ok(v) => v,
Err(e) => {
tracing::warn!("Failed to fetch versions for '{}': {}", package_name, e);
return vec![];
}
};
let clean_prefix = prefix.trim_start_matches(operator_chars).trim();
let filtered_versions: Vec<_> = versions
.iter()
.filter(|v| v.version_string().starts_with(clean_prefix))
.collect();
let display_items = if filtered_versions.is_empty() {
prepare_version_display_items(&versions, package_name)
} else {
prepare_version_display_items(&filtered_versions, package_name)
};
display_items
.iter()
.map(|item| build_version_completion(item, None))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::any::Any;
struct MockDependency {
name: String,
name_range: Range,
version_range: Option<Range>,
features_range: Option<Range>,
}
impl crate::ecosystem::Dependency for MockDependency {
fn name(&self) -> &str {
&self.name
}
fn name_range(&self) -> Range {
self.name_range
}
fn version_requirement(&self) -> Option<&str> {
Some("1.0")
}
fn version_range(&self) -> Option<Range> {
self.version_range
}
fn features_range(&self) -> Option<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) -> &tower_lsp_server::ls_types::Uri {
static URL: std::sync::LazyLock<tower_lsp_server::ls_types::Uri> =
std::sync::LazyLock::new(|| "file:///test/Cargo.toml".parse().unwrap());
&URL
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct MockVersion {
version: String,
yanked: bool,
prerelease: bool,
}
impl crate::registry::Version for MockVersion {
fn version_string(&self) -> &str {
&self.version
}
fn is_yanked(&self) -> bool {
self.yanked
}
fn is_prerelease(&self) -> bool {
self.prerelease
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct MockMetadata {
name: String,
description: Option<String>,
repository: Option<String>,
documentation: Option<String>,
latest_version: String,
}
impl crate::registry::Metadata for MockMetadata {
fn name(&self) -> &str {
&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) -> &str {
&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 str,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Version>>>>
{
let versions: Vec<Box<dyn crate::Version>> = self
.versions
.iter()
.map(|v| {
Box::new(MockVersion {
version: v.version.clone(),
yanked: v.yanked,
prerelease: v.prerelease,
}) as Box<dyn crate::Version>
})
.collect();
Box::pin(async move { Ok(versions) })
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a str,
_req: &'a str,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Option<Box<dyn crate::Version>>>>
{
Box::pin(async move { Ok(None) })
}
fn search<'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 package_url(&self, _name: &str) -> String {
String::new()
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[test]
fn test_detect_package_name_context_at_start() {
let parse_result = MockParseResult {
dependencies: vec![MockDependency {
name: "serde".to_string(),
name_range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
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 } => {
assert_eq!(prefix, "");
}
_ => panic!("Expected PackageName context, got {:?}", context),
}
}
#[test]
fn test_detect_package_name_context_partial() {
let parse_result = MockParseResult {
dependencies: vec![MockDependency {
name: "serde".to_string(),
name_range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
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 } => {
assert_eq!(prefix, "ser");
}
_ => panic!("Expected PackageName context, got {:?}", context),
}
}
#[test]
fn test_detect_version_context() {
let parse_result = MockParseResult {
dependencies: vec![MockDependency {
name: "serde".to_string(),
name_range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
version_range: Some(Range {
start: Position {
line: 0,
character: 9,
},
end: Position {
line: 0,
character: 14,
},
}),
features_range: None,
}],
};
let content = r#"serde = "1.0.1""#;
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_no_context_before_dependencies() {
let parse_result = MockParseResult {
dependencies: vec![MockDependency {
name: "serde".to_string(),
name_range: Range {
start: Position {
line: 5,
character: 0,
},
end: Position {
line: 5,
character: 5,
},
},
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".to_string(),
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".to_string(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range);
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(_))
));
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_minimal() {
let metadata = MockMetadata {
name: "test-pkg".to_string(),
description: None,
repository: None,
documentation: None,
latest_version: "0.1.0".to_string(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range);
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_version_completion_stable() {
let version = MockVersion {
version: "1.0.0".to_string(),
yanked: false,
prerelease: false,
};
let display_item = VersionDisplayItem::new(&version, "serde", 0, false);
let item = build_version_completion(&display_item, None);
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_latest() {
let version = MockVersion {
version: "1.0.0".to_string(),
yanked: false,
prerelease: false,
};
let display_item = VersionDisplayItem::new(&version, "serde", 0, true);
let item = build_version_completion(&display_item, None);
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 {
version: "0.9.0".to_string(),
yanked: false,
prerelease: false,
};
let display_item = VersionDisplayItem::new(&version, "tokio", 1, false);
let item = build_version_completion(&display_item, None);
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 {
version: "1.0.0".to_string(),
yanked: false,
prerelease: false,
};
let v2 = MockVersion {
version: "0.9.0".to_string(),
yanked: false,
prerelease: false,
};
let v3 = MockVersion {
version: "0.8.0".to_string(),
yanked: false,
prerelease: false,
};
let display_item1 = VersionDisplayItem::new(&v1, "test", 0, true);
let display_item2 = VersionDisplayItem::new(&v2, "test", 1, false);
let display_item3 = VersionDisplayItem::new(&v3, "test", 2, false);
let item1 = build_version_completion(&display_item1, None);
let item2 = build_version_completion(&display_item2, None);
let item3 = build_version_completion(&display_item3, None);
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 {
version: "0.14.0".to_string(),
yanked: false,
prerelease: false,
},
MockVersion {
version: "0.8.0".to_string(),
yanked: false,
prerelease: false,
},
MockVersion {
version: "0.2.0".to_string(),
yanked: false,
prerelease: false,
},
];
let items: Vec<_> = versions
.iter()
.enumerate()
.map(|(idx, v)| {
let display_item = VersionDisplayItem::new(v, "test", idx, idx == 0);
build_version_completion(&display_item, None)
})
.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 items: Vec<_> = versions
.iter()
.enumerate()
.map(|(idx, ver)| {
let v = MockVersion {
version: ver.to_string(),
yanked: false,
prerelease: false,
};
let display_item = VersionDisplayItem::new(&v, "test", idx, idx == 0);
build_version_completion(&display_item, None)
})
.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 {
version: "1.0.0".to_string(),
yanked: false,
prerelease: false,
};
let item = VersionDisplayItem::new(&version, "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 {
version: "0.9.0".to_string(),
yanked: false,
prerelease: false,
};
let item = VersionDisplayItem::new(&version, "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 {
version: "1.0.0".to_string(),
yanked: false,
prerelease: false,
}),
std::sync::Arc::new(MockVersion {
version: "0.9.0".to_string(),
yanked: true,
prerelease: false,
}),
std::sync::Arc::new(MockVersion {
version: "0.8.0".to_string(),
yanked: false,
prerelease: false,
}),
];
let items = prepare_version_display_items(&versions, "test");
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 {
version: format!("1.0.{}", i),
yanked: false,
prerelease: false,
}) as std::sync::Arc<dyn crate::Version>
})
.collect();
let items = prepare_version_display_items(&versions, "test");
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, "test");
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 {
version: "1.0.0".to_string(),
yanked: true,
prerelease: false,
}),
std::sync::Arc::new(MockVersion {
version: "0.9.0".to_string(),
yanked: true,
prerelease: false,
}),
];
let items = prepare_version_display_items(&versions, "test");
assert_eq!(items.len(), 0);
}
#[test]
fn test_build_feature_completion() {
let item = build_feature_completion("derive", "serde", None);
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", "serde", Some(range));
assert_eq!(item.label, "derive");
assert!(item.text_edit.is_some());
}
#[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_build_package_completion_long_description_ascii() {
let long_desc = "a".repeat(250);
let metadata = MockMetadata {
name: "test-pkg".to_string(),
description: Some(long_desc),
repository: None,
documentation: None,
latest_version: "1.0.0".to_string(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range);
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".to_string(),
description: Some(long_desc),
repository: None,
documentation: None,
latest_version: "1.0.0".to_string(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range);
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".to_string(),
description: Some(long_desc),
repository: None,
documentation: None,
latest_version: "1.0.0".to_string(),
};
let range = Range::default();
let item = build_package_completion(&metadata, range);
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 {
version: "1.0.0".to_string(),
yanked: false,
prerelease: false,
},
MockVersion {
version: "1.0.1".to_string(),
yanked: false,
prerelease: false,
},
MockVersion {
version: "1.1.0".to_string(),
yanked: false,
prerelease: false,
},
MockVersion {
version: "2.0.0".to_string(),
yanked: false,
prerelease: false,
},
],
};
let items =
complete_versions_generic(®istry, "test-pkg", "^1.0", &['^', '~', '=', '<', '>'])
.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(®istry, "test-pkg", "~1.1", &['^', '~', '=', '<', '>'])
.await;
assert_eq!(items.len(), 1);
assert_eq!(items[0].label, "1.1.0 (latest)");
let items =
complete_versions_generic(®istry, "test-pkg", "=2.0", &['^', '~', '=', '<', '>'])
.await;
assert_eq!(items.len(), 1);
assert_eq!(items[0].label, "2.0.0 (latest)");
let items =
complete_versions_generic(®istry, "test-pkg", "1.0", &['^', '~', '=', '<', '>'])
.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_fallback_when_no_prefix_match() {
let registry = MockRegistry {
versions: vec![
MockVersion {
version: "1.0.0".to_string(),
yanked: false,
prerelease: false,
},
MockVersion {
version: "1.1.0".to_string(),
yanked: false,
prerelease: false,
},
MockVersion {
version: "2.0.0".to_string(),
yanked: false,
prerelease: false,
},
MockVersion {
version: "2.1.0".to_string(),
yanked: true, prerelease: false,
},
],
};
let items =
complete_versions_generic(®istry, "test-pkg", "3.0", &['^', '~', '=', '<', '>'])
.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(®istry, "test-pkg", "", &[]).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 {
version: "1.0.0".to_string(),
yanked: false,
prerelease: false,
},
MockVersion {
version: "1.0.1".to_string(),
yanked: true, prerelease: false,
},
MockVersion {
version: "1.0.2".to_string(),
yanked: false,
prerelease: false,
},
],
};
let items = complete_versions_generic(®istry, "test-pkg", "1.0", &[]).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_limit_5() {
let versions: Vec<_> = (0..10)
.map(|i| MockVersion {
version: format!("1.0.{}", i),
yanked: false,
prerelease: false,
})
.collect();
let registry = MockRegistry { versions };
let items = complete_versions_generic(®istry, "test-pkg", "1.0", &[]).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 {
version: "v1.9.0".to_string(),
yanked: false,
prerelease: false,
},
MockVersion {
version: "v1.9.1".to_string(),
yanked: false,
prerelease: false,
},
MockVersion {
version: "v1.10.0".to_string(),
yanked: false,
prerelease: false,
},
],
};
let items =
complete_versions_generic(®istry, "github.com/gin-gonic/gin", "v1.9", &[]).await;
assert_eq!(items.len(), 2);
assert_eq!(items[0].label, "v1.9.0 (latest)");
assert_eq!(items[1].label, "v1.9.1");
}
fn make_dep_with_features_range(
name: &str,
name_range: Range,
features_range: Range,
) -> MockDependency {
MockDependency {
name: name.to_string(),
name_range,
version_range: None,
features_range: Some(features_range),
}
}
#[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, "");
}
}