perl_lsp_completion_item/
lib.rs1#![warn(missing_docs)]
2use perl_parser_core::SourceLocation;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11pub enum CompletionItemKind {
12 Variable,
14 Function,
16 Keyword,
18 Module,
20 File,
22 Snippet,
24 Constant,
26 Property,
28}
29
30#[derive(Debug, Clone)]
32pub struct CompletionItem {
33 pub label: String,
35 pub kind: CompletionItemKind,
37 pub detail: Option<String>,
39 pub documentation: Option<String>,
41 pub insert_text: Option<String>,
43 pub sort_text: Option<String>,
45 pub filter_text: Option<String>,
47 pub additional_edits: Vec<(SourceLocation, String)>,
49 pub text_edit_range: Option<(usize, usize)>, pub commit_characters: Option<Vec<String>>,
54}
55
56#[must_use]
58pub fn deduplicate_and_sort(mut completions: Vec<CompletionItem>) -> Vec<CompletionItem> {
59 if completions.is_empty() {
60 return completions;
61 }
62
63 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 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 to_remove.insert(existing_idx);
84 seen.insert(item.label.clone(), i);
85 } else {
86 to_remove.insert(i);
88 }
89 } else {
90 seen.insert(item.label.clone(), i);
91 }
92 }
93
94 let mut indices: Vec<usize> = to_remove.into_iter().collect();
96 indices.sort_by(|a, b| b.cmp(a)); for idx in indices {
98 completions.remove(idx);
99 }
100
101 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 match a_sort.cmp(b_sort) {
108 std::cmp::Ordering::Equal => {
109 match a.kind.cmp(&b.kind) {
111 std::cmp::Ordering::Equal => {
112 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}