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        // TODO: Feature detection - ecosystem-specific, requires more context
142    }
143
144    CompletionContext::None
145}
146
147/// Checks if a position is within or at the end of a range.
148///
149/// LSP ranges are inclusive of start, exclusive of end.
150/// We also consider the position to be "in range" if it's immediately
151/// after the range end (for completion after typing).
152const fn position_in_range(position: Position, range: Range) -> bool {
153    // Before range start
154    if position.line < range.start.line {
155        return false;
156    }
157
158    if position.line == range.start.line && position.character < range.start.character {
159        return false;
160    }
161
162    // After range end (allow one position past for completion)
163    if position.line > range.end.line {
164        return false;
165    }
166
167    if position.line == range.end.line && position.character > range.end.character + 1 {
168        return false;
169    }
170
171    true
172}
173
174/// Converts UTF-16 offset to byte offset in a string.
175///
176/// LSP uses UTF-16 code units for character positions (for compatibility with
177/// JavaScript and other languages). This function converts from UTF-16 offset
178/// to byte offset for Rust string indexing.
179///
180/// # Arguments
181///
182/// * `s` - The string to index into
183/// * `utf16_offset` - UTF-16 code unit offset (from LSP Position.character)
184///
185/// # Returns
186///
187/// Byte offset if valid, `None` if the UTF-16 offset is out of bounds.
188///
189/// # Examples
190///
191/// ```
192/// # use deps_core::completion::utf16_to_byte_offset;
193/// // ASCII: UTF-16 offset equals byte offset
194/// assert_eq!(utf16_to_byte_offset("hello", 2), Some(2));
195///
196/// // Unicode: "日本語" - each char is 3 bytes but 1 UTF-16 code unit
197/// assert_eq!(utf16_to_byte_offset("日本語", 0), Some(0));
198/// assert_eq!(utf16_to_byte_offset("日本語", 1), Some(3));
199/// assert_eq!(utf16_to_byte_offset("日本語", 2), Some(6));
200///
201/// // Emoji: "😀" is 4 bytes but 2 UTF-16 code units (surrogate pair)
202/// assert_eq!(utf16_to_byte_offset("😀test", 2), Some(4));
203/// ```
204pub fn utf16_to_byte_offset(s: &str, utf16_offset: u32) -> Option<usize> {
205    let mut utf16_count = 0u32;
206    for (byte_idx, ch) in s.char_indices() {
207        if utf16_count >= utf16_offset {
208            return Some(byte_idx);
209        }
210        utf16_count += ch.len_utf16() as u32;
211    }
212    if utf16_count == utf16_offset {
213        return Some(s.len());
214    }
215    None
216}
217
218/// Extracts the prefix text from content at a position within a range.
219///
220/// This function finds the text from the start of the range up to the
221/// cursor position, excluding any quote characters.
222///
223/// # Arguments
224///
225/// * `content` - Full document content
226/// * `position` - Cursor position (LSP Position, 0-based line, UTF-16 character offset)
227/// * `range` - Range containing the token (name, version, etc.)
228///
229/// # Returns
230///
231/// The prefix string typed so far, with quotes and extra whitespace removed.
232///
233/// # Examples
234///
235/// ```no_run
236/// use deps_core::completion::extract_prefix;
237/// use tower_lsp_server::ls_types::{Position, Range};
238///
239/// let content = r#"serde = "1.0""#;
240/// let position = Position { line: 0, character: 11 }; // After "1."
241/// let range = Range {
242///     start: Position { line: 0, character: 9 },
243///     end: Position { line: 0, character: 13 },
244/// };
245///
246/// let prefix = extract_prefix(content, position, range);
247/// assert_eq!(prefix, "1.");
248/// ```
249pub fn extract_prefix(content: &str, position: Position, range: Range) -> String {
250    // Get the line at the position - use nth() instead of collecting all lines
251    let line = match content.lines().nth(position.line as usize) {
252        Some(l) => l,
253        None => return String::new(),
254    };
255
256    // Convert UTF-16 positions to byte offsets
257    let start_byte = if position.line == range.start.line {
258        match utf16_to_byte_offset(line, range.start.character) {
259            Some(offset) => offset,
260            None => return String::new(),
261        }
262    } else {
263        0
264    };
265
266    let cursor_byte = match utf16_to_byte_offset(line, position.character) {
267        Some(offset) => offset,
268        None => return String::new(),
269    };
270
271    // Safety: ensure byte offsets are within bounds
272    if start_byte > line.len() || cursor_byte > line.len() || start_byte > cursor_byte {
273        return String::new();
274    }
275
276    // Extract substring
277    let prefix = &line[start_byte..cursor_byte];
278
279    // Remove quotes and trim whitespace
280    prefix
281        .trim()
282        .trim_matches('"')
283        .trim_matches('\'')
284        .trim()
285        .to_string()
286}
287
288/// Builds a completion item for a package name.
289///
290/// Creates a properly formatted LSP CompletionItem with documentation,
291/// version information, and links to repository/docs.
292///
293/// # Arguments
294///
295/// * `metadata` - Package metadata from registry search
296/// * `insert_range` - LSP range where the completion should be inserted
297///
298/// # Returns
299///
300/// A complete `CompletionItem` ready to send to the LSP client.
301///
302/// # Examples
303///
304/// ```no_run
305/// use deps_core::completion::build_package_completion;
306/// use tower_lsp_server::ls_types::Range;
307///
308/// # async fn example(metadata: &dyn deps_core::Metadata) {
309/// let range = Range::default(); // Use actual range from context
310/// let item = build_package_completion(metadata, range);
311/// assert_eq!(item.label, metadata.name());
312/// # }
313/// ```
314pub fn build_package_completion(metadata: &dyn Metadata, insert_range: Range) -> CompletionItem {
315    let name = metadata.name();
316    let latest = metadata.latest_version();
317
318    // Build markdown documentation
319    let mut doc_parts = vec![format!("**{}** v{}", name, latest)];
320
321    if let Some(desc) = metadata.description() {
322        doc_parts.push(String::new()); // Empty line
323        let truncated = if desc.len() > 200 {
324            let mut end = 200;
325            while end > 0 && !desc.is_char_boundary(end) {
326                end -= 1;
327            }
328            format!("{}...", &desc[..end])
329        } else {
330            desc.to_string()
331        };
332        doc_parts.push(truncated);
333    }
334
335    // Add links section if we have any links
336    let mut links = Vec::new();
337    if let Some(repo) = metadata.repository() {
338        links.push(format!("[Repository]({})", repo));
339    }
340    if let Some(docs) = metadata.documentation() {
341        links.push(format!("[Documentation]({})", docs));
342    }
343
344    if !links.is_empty() {
345        doc_parts.push(String::new()); // Empty line
346        doc_parts.push(links.join(" | "));
347    }
348
349    CompletionItem {
350        label: name.to_string(),
351        kind: Some(CompletionItemKind::MODULE),
352        detail: Some(format!("v{}", latest)),
353        documentation: Some(Documentation::MarkupContent(MarkupContent {
354            kind: MarkupKind::Markdown,
355            value: doc_parts.join("\n"),
356        })),
357        insert_text: Some(name.to_string()),
358        text_edit: Some(CompletionTextEdit::Edit(TextEdit {
359            range: insert_range,
360            new_text: name.to_string(),
361        })),
362        sort_text: Some(name.to_string()),
363        filter_text: Some(name.to_string()),
364        ..Default::default()
365    }
366}
367
368/// Builds a completion item for a version string.
369///
370/// Creates a properly formatted LSP CompletionItem with version metadata
371/// in a simplified format matching Code Actions (Cmd+.) style.
372///
373/// # Arguments
374///
375/// * `display_item` - Version display metadata with label, description, and flags
376/// * `insert_range` - Optional LSP range where the completion should replace text.
377///   If `None`, the completion will insert at cursor position without replacing.
378///
379/// # Returns
380///
381/// A complete `CompletionItem` with simple index-based sorting and preselect.
382///
383/// # Format
384///
385/// - Label: `"version"` or `"version (latest)"` for the latest version
386/// - Detail: `"Update package_name to version"`
387/// - Preselect: `true` for latest version, `false` otherwise
388/// - Sort: Index-based (00000, 00001, etc.)
389///
390/// # Examples
391///
392/// ```no_run
393/// use deps_core::completion::{build_version_completion, VersionDisplayItem};
394/// use tower_lsp_server::ls_types::Range;
395///
396/// # async fn example(version: &dyn deps_core::Version) {
397/// // Without range - insert at cursor
398/// let display_item = VersionDisplayItem::new(version, "serde", 0, true);
399/// let item = build_version_completion(&display_item, None);
400/// assert_eq!(item.label, display_item.label);
401///
402/// // With range - replace existing text
403/// let range = Range::default();
404/// let item = build_version_completion(&display_item, Some(range));
405/// # }
406/// ```
407pub fn build_version_completion(
408    display_item: &VersionDisplayItem,
409    insert_range: Option<Range>,
410) -> CompletionItem {
411    // Simple index-based sorting (00000, 00001, etc.)
412    let sort_text = format!("{:05}", display_item.index);
413
414    CompletionItem {
415        label: display_item.label.clone(),
416        kind: Some(CompletionItemKind::VALUE),
417        detail: Some(display_item.description.clone()),
418        documentation: None,
419        insert_text: Some(display_item.version.clone()),
420        text_edit: insert_range.map(|range| {
421            CompletionTextEdit::Edit(TextEdit {
422                range,
423                new_text: display_item.version.clone(),
424            })
425        }),
426        sort_text: Some(sort_text),
427        preselect: Some(display_item.is_latest),
428        ..Default::default()
429    }
430}
431
432/// Display metadata for a single version in LSP responses.
433///
434/// Captures common formatting logic shared between completion items and code actions.
435#[derive(Debug, Clone)]
436pub struct VersionDisplayItem {
437    /// Raw version string (e.g., "1.0.0")
438    pub version: String,
439    /// Display label with "(latest)" suffix for first item
440    pub label: String,
441    /// Action description (e.g., "Update serde to 1.0.0")
442    pub description: String,
443    /// Zero-based index for sorting
444    pub index: usize,
445    /// True if this is the latest non-yanked version
446    pub is_latest: bool,
447}
448
449impl VersionDisplayItem {
450    /// Creates a display item from version metadata.
451    pub fn new(version: &dyn Version, package_name: &str, index: usize, is_latest: bool) -> Self {
452        let version_str = version.version_string();
453        let label = if is_latest {
454            format!("{} (latest)", version_str)
455        } else {
456            version_str.to_string()
457        };
458        let description = format!("Update {} to {}", package_name, version_str);
459
460        Self {
461            version: version_str.to_string(),
462            label,
463            description,
464            index,
465            is_latest,
466        }
467    }
468}
469
470/// Filters and formats versions for LSP display.
471///
472/// Returns up to 5 non-yanked versions with display metadata.
473pub fn prepare_version_display_items<V: AsRef<dyn Version>>(
474    versions: &[V],
475    package_name: &str,
476) -> Vec<VersionDisplayItem> {
477    versions
478        .iter()
479        .map(|v| v.as_ref())
480        .filter(|v| !v.is_yanked())
481        .take(MAX_COMPLETION_VERSIONS)
482        .enumerate()
483        .map(|(index, version)| VersionDisplayItem::new(version, package_name, index, index == 0))
484        .collect()
485}
486
487/// Builds a completion item for a feature flag.
488///
489/// Creates a properly formatted LSP CompletionItem for feature flag names.
490/// Only applicable to ecosystems that support features (e.g., Cargo).
491///
492/// # Arguments
493///
494/// * `feature_name` - Name of the feature flag
495/// * `package_name` - Name of the package this feature belongs to
496/// * `insert_range` - LSP range where the completion should be inserted
497///
498/// # Returns
499///
500/// A complete `CompletionItem` for the feature flag.
501///
502/// # Examples
503///
504/// ```no_run
505/// use deps_core::completion::build_feature_completion;
506/// use tower_lsp_server::ls_types::Range;
507///
508/// let range = Range::default();
509/// let item = build_feature_completion("derive", "serde", range);
510/// assert_eq!(item.label, "derive");
511/// ```
512pub fn build_feature_completion(
513    feature_name: &str,
514    package_name: &str,
515    insert_range: Range,
516) -> CompletionItem {
517    CompletionItem {
518        label: feature_name.to_string(),
519        kind: Some(CompletionItemKind::PROPERTY),
520        detail: Some(format!("Feature of {}", package_name)),
521        documentation: None,
522        insert_text: Some(feature_name.to_string()),
523        text_edit: Some(CompletionTextEdit::Edit(TextEdit {
524            range: insert_range,
525            new_text: feature_name.to_string(),
526        })),
527        sort_text: Some(feature_name.to_string()),
528        ..Default::default()
529    }
530}
531
532/// Maximum number of version completions to show (matches Code Actions limit).
533const MAX_COMPLETION_VERSIONS: usize = 5;
534
535/// Generic version completion logic used by all ecosystems.
536///
537/// Filters versions by prefix (stripping ecosystem-specific operators),
538/// hides yanked/deprecated versions, returns up to 5 completion items.
539///
540/// # Arguments
541///
542/// * `registry` - Package registry to fetch versions from
543/// * `package_name` - Name of the package
544/// * `prefix` - Partial version string typed by user (may include operators)
545/// * `operator_chars` - Ecosystem-specific version operators to strip (e.g., `&['^', '~']`)
546///
547/// # Returns
548///
549/// Up to 5 completion items for non-yanked versions, filtered by prefix.
550/// If no versions match the prefix, returns up to 5 non-yanked versions.
551/// The first item (latest version) is marked with "(latest)" suffix and preselected.
552///
553/// # Examples
554///
555/// ```no_run
556/// use deps_core::completion::complete_versions_generic;
557///
558/// # async fn example(registry: &dyn deps_core::Registry) {
559/// // Cargo: strip ^, ~, =, <, > operators
560/// let items = complete_versions_generic(
561///     registry,
562///     "serde",
563///     "^1.0",
564///     &['^', '~', '=', '<', '>'],
565/// ).await;
566///
567/// // Go: no operators to strip
568/// let items = complete_versions_generic(
569///     registry,
570///     "github.com/gin-gonic/gin",
571///     "v1.9",
572///     &[],
573/// ).await;
574/// # }
575/// ```
576pub async fn complete_versions_generic(
577    registry: &dyn crate::Registry,
578    package_name: &str,
579    prefix: &str,
580    operator_chars: &[char],
581) -> Vec<CompletionItem> {
582    let versions = match registry.get_versions(package_name).await {
583        Ok(v) => v,
584        Err(e) => {
585            tracing::warn!("Failed to fetch versions for '{}': {}", package_name, e);
586            return vec![];
587        }
588    };
589
590    let clean_prefix = prefix.trim_start_matches(operator_chars).trim();
591
592    // Filter versions by prefix first
593    let filtered_versions: Vec<_> = versions
594        .iter()
595        .filter(|v| v.version_string().starts_with(clean_prefix))
596        .collect();
597
598    // Use filtered or all versions, prepare_version_display_items will handle yanked filtering
599    let display_items = if filtered_versions.is_empty() {
600        prepare_version_display_items(&versions, package_name)
601    } else {
602        prepare_version_display_items(&filtered_versions, package_name)
603    };
604
605    // Don't provide text_edit range - let LSP client insert at cursor position
606    display_items
607        .iter()
608        .map(|item| build_version_completion(item, None))
609        .collect()
610}
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615    use std::any::Any;
616
617    // Mock implementations for testing
618
619    struct MockDependency {
620        name: String,
621        name_range: Range,
622        version_range: Option<Range>,
623    }
624
625    impl crate::ecosystem::Dependency for MockDependency {
626        fn name(&self) -> &str {
627            &self.name
628        }
629
630        fn name_range(&self) -> Range {
631            self.name_range
632        }
633
634        fn version_requirement(&self) -> Option<&str> {
635            Some("1.0")
636        }
637
638        fn version_range(&self) -> Option<Range> {
639            self.version_range
640        }
641
642        fn source(&self) -> crate::parser::DependencySource {
643            crate::parser::DependencySource::Registry
644        }
645
646        fn as_any(&self) -> &dyn Any {
647            self
648        }
649    }
650
651    struct MockParseResult {
652        dependencies: Vec<MockDependency>,
653    }
654
655    impl ParseResult for MockParseResult {
656        fn dependencies(&self) -> Vec<&dyn crate::ecosystem::Dependency> {
657            self.dependencies
658                .iter()
659                .map(|d| d as &dyn crate::ecosystem::Dependency)
660                .collect()
661        }
662
663        fn workspace_root(&self) -> Option<&std::path::Path> {
664            None
665        }
666
667        fn uri(&self) -> &tower_lsp_server::ls_types::Uri {
668            // Create a dummy URL for testing
669            static URL_STR: &str = "file:///test/Cargo.toml";
670            static URL: once_cell::sync::Lazy<tower_lsp_server::ls_types::Uri> =
671                once_cell::sync::Lazy::new(|| URL_STR.parse().unwrap());
672            &URL
673        }
674
675        fn as_any(&self) -> &dyn Any {
676            self
677        }
678    }
679
680    struct MockVersion {
681        version: String,
682        yanked: bool,
683        prerelease: bool,
684    }
685
686    impl crate::registry::Version for MockVersion {
687        fn version_string(&self) -> &str {
688            &self.version
689        }
690
691        fn is_yanked(&self) -> bool {
692            self.yanked
693        }
694
695        fn is_prerelease(&self) -> bool {
696            self.prerelease
697        }
698
699        fn as_any(&self) -> &dyn Any {
700            self
701        }
702    }
703
704    struct MockMetadata {
705        name: String,
706        description: Option<String>,
707        repository: Option<String>,
708        documentation: Option<String>,
709        latest_version: String,
710    }
711
712    impl crate::registry::Metadata for MockMetadata {
713        fn name(&self) -> &str {
714            &self.name
715        }
716
717        fn description(&self) -> Option<&str> {
718            self.description.as_deref()
719        }
720
721        fn repository(&self) -> Option<&str> {
722            self.repository.as_deref()
723        }
724
725        fn documentation(&self) -> Option<&str> {
726            self.documentation.as_deref()
727        }
728
729        fn latest_version(&self) -> &str {
730            &self.latest_version
731        }
732
733        fn as_any(&self) -> &dyn Any {
734            self
735        }
736    }
737
738    struct MockRegistry {
739        versions: Vec<MockVersion>,
740    }
741
742    #[async_trait::async_trait]
743    impl crate::Registry for MockRegistry {
744        async fn get_versions(
745            &self,
746            _package_name: &str,
747        ) -> crate::error::Result<Vec<Box<dyn crate::Version>>> {
748            Ok(self
749                .versions
750                .iter()
751                .map(|v| {
752                    Box::new(MockVersion {
753                        version: v.version.clone(),
754                        yanked: v.yanked,
755                        prerelease: v.prerelease,
756                    }) as Box<dyn crate::Version>
757                })
758                .collect())
759        }
760
761        async fn get_latest_matching(
762            &self,
763            _name: &str,
764            _req: &str,
765        ) -> crate::error::Result<Option<Box<dyn crate::Version>>> {
766            Ok(None)
767        }
768
769        async fn search(
770            &self,
771            _query: &str,
772            _limit: usize,
773        ) -> crate::error::Result<Vec<Box<dyn crate::Metadata>>> {
774            Ok(vec![])
775        }
776
777        fn package_url(&self, _name: &str) -> String {
778            String::new()
779        }
780
781        fn as_any(&self) -> &dyn Any {
782            self
783        }
784    }
785
786    // Context detection tests
787
788    #[test]
789    fn test_detect_package_name_context_at_start() {
790        let parse_result = MockParseResult {
791            dependencies: vec![MockDependency {
792                name: "serde".to_string(),
793                name_range: Range {
794                    start: Position {
795                        line: 0,
796                        character: 0,
797                    },
798                    end: Position {
799                        line: 0,
800                        character: 5,
801                    },
802                },
803                version_range: None,
804            }],
805        };
806
807        let content = "serde";
808        let position = Position {
809            line: 0,
810            character: 0,
811        };
812
813        let context = detect_completion_context(&parse_result, position, content);
814
815        match context {
816            CompletionContext::PackageName { prefix } => {
817                assert_eq!(prefix, "");
818            }
819            _ => panic!("Expected PackageName context, got {:?}", context),
820        }
821    }
822
823    #[test]
824    fn test_detect_package_name_context_partial() {
825        let parse_result = MockParseResult {
826            dependencies: vec![MockDependency {
827                name: "serde".to_string(),
828                name_range: Range {
829                    start: Position {
830                        line: 0,
831                        character: 0,
832                    },
833                    end: Position {
834                        line: 0,
835                        character: 5,
836                    },
837                },
838                version_range: None,
839            }],
840        };
841
842        let content = "serde";
843        let position = Position {
844            line: 0,
845            character: 3,
846        };
847
848        let context = detect_completion_context(&parse_result, position, content);
849
850        match context {
851            CompletionContext::PackageName { prefix } => {
852                assert_eq!(prefix, "ser");
853            }
854            _ => panic!("Expected PackageName context, got {:?}", context),
855        }
856    }
857
858    #[test]
859    fn test_detect_version_context() {
860        let parse_result = MockParseResult {
861            dependencies: vec![MockDependency {
862                name: "serde".to_string(),
863                name_range: Range {
864                    start: Position {
865                        line: 0,
866                        character: 0,
867                    },
868                    end: Position {
869                        line: 0,
870                        character: 5,
871                    },
872                },
873                version_range: Some(Range {
874                    start: Position {
875                        line: 0,
876                        character: 9,
877                    },
878                    end: Position {
879                        line: 0,
880                        character: 14,
881                    },
882                }),
883            }],
884        };
885
886        let content = r#"serde = "1.0.1""#;
887        let position = Position {
888            line: 0,
889            character: 11,
890        };
891
892        let context = detect_completion_context(&parse_result, position, content);
893
894        match context {
895            CompletionContext::Version {
896                package_name,
897                prefix,
898            } => {
899                assert_eq!(package_name, "serde");
900                assert_eq!(prefix, "1.");
901            }
902            _ => panic!("Expected Version context, got {:?}", context),
903        }
904    }
905
906    #[test]
907    fn test_detect_no_context_before_dependencies() {
908        let parse_result = MockParseResult {
909            dependencies: vec![MockDependency {
910                name: "serde".to_string(),
911                name_range: Range {
912                    start: Position {
913                        line: 5,
914                        character: 0,
915                    },
916                    end: Position {
917                        line: 5,
918                        character: 5,
919                    },
920                },
921                version_range: None,
922            }],
923        };
924
925        let content = "[dependencies]\nserde";
926        let position = Position {
927            line: 0,
928            character: 10,
929        };
930
931        let context = detect_completion_context(&parse_result, position, content);
932
933        assert_eq!(context, CompletionContext::None);
934    }
935
936    #[test]
937    fn test_detect_no_context_invalid_position() {
938        let parse_result = MockParseResult {
939            dependencies: vec![],
940        };
941
942        let content = "";
943        let position = Position {
944            line: 100,
945            character: 100,
946        };
947
948        let context = detect_completion_context(&parse_result, position, content);
949
950        assert_eq!(context, CompletionContext::None);
951    }
952
953    // Prefix extraction tests
954
955    #[test]
956    fn test_extract_prefix_at_start() {
957        let content = "serde";
958        let position = Position {
959            line: 0,
960            character: 0,
961        };
962        let range = Range {
963            start: Position {
964                line: 0,
965                character: 0,
966            },
967            end: Position {
968                line: 0,
969                character: 5,
970            },
971        };
972
973        let prefix = extract_prefix(content, position, range);
974        assert_eq!(prefix, "");
975    }
976
977    #[test]
978    fn test_extract_prefix_partial() {
979        let content = "serde";
980        let position = Position {
981            line: 0,
982            character: 3,
983        };
984        let range = Range {
985            start: Position {
986                line: 0,
987                character: 0,
988            },
989            end: Position {
990                line: 0,
991                character: 5,
992            },
993        };
994
995        let prefix = extract_prefix(content, position, range);
996        assert_eq!(prefix, "ser");
997    }
998
999    #[test]
1000    fn test_extract_prefix_with_quotes() {
1001        let content = r#"serde = "1.0""#;
1002        let position = Position {
1003            line: 0,
1004            character: 11,
1005        };
1006        let range = Range {
1007            start: Position {
1008                line: 0,
1009                character: 9,
1010            },
1011            end: Position {
1012                line: 0,
1013                character: 13,
1014            },
1015        };
1016
1017        let prefix = extract_prefix(content, position, range);
1018        assert_eq!(prefix, "1.");
1019    }
1020
1021    #[test]
1022    fn test_extract_prefix_empty() {
1023        let content = r#"serde = """#;
1024        let position = Position {
1025            line: 0,
1026            character: 9,
1027        };
1028        let range = Range {
1029            start: Position {
1030                line: 0,
1031                character: 9,
1032            },
1033            end: Position {
1034                line: 0,
1035                character: 11,
1036            },
1037        };
1038
1039        let prefix = extract_prefix(content, position, range);
1040        assert_eq!(prefix, "");
1041    }
1042
1043    #[test]
1044    fn test_extract_prefix_version_with_operator() {
1045        let content = r#"serde = "^1.0""#;
1046        let position = Position {
1047            line: 0,
1048            character: 12,
1049        };
1050        let range = Range {
1051            start: Position {
1052                line: 0,
1053                character: 9,
1054            },
1055            end: Position {
1056                line: 0,
1057                character: 14,
1058            },
1059        };
1060
1061        let prefix = extract_prefix(content, position, range);
1062        assert_eq!(prefix, "^1.");
1063    }
1064
1065    // CompletionItem builder tests
1066
1067    #[test]
1068    fn test_build_package_completion_full() {
1069        let metadata = MockMetadata {
1070            name: "serde".to_string(),
1071            description: Some("Serialization framework".to_string()),
1072            repository: Some("https://github.com/serde-rs/serde".to_string()),
1073            documentation: Some("https://docs.rs/serde".to_string()),
1074            latest_version: "1.0.214".to_string(),
1075        };
1076
1077        let range = Range::default();
1078        let item = build_package_completion(&metadata, range);
1079
1080        assert_eq!(item.label, "serde");
1081        assert_eq!(item.kind, Some(CompletionItemKind::MODULE));
1082        assert_eq!(item.detail, Some("v1.0.214".to_string()));
1083        assert!(matches!(
1084            item.documentation,
1085            Some(Documentation::MarkupContent(_))
1086        ));
1087
1088        if let Some(Documentation::MarkupContent(content)) = item.documentation {
1089            assert!(content.value.contains("**serde** v1.0.214"));
1090            assert!(content.value.contains("Serialization framework"));
1091            assert!(content.value.contains("Repository"));
1092            assert!(content.value.contains("Documentation"));
1093        }
1094    }
1095
1096    #[test]
1097    fn test_build_package_completion_minimal() {
1098        let metadata = MockMetadata {
1099            name: "test-pkg".to_string(),
1100            description: None,
1101            repository: None,
1102            documentation: None,
1103            latest_version: "0.1.0".to_string(),
1104        };
1105
1106        let range = Range::default();
1107        let item = build_package_completion(&metadata, range);
1108
1109        assert_eq!(item.label, "test-pkg");
1110        assert_eq!(item.detail, Some("v0.1.0".to_string()));
1111
1112        if let Some(Documentation::MarkupContent(content)) = item.documentation {
1113            assert!(content.value.contains("**test-pkg** v0.1.0"));
1114            assert!(!content.value.contains("Repository"));
1115        }
1116    }
1117
1118    #[test]
1119    fn test_build_version_completion_stable() {
1120        let version = MockVersion {
1121            version: "1.0.0".to_string(),
1122            yanked: false,
1123            prerelease: false,
1124        };
1125
1126        let display_item = VersionDisplayItem::new(&version, "serde", 0, false);
1127        let item = build_version_completion(&display_item, None);
1128
1129        assert_eq!(item.label, "1.0.0");
1130        assert_eq!(item.kind, Some(CompletionItemKind::VALUE));
1131        assert_eq!(item.detail, Some("Update serde to 1.0.0".to_string()));
1132        assert_eq!(item.documentation, None);
1133        assert_eq!(item.preselect, Some(false));
1134        assert_eq!(item.sort_text, Some("00000".to_string()));
1135        assert_eq!(item.text_edit, None); // No text_edit when range is None
1136    }
1137
1138    #[test]
1139    fn test_build_version_completion_latest() {
1140        let version = MockVersion {
1141            version: "1.0.0".to_string(),
1142            yanked: false,
1143            prerelease: false,
1144        };
1145
1146        let display_item = VersionDisplayItem::new(&version, "serde", 0, true);
1147        let item = build_version_completion(&display_item, None);
1148
1149        assert_eq!(item.label, "1.0.0 (latest)");
1150        assert_eq!(item.kind, Some(CompletionItemKind::VALUE));
1151        assert_eq!(item.detail, Some("Update serde to 1.0.0".to_string()));
1152        assert_eq!(item.documentation, None);
1153        assert_eq!(item.preselect, Some(true));
1154        assert_eq!(item.sort_text, Some("00000".to_string()));
1155        assert_eq!(item.text_edit, None); // No text_edit when range is None
1156    }
1157
1158    #[test]
1159    fn test_build_version_completion_not_latest() {
1160        let version = MockVersion {
1161            version: "0.9.0".to_string(),
1162            yanked: false,
1163            prerelease: false,
1164        };
1165
1166        let display_item = VersionDisplayItem::new(&version, "tokio", 1, false);
1167        let item = build_version_completion(&display_item, None);
1168
1169        assert_eq!(item.label, "0.9.0");
1170        assert_eq!(item.detail, Some("Update tokio to 0.9.0".to_string()));
1171        assert_eq!(item.documentation, None);
1172        assert_eq!(item.preselect, Some(false));
1173        assert_eq!(item.sort_text, Some("00001".to_string()));
1174        assert_eq!(item.text_edit, None); // No text_edit when range is None
1175    }
1176
1177    #[test]
1178    fn test_build_version_completion_sort_order() {
1179        let v1 = MockVersion {
1180            version: "1.0.0".to_string(),
1181            yanked: false,
1182            prerelease: false,
1183        };
1184        let v2 = MockVersion {
1185            version: "0.9.0".to_string(),
1186            yanked: false,
1187            prerelease: false,
1188        };
1189        let v3 = MockVersion {
1190            version: "0.8.0".to_string(),
1191            yanked: false,
1192            prerelease: false,
1193        };
1194
1195        let display_item1 = VersionDisplayItem::new(&v1, "test", 0, true);
1196        let display_item2 = VersionDisplayItem::new(&v2, "test", 1, false);
1197        let display_item3 = VersionDisplayItem::new(&v3, "test", 2, false);
1198        let item1 = build_version_completion(&display_item1, None);
1199        let item2 = build_version_completion(&display_item2, None);
1200        let item3 = build_version_completion(&display_item3, None);
1201
1202        // Simple index-based sorting
1203        assert_eq!(item1.sort_text.as_ref().unwrap(), "00000");
1204        assert_eq!(item2.sort_text.as_ref().unwrap(), "00001");
1205        assert_eq!(item3.sort_text.as_ref().unwrap(), "00002");
1206
1207        // First item should be preselected
1208        assert_eq!(item1.preselect, Some(true));
1209        assert_eq!(item2.preselect, Some(false));
1210        assert_eq!(item3.preselect, Some(false));
1211    }
1212
1213    #[test]
1214    fn test_version_completion_semantic_ordering() {
1215        let versions = [
1216            MockVersion {
1217                version: "0.14.0".to_string(),
1218                yanked: false,
1219                prerelease: false,
1220            },
1221            MockVersion {
1222                version: "0.8.0".to_string(),
1223                yanked: false,
1224                prerelease: false,
1225            },
1226            MockVersion {
1227                version: "0.2.0".to_string(),
1228                yanked: false,
1229                prerelease: false,
1230            },
1231        ];
1232
1233        let items: Vec<_> = versions
1234            .iter()
1235            .enumerate()
1236            .map(|(idx, v)| {
1237                let display_item = VersionDisplayItem::new(v, "test", idx, idx == 0);
1238                build_version_completion(&display_item, None)
1239            })
1240            .collect();
1241
1242        assert_eq!(items[0].sort_text.as_ref().unwrap(), "00000");
1243        assert_eq!(items[1].sort_text.as_ref().unwrap(), "00001");
1244        assert_eq!(items[2].sort_text.as_ref().unwrap(), "00002");
1245
1246        let mut sorted_items = items;
1247        sorted_items.sort_by(|a, b| {
1248            a.sort_text
1249                .as_ref()
1250                .unwrap()
1251                .cmp(b.sort_text.as_ref().unwrap())
1252        });
1253
1254        assert_eq!(sorted_items[0].label, "0.14.0 (latest)");
1255        assert_eq!(sorted_items[1].label, "0.8.0");
1256        assert_eq!(sorted_items[2].label, "0.2.0");
1257    }
1258
1259    #[test]
1260    fn test_version_completion_index_ordering() {
1261        let versions = ["1.20.0", "1.9.0", "1.2.0", "0.99.0", "0.50.0"];
1262
1263        let items: Vec<_> = versions
1264            .iter()
1265            .enumerate()
1266            .map(|(idx, ver)| {
1267                let v = MockVersion {
1268                    version: ver.to_string(),
1269                    yanked: false,
1270                    prerelease: false,
1271                };
1272                let display_item = VersionDisplayItem::new(&v, "test", idx, idx == 0);
1273                build_version_completion(&display_item, None)
1274            })
1275            .collect();
1276
1277        assert_eq!(items[0].sort_text.as_ref().unwrap(), "00000");
1278        assert_eq!(items[1].sort_text.as_ref().unwrap(), "00001");
1279        assert_eq!(items[2].sort_text.as_ref().unwrap(), "00002");
1280        assert_eq!(items[3].sort_text.as_ref().unwrap(), "00003");
1281        assert_eq!(items[4].sort_text.as_ref().unwrap(), "00004");
1282
1283        let mut sorted_items = items;
1284        sorted_items.sort_by(|a, b| {
1285            a.sort_text
1286                .as_ref()
1287                .unwrap()
1288                .cmp(b.sort_text.as_ref().unwrap())
1289        });
1290
1291        assert_eq!(sorted_items[0].label, "1.20.0 (latest)");
1292        assert_eq!(sorted_items[1].label, "1.9.0");
1293        assert_eq!(sorted_items[2].label, "1.2.0");
1294        assert_eq!(sorted_items[3].label, "0.99.0");
1295        assert_eq!(sorted_items[4].label, "0.50.0");
1296    }
1297
1298    #[test]
1299    fn test_version_display_item_latest() {
1300        let version = MockVersion {
1301            version: "1.0.0".to_string(),
1302            yanked: false,
1303            prerelease: false,
1304        };
1305
1306        let item = VersionDisplayItem::new(&version, "serde", 0, true);
1307
1308        assert_eq!(item.version, "1.0.0");
1309        assert_eq!(item.label, "1.0.0 (latest)");
1310        assert_eq!(item.description, "Update serde to 1.0.0");
1311        assert_eq!(item.index, 0);
1312        assert!(item.is_latest);
1313    }
1314
1315    #[test]
1316    fn test_version_display_item_not_latest() {
1317        let version = MockVersion {
1318            version: "0.9.0".to_string(),
1319            yanked: false,
1320            prerelease: false,
1321        };
1322
1323        let item = VersionDisplayItem::new(&version, "tokio", 1, false);
1324
1325        assert_eq!(item.version, "0.9.0");
1326        assert_eq!(item.label, "0.9.0");
1327        assert_eq!(item.description, "Update tokio to 0.9.0");
1328        assert_eq!(item.index, 1);
1329        assert!(!item.is_latest);
1330    }
1331
1332    #[test]
1333    fn test_prepare_version_display_items_filters_yanked() {
1334        let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![
1335            std::sync::Arc::new(MockVersion {
1336                version: "1.0.0".to_string(),
1337                yanked: false,
1338                prerelease: false,
1339            }),
1340            std::sync::Arc::new(MockVersion {
1341                version: "0.9.0".to_string(),
1342                yanked: true,
1343                prerelease: false,
1344            }),
1345            std::sync::Arc::new(MockVersion {
1346                version: "0.8.0".to_string(),
1347                yanked: false,
1348                prerelease: false,
1349            }),
1350        ];
1351
1352        let items = prepare_version_display_items(&versions, "test");
1353
1354        assert_eq!(items.len(), 2);
1355        assert_eq!(items[0].version, "1.0.0");
1356        assert_eq!(items[0].label, "1.0.0 (latest)");
1357        assert!(items[0].is_latest);
1358        assert_eq!(items[1].version, "0.8.0");
1359        assert_eq!(items[1].label, "0.8.0");
1360        assert!(!items[1].is_latest);
1361    }
1362
1363    #[test]
1364    fn test_prepare_version_display_items_limits_to_5() {
1365        let versions: Vec<std::sync::Arc<dyn crate::Version>> = (0..10)
1366            .map(|i| {
1367                std::sync::Arc::new(MockVersion {
1368                    version: format!("1.0.{}", i),
1369                    yanked: false,
1370                    prerelease: false,
1371                }) as std::sync::Arc<dyn crate::Version>
1372            })
1373            .collect();
1374
1375        let items = prepare_version_display_items(&versions, "test");
1376
1377        assert_eq!(items.len(), 5);
1378        assert_eq!(items[0].version, "1.0.0");
1379        assert_eq!(items[0].label, "1.0.0 (latest)");
1380        assert_eq!(items[4].version, "1.0.4");
1381        assert_eq!(items[4].label, "1.0.4");
1382    }
1383
1384    #[test]
1385    fn test_prepare_version_display_items_empty() {
1386        let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![];
1387
1388        let items = prepare_version_display_items(&versions, "test");
1389
1390        assert_eq!(items.len(), 0);
1391    }
1392
1393    #[test]
1394    fn test_prepare_version_display_items_all_yanked() {
1395        let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![
1396            std::sync::Arc::new(MockVersion {
1397                version: "1.0.0".to_string(),
1398                yanked: true,
1399                prerelease: false,
1400            }),
1401            std::sync::Arc::new(MockVersion {
1402                version: "0.9.0".to_string(),
1403                yanked: true,
1404                prerelease: false,
1405            }),
1406        ];
1407
1408        let items = prepare_version_display_items(&versions, "test");
1409
1410        assert_eq!(items.len(), 0);
1411    }
1412
1413    #[test]
1414    fn test_build_feature_completion() {
1415        let range = Range::default();
1416        let item = build_feature_completion("derive", "serde", range);
1417
1418        assert_eq!(item.label, "derive");
1419        assert_eq!(item.kind, Some(CompletionItemKind::PROPERTY));
1420        assert_eq!(item.detail, Some("Feature of serde".to_string()));
1421        assert!(item.documentation.is_none());
1422        assert_eq!(item.sort_text, Some("derive".to_string()));
1423    }
1424
1425    #[test]
1426    fn test_position_in_range_within() {
1427        let range = Range {
1428            start: Position {
1429                line: 0,
1430                character: 5,
1431            },
1432            end: Position {
1433                line: 0,
1434                character: 10,
1435            },
1436        };
1437
1438        let position = Position {
1439            line: 0,
1440            character: 7,
1441        };
1442
1443        assert!(position_in_range(position, range));
1444    }
1445
1446    #[test]
1447    fn test_position_in_range_at_start() {
1448        let range = Range {
1449            start: Position {
1450                line: 0,
1451                character: 5,
1452            },
1453            end: Position {
1454                line: 0,
1455                character: 10,
1456            },
1457        };
1458
1459        let position = Position {
1460            line: 0,
1461            character: 5,
1462        };
1463
1464        assert!(position_in_range(position, range));
1465    }
1466
1467    #[test]
1468    fn test_position_in_range_at_end() {
1469        let range = Range {
1470            start: Position {
1471                line: 0,
1472                character: 5,
1473            },
1474            end: Position {
1475                line: 0,
1476                character: 10,
1477            },
1478        };
1479
1480        let position = Position {
1481            line: 0,
1482            character: 10,
1483        };
1484
1485        assert!(position_in_range(position, range));
1486    }
1487
1488    #[test]
1489    fn test_position_in_range_one_past_end() {
1490        let range = Range {
1491            start: Position {
1492                line: 0,
1493                character: 5,
1494            },
1495            end: Position {
1496                line: 0,
1497                character: 10,
1498            },
1499        };
1500
1501        // Allow one character past end for completion
1502        let position = Position {
1503            line: 0,
1504            character: 11,
1505        };
1506
1507        assert!(position_in_range(position, range));
1508    }
1509
1510    #[test]
1511    fn test_position_in_range_before() {
1512        let range = Range {
1513            start: Position {
1514                line: 0,
1515                character: 5,
1516            },
1517            end: Position {
1518                line: 0,
1519                character: 10,
1520            },
1521        };
1522
1523        let position = Position {
1524            line: 0,
1525            character: 4,
1526        };
1527
1528        assert!(!position_in_range(position, range));
1529    }
1530
1531    #[test]
1532    fn test_position_in_range_after() {
1533        let range = Range {
1534            start: Position {
1535                line: 0,
1536                character: 5,
1537            },
1538            end: Position {
1539                line: 0,
1540                character: 10,
1541            },
1542        };
1543
1544        let position = Position {
1545            line: 0,
1546            character: 12,
1547        };
1548
1549        assert!(!position_in_range(position, range));
1550    }
1551
1552    // UTF-16 to byte offset conversion tests
1553
1554    #[test]
1555    fn test_utf16_to_byte_offset_ascii() {
1556        let s = "hello";
1557        assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
1558        assert_eq!(utf16_to_byte_offset(s, 2), Some(2));
1559        assert_eq!(utf16_to_byte_offset(s, 5), Some(5));
1560    }
1561
1562    #[test]
1563    fn test_utf16_to_byte_offset_multibyte() {
1564        // "日本語" - each character is 3 bytes, 1 UTF-16 code unit
1565        let s = "日本語";
1566        assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
1567        assert_eq!(utf16_to_byte_offset(s, 1), Some(3));
1568        assert_eq!(utf16_to_byte_offset(s, 2), Some(6));
1569        assert_eq!(utf16_to_byte_offset(s, 3), Some(9));
1570    }
1571
1572    #[test]
1573    fn test_utf16_to_byte_offset_emoji() {
1574        // "😀" is 4 bytes but 2 UTF-16 code units (surrogate pair)
1575        let s = "😀test";
1576        assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
1577        assert_eq!(utf16_to_byte_offset(s, 2), Some(4)); // After emoji
1578        assert_eq!(utf16_to_byte_offset(s, 3), Some(5)); // After 't'
1579    }
1580
1581    #[test]
1582    fn test_utf16_to_byte_offset_mixed() {
1583        // Mix of ASCII, multi-byte, and emoji
1584        let s = "hello 世界 😀!";
1585        assert_eq!(utf16_to_byte_offset(s, 0), Some(0)); // 'h'
1586        assert_eq!(utf16_to_byte_offset(s, 6), Some(6)); // '世'
1587        assert_eq!(utf16_to_byte_offset(s, 7), Some(9)); // '界'
1588        assert_eq!(utf16_to_byte_offset(s, 9), Some(13)); // '😀' (2 UTF-16 units)
1589        assert_eq!(utf16_to_byte_offset(s, 11), Some(17)); // '!'
1590    }
1591
1592    #[test]
1593    fn test_utf16_to_byte_offset_out_of_bounds() {
1594        let s = "hello";
1595        assert_eq!(utf16_to_byte_offset(s, 100), None);
1596    }
1597
1598    #[test]
1599    fn test_utf16_to_byte_offset_empty() {
1600        let s = "";
1601        assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
1602        assert_eq!(utf16_to_byte_offset(s, 1), None);
1603    }
1604
1605    // Unicode truncation tests
1606
1607    #[test]
1608    fn test_build_package_completion_long_description_ascii() {
1609        let long_desc = "a".repeat(250);
1610        let metadata = MockMetadata {
1611            name: "test-pkg".to_string(),
1612            description: Some(long_desc),
1613            repository: None,
1614            documentation: None,
1615            latest_version: "1.0.0".to_string(),
1616        };
1617
1618        let range = Range::default();
1619        let item = build_package_completion(&metadata, range);
1620
1621        if let Some(Documentation::MarkupContent(content)) = item.documentation {
1622            // Should be truncated to 200 chars + "..."
1623            let lines: Vec<_> = content.value.lines().collect();
1624            assert!(lines[2].ends_with("..."));
1625            assert!(lines[2].len() <= 203); // 200 + "..."
1626        } else {
1627            panic!("Expected MarkupContent documentation");
1628        }
1629    }
1630
1631    #[test]
1632    fn test_build_package_completion_long_description_unicode() {
1633        // Create description with Unicode chars at the boundary
1634        // Each '日' is 3 bytes, so 67 chars = 201 bytes
1635        let mut long_desc = String::new();
1636        for _ in 0..67 {
1637            long_desc.push('日');
1638        }
1639
1640        let metadata = MockMetadata {
1641            name: "test-pkg".to_string(),
1642            description: Some(long_desc),
1643            repository: None,
1644            documentation: None,
1645            latest_version: "1.0.0".to_string(),
1646        };
1647
1648        let range = Range::default();
1649        let item = build_package_completion(&metadata, range);
1650
1651        // Should not panic on truncation
1652        if let Some(Documentation::MarkupContent(content)) = item.documentation {
1653            let lines: Vec<_> = content.value.lines().collect();
1654            assert!(lines[2].ends_with("..."));
1655            // Truncation should happen at a char boundary
1656            assert!(lines[2].is_char_boundary(lines[2].len()));
1657        } else {
1658            panic!("Expected MarkupContent documentation");
1659        }
1660    }
1661
1662    #[test]
1663    fn test_build_package_completion_long_description_emoji() {
1664        // Emoji "😀" is 4 bytes each
1665        // 51 emoji = 204 bytes
1666        let long_desc = "😀".repeat(51);
1667
1668        let metadata = MockMetadata {
1669            name: "test-pkg".to_string(),
1670            description: Some(long_desc),
1671            repository: None,
1672            documentation: None,
1673            latest_version: "1.0.0".to_string(),
1674        };
1675
1676        let range = Range::default();
1677        let item = build_package_completion(&metadata, range);
1678
1679        // Should not panic on truncation
1680        if let Some(Documentation::MarkupContent(content)) = item.documentation {
1681            let lines: Vec<_> = content.value.lines().collect();
1682            assert!(lines[2].ends_with("..."));
1683            // Truncation should happen at a char boundary
1684            assert!(lines[2].is_char_boundary(lines[2].len()));
1685        } else {
1686            panic!("Expected MarkupContent documentation");
1687        }
1688    }
1689
1690    #[test]
1691    fn test_extract_prefix_unicode_package_name() {
1692        // Package name with Unicode characters
1693        let content = "日本語-crate = \"1.0\"";
1694        let position = Position {
1695            line: 0,
1696            character: 3, // UTF-16 offset after "日本語"
1697        };
1698        let range = Range {
1699            start: Position {
1700                line: 0,
1701                character: 0,
1702            },
1703            end: Position {
1704                line: 0,
1705                character: 10,
1706            },
1707        };
1708
1709        let prefix = extract_prefix(content, position, range);
1710        assert_eq!(prefix, "日本語");
1711    }
1712
1713    #[test]
1714    fn test_extract_prefix_emoji_in_content() {
1715        // Content with emoji (rare but should handle gracefully)
1716        let content = "emoji-😀-crate = \"1.0\"";
1717        let position = Position {
1718            line: 0,
1719            character: 8, // UTF-16 offset after "emoji-😀"
1720        };
1721        let range = Range {
1722            start: Position {
1723                line: 0,
1724                character: 0,
1725            },
1726            end: Position {
1727                line: 0,
1728                character: 14,
1729            },
1730        };
1731
1732        let prefix = extract_prefix(content, position, range);
1733        assert_eq!(prefix, "emoji-😀");
1734    }
1735
1736    // Generic version completion tests
1737
1738    #[tokio::test]
1739    async fn test_complete_versions_generic_operator_stripping() {
1740        let registry = MockRegistry {
1741            versions: vec![
1742                MockVersion {
1743                    version: "1.0.0".to_string(),
1744                    yanked: false,
1745                    prerelease: false,
1746                },
1747                MockVersion {
1748                    version: "1.0.1".to_string(),
1749                    yanked: false,
1750                    prerelease: false,
1751                },
1752                MockVersion {
1753                    version: "1.1.0".to_string(),
1754                    yanked: false,
1755                    prerelease: false,
1756                },
1757                MockVersion {
1758                    version: "2.0.0".to_string(),
1759                    yanked: false,
1760                    prerelease: false,
1761                },
1762            ],
1763        };
1764
1765        // Test with Cargo-style operators (^, ~, =, <, >)
1766        let items =
1767            complete_versions_generic(&registry, "test-pkg", "^1.0", &['^', '~', '=', '<', '>'])
1768                .await;
1769
1770        // Should return versions starting with "1.0" (after stripping ^)
1771        assert_eq!(items.len(), 2);
1772        assert_eq!(items[0].label, "1.0.0 (latest)");
1773        assert_eq!(items[1].label, "1.0.1");
1774
1775        // Test with tilde operator
1776        let items =
1777            complete_versions_generic(&registry, "test-pkg", "~1.1", &['^', '~', '=', '<', '>'])
1778                .await;
1779
1780        assert_eq!(items.len(), 1);
1781        assert_eq!(items[0].label, "1.1.0 (latest)");
1782
1783        // Test with equals operator
1784        let items =
1785            complete_versions_generic(&registry, "test-pkg", "=2.0", &['^', '~', '=', '<', '>'])
1786                .await;
1787
1788        assert_eq!(items.len(), 1);
1789        assert_eq!(items[0].label, "2.0.0 (latest)");
1790
1791        // Test with no operator (should work the same)
1792        let items =
1793            complete_versions_generic(&registry, "test-pkg", "1.0", &['^', '~', '=', '<', '>'])
1794                .await;
1795
1796        assert_eq!(items.len(), 2);
1797        assert_eq!(items[0].label, "1.0.0 (latest)");
1798        assert_eq!(items[1].label, "1.0.1");
1799    }
1800
1801    #[tokio::test]
1802    async fn test_complete_versions_generic_fallback_when_no_prefix_match() {
1803        let registry = MockRegistry {
1804            versions: vec![
1805                MockVersion {
1806                    version: "1.0.0".to_string(),
1807                    yanked: false,
1808                    prerelease: false,
1809                },
1810                MockVersion {
1811                    version: "1.1.0".to_string(),
1812                    yanked: false,
1813                    prerelease: false,
1814                },
1815                MockVersion {
1816                    version: "2.0.0".to_string(),
1817                    yanked: false,
1818                    prerelease: false,
1819                },
1820                MockVersion {
1821                    version: "2.1.0".to_string(),
1822                    yanked: true, // Yanked version
1823                    prerelease: false,
1824                },
1825            ],
1826        };
1827
1828        // Test with prefix that doesn't match any version
1829        let items =
1830            complete_versions_generic(&registry, "test-pkg", "3.0", &['^', '~', '=', '<', '>'])
1831                .await;
1832
1833        // Should fallback to showing all non-yanked versions
1834        assert_eq!(items.len(), 3);
1835        assert_eq!(items[0].label, "1.0.0 (latest)");
1836        assert_eq!(items[1].label, "1.1.0");
1837        assert_eq!(items[2].label, "2.0.0");
1838
1839        // Yanked version should not be included in fallback
1840        assert!(!items.iter().any(|item| item.label == "2.1.0"));
1841
1842        // Test with empty prefix (should show all non-yanked)
1843        let items = complete_versions_generic(&registry, "test-pkg", "", &[]).await;
1844
1845        assert_eq!(items.len(), 3);
1846        assert_eq!(items[0].label, "1.0.0 (latest)");
1847        assert_eq!(items[1].label, "1.1.0");
1848        assert_eq!(items[2].label, "2.0.0");
1849    }
1850
1851    #[tokio::test]
1852    async fn test_complete_versions_generic_filters_yanked_in_prefix_match() {
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: true, // Yanked version
1863                    prerelease: false,
1864                },
1865                MockVersion {
1866                    version: "1.0.2".to_string(),
1867                    yanked: false,
1868                    prerelease: false,
1869                },
1870            ],
1871        };
1872
1873        // Test that yanked versions are filtered out even when prefix matches
1874        let items = complete_versions_generic(&registry, "test-pkg", "1.0", &[]).await;
1875
1876        // Should only include non-yanked versions
1877        assert_eq!(items.len(), 2);
1878        assert_eq!(items[0].label, "1.0.0 (latest)");
1879        assert_eq!(items[1].label, "1.0.2");
1880
1881        // Yanked version 1.0.1 should not be included
1882        assert!(!items.iter().any(|item| item.label == "1.0.1"));
1883    }
1884
1885    #[tokio::test]
1886    async fn test_complete_versions_generic_limit_5() {
1887        // Create more than 5 versions
1888        let versions: Vec<_> = (0..10)
1889            .map(|i| MockVersion {
1890                version: format!("1.0.{}", i),
1891                yanked: false,
1892                prerelease: false,
1893            })
1894            .collect();
1895
1896        let registry = MockRegistry { versions };
1897
1898        // Test that we only return 5 items
1899        let items = complete_versions_generic(&registry, "test-pkg", "1.0", &[]).await;
1900
1901        assert_eq!(items.len(), 5);
1902        assert_eq!(items[0].label, "1.0.0 (latest)");
1903        assert_eq!(items[4].label, "1.0.4");
1904    }
1905
1906    #[tokio::test]
1907    async fn test_complete_versions_generic_go_no_operators() {
1908        let registry = MockRegistry {
1909            versions: vec![
1910                MockVersion {
1911                    version: "v1.9.0".to_string(),
1912                    yanked: false,
1913                    prerelease: false,
1914                },
1915                MockVersion {
1916                    version: "v1.9.1".to_string(),
1917                    yanked: false,
1918                    prerelease: false,
1919                },
1920                MockVersion {
1921                    version: "v1.10.0".to_string(),
1922                    yanked: false,
1923                    prerelease: false,
1924                },
1925            ],
1926        };
1927
1928        // Go has no operators, so empty array
1929        let items =
1930            complete_versions_generic(&registry, "github.com/gin-gonic/gin", "v1.9", &[]).await;
1931
1932        assert_eq!(items.len(), 2);
1933        assert_eq!(items[0].label, "v1.9.0 (latest)");
1934        assert_eq!(items[1].label, "v1.9.1");
1935    }
1936}