Skip to main content

deps_core/
completion.rs

1//! Core completion infrastructure for deps-lsp.
2//!
3//! This module provides generic completion functionality that works across
4//! all package ecosystems (Cargo, npm, PyPI, etc.). It handles:
5//!
6//! - Context detection - determining what type of completion is appropriate
7//! - Prefix extraction - getting the text typed so far
8//! - CompletionItem builders - creating LSP completion responses
9//!
10//! # Architecture
11//!
12//! The completion system uses trait objects (`dyn Dependency`, `dyn ParseResult`,
13//! `dyn Version`, `dyn Metadata`) to work generically across ecosystems.
14//!
15//! # Examples
16//!
17//! ```no_run
18//! use deps_core::completion::{detect_completion_context, CompletionContext};
19//! use tower_lsp_server::ls_types::Position;
20//!
21//! // In your ecosystem's generate_completions implementation:
22//! async fn generate_completions(
23//!     parse_result: &dyn deps_core::ParseResult,
24//!     position: Position,
25//!     content: &str,
26//! ) -> Vec<tower_lsp_server::ls_types::CompletionItem> {
27//!     let context = detect_completion_context(parse_result, position, content);
28//!
29//!     match context {
30//!         CompletionContext::PackageName { prefix } => {
31//!             // Search registry and build completions
32//!             vec![]
33//!         }
34//!         CompletionContext::Version { package_name, prefix } => {
35//!             // Fetch versions and build completions
36//!             vec![]
37//!         }
38//!         _ => vec![],
39//!     }
40//! }
41//! ```
42
43use crate::{Metadata, ParseResult, Version};
44use tower_lsp_server::ls_types::{
45    CompletionItem, CompletionItemKind, CompletionTextEdit, Documentation, MarkupContent,
46    MarkupKind, Position, Range, TextEdit,
47};
48
49/// Context for completion request based on cursor position.
50///
51/// This enum represents what type of completion is appropriate at the
52/// current cursor location within a manifest file.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum CompletionContext {
55    /// Cursor is within or after a package name.
56    ///
57    /// Example: `serd|` or `tokio|` where | represents cursor position.
58    PackageName {
59        /// Partial package name typed so far (may be empty).
60        prefix: String,
61    },
62
63    /// Cursor is within a version string.
64    ///
65    /// Example: `"1.0|"` or `"^2.|"` where | represents cursor position.
66    Version {
67        /// Package name this version belongs to.
68        package_name: String,
69        /// Partial version typed so far (may include operators like ^, ~).
70        prefix: String,
71    },
72
73    /// Cursor is within a feature array.
74    ///
75    /// Example: `features = ["deri|"]` where | represents cursor position.
76    Feature {
77        /// Package name whose features are being completed.
78        package_name: String,
79        /// Partial feature name typed so far (may be empty).
80        prefix: String,
81    },
82
83    /// Cursor is not in a valid completion position.
84    None,
85}
86
87/// Detects the completion context based on cursor position.
88///
89/// This function analyzes the cursor position relative to parsed dependencies
90/// to determine what type of completion should be offered.
91///
92/// # Arguments
93///
94/// * `parse_result` - Parsed manifest with dependency information
95/// * `position` - Cursor position in the document (LSP Position, 0-based line, 0-based character)
96/// * `content` - Full document content for prefix extraction
97///
98/// # Returns
99///
100/// A `CompletionContext` indicating what type of completion is appropriate,
101/// or `CompletionContext::None` if the cursor is not in a valid position.
102///
103/// # Examples
104///
105/// ```no_run
106/// use deps_core::completion::detect_completion_context;
107/// use tower_lsp_server::ls_types::Position;
108///
109/// # async fn example(parse_result: &dyn deps_core::ParseResult, content: &str) {
110/// // Cursor at position after "ser" in "serde"
111/// let position = Position { line: 5, character: 3 };
112/// let context = detect_completion_context(parse_result, position, content);
113/// # }
114/// ```
115pub fn detect_completion_context(
116    parse_result: &dyn ParseResult,
117    position: Position,
118    content: &str,
119) -> CompletionContext {
120    let dependencies = parse_result.dependencies();
121
122    for dep in dependencies {
123        // Check if position is within the dependency name range
124        let name_range = dep.name_range();
125        if position_in_range(position, name_range) {
126            let prefix = extract_prefix(content, position, name_range);
127            return CompletionContext::PackageName { prefix };
128        }
129
130        // Check if position is within the version range
131        if let Some(version_range) = dep.version_range()
132            && position_in_range(position, version_range)
133        {
134            let prefix = extract_prefix(content, position, version_range);
135            return CompletionContext::Version {
136                package_name: dep.name().to_string(),
137                prefix,
138            };
139        }
140
141        // Check if position is within the features array range
142        if let Some(features_range) = dep.features_range()
143            && position_in_range(position, features_range)
144        {
145            let prefix = extract_feature_prefix(content, position);
146            return CompletionContext::Feature {
147                package_name: dep.name().to_string(),
148                prefix,
149            };
150        }
151    }
152
153    CompletionContext::None
154}
155
156/// Checks if a position is within or at the end of a range.
157///
158/// LSP ranges are inclusive of start, exclusive of end.
159/// We also consider the position to be "in range" if it's immediately
160/// after the range end (for completion after typing).
161const fn position_in_range(position: Position, range: Range) -> bool {
162    // Before range start
163    if position.line < range.start.line {
164        return false;
165    }
166
167    if position.line == range.start.line && position.character < range.start.character {
168        return false;
169    }
170
171    // After range end (allow one position past for completion)
172    if position.line > range.end.line {
173        return false;
174    }
175
176    if position.line == range.end.line && position.character > range.end.character + 1 {
177        return false;
178    }
179
180    true
181}
182
183/// Converts UTF-16 offset to byte offset in a string.
184///
185/// LSP uses UTF-16 code units for character positions (for compatibility with
186/// JavaScript and other languages). This function converts from UTF-16 offset
187/// to byte offset for Rust string indexing.
188///
189/// # Arguments
190///
191/// * `s` - The string to index into
192/// * `utf16_offset` - UTF-16 code unit offset (from LSP Position.character)
193///
194/// # Returns
195///
196/// Byte offset if valid, `None` if the UTF-16 offset is out of bounds.
197///
198/// # Examples
199///
200/// ```
201/// # use deps_core::completion::utf16_to_byte_offset;
202/// // ASCII: UTF-16 offset equals byte offset
203/// assert_eq!(utf16_to_byte_offset("hello", 2), Some(2));
204///
205/// // Unicode: "日本語" - each char is 3 bytes but 1 UTF-16 code unit
206/// assert_eq!(utf16_to_byte_offset("日本語", 0), Some(0));
207/// assert_eq!(utf16_to_byte_offset("日本語", 1), Some(3));
208/// assert_eq!(utf16_to_byte_offset("日本語", 2), Some(6));
209///
210/// // Emoji: "😀" is 4 bytes but 2 UTF-16 code units (surrogate pair)
211/// assert_eq!(utf16_to_byte_offset("😀test", 2), Some(4));
212/// ```
213pub fn utf16_to_byte_offset(s: &str, utf16_offset: u32) -> Option<usize> {
214    let mut utf16_count = 0u32;
215    for (byte_idx, ch) in s.char_indices() {
216        if utf16_count >= utf16_offset {
217            return Some(byte_idx);
218        }
219        utf16_count += ch.len_utf16() as u32;
220    }
221    if utf16_count == utf16_offset {
222        return Some(s.len());
223    }
224    None
225}
226
227/// Extracts the prefix text from content at a position within a range.
228///
229/// This function finds the text from the start of the range up to the
230/// cursor position, excluding any quote characters.
231///
232/// # Arguments
233///
234/// * `content` - Full document content
235/// * `position` - Cursor position (LSP Position, 0-based line, UTF-16 character offset)
236/// * `range` - Range containing the token (name, version, etc.)
237///
238/// # Returns
239///
240/// The prefix string typed so far, with quotes and extra whitespace removed.
241///
242/// # Examples
243///
244/// ```no_run
245/// use deps_core::completion::extract_prefix;
246/// use tower_lsp_server::ls_types::{Position, Range};
247///
248/// let content = r#"serde = "1.0""#;
249/// let position = Position { line: 0, character: 11 }; // After "1."
250/// let range = Range {
251///     start: Position { line: 0, character: 9 },
252///     end: Position { line: 0, character: 13 },
253/// };
254///
255/// let prefix = extract_prefix(content, position, range);
256/// assert_eq!(prefix, "1.");
257/// ```
258pub fn extract_prefix(content: &str, position: Position, range: Range) -> String {
259    // Get the line at the position - use nth() instead of collecting all lines
260    let line = match content.lines().nth(position.line as usize) {
261        Some(l) => l,
262        None => return String::new(),
263    };
264
265    // Convert UTF-16 positions to byte offsets
266    let start_byte = if position.line == range.start.line {
267        match utf16_to_byte_offset(line, range.start.character) {
268            Some(offset) => offset,
269            None => return String::new(),
270        }
271    } else {
272        0
273    };
274
275    let cursor_byte = match utf16_to_byte_offset(line, position.character) {
276        Some(offset) => offset,
277        None => return String::new(),
278    };
279
280    // Safety: ensure byte offsets are within bounds
281    if start_byte > line.len() || cursor_byte > line.len() || start_byte > cursor_byte {
282        return String::new();
283    }
284
285    // Extract substring
286    let prefix = &line[start_byte..cursor_byte];
287
288    // Remove quotes and trim whitespace
289    prefix
290        .trim()
291        .trim_matches('"')
292        .trim_matches('\'')
293        .trim()
294        .to_string()
295}
296
297/// Extracts the partial feature name typed at the cursor position.
298///
299/// Scans backwards from the cursor on the current line to find the start of
300/// the feature string being typed. Handles both inline and multi-line arrays.
301///
302/// Returns an empty string when the cursor is not inside a quoted string
303/// (e.g. right after `[` or between `, ` and the next `"`).
304///
305/// # Examples
306///
307/// ```no_run
308/// # use deps_core::completion::extract_feature_prefix;
309/// # use tower_lsp_server::ls_types::Position;
310/// // Cursor inside: features = ["derive", "std", "ser|"]
311/// let content = r#"serde = { version = "1", features = ["derive", "std", "ser"] }"#;
312/// // cursor_char = index after "ser" inside the last quoted element
313/// let ser_start = content.find(r#""ser""#).unwrap() + 1; // skip opening quote
314/// let pos = Position { line: 0, character: (ser_start + "ser".len()) as u32 };
315/// let prefix = extract_feature_prefix(content, pos);
316/// assert_eq!(prefix, "ser");
317/// ```
318pub fn extract_feature_prefix(content: &str, position: Position) -> String {
319    let line = match content.lines().nth(position.line as usize) {
320        Some(l) => l,
321        None => return String::new(),
322    };
323
324    let cursor_byte = match utf16_to_byte_offset(line, position.character) {
325        Some(offset) => offset.min(line.len()),
326        None => return String::new(),
327    };
328
329    let before_cursor = &line[..cursor_byte];
330
331    // Use the text after the last '[' on this line as the relevant segment
332    // (handles inline arrays; for multi-line arrays there is no '[' and we
333    // use the whole line up to the cursor).
334    let segment_start = before_cursor.rfind('[').map_or(0, |i| i + 1);
335    let segment = &before_cursor[segment_start..];
336
337    // Count '"' characters to determine whether the cursor is inside a string.
338    // An odd count means the cursor is inside an open string literal.
339    let quote_count = segment.chars().filter(|&c| c == '"').count();
340    if quote_count % 2 == 0 {
341        return String::new();
342    }
343
344    // Find the last opening quote and return the text after it.
345    match segment.rfind('"') {
346        Some(pos) => segment[pos + 1..].to_string(),
347        None => String::new(),
348    }
349}
350
351/// Builds a completion item for a package name.
352///
353/// Creates a properly formatted LSP CompletionItem with documentation,
354/// version information, and links to repository/docs.
355///
356/// # Arguments
357///
358/// * `metadata` - Package metadata from registry search
359/// * `insert_range` - LSP range where the completion should be inserted
360///
361/// # Returns
362///
363/// A complete `CompletionItem` ready to send to the LSP client.
364///
365/// # Examples
366///
367/// ```no_run
368/// use deps_core::completion::build_package_completion;
369/// use tower_lsp_server::ls_types::Range;
370///
371/// # async fn example(metadata: &dyn deps_core::Metadata) {
372/// let range = Range::default(); // Use actual range from context
373/// let item = build_package_completion(metadata, range);
374/// assert_eq!(item.label, metadata.name());
375/// # }
376/// ```
377pub fn build_package_completion(metadata: &dyn Metadata, insert_range: Range) -> CompletionItem {
378    let name = metadata.name();
379    let latest = metadata.latest_version();
380
381    // Build markdown documentation
382    let mut doc_parts = vec![format!("**{}** v{}", name, latest)];
383
384    if let Some(desc) = metadata.description() {
385        doc_parts.push(String::new()); // Empty line
386        let truncated = if desc.len() > 200 {
387            let mut end = 200;
388            while end > 0 && !desc.is_char_boundary(end) {
389                end -= 1;
390            }
391            format!("{}...", &desc[..end])
392        } else {
393            desc.to_string()
394        };
395        doc_parts.push(truncated);
396    }
397
398    // Add links section if we have any links
399    let mut links = Vec::new();
400    if let Some(repo) = metadata.repository() {
401        links.push(format!("[Repository]({})", repo));
402    }
403    if let Some(docs) = metadata.documentation() {
404        links.push(format!("[Documentation]({})", docs));
405    }
406
407    if !links.is_empty() {
408        doc_parts.push(String::new()); // Empty line
409        doc_parts.push(links.join(" | "));
410    }
411
412    CompletionItem {
413        label: name.to_string(),
414        kind: Some(CompletionItemKind::MODULE),
415        detail: Some(format!("v{}", latest)),
416        documentation: Some(Documentation::MarkupContent(MarkupContent {
417            kind: MarkupKind::Markdown,
418            value: doc_parts.join("\n"),
419        })),
420        insert_text: Some(name.to_string()),
421        text_edit: Some(CompletionTextEdit::Edit(TextEdit {
422            range: insert_range,
423            new_text: name.to_string(),
424        })),
425        sort_text: Some(name.to_string()),
426        filter_text: Some(name.to_string()),
427        ..Default::default()
428    }
429}
430
431/// Builds a completion item for a version string.
432///
433/// Creates a properly formatted LSP CompletionItem with version metadata
434/// in a simplified format matching Code Actions (Cmd+.) style.
435///
436/// # Arguments
437///
438/// * `display_item` - Version display metadata with label, description, and flags
439/// * `insert_range` - Optional LSP range where the completion should replace text.
440///   If `None`, the completion will insert at cursor position without replacing.
441///
442/// # Returns
443///
444/// A complete `CompletionItem` with simple index-based sorting and preselect.
445///
446/// # Format
447///
448/// - Label: `"version"` or `"version (latest)"` for the latest version
449/// - Detail: `"Update package_name to version"`
450/// - Preselect: `true` for latest version, `false` otherwise
451/// - Sort: Index-based (00000, 00001, etc.)
452///
453/// # Examples
454///
455/// ```no_run
456/// use deps_core::completion::{build_version_completion, VersionDisplayItem};
457/// use tower_lsp_server::ls_types::Range;
458///
459/// # async fn example(version: &dyn deps_core::Version) {
460/// // Without range - insert at cursor
461/// let display_item = VersionDisplayItem::new(version, "serde", 0, true);
462/// let item = build_version_completion(&display_item, None);
463/// assert_eq!(item.label, display_item.label);
464///
465/// // With range - replace existing text
466/// let range = Range::default();
467/// let item = build_version_completion(&display_item, Some(range));
468/// # }
469/// ```
470pub fn build_version_completion(
471    display_item: &VersionDisplayItem,
472    insert_range: Option<Range>,
473) -> CompletionItem {
474    // Simple index-based sorting (00000, 00001, etc.)
475    let sort_text = format!("{:05}", display_item.index);
476
477    CompletionItem {
478        label: display_item.label.clone(),
479        kind: Some(CompletionItemKind::VALUE),
480        detail: Some(display_item.description.clone()),
481        documentation: None,
482        insert_text: Some(display_item.version.clone()),
483        text_edit: insert_range.map(|range| {
484            CompletionTextEdit::Edit(TextEdit {
485                range,
486                new_text: display_item.version.clone(),
487            })
488        }),
489        sort_text: Some(sort_text),
490        preselect: Some(display_item.is_latest),
491        ..Default::default()
492    }
493}
494
495/// Display metadata for a single version in LSP responses.
496///
497/// Captures common formatting logic shared between completion items and code actions.
498#[derive(Debug, Clone)]
499pub struct VersionDisplayItem {
500    /// Raw version string (e.g., "1.0.0")
501    pub version: String,
502    /// Display label with "(latest)" suffix for first item
503    pub label: String,
504    /// Action description (e.g., "Update serde to 1.0.0")
505    pub description: String,
506    /// Zero-based index for sorting
507    pub index: usize,
508    /// True if this is the latest non-yanked version
509    pub is_latest: bool,
510}
511
512impl VersionDisplayItem {
513    /// Creates a display item from version metadata.
514    pub fn new(version: &dyn Version, package_name: &str, index: usize, is_latest: bool) -> Self {
515        let version_str = version.version_string();
516        let label = if is_latest {
517            format!("{} (latest)", version_str)
518        } else {
519            version_str.to_string()
520        };
521        let description = format!("Update {} to {}", package_name, version_str);
522
523        Self {
524            version: version_str.to_string(),
525            label,
526            description,
527            index,
528            is_latest,
529        }
530    }
531}
532
533/// Filters and formats versions for LSP display.
534///
535/// Returns up to 5 non-yanked versions with display metadata.
536pub fn prepare_version_display_items<V: AsRef<dyn Version>>(
537    versions: &[V],
538    package_name: &str,
539) -> Vec<VersionDisplayItem> {
540    versions
541        .iter()
542        .map(|v| v.as_ref())
543        .filter(|v| !v.is_yanked())
544        .take(MAX_COMPLETION_VERSIONS)
545        .enumerate()
546        .map(|(index, version)| VersionDisplayItem::new(version, package_name, index, index == 0))
547        .collect()
548}
549
550/// Builds a completion item for a feature flag.
551///
552/// Creates a properly formatted LSP CompletionItem for feature flag names.
553/// Only applicable to ecosystems that support features (e.g., Cargo).
554///
555/// # Arguments
556///
557/// * `feature_name` - Name of the feature flag
558/// * `package_name` - Name of the package this feature belongs to
559/// * `insert_range` - LSP range where the completion should be inserted, or `None` to omit
560///   `textEdit` and let the client insert at cursor position via `insertText`
561///
562/// # Returns
563///
564/// A complete `CompletionItem` for the feature flag.
565///
566/// # Examples
567///
568/// ```no_run
569/// use deps_core::completion::build_feature_completion;
570///
571/// let item = build_feature_completion("derive", "serde", None);
572/// assert_eq!(item.label, "derive");
573/// ```
574pub fn build_feature_completion(
575    feature_name: &str,
576    package_name: &str,
577    insert_range: Option<Range>,
578) -> CompletionItem {
579    CompletionItem {
580        label: feature_name.to_string(),
581        kind: Some(CompletionItemKind::PROPERTY),
582        detail: Some(format!("Feature of {}", package_name)),
583        documentation: None,
584        insert_text: Some(feature_name.to_string()),
585        text_edit: insert_range.map(|range| {
586            CompletionTextEdit::Edit(TextEdit {
587                range,
588                new_text: feature_name.to_string(),
589            })
590        }),
591        sort_text: Some(feature_name.to_string()),
592        ..Default::default()
593    }
594}
595
596/// Maximum number of version completions to show (matches Code Actions limit).
597const MAX_COMPLETION_VERSIONS: usize = 5;
598
599/// Generic version completion logic used by all ecosystems.
600///
601/// Filters versions by prefix (stripping ecosystem-specific operators),
602/// hides yanked/deprecated versions, returns up to 5 completion items.
603///
604/// # Arguments
605///
606/// * `registry` - Package registry to fetch versions from
607/// * `package_name` - Name of the package
608/// * `prefix` - Partial version string typed by user (may include operators)
609/// * `operator_chars` - Ecosystem-specific version operators to strip (e.g., `&['^', '~']`)
610///
611/// # Returns
612///
613/// Up to 5 completion items for non-yanked versions, filtered by prefix.
614/// If no versions match the prefix, returns up to 5 non-yanked versions.
615/// The first item (latest version) is marked with "(latest)" suffix and preselected.
616///
617/// # Examples
618///
619/// ```no_run
620/// use deps_core::completion::complete_versions_generic;
621///
622/// # async fn example(registry: &dyn deps_core::Registry) {
623/// // Cargo: strip ^, ~, =, <, > operators
624/// let items = complete_versions_generic(
625///     registry,
626///     "serde",
627///     "^1.0",
628///     &['^', '~', '=', '<', '>'],
629/// ).await;
630///
631/// // Go: no operators to strip
632/// let items = complete_versions_generic(
633///     registry,
634///     "github.com/gin-gonic/gin",
635///     "v1.9",
636///     &[],
637/// ).await;
638/// # }
639/// ```
640/// Generic package name completion using any `Registry` implementation.
641///
642/// Searches the registry for packages matching `prefix` and returns up to `limit`
643/// completion items. Returns empty vec if `prefix` is shorter than 2 characters or
644/// longer than 200 characters.
645pub async fn complete_package_names_generic(
646    registry: &dyn crate::Registry,
647    prefix: &str,
648    limit: usize,
649) -> Vec<CompletionItem> {
650    if prefix.len() < 2 || prefix.len() > 200 {
651        return vec![];
652    }
653
654    let results = match registry.search(prefix, limit).await {
655        Ok(r) => r,
656        Err(e) => {
657            tracing::warn!("Registry search failed for '{}': {}", prefix, e);
658            return vec![];
659        }
660    };
661
662    let insert_range = tower_lsp_server::ls_types::Range::default();
663
664    results
665        .into_iter()
666        .map(|metadata| build_package_completion(metadata.as_ref(), insert_range))
667        .collect()
668}
669
670pub async fn complete_versions_generic(
671    registry: &dyn crate::Registry,
672    package_name: &str,
673    prefix: &str,
674    operator_chars: &[char],
675) -> Vec<CompletionItem> {
676    let versions = match registry.get_versions(package_name).await {
677        Ok(v) => v,
678        Err(e) => {
679            tracing::warn!("Failed to fetch versions for '{}': {}", package_name, e);
680            return vec![];
681        }
682    };
683
684    let clean_prefix = prefix.trim_start_matches(operator_chars).trim();
685
686    // Filter versions by prefix first
687    let filtered_versions: Vec<_> = versions
688        .iter()
689        .filter(|v| v.version_string().starts_with(clean_prefix))
690        .collect();
691
692    // Use filtered or all versions, prepare_version_display_items will handle yanked filtering
693    let display_items = if filtered_versions.is_empty() {
694        prepare_version_display_items(&versions, package_name)
695    } else {
696        prepare_version_display_items(&filtered_versions, package_name)
697    };
698
699    // Don't provide text_edit range - let LSP client insert at cursor position
700    display_items
701        .iter()
702        .map(|item| build_version_completion(item, None))
703        .collect()
704}
705
706#[cfg(test)]
707mod tests {
708    use super::*;
709    use std::any::Any;
710
711    // Mock implementations for testing
712
713    struct MockDependency {
714        name: String,
715        name_range: Range,
716        version_range: Option<Range>,
717        features_range: Option<Range>,
718    }
719
720    impl crate::ecosystem::Dependency for MockDependency {
721        fn name(&self) -> &str {
722            &self.name
723        }
724
725        fn name_range(&self) -> Range {
726            self.name_range
727        }
728
729        fn version_requirement(&self) -> Option<&str> {
730            Some("1.0")
731        }
732
733        fn version_range(&self) -> Option<Range> {
734            self.version_range
735        }
736
737        fn features_range(&self) -> Option<Range> {
738            self.features_range
739        }
740
741        fn source(&self) -> crate::parser::DependencySource {
742            crate::parser::DependencySource::Registry
743        }
744
745        fn as_any(&self) -> &dyn Any {
746            self
747        }
748    }
749
750    struct MockParseResult {
751        dependencies: Vec<MockDependency>,
752    }
753
754    impl ParseResult for MockParseResult {
755        fn dependencies(&self) -> Vec<&dyn crate::ecosystem::Dependency> {
756            self.dependencies
757                .iter()
758                .map(|d| d as &dyn crate::ecosystem::Dependency)
759                .collect()
760        }
761
762        fn workspace_root(&self) -> Option<&std::path::Path> {
763            None
764        }
765
766        fn uri(&self) -> &tower_lsp_server::ls_types::Uri {
767            static URL: std::sync::LazyLock<tower_lsp_server::ls_types::Uri> =
768                std::sync::LazyLock::new(|| "file:///test/Cargo.toml".parse().unwrap());
769            &URL
770        }
771
772        fn as_any(&self) -> &dyn Any {
773            self
774        }
775    }
776
777    struct MockVersion {
778        version: String,
779        yanked: bool,
780        prerelease: bool,
781    }
782
783    impl crate::registry::Version for MockVersion {
784        fn version_string(&self) -> &str {
785            &self.version
786        }
787
788        fn is_yanked(&self) -> bool {
789            self.yanked
790        }
791
792        fn is_prerelease(&self) -> bool {
793            self.prerelease
794        }
795
796        fn as_any(&self) -> &dyn Any {
797            self
798        }
799    }
800
801    struct MockMetadata {
802        name: String,
803        description: Option<String>,
804        repository: Option<String>,
805        documentation: Option<String>,
806        latest_version: String,
807    }
808
809    impl crate::registry::Metadata for MockMetadata {
810        fn name(&self) -> &str {
811            &self.name
812        }
813
814        fn description(&self) -> Option<&str> {
815            self.description.as_deref()
816        }
817
818        fn repository(&self) -> Option<&str> {
819            self.repository.as_deref()
820        }
821
822        fn documentation(&self) -> Option<&str> {
823            self.documentation.as_deref()
824        }
825
826        fn latest_version(&self) -> &str {
827            &self.latest_version
828        }
829
830        fn as_any(&self) -> &dyn Any {
831            self
832        }
833    }
834
835    struct MockRegistry {
836        versions: Vec<MockVersion>,
837    }
838
839    impl crate::Registry for MockRegistry {
840        fn get_versions<'a>(
841            &'a self,
842            _package_name: &'a str,
843        ) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Version>>>>
844        {
845            let versions: Vec<Box<dyn crate::Version>> = self
846                .versions
847                .iter()
848                .map(|v| {
849                    Box::new(MockVersion {
850                        version: v.version.clone(),
851                        yanked: v.yanked,
852                        prerelease: v.prerelease,
853                    }) as Box<dyn crate::Version>
854                })
855                .collect();
856            Box::pin(async move { Ok(versions) })
857        }
858
859        fn get_latest_matching<'a>(
860            &'a self,
861            _name: &'a str,
862            _req: &'a str,
863        ) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Option<Box<dyn crate::Version>>>>
864        {
865            Box::pin(async move { Ok(None) })
866        }
867
868        fn search<'a>(
869            &'a self,
870            _query: &'a str,
871            _limit: usize,
872        ) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Metadata>>>>
873        {
874            Box::pin(async move { Ok(vec![]) })
875        }
876
877        fn package_url(&self, _name: &str) -> String {
878            String::new()
879        }
880
881        fn as_any(&self) -> &dyn Any {
882            self
883        }
884    }
885
886    // Context detection tests
887
888    #[test]
889    fn test_detect_package_name_context_at_start() {
890        let parse_result = MockParseResult {
891            dependencies: vec![MockDependency {
892                name: "serde".to_string(),
893                name_range: Range {
894                    start: Position {
895                        line: 0,
896                        character: 0,
897                    },
898                    end: Position {
899                        line: 0,
900                        character: 5,
901                    },
902                },
903                version_range: None,
904                features_range: None,
905            }],
906        };
907
908        let content = "serde";
909        let position = Position {
910            line: 0,
911            character: 0,
912        };
913
914        let context = detect_completion_context(&parse_result, position, content);
915
916        match context {
917            CompletionContext::PackageName { prefix } => {
918                assert_eq!(prefix, "");
919            }
920            _ => panic!("Expected PackageName context, got {:?}", context),
921        }
922    }
923
924    #[test]
925    fn test_detect_package_name_context_partial() {
926        let parse_result = MockParseResult {
927            dependencies: vec![MockDependency {
928                name: "serde".to_string(),
929                name_range: Range {
930                    start: Position {
931                        line: 0,
932                        character: 0,
933                    },
934                    end: Position {
935                        line: 0,
936                        character: 5,
937                    },
938                },
939                version_range: None,
940                features_range: None,
941            }],
942        };
943
944        let content = "serde";
945        let position = Position {
946            line: 0,
947            character: 3,
948        };
949
950        let context = detect_completion_context(&parse_result, position, content);
951
952        match context {
953            CompletionContext::PackageName { prefix } => {
954                assert_eq!(prefix, "ser");
955            }
956            _ => panic!("Expected PackageName context, got {:?}", context),
957        }
958    }
959
960    #[test]
961    fn test_detect_version_context() {
962        let parse_result = MockParseResult {
963            dependencies: vec![MockDependency {
964                name: "serde".to_string(),
965                name_range: Range {
966                    start: Position {
967                        line: 0,
968                        character: 0,
969                    },
970                    end: Position {
971                        line: 0,
972                        character: 5,
973                    },
974                },
975                version_range: Some(Range {
976                    start: Position {
977                        line: 0,
978                        character: 9,
979                    },
980                    end: Position {
981                        line: 0,
982                        character: 14,
983                    },
984                }),
985                features_range: None,
986            }],
987        };
988
989        let content = r#"serde = "1.0.1""#;
990        let position = Position {
991            line: 0,
992            character: 11,
993        };
994
995        let context = detect_completion_context(&parse_result, position, content);
996
997        match context {
998            CompletionContext::Version {
999                package_name,
1000                prefix,
1001            } => {
1002                assert_eq!(package_name, "serde");
1003                assert_eq!(prefix, "1.");
1004            }
1005            _ => panic!("Expected Version context, got {:?}", context),
1006        }
1007    }
1008
1009    #[test]
1010    fn test_detect_no_context_before_dependencies() {
1011        let parse_result = MockParseResult {
1012            dependencies: vec![MockDependency {
1013                name: "serde".to_string(),
1014                name_range: Range {
1015                    start: Position {
1016                        line: 5,
1017                        character: 0,
1018                    },
1019                    end: Position {
1020                        line: 5,
1021                        character: 5,
1022                    },
1023                },
1024                version_range: None,
1025                features_range: None,
1026            }],
1027        };
1028
1029        let content = "[dependencies]\nserde";
1030        let position = Position {
1031            line: 0,
1032            character: 10,
1033        };
1034
1035        let context = detect_completion_context(&parse_result, position, content);
1036
1037        assert_eq!(context, CompletionContext::None);
1038    }
1039
1040    #[test]
1041    fn test_detect_no_context_invalid_position() {
1042        let parse_result = MockParseResult {
1043            dependencies: vec![],
1044        };
1045
1046        let content = "";
1047        let position = Position {
1048            line: 100,
1049            character: 100,
1050        };
1051
1052        let context = detect_completion_context(&parse_result, position, content);
1053
1054        assert_eq!(context, CompletionContext::None);
1055    }
1056
1057    // Prefix extraction tests
1058
1059    #[test]
1060    fn test_extract_prefix_at_start() {
1061        let content = "serde";
1062        let position = Position {
1063            line: 0,
1064            character: 0,
1065        };
1066        let range = Range {
1067            start: Position {
1068                line: 0,
1069                character: 0,
1070            },
1071            end: Position {
1072                line: 0,
1073                character: 5,
1074            },
1075        };
1076
1077        let prefix = extract_prefix(content, position, range);
1078        assert_eq!(prefix, "");
1079    }
1080
1081    #[test]
1082    fn test_extract_prefix_partial() {
1083        let content = "serde";
1084        let position = Position {
1085            line: 0,
1086            character: 3,
1087        };
1088        let range = Range {
1089            start: Position {
1090                line: 0,
1091                character: 0,
1092            },
1093            end: Position {
1094                line: 0,
1095                character: 5,
1096            },
1097        };
1098
1099        let prefix = extract_prefix(content, position, range);
1100        assert_eq!(prefix, "ser");
1101    }
1102
1103    #[test]
1104    fn test_extract_prefix_with_quotes() {
1105        let content = r#"serde = "1.0""#;
1106        let position = Position {
1107            line: 0,
1108            character: 11,
1109        };
1110        let range = Range {
1111            start: Position {
1112                line: 0,
1113                character: 9,
1114            },
1115            end: Position {
1116                line: 0,
1117                character: 13,
1118            },
1119        };
1120
1121        let prefix = extract_prefix(content, position, range);
1122        assert_eq!(prefix, "1.");
1123    }
1124
1125    #[test]
1126    fn test_extract_prefix_empty() {
1127        let content = r#"serde = """#;
1128        let position = Position {
1129            line: 0,
1130            character: 9,
1131        };
1132        let range = Range {
1133            start: Position {
1134                line: 0,
1135                character: 9,
1136            },
1137            end: Position {
1138                line: 0,
1139                character: 11,
1140            },
1141        };
1142
1143        let prefix = extract_prefix(content, position, range);
1144        assert_eq!(prefix, "");
1145    }
1146
1147    #[test]
1148    fn test_extract_prefix_version_with_operator() {
1149        let content = r#"serde = "^1.0""#;
1150        let position = Position {
1151            line: 0,
1152            character: 12,
1153        };
1154        let range = Range {
1155            start: Position {
1156                line: 0,
1157                character: 9,
1158            },
1159            end: Position {
1160                line: 0,
1161                character: 14,
1162            },
1163        };
1164
1165        let prefix = extract_prefix(content, position, range);
1166        assert_eq!(prefix, "^1.");
1167    }
1168
1169    // CompletionItem builder tests
1170
1171    #[test]
1172    fn test_build_package_completion_full() {
1173        let metadata = MockMetadata {
1174            name: "serde".to_string(),
1175            description: Some("Serialization framework".to_string()),
1176            repository: Some("https://github.com/serde-rs/serde".to_string()),
1177            documentation: Some("https://docs.rs/serde".to_string()),
1178            latest_version: "1.0.214".to_string(),
1179        };
1180
1181        let range = Range::default();
1182        let item = build_package_completion(&metadata, range);
1183
1184        assert_eq!(item.label, "serde");
1185        assert_eq!(item.kind, Some(CompletionItemKind::MODULE));
1186        assert_eq!(item.detail, Some("v1.0.214".to_string()));
1187        assert!(matches!(
1188            item.documentation,
1189            Some(Documentation::MarkupContent(_))
1190        ));
1191
1192        if let Some(Documentation::MarkupContent(content)) = item.documentation {
1193            assert!(content.value.contains("**serde** v1.0.214"));
1194            assert!(content.value.contains("Serialization framework"));
1195            assert!(content.value.contains("Repository"));
1196            assert!(content.value.contains("Documentation"));
1197        }
1198    }
1199
1200    #[test]
1201    fn test_build_package_completion_minimal() {
1202        let metadata = MockMetadata {
1203            name: "test-pkg".to_string(),
1204            description: None,
1205            repository: None,
1206            documentation: None,
1207            latest_version: "0.1.0".to_string(),
1208        };
1209
1210        let range = Range::default();
1211        let item = build_package_completion(&metadata, range);
1212
1213        assert_eq!(item.label, "test-pkg");
1214        assert_eq!(item.detail, Some("v0.1.0".to_string()));
1215
1216        if let Some(Documentation::MarkupContent(content)) = item.documentation {
1217            assert!(content.value.contains("**test-pkg** v0.1.0"));
1218            assert!(!content.value.contains("Repository"));
1219        }
1220    }
1221
1222    #[test]
1223    fn test_build_version_completion_stable() {
1224        let version = MockVersion {
1225            version: "1.0.0".to_string(),
1226            yanked: false,
1227            prerelease: false,
1228        };
1229
1230        let display_item = VersionDisplayItem::new(&version, "serde", 0, false);
1231        let item = build_version_completion(&display_item, None);
1232
1233        assert_eq!(item.label, "1.0.0");
1234        assert_eq!(item.kind, Some(CompletionItemKind::VALUE));
1235        assert_eq!(item.detail, Some("Update serde to 1.0.0".to_string()));
1236        assert_eq!(item.documentation, None);
1237        assert_eq!(item.preselect, Some(false));
1238        assert_eq!(item.sort_text, Some("00000".to_string()));
1239        assert_eq!(item.text_edit, None); // No text_edit when range is None
1240    }
1241
1242    #[test]
1243    fn test_build_version_completion_latest() {
1244        let version = MockVersion {
1245            version: "1.0.0".to_string(),
1246            yanked: false,
1247            prerelease: false,
1248        };
1249
1250        let display_item = VersionDisplayItem::new(&version, "serde", 0, true);
1251        let item = build_version_completion(&display_item, None);
1252
1253        assert_eq!(item.label, "1.0.0 (latest)");
1254        assert_eq!(item.kind, Some(CompletionItemKind::VALUE));
1255        assert_eq!(item.detail, Some("Update serde to 1.0.0".to_string()));
1256        assert_eq!(item.documentation, None);
1257        assert_eq!(item.preselect, Some(true));
1258        assert_eq!(item.sort_text, Some("00000".to_string()));
1259        assert_eq!(item.text_edit, None); // No text_edit when range is None
1260    }
1261
1262    #[test]
1263    fn test_build_version_completion_not_latest() {
1264        let version = MockVersion {
1265            version: "0.9.0".to_string(),
1266            yanked: false,
1267            prerelease: false,
1268        };
1269
1270        let display_item = VersionDisplayItem::new(&version, "tokio", 1, false);
1271        let item = build_version_completion(&display_item, None);
1272
1273        assert_eq!(item.label, "0.9.0");
1274        assert_eq!(item.detail, Some("Update tokio to 0.9.0".to_string()));
1275        assert_eq!(item.documentation, None);
1276        assert_eq!(item.preselect, Some(false));
1277        assert_eq!(item.sort_text, Some("00001".to_string()));
1278        assert_eq!(item.text_edit, None); // No text_edit when range is None
1279    }
1280
1281    #[test]
1282    fn test_build_version_completion_sort_order() {
1283        let v1 = MockVersion {
1284            version: "1.0.0".to_string(),
1285            yanked: false,
1286            prerelease: false,
1287        };
1288        let v2 = MockVersion {
1289            version: "0.9.0".to_string(),
1290            yanked: false,
1291            prerelease: false,
1292        };
1293        let v3 = MockVersion {
1294            version: "0.8.0".to_string(),
1295            yanked: false,
1296            prerelease: false,
1297        };
1298
1299        let display_item1 = VersionDisplayItem::new(&v1, "test", 0, true);
1300        let display_item2 = VersionDisplayItem::new(&v2, "test", 1, false);
1301        let display_item3 = VersionDisplayItem::new(&v3, "test", 2, false);
1302        let item1 = build_version_completion(&display_item1, None);
1303        let item2 = build_version_completion(&display_item2, None);
1304        let item3 = build_version_completion(&display_item3, None);
1305
1306        // Simple index-based sorting
1307        assert_eq!(item1.sort_text.as_ref().unwrap(), "00000");
1308        assert_eq!(item2.sort_text.as_ref().unwrap(), "00001");
1309        assert_eq!(item3.sort_text.as_ref().unwrap(), "00002");
1310
1311        // First item should be preselected
1312        assert_eq!(item1.preselect, Some(true));
1313        assert_eq!(item2.preselect, Some(false));
1314        assert_eq!(item3.preselect, Some(false));
1315    }
1316
1317    #[test]
1318    fn test_version_completion_semantic_ordering() {
1319        let versions = [
1320            MockVersion {
1321                version: "0.14.0".to_string(),
1322                yanked: false,
1323                prerelease: false,
1324            },
1325            MockVersion {
1326                version: "0.8.0".to_string(),
1327                yanked: false,
1328                prerelease: false,
1329            },
1330            MockVersion {
1331                version: "0.2.0".to_string(),
1332                yanked: false,
1333                prerelease: false,
1334            },
1335        ];
1336
1337        let items: Vec<_> = versions
1338            .iter()
1339            .enumerate()
1340            .map(|(idx, v)| {
1341                let display_item = VersionDisplayItem::new(v, "test", idx, idx == 0);
1342                build_version_completion(&display_item, None)
1343            })
1344            .collect();
1345
1346        assert_eq!(items[0].sort_text.as_ref().unwrap(), "00000");
1347        assert_eq!(items[1].sort_text.as_ref().unwrap(), "00001");
1348        assert_eq!(items[2].sort_text.as_ref().unwrap(), "00002");
1349
1350        let mut sorted_items = items;
1351        sorted_items.sort_by(|a, b| {
1352            a.sort_text
1353                .as_ref()
1354                .unwrap()
1355                .cmp(b.sort_text.as_ref().unwrap())
1356        });
1357
1358        assert_eq!(sorted_items[0].label, "0.14.0 (latest)");
1359        assert_eq!(sorted_items[1].label, "0.8.0");
1360        assert_eq!(sorted_items[2].label, "0.2.0");
1361    }
1362
1363    #[test]
1364    fn test_version_completion_index_ordering() {
1365        let versions = ["1.20.0", "1.9.0", "1.2.0", "0.99.0", "0.50.0"];
1366
1367        let items: Vec<_> = versions
1368            .iter()
1369            .enumerate()
1370            .map(|(idx, ver)| {
1371                let v = MockVersion {
1372                    version: ver.to_string(),
1373                    yanked: false,
1374                    prerelease: false,
1375                };
1376                let display_item = VersionDisplayItem::new(&v, "test", idx, idx == 0);
1377                build_version_completion(&display_item, None)
1378            })
1379            .collect();
1380
1381        assert_eq!(items[0].sort_text.as_ref().unwrap(), "00000");
1382        assert_eq!(items[1].sort_text.as_ref().unwrap(), "00001");
1383        assert_eq!(items[2].sort_text.as_ref().unwrap(), "00002");
1384        assert_eq!(items[3].sort_text.as_ref().unwrap(), "00003");
1385        assert_eq!(items[4].sort_text.as_ref().unwrap(), "00004");
1386
1387        let mut sorted_items = items;
1388        sorted_items.sort_by(|a, b| {
1389            a.sort_text
1390                .as_ref()
1391                .unwrap()
1392                .cmp(b.sort_text.as_ref().unwrap())
1393        });
1394
1395        assert_eq!(sorted_items[0].label, "1.20.0 (latest)");
1396        assert_eq!(sorted_items[1].label, "1.9.0");
1397        assert_eq!(sorted_items[2].label, "1.2.0");
1398        assert_eq!(sorted_items[3].label, "0.99.0");
1399        assert_eq!(sorted_items[4].label, "0.50.0");
1400    }
1401
1402    #[test]
1403    fn test_version_display_item_latest() {
1404        let version = MockVersion {
1405            version: "1.0.0".to_string(),
1406            yanked: false,
1407            prerelease: false,
1408        };
1409
1410        let item = VersionDisplayItem::new(&version, "serde", 0, true);
1411
1412        assert_eq!(item.version, "1.0.0");
1413        assert_eq!(item.label, "1.0.0 (latest)");
1414        assert_eq!(item.description, "Update serde to 1.0.0");
1415        assert_eq!(item.index, 0);
1416        assert!(item.is_latest);
1417    }
1418
1419    #[test]
1420    fn test_version_display_item_not_latest() {
1421        let version = MockVersion {
1422            version: "0.9.0".to_string(),
1423            yanked: false,
1424            prerelease: false,
1425        };
1426
1427        let item = VersionDisplayItem::new(&version, "tokio", 1, false);
1428
1429        assert_eq!(item.version, "0.9.0");
1430        assert_eq!(item.label, "0.9.0");
1431        assert_eq!(item.description, "Update tokio to 0.9.0");
1432        assert_eq!(item.index, 1);
1433        assert!(!item.is_latest);
1434    }
1435
1436    #[test]
1437    fn test_prepare_version_display_items_filters_yanked() {
1438        let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![
1439            std::sync::Arc::new(MockVersion {
1440                version: "1.0.0".to_string(),
1441                yanked: false,
1442                prerelease: false,
1443            }),
1444            std::sync::Arc::new(MockVersion {
1445                version: "0.9.0".to_string(),
1446                yanked: true,
1447                prerelease: false,
1448            }),
1449            std::sync::Arc::new(MockVersion {
1450                version: "0.8.0".to_string(),
1451                yanked: false,
1452                prerelease: false,
1453            }),
1454        ];
1455
1456        let items = prepare_version_display_items(&versions, "test");
1457
1458        assert_eq!(items.len(), 2);
1459        assert_eq!(items[0].version, "1.0.0");
1460        assert_eq!(items[0].label, "1.0.0 (latest)");
1461        assert!(items[0].is_latest);
1462        assert_eq!(items[1].version, "0.8.0");
1463        assert_eq!(items[1].label, "0.8.0");
1464        assert!(!items[1].is_latest);
1465    }
1466
1467    #[test]
1468    fn test_prepare_version_display_items_limits_to_5() {
1469        let versions: Vec<std::sync::Arc<dyn crate::Version>> = (0..10)
1470            .map(|i| {
1471                std::sync::Arc::new(MockVersion {
1472                    version: format!("1.0.{}", i),
1473                    yanked: false,
1474                    prerelease: false,
1475                }) as std::sync::Arc<dyn crate::Version>
1476            })
1477            .collect();
1478
1479        let items = prepare_version_display_items(&versions, "test");
1480
1481        assert_eq!(items.len(), 5);
1482        assert_eq!(items[0].version, "1.0.0");
1483        assert_eq!(items[0].label, "1.0.0 (latest)");
1484        assert_eq!(items[4].version, "1.0.4");
1485        assert_eq!(items[4].label, "1.0.4");
1486    }
1487
1488    #[test]
1489    fn test_prepare_version_display_items_empty() {
1490        let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![];
1491
1492        let items = prepare_version_display_items(&versions, "test");
1493
1494        assert_eq!(items.len(), 0);
1495    }
1496
1497    #[test]
1498    fn test_prepare_version_display_items_all_yanked() {
1499        let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![
1500            std::sync::Arc::new(MockVersion {
1501                version: "1.0.0".to_string(),
1502                yanked: true,
1503                prerelease: false,
1504            }),
1505            std::sync::Arc::new(MockVersion {
1506                version: "0.9.0".to_string(),
1507                yanked: true,
1508                prerelease: false,
1509            }),
1510        ];
1511
1512        let items = prepare_version_display_items(&versions, "test");
1513
1514        assert_eq!(items.len(), 0);
1515    }
1516
1517    #[test]
1518    fn test_build_feature_completion() {
1519        let item = build_feature_completion("derive", "serde", None);
1520
1521        assert_eq!(item.label, "derive");
1522        assert_eq!(item.kind, Some(CompletionItemKind::PROPERTY));
1523        assert_eq!(item.detail, Some("Feature of serde".to_string()));
1524        assert!(item.documentation.is_none());
1525        assert!(item.text_edit.is_none());
1526        assert_eq!(item.sort_text, Some("derive".to_string()));
1527    }
1528
1529    #[test]
1530    fn test_build_feature_completion_with_range() {
1531        let range = Range::default();
1532        let item = build_feature_completion("derive", "serde", Some(range));
1533
1534        assert_eq!(item.label, "derive");
1535        assert!(item.text_edit.is_some());
1536    }
1537
1538    #[test]
1539    fn test_position_in_range_within() {
1540        let range = Range {
1541            start: Position {
1542                line: 0,
1543                character: 5,
1544            },
1545            end: Position {
1546                line: 0,
1547                character: 10,
1548            },
1549        };
1550
1551        let position = Position {
1552            line: 0,
1553            character: 7,
1554        };
1555
1556        assert!(position_in_range(position, range));
1557    }
1558
1559    #[test]
1560    fn test_position_in_range_at_start() {
1561        let range = Range {
1562            start: Position {
1563                line: 0,
1564                character: 5,
1565            },
1566            end: Position {
1567                line: 0,
1568                character: 10,
1569            },
1570        };
1571
1572        let position = Position {
1573            line: 0,
1574            character: 5,
1575        };
1576
1577        assert!(position_in_range(position, range));
1578    }
1579
1580    #[test]
1581    fn test_position_in_range_at_end() {
1582        let range = Range {
1583            start: Position {
1584                line: 0,
1585                character: 5,
1586            },
1587            end: Position {
1588                line: 0,
1589                character: 10,
1590            },
1591        };
1592
1593        let position = Position {
1594            line: 0,
1595            character: 10,
1596        };
1597
1598        assert!(position_in_range(position, range));
1599    }
1600
1601    #[test]
1602    fn test_position_in_range_one_past_end() {
1603        let range = Range {
1604            start: Position {
1605                line: 0,
1606                character: 5,
1607            },
1608            end: Position {
1609                line: 0,
1610                character: 10,
1611            },
1612        };
1613
1614        // Allow one character past end for completion
1615        let position = Position {
1616            line: 0,
1617            character: 11,
1618        };
1619
1620        assert!(position_in_range(position, range));
1621    }
1622
1623    #[test]
1624    fn test_position_in_range_before() {
1625        let range = Range {
1626            start: Position {
1627                line: 0,
1628                character: 5,
1629            },
1630            end: Position {
1631                line: 0,
1632                character: 10,
1633            },
1634        };
1635
1636        let position = Position {
1637            line: 0,
1638            character: 4,
1639        };
1640
1641        assert!(!position_in_range(position, range));
1642    }
1643
1644    #[test]
1645    fn test_position_in_range_after() {
1646        let range = Range {
1647            start: Position {
1648                line: 0,
1649                character: 5,
1650            },
1651            end: Position {
1652                line: 0,
1653                character: 10,
1654            },
1655        };
1656
1657        let position = Position {
1658            line: 0,
1659            character: 12,
1660        };
1661
1662        assert!(!position_in_range(position, range));
1663    }
1664
1665    // UTF-16 to byte offset conversion tests
1666
1667    #[test]
1668    fn test_utf16_to_byte_offset_ascii() {
1669        let s = "hello";
1670        assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
1671        assert_eq!(utf16_to_byte_offset(s, 2), Some(2));
1672        assert_eq!(utf16_to_byte_offset(s, 5), Some(5));
1673    }
1674
1675    #[test]
1676    fn test_utf16_to_byte_offset_multibyte() {
1677        // "日本語" - each character is 3 bytes, 1 UTF-16 code unit
1678        let s = "日本語";
1679        assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
1680        assert_eq!(utf16_to_byte_offset(s, 1), Some(3));
1681        assert_eq!(utf16_to_byte_offset(s, 2), Some(6));
1682        assert_eq!(utf16_to_byte_offset(s, 3), Some(9));
1683    }
1684
1685    #[test]
1686    fn test_utf16_to_byte_offset_emoji() {
1687        // "😀" is 4 bytes but 2 UTF-16 code units (surrogate pair)
1688        let s = "😀test";
1689        assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
1690        assert_eq!(utf16_to_byte_offset(s, 2), Some(4)); // After emoji
1691        assert_eq!(utf16_to_byte_offset(s, 3), Some(5)); // After 't'
1692    }
1693
1694    #[test]
1695    fn test_utf16_to_byte_offset_mixed() {
1696        // Mix of ASCII, multi-byte, and emoji
1697        let s = "hello 世界 😀!";
1698        assert_eq!(utf16_to_byte_offset(s, 0), Some(0)); // 'h'
1699        assert_eq!(utf16_to_byte_offset(s, 6), Some(6)); // '世'
1700        assert_eq!(utf16_to_byte_offset(s, 7), Some(9)); // '界'
1701        assert_eq!(utf16_to_byte_offset(s, 9), Some(13)); // '😀' (2 UTF-16 units)
1702        assert_eq!(utf16_to_byte_offset(s, 11), Some(17)); // '!'
1703    }
1704
1705    #[test]
1706    fn test_utf16_to_byte_offset_out_of_bounds() {
1707        let s = "hello";
1708        assert_eq!(utf16_to_byte_offset(s, 100), None);
1709    }
1710
1711    #[test]
1712    fn test_utf16_to_byte_offset_empty() {
1713        let s = "";
1714        assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
1715        assert_eq!(utf16_to_byte_offset(s, 1), None);
1716    }
1717
1718    // Unicode truncation tests
1719
1720    #[test]
1721    fn test_build_package_completion_long_description_ascii() {
1722        let long_desc = "a".repeat(250);
1723        let metadata = MockMetadata {
1724            name: "test-pkg".to_string(),
1725            description: Some(long_desc),
1726            repository: None,
1727            documentation: None,
1728            latest_version: "1.0.0".to_string(),
1729        };
1730
1731        let range = Range::default();
1732        let item = build_package_completion(&metadata, range);
1733
1734        if let Some(Documentation::MarkupContent(content)) = item.documentation {
1735            // Should be truncated to 200 chars + "..."
1736            let lines: Vec<_> = content.value.lines().collect();
1737            assert!(lines[2].ends_with("..."));
1738            assert!(lines[2].len() <= 203); // 200 + "..."
1739        } else {
1740            panic!("Expected MarkupContent documentation");
1741        }
1742    }
1743
1744    #[test]
1745    fn test_build_package_completion_long_description_unicode() {
1746        // Create description with Unicode chars at the boundary
1747        // Each '日' is 3 bytes, so 67 chars = 201 bytes
1748        let mut long_desc = String::new();
1749        for _ in 0..67 {
1750            long_desc.push('日');
1751        }
1752
1753        let metadata = MockMetadata {
1754            name: "test-pkg".to_string(),
1755            description: Some(long_desc),
1756            repository: None,
1757            documentation: None,
1758            latest_version: "1.0.0".to_string(),
1759        };
1760
1761        let range = Range::default();
1762        let item = build_package_completion(&metadata, range);
1763
1764        // Should not panic on truncation
1765        if let Some(Documentation::MarkupContent(content)) = item.documentation {
1766            let lines: Vec<_> = content.value.lines().collect();
1767            assert!(lines[2].ends_with("..."));
1768            // Truncation should happen at a char boundary
1769            assert!(lines[2].is_char_boundary(lines[2].len()));
1770        } else {
1771            panic!("Expected MarkupContent documentation");
1772        }
1773    }
1774
1775    #[test]
1776    fn test_build_package_completion_long_description_emoji() {
1777        // Emoji "😀" is 4 bytes each
1778        // 51 emoji = 204 bytes
1779        let long_desc = "😀".repeat(51);
1780
1781        let metadata = MockMetadata {
1782            name: "test-pkg".to_string(),
1783            description: Some(long_desc),
1784            repository: None,
1785            documentation: None,
1786            latest_version: "1.0.0".to_string(),
1787        };
1788
1789        let range = Range::default();
1790        let item = build_package_completion(&metadata, range);
1791
1792        // Should not panic on truncation
1793        if let Some(Documentation::MarkupContent(content)) = item.documentation {
1794            let lines: Vec<_> = content.value.lines().collect();
1795            assert!(lines[2].ends_with("..."));
1796            // Truncation should happen at a char boundary
1797            assert!(lines[2].is_char_boundary(lines[2].len()));
1798        } else {
1799            panic!("Expected MarkupContent documentation");
1800        }
1801    }
1802
1803    #[test]
1804    fn test_extract_prefix_unicode_package_name() {
1805        // Package name with Unicode characters
1806        let content = "日本語-crate = \"1.0\"";
1807        let position = Position {
1808            line: 0,
1809            character: 3, // UTF-16 offset after "日本語"
1810        };
1811        let range = Range {
1812            start: Position {
1813                line: 0,
1814                character: 0,
1815            },
1816            end: Position {
1817                line: 0,
1818                character: 10,
1819            },
1820        };
1821
1822        let prefix = extract_prefix(content, position, range);
1823        assert_eq!(prefix, "日本語");
1824    }
1825
1826    #[test]
1827    fn test_extract_prefix_emoji_in_content() {
1828        // Content with emoji (rare but should handle gracefully)
1829        let content = "emoji-😀-crate = \"1.0\"";
1830        let position = Position {
1831            line: 0,
1832            character: 8, // UTF-16 offset after "emoji-😀"
1833        };
1834        let range = Range {
1835            start: Position {
1836                line: 0,
1837                character: 0,
1838            },
1839            end: Position {
1840                line: 0,
1841                character: 14,
1842            },
1843        };
1844
1845        let prefix = extract_prefix(content, position, range);
1846        assert_eq!(prefix, "emoji-😀");
1847    }
1848
1849    // Generic version completion tests
1850
1851    #[tokio::test]
1852    async fn test_complete_versions_generic_operator_stripping() {
1853        let registry = MockRegistry {
1854            versions: vec![
1855                MockVersion {
1856                    version: "1.0.0".to_string(),
1857                    yanked: false,
1858                    prerelease: false,
1859                },
1860                MockVersion {
1861                    version: "1.0.1".to_string(),
1862                    yanked: false,
1863                    prerelease: false,
1864                },
1865                MockVersion {
1866                    version: "1.1.0".to_string(),
1867                    yanked: false,
1868                    prerelease: false,
1869                },
1870                MockVersion {
1871                    version: "2.0.0".to_string(),
1872                    yanked: false,
1873                    prerelease: false,
1874                },
1875            ],
1876        };
1877
1878        // Test with Cargo-style operators (^, ~, =, <, >)
1879        let items =
1880            complete_versions_generic(&registry, "test-pkg", "^1.0", &['^', '~', '=', '<', '>'])
1881                .await;
1882
1883        // Should return versions starting with "1.0" (after stripping ^)
1884        assert_eq!(items.len(), 2);
1885        assert_eq!(items[0].label, "1.0.0 (latest)");
1886        assert_eq!(items[1].label, "1.0.1");
1887
1888        // Test with tilde operator
1889        let items =
1890            complete_versions_generic(&registry, "test-pkg", "~1.1", &['^', '~', '=', '<', '>'])
1891                .await;
1892
1893        assert_eq!(items.len(), 1);
1894        assert_eq!(items[0].label, "1.1.0 (latest)");
1895
1896        // Test with equals operator
1897        let items =
1898            complete_versions_generic(&registry, "test-pkg", "=2.0", &['^', '~', '=', '<', '>'])
1899                .await;
1900
1901        assert_eq!(items.len(), 1);
1902        assert_eq!(items[0].label, "2.0.0 (latest)");
1903
1904        // Test with no operator (should work the same)
1905        let items =
1906            complete_versions_generic(&registry, "test-pkg", "1.0", &['^', '~', '=', '<', '>'])
1907                .await;
1908
1909        assert_eq!(items.len(), 2);
1910        assert_eq!(items[0].label, "1.0.0 (latest)");
1911        assert_eq!(items[1].label, "1.0.1");
1912    }
1913
1914    #[tokio::test]
1915    async fn test_complete_versions_generic_fallback_when_no_prefix_match() {
1916        let registry = MockRegistry {
1917            versions: vec![
1918                MockVersion {
1919                    version: "1.0.0".to_string(),
1920                    yanked: false,
1921                    prerelease: false,
1922                },
1923                MockVersion {
1924                    version: "1.1.0".to_string(),
1925                    yanked: false,
1926                    prerelease: false,
1927                },
1928                MockVersion {
1929                    version: "2.0.0".to_string(),
1930                    yanked: false,
1931                    prerelease: false,
1932                },
1933                MockVersion {
1934                    version: "2.1.0".to_string(),
1935                    yanked: true, // Yanked version
1936                    prerelease: false,
1937                },
1938            ],
1939        };
1940
1941        // Test with prefix that doesn't match any version
1942        let items =
1943            complete_versions_generic(&registry, "test-pkg", "3.0", &['^', '~', '=', '<', '>'])
1944                .await;
1945
1946        // Should fallback to showing all non-yanked versions
1947        assert_eq!(items.len(), 3);
1948        assert_eq!(items[0].label, "1.0.0 (latest)");
1949        assert_eq!(items[1].label, "1.1.0");
1950        assert_eq!(items[2].label, "2.0.0");
1951
1952        // Yanked version should not be included in fallback
1953        assert!(!items.iter().any(|item| item.label == "2.1.0"));
1954
1955        // Test with empty prefix (should show all non-yanked)
1956        let items = complete_versions_generic(&registry, "test-pkg", "", &[]).await;
1957
1958        assert_eq!(items.len(), 3);
1959        assert_eq!(items[0].label, "1.0.0 (latest)");
1960        assert_eq!(items[1].label, "1.1.0");
1961        assert_eq!(items[2].label, "2.0.0");
1962    }
1963
1964    #[tokio::test]
1965    async fn test_complete_versions_generic_filters_yanked_in_prefix_match() {
1966        let registry = MockRegistry {
1967            versions: vec![
1968                MockVersion {
1969                    version: "1.0.0".to_string(),
1970                    yanked: false,
1971                    prerelease: false,
1972                },
1973                MockVersion {
1974                    version: "1.0.1".to_string(),
1975                    yanked: true, // Yanked version
1976                    prerelease: false,
1977                },
1978                MockVersion {
1979                    version: "1.0.2".to_string(),
1980                    yanked: false,
1981                    prerelease: false,
1982                },
1983            ],
1984        };
1985
1986        // Test that yanked versions are filtered out even when prefix matches
1987        let items = complete_versions_generic(&registry, "test-pkg", "1.0", &[]).await;
1988
1989        // Should only include non-yanked versions
1990        assert_eq!(items.len(), 2);
1991        assert_eq!(items[0].label, "1.0.0 (latest)");
1992        assert_eq!(items[1].label, "1.0.2");
1993
1994        // Yanked version 1.0.1 should not be included
1995        assert!(!items.iter().any(|item| item.label == "1.0.1"));
1996    }
1997
1998    #[tokio::test]
1999    async fn test_complete_versions_generic_limit_5() {
2000        // Create more than 5 versions
2001        let versions: Vec<_> = (0..10)
2002            .map(|i| MockVersion {
2003                version: format!("1.0.{}", i),
2004                yanked: false,
2005                prerelease: false,
2006            })
2007            .collect();
2008
2009        let registry = MockRegistry { versions };
2010
2011        // Test that we only return 5 items
2012        let items = complete_versions_generic(&registry, "test-pkg", "1.0", &[]).await;
2013
2014        assert_eq!(items.len(), 5);
2015        assert_eq!(items[0].label, "1.0.0 (latest)");
2016        assert_eq!(items[4].label, "1.0.4");
2017    }
2018
2019    #[tokio::test]
2020    async fn test_complete_versions_generic_go_no_operators() {
2021        let registry = MockRegistry {
2022            versions: vec![
2023                MockVersion {
2024                    version: "v1.9.0".to_string(),
2025                    yanked: false,
2026                    prerelease: false,
2027                },
2028                MockVersion {
2029                    version: "v1.9.1".to_string(),
2030                    yanked: false,
2031                    prerelease: false,
2032                },
2033                MockVersion {
2034                    version: "v1.10.0".to_string(),
2035                    yanked: false,
2036                    prerelease: false,
2037                },
2038            ],
2039        };
2040
2041        // Go has no operators, so empty array
2042        let items =
2043            complete_versions_generic(&registry, "github.com/gin-gonic/gin", "v1.9", &[]).await;
2044
2045        assert_eq!(items.len(), 2);
2046        assert_eq!(items[0].label, "v1.9.0 (latest)");
2047        assert_eq!(items[1].label, "v1.9.1");
2048    }
2049
2050    // --- Feature completion detection tests ---
2051
2052    fn make_dep_with_features_range(
2053        name: &str,
2054        name_range: Range,
2055        features_range: Range,
2056    ) -> MockDependency {
2057        MockDependency {
2058            name: name.to_string(),
2059            name_range,
2060            version_range: None,
2061            features_range: Some(features_range),
2062        }
2063    }
2064
2065    #[test]
2066    fn test_detect_feature_context_inline() {
2067        // serde = { version = "1", features = ["derive", "std"] }
2068        // col:                                 36              52
2069        let features_range = Range {
2070            start: Position {
2071                line: 0,
2072                character: 36,
2073            },
2074            end: Position {
2075                line: 0,
2076                character: 52,
2077            },
2078        };
2079        let dep = make_dep_with_features_range(
2080            "serde",
2081            Range {
2082                start: Position {
2083                    line: 0,
2084                    character: 0,
2085                },
2086                end: Position {
2087                    line: 0,
2088                    character: 5,
2089                },
2090            },
2091            features_range,
2092        );
2093        let parse_result = MockParseResult {
2094            dependencies: vec![dep],
2095        };
2096
2097        let content = r#"serde = { version = "1", features = ["derive", "std"] }"#;
2098
2099        // Content: ...["derive",...  => '"' is at char 37, 'd'=38, 'e'=39, 'r'=40
2100        // Cursor after 'r' (insertion point) = character 41
2101        let position = Position {
2102            line: 0,
2103            character: 41,
2104        };
2105        let context = detect_completion_context(&parse_result, position, content);
2106        assert!(
2107            matches!(context, CompletionContext::Feature { ref package_name, ref prefix }
2108                if package_name == "serde" && prefix == "der"),
2109            "Expected Feature context with prefix 'der', got {context:?}"
2110        );
2111    }
2112
2113    #[test]
2114    fn test_detect_feature_context_empty_prefix() {
2115        // Cursor right after opening quote: features = ["|"]
2116        let features_range = Range {
2117            start: Position {
2118                line: 0,
2119                character: 11,
2120            },
2121            end: Position {
2122                line: 0,
2123                character: 15,
2124            },
2125        };
2126        let dep = make_dep_with_features_range(
2127            "tokio",
2128            Range {
2129                start: Position {
2130                    line: 0,
2131                    character: 0,
2132                },
2133                end: Position {
2134                    line: 0,
2135                    character: 5,
2136                },
2137            },
2138            features_range,
2139        );
2140        let parse_result = MockParseResult {
2141            dependencies: vec![dep],
2142        };
2143
2144        let content = r#"features = [""]"#;
2145        // Cursor between the two quotes: position character 13
2146        let position = Position {
2147            line: 0,
2148            character: 13,
2149        };
2150        let context = detect_completion_context(&parse_result, position, content);
2151        assert!(
2152            matches!(context, CompletionContext::Feature { ref package_name, ref prefix }
2153                if package_name == "tokio" && prefix.is_empty()),
2154            "Expected Feature context with empty prefix, got {context:?}"
2155        );
2156    }
2157
2158    #[test]
2159    fn test_detect_feature_context_second_item() {
2160        // features = ["full", "rt-|"]
2161        let features_range = Range {
2162            start: Position {
2163                line: 0,
2164                character: 11,
2165            },
2166            end: Position {
2167                line: 0,
2168                character: 28,
2169            },
2170        };
2171        let dep = make_dep_with_features_range(
2172            "tokio",
2173            Range {
2174                start: Position {
2175                    line: 0,
2176                    character: 0,
2177                },
2178                end: Position {
2179                    line: 0,
2180                    character: 5,
2181                },
2182            },
2183            features_range,
2184        );
2185        let parse_result = MockParseResult {
2186            dependencies: vec![dep],
2187        };
2188
2189        let content = r#"features = ["full", "rt-"]"#;
2190        // Cursor after "rt-": character 24
2191        let position = Position {
2192            line: 0,
2193            character: 24,
2194        };
2195        let context = detect_completion_context(&parse_result, position, content);
2196        assert!(
2197            matches!(context, CompletionContext::Feature { ref package_name, ref prefix }
2198                if package_name == "tokio" && prefix == "rt-"),
2199            "Expected Feature context with prefix 'rt-', got {context:?}"
2200        );
2201    }
2202
2203    #[test]
2204    fn test_detect_no_feature_context_outside_range() {
2205        let features_range = Range {
2206            start: Position {
2207                line: 2,
2208                character: 11,
2209            },
2210            end: Position {
2211                line: 2,
2212                character: 20,
2213            },
2214        };
2215        let dep = make_dep_with_features_range(
2216            "serde",
2217            Range {
2218                start: Position {
2219                    line: 2,
2220                    character: 0,
2221                },
2222                end: Position {
2223                    line: 2,
2224                    character: 5,
2225                },
2226            },
2227            features_range,
2228        );
2229        let parse_result = MockParseResult {
2230            dependencies: vec![dep],
2231        };
2232
2233        // Cursor is on line 0, not line 2 where features are
2234        let content = "[package]\nname = \"test\"\nfeatures = [\"full\"]";
2235        let position = Position {
2236            line: 0,
2237            character: 5,
2238        };
2239        let context = detect_completion_context(&parse_result, position, content);
2240        assert_eq!(context, CompletionContext::None);
2241    }
2242
2243    #[test]
2244    fn test_extract_feature_prefix_basic() {
2245        let content = r#"serde = { features = ["derive"] }"#;
2246        // '"' is at char 22, 'd'=23, 'e'=24, 'r'=25, 'i'=26
2247        // Cursor after 'i' (insertion point) = character 27
2248        let position = Position {
2249            line: 0,
2250            character: 27,
2251        };
2252        let prefix = extract_feature_prefix(content, position);
2253        assert_eq!(prefix, "deri");
2254    }
2255
2256    #[test]
2257    fn test_extract_feature_prefix_empty() {
2258        let content = r#"features = [""]"#;
2259        // Cursor between opening and closing quote at character 13
2260        let position = Position {
2261            line: 0,
2262            character: 13,
2263        };
2264        let prefix = extract_feature_prefix(content, position);
2265        assert_eq!(prefix, "");
2266    }
2267
2268    #[test]
2269    fn test_extract_feature_prefix_multiline() {
2270        let content = "features = [\n    \"rt-multi-thread\",\n    \"mac\"\n]";
2271        // Line 2: `    "mac"` — '"' at char 4, 'm'=5, 'a'=6, 'c'=7
2272        // Cursor after 'c' (insertion point) = character 8
2273        let position = Position {
2274            line: 2,
2275            character: 8,
2276        };
2277        let prefix = extract_feature_prefix(content, position);
2278        assert_eq!(prefix, "mac");
2279    }
2280
2281    #[test]
2282    fn test_extract_feature_prefix_no_quote() {
2283        let content = "features = [\n    \n]";
2284        // Cursor on blank line inside array
2285        let position = Position {
2286            line: 1,
2287            character: 4,
2288        };
2289        let prefix = extract_feature_prefix(content, position);
2290        assert_eq!(prefix, "");
2291    }
2292
2293    #[test]
2294    fn test_extract_feature_prefix_between_items_no_quote() {
2295        // Cursor between a comma and the next opening quote: ["full", |]
2296        // After "full" the quote count is 2 (even) → not inside a string → empty prefix
2297        let content = r#"features = ["full", ]"#;
2298        // Cursor after ", " at character 19 (before `]`)
2299        let position = Position {
2300            line: 0,
2301            character: 19,
2302        };
2303        let prefix = extract_feature_prefix(content, position);
2304        assert_eq!(prefix, "");
2305    }
2306
2307    #[test]
2308    fn test_extract_feature_prefix_cursor_after_opening_bracket() {
2309        // Cursor right after `[`, before any quote: features = [|]
2310        let content = "features = []";
2311        let position = Position {
2312            line: 0,
2313            character: 12,
2314        };
2315        let prefix = extract_feature_prefix(content, position);
2316        assert_eq!(prefix, "");
2317    }
2318}