1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19#[non_exhaustive]
20pub enum CompletionKind {
21 Function,
22 Method,
23 Variable,
24 Field,
25 Class,
26 Module,
27 Interface,
28 Enum,
29 Constant,
30 Property,
31 Snippet,
32 Keyword,
33 File,
34 Folder,
35 #[default]
36 Other,
37}
38
39impl CompletionKind {
40 pub fn icon(self) -> char {
43 match self {
44 CompletionKind::Function | CompletionKind::Method => '\u{0192}', CompletionKind::Variable => 'v',
46 CompletionKind::Field | CompletionKind::Property => '\u{00B7}', CompletionKind::Class | CompletionKind::Interface => 'C',
48 CompletionKind::Module => 'M',
49 CompletionKind::Enum => 'E',
50 CompletionKind::Constant => 'k',
51 CompletionKind::Snippet => '\u{25C6}', CompletionKind::Keyword => 'K',
53 CompletionKind::File | CompletionKind::Folder => '\u{25F0}', CompletionKind::Other => '\u{00B7}', }
56 }
57}
58
59#[derive(Debug, Clone)]
61#[non_exhaustive]
62pub struct CompletionItem {
63 pub label: String,
64 pub detail: Option<String>,
65 pub kind: CompletionKind,
66 pub insert_text: String,
68 pub filter_text: Option<String>,
70 }
73
74impl CompletionItem {
75 pub fn new(label: impl Into<String>) -> Self {
78 let label = label.into();
79 let insert_text = label.clone();
80 Self {
81 label,
82 detail: None,
83 kind: CompletionKind::Other,
84 insert_text,
85 filter_text: None,
86 }
87 }
88}
89
90impl Default for CompletionItem {
91 fn default() -> Self {
92 Self::new("")
93 }
94}
95
96#[derive(Debug, Clone)]
98#[non_exhaustive]
99pub struct Completion {
100 pub anchor_row: usize,
102 pub anchor_col: usize,
104 pub all_items: Vec<CompletionItem>,
106 pub visible: Vec<usize>,
108 pub selected: usize,
110 pub prefix: String,
112 flipped: std::cell::Cell<bool>,
119}
120
121impl Completion {
122 pub fn new(anchor_row: usize, anchor_col: usize, items: Vec<CompletionItem>) -> Self {
136 let visible: Vec<usize> = (0..items.len()).collect();
137 Self {
138 anchor_row,
139 anchor_col,
140 all_items: items,
141 visible,
142 selected: 0,
143 prefix: String::new(),
144 flipped: std::cell::Cell::new(false),
145 }
146 }
147
148 pub fn set_prefix(&mut self, prefix: &str) {
156 self.prefix = prefix.to_string();
157 let needle = prefix.to_lowercase();
158 let mut scored: Vec<(usize, i32)> = self
159 .all_items
160 .iter()
161 .enumerate()
162 .filter_map(|(idx, item)| {
163 let haystack = item
164 .filter_text
165 .as_deref()
166 .unwrap_or(&item.label)
167 .to_lowercase();
168 match_score(&haystack, &needle).map(|score| (idx, score))
169 })
170 .collect();
171 scored.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
174 self.visible = scored.into_iter().map(|(idx, _)| idx).collect();
175 self.selected = 0;
176 }
177
178 pub fn select_next(&mut self) {
180 if self.visible.is_empty() {
181 return;
182 }
183 self.selected = (self.selected + 1) % self.visible.len();
184 }
185
186 pub fn select_prev(&mut self) {
188 if self.visible.is_empty() {
189 return;
190 }
191 if self.selected == 0 {
192 self.selected = self.visible.len() - 1;
193 } else {
194 self.selected -= 1;
195 }
196 }
197
198 pub fn note_flip(&self, flipped: bool) {
201 self.flipped.set(flipped);
202 }
203
204 pub fn is_flipped(&self) -> bool {
206 self.flipped.get()
207 }
208
209 pub fn cycle_down(&mut self) {
214 if self.flipped.get() {
215 self.select_prev();
216 } else {
217 self.select_next();
218 }
219 }
220
221 pub fn cycle_up(&mut self) {
224 if self.flipped.get() {
225 self.select_next();
226 } else {
227 self.select_prev();
228 }
229 }
230
231 pub fn selected_item(&self) -> Option<&CompletionItem> {
233 self.visible
234 .get(self.selected)
235 .and_then(|&idx| self.all_items.get(idx))
236 }
237
238 pub fn is_empty(&self) -> bool {
240 self.visible.is_empty()
241 }
242}
243
244impl Default for Completion {
245 fn default() -> Self {
246 Self::new(0, 0, Vec::new())
247 }
248}
249
250fn match_score(haystack: &str, needle: &str) -> Option<i32> {
264 if needle.is_empty() {
265 return Some(0);
266 }
267
268 let h: Vec<char> = haystack.chars().collect();
269 let mut needle_iter = needle.chars();
270 let mut want = needle_iter.next();
271 let mut score: i32 = 0;
272 let mut first_match: Option<usize> = None;
273 let mut prev_match: Option<usize> = None;
274
275 for (i, &hc) in h.iter().enumerate() {
276 let Some(nc) = want else { break };
277 if hc == nc {
278 if first_match.is_none() {
279 first_match = Some(i);
280 }
281 match prev_match {
282 Some(p) if p + 1 == i => score += 15, Some(p) => score -= (i - p - 1) as i32, None => {}
285 }
286 if i == 0 || h[i - 1] == '_' {
288 score += 10;
289 }
290 prev_match = Some(i);
291 want = needle_iter.next();
292 }
293 }
294
295 if want.is_some() {
297 return None;
298 }
299
300 if let Some(f) = first_match {
301 score -= f as i32; }
303 if h == needle.chars().collect::<Vec<_>>() {
304 score += 1000; } else if haystack.starts_with(needle) {
306 score += 100; }
308 score -= (h.len() as i32) / 4; Some(score)
310}
311
312pub fn kind_from_lsp(k: Option<lsp_types::CompletionItemKind>) -> CompletionKind {
316 use lsp_types::CompletionItemKind as K;
317 match k {
318 Some(K::FUNCTION) => CompletionKind::Function,
319 Some(K::METHOD) => CompletionKind::Method,
320 Some(K::VARIABLE) => CompletionKind::Variable,
321 Some(K::FIELD) => CompletionKind::Field,
322 Some(K::CLASS) => CompletionKind::Class,
323 Some(K::MODULE) => CompletionKind::Module,
324 Some(K::INTERFACE) => CompletionKind::Interface,
325 Some(K::ENUM) => CompletionKind::Enum,
326 Some(K::CONSTANT) | Some(K::ENUM_MEMBER) => CompletionKind::Constant,
327 Some(K::PROPERTY) => CompletionKind::Property,
328 Some(K::SNIPPET) => CompletionKind::Snippet,
329 Some(K::KEYWORD) => CompletionKind::Keyword,
330 Some(K::FILE) => CompletionKind::File,
331 Some(K::FOLDER) => CompletionKind::Folder,
332 _ => CompletionKind::Other,
333 }
334}
335
336pub fn item_from_lsp(src: lsp_types::CompletionItem) -> CompletionItem {
338 let insert_text = match src.text_edit.as_ref() {
339 Some(lsp_types::CompletionTextEdit::Edit(te)) => te.new_text.clone(),
340 Some(lsp_types::CompletionTextEdit::InsertAndReplace(ite)) => ite.new_text.clone(),
341 None => src.insert_text.clone().unwrap_or_else(|| src.label.clone()),
342 };
343 CompletionItem {
344 label: src.label.clone(),
345 detail: src.detail.clone(),
346 kind: kind_from_lsp(src.kind),
347 insert_text,
348 filter_text: src.filter_text.clone(),
349 }
350}
351
352#[cfg(test)]
355mod tests {
356 use super::*;
357
358 fn make_item(label: &str) -> CompletionItem {
359 CompletionItem {
360 label: label.to_string(),
361 detail: None,
362 kind: CompletionKind::Other,
363 insert_text: label.to_string(),
364 filter_text: None,
365 }
366 }
367
368 fn popup(labels: &[&str]) -> Completion {
369 Completion::new(0, 0, labels.iter().map(|l| make_item(l)).collect())
370 }
371
372 #[test]
373 fn set_prefix_filters_with_subseq_match() {
374 let mut c = popup(&["foo_bar", "foobar", "baz"]);
375 c.set_prefix("fb");
376 assert_eq!(c.visible.len(), 2, "visible: {:?}", c.visible);
378 }
379
380 #[test]
381 fn set_prefix_case_insensitive() {
382 let mut c = popup(&["FooBar", "foobar"]);
383 c.set_prefix("FB");
384 assert_eq!(c.visible.len(), 2);
385 }
386
387 #[test]
388 fn set_prefix_ranks_exact_match_first() {
389 let mut c = popup(&["STATUS_LINE_HEIGHT", "letter", "let", "delete"]);
392 c.set_prefix("let");
393 let ranked: Vec<&str> = c
394 .visible
395 .iter()
396 .map(|&i| c.all_items[i].label.as_str())
397 .collect();
398 assert_eq!(ranked.first(), Some(&"let"), "ranked: {ranked:?}");
399 let letter_pos = ranked.iter().position(|&l| l == "letter").unwrap();
401 let status_pos = ranked
402 .iter()
403 .position(|&l| l == "STATUS_LINE_HEIGHT")
404 .unwrap();
405 assert!(
406 letter_pos < status_pos,
407 "prefix match must rank above scattered: {ranked:?}"
408 );
409 }
410
411 #[test]
412 fn set_prefix_prefers_shorter_on_prefix_tie() {
413 let mut c = popup(&["instantiate", "in"]);
415 c.set_prefix("in");
416 let first = c.all_items[c.visible[0]].label.as_str();
417 assert_eq!(first, "in");
418 }
419
420 #[test]
421 fn set_prefix_empty_resets_to_all_items() {
422 let mut c = popup(&["alpha", "beta", "gamma"]);
423 c.set_prefix("alp");
424 assert_eq!(c.visible.len(), 1);
425 c.set_prefix("");
426 assert_eq!(c.visible.len(), 3);
427 }
428
429 #[test]
430 fn select_next_wraps_at_end() {
431 let mut c = popup(&["a", "b", "c"]);
432 c.selected = 2;
433 c.select_next();
434 assert_eq!(c.selected, 0);
435 }
436
437 #[test]
438 fn select_prev_wraps_at_start() {
439 let mut c = popup(&["a", "b", "c"]);
440 c.selected = 0;
441 c.select_prev();
442 assert_eq!(c.selected, 2);
443 }
444
445 #[test]
446 fn cycle_matches_logical_direction_when_not_flipped() {
447 let mut c = popup(&["a", "b", "c"]);
448 c.note_flip(false);
449 assert_eq!(c.selected, 0);
450 c.cycle_down(); assert_eq!(c.selected, 1);
452 c.cycle_up();
453 assert_eq!(c.selected, 0);
454 }
455
456 #[test]
457 fn cycle_inverts_logical_direction_when_flipped() {
458 let mut c = popup(&["a", "b", "c"]);
462 c.note_flip(true);
463 assert_eq!(c.selected, 0);
464 c.cycle_up(); assert_eq!(c.selected, 1);
466 c.cycle_up();
467 assert_eq!(c.selected, 2);
468 c.cycle_down(); assert_eq!(c.selected, 1);
470 c.selected = 0;
472 c.cycle_down();
473 assert_eq!(c.selected, 2);
474 }
475
476 #[test]
477 fn is_empty_after_no_match_filter() {
478 let mut c = popup(&["alpha", "beta"]);
479 c.set_prefix("xyz");
480 assert!(c.is_empty());
481 }
482
483 #[test]
484 fn selected_item_returns_correct_item() {
485 let mut c = popup(&["alpha", "beta", "gamma"]);
486 c.set_prefix("bet");
487 assert_eq!(c.visible.len(), 1);
489 assert_eq!(c.selected_item().map(|i| i.label.as_str()), Some("beta"));
490 }
491
492 #[test]
493 fn default_completion_is_empty() {
494 let c = Completion::default();
495 assert!(c.is_empty());
496 assert_eq!(c.anchor_row, 0);
497 assert_eq!(c.anchor_col, 0);
498 }
499
500 #[test]
501 fn completion_item_new_sets_insert_text_from_label() {
502 let item = CompletionItem::new("my_fn");
503 assert_eq!(item.label, "my_fn");
504 assert_eq!(item.insert_text, "my_fn");
505 assert!(matches!(item.kind, CompletionKind::Other));
506 }
507
508 #[test]
509 fn completion_kind_icon_coverage() {
510 assert_eq!(CompletionKind::Function.icon(), '\u{0192}');
511 assert_eq!(CompletionKind::Snippet.icon(), '\u{25C6}');
512 assert_eq!(CompletionKind::Other.icon(), '\u{00B7}');
513 }
514}