Skip to main content

perl_lsp_completion_item/
lib.rs

1#![warn(missing_docs)]
2//! Completion item domain types and sorting utilities.
3//!
4//! This microcrate isolates completion payload representation and deterministic
5//! ordering/deduplication policy from provider logic.
6
7use perl_parser_core::SourceLocation;
8
9/// Type of completion item.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11pub enum CompletionItemKind {
12    /// Variable (scalar, array, hash).
13    Variable,
14    /// Function or method.
15    Function,
16    /// Perl keyword.
17    Keyword,
18    /// Package or module.
19    Module,
20    /// File path.
21    File,
22    /// Snippet with placeholders.
23    Snippet,
24    /// Constant value.
25    Constant,
26    /// Property or hash key.
27    Property,
28}
29
30/// A single completion suggestion.
31#[derive(Debug, Clone)]
32pub struct CompletionItem {
33    /// The text to insert.
34    pub label: String,
35    /// Kind of completion.
36    pub kind: CompletionItemKind,
37    /// Optional detail text.
38    pub detail: Option<String>,
39    /// Optional documentation.
40    pub documentation: Option<String>,
41    /// Text to insert (if different from label).
42    pub insert_text: Option<String>,
43    /// Sort priority (lower is better).
44    pub sort_text: Option<String>,
45    /// Filter text for matching.
46    pub filter_text: Option<String>,
47    /// Additional text edits to apply.
48    pub additional_edits: Vec<(SourceLocation, String)>,
49    /// Range to replace in the document (for proper prefix handling).
50    pub text_edit_range: Option<(usize, usize)>, // (start, end) offsets
51    /// Commit characters that trigger auto-insertion (LSP 3.0+).
52    /// Each entry must be exactly one character per LSP spec.
53    pub commit_characters: Option<Vec<String>>,
54}
55
56/// Remove duplicates and sort completions with stable, deterministic ordering.
57#[must_use]
58pub fn deduplicate_and_sort(mut completions: Vec<CompletionItem>) -> Vec<CompletionItem> {
59    if completions.is_empty() {
60        return completions;
61    }
62
63    // Remove duplicates based on label, keeping the one with better sort_text.
64    let mut seen = std::collections::HashMap::<String, usize>::new();
65    let mut to_remove = std::collections::HashSet::<usize>::new();
66
67    for (i, item) in completions.iter().enumerate() {
68        if item.label.is_empty() {
69            // Skip items with empty labels.
70            to_remove.insert(i);
71            continue;
72        }
73
74        if let Some(&existing_idx) = seen.get(&item.label) {
75            let existing_sort = completions[existing_idx]
76                .sort_text
77                .as_ref()
78                .unwrap_or(&completions[existing_idx].label);
79            let current_sort = item.sort_text.as_ref().unwrap_or(&item.label);
80
81            if current_sort < existing_sort {
82                // Current item is better, remove the existing one.
83                to_remove.insert(existing_idx);
84                seen.insert(item.label.clone(), i);
85            } else {
86                // Existing item is better, remove current one.
87                to_remove.insert(i);
88            }
89        } else {
90            seen.insert(item.label.clone(), i);
91        }
92    }
93
94    // Remove marked duplicates in reverse order to maintain indices.
95    let mut indices: Vec<usize> = to_remove.into_iter().collect();
96    indices.sort_by(|a, b| b.cmp(a)); // Sort in descending order.
97    for idx in indices {
98        completions.remove(idx);
99    }
100
101    // Sort with stable, deterministic ordering.
102    completions.sort_by(|a, b| {
103        let a_sort = a.sort_text.as_ref().unwrap_or(&a.label);
104        let b_sort = b.sort_text.as_ref().unwrap_or(&b.label);
105
106        // Primary sort: by sort_text/label.
107        match a_sort.cmp(b_sort) {
108            std::cmp::Ordering::Equal => {
109                // Secondary sort: by completion kind for stability.
110                match a.kind.cmp(&b.kind) {
111                    std::cmp::Ordering::Equal => {
112                        // Tertiary sort: by label for full determinism.
113                        a.label.cmp(&b.label)
114                    }
115                    other => other,
116                }
117            }
118            other => other,
119        }
120    });
121
122    completions
123}
124
125#[cfg(test)]
126mod tests {
127    use super::{CompletionItem, CompletionItemKind, deduplicate_and_sort};
128
129    fn item(label: &str, kind: CompletionItemKind, sort_text: Option<&str>) -> CompletionItem {
130        CompletionItem {
131            label: label.to_string(),
132            kind,
133            detail: None,
134            documentation: None,
135            insert_text: None,
136            sort_text: sort_text.map(str::to_string),
137            filter_text: None,
138            additional_edits: Vec::new(),
139            text_edit_range: None,
140            commit_characters: None,
141        }
142    }
143
144    #[test]
145    fn deduplicates_on_label_using_best_sort_text() {
146        let items = vec![
147            item("foo", CompletionItemKind::Function, Some("200")),
148            item("foo", CompletionItemKind::Variable, Some("050")),
149            item("bar", CompletionItemKind::Function, Some("100")),
150        ];
151
152        let result = deduplicate_and_sort(items);
153        assert_eq!(result.len(), 2);
154        assert_eq!(result[0].label, "foo");
155        assert_eq!(result[0].kind, CompletionItemKind::Variable);
156        assert_eq!(result[1].label, "bar");
157    }
158
159    #[test]
160    fn drops_empty_labels() {
161        let items = vec![
162            item("", CompletionItemKind::Function, Some("001")),
163            item("ok", CompletionItemKind::Function, Some("002")),
164        ];
165
166        let result = deduplicate_and_sort(items);
167        assert_eq!(result.len(), 1);
168        assert_eq!(result[0].label, "ok");
169    }
170}