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 Self::Function | Self::Method => '\u{0192}', Self::Variable => 'v',
46 Self::Field | Self::Property => '\u{00B7}', Self::Class | Self::Interface => 'C',
48 Self::Module => 'M',
49 Self::Enum => 'E',
50 Self::Constant => 'k',
51 Self::Snippet => '\u{25C6}', Self::Keyword => 'K',
53 Self::File | Self::Folder => '\u{25F0}', Self::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 lower_cache: Vec<(String, Vec<char>)>,
126 lower_cache_len: usize,
129}
130
131impl Completion {
132 pub fn new(anchor_row: usize, anchor_col: usize, items: Vec<CompletionItem>) -> Self {
146 let visible: Vec<usize> = (0..items.len()).collect();
147 Self {
148 anchor_row,
149 anchor_col,
150 all_items: items,
151 visible,
152 selected: 0,
153 prefix: String::new(),
154 flipped: std::cell::Cell::new(false),
155 lower_cache: Vec::new(),
156 lower_cache_len: 0,
157 }
158 }
159
160 pub fn set_prefix(&mut self, prefix: &str) {
168 self.prefix = prefix.to_string();
169 if self.lower_cache_len != self.all_items.len() {
173 self.lower_cache = self
174 .all_items
175 .iter()
176 .map(|item| {
177 let haystack = item
178 .filter_text
179 .as_deref()
180 .unwrap_or(&item.label)
181 .to_lowercase();
182 let chars: Vec<char> = haystack.chars().collect();
183 (haystack, chars)
184 })
185 .collect();
186 self.lower_cache_len = self.all_items.len();
187 }
188 let needle = prefix.to_lowercase();
189 let needle_chars: Vec<char> = needle.chars().collect();
190 let mut scored: Vec<(usize, i32)> = (0..self.all_items.len())
191 .filter_map(|idx| {
192 let (haystack, chars) = &self.lower_cache[idx];
193 match_score_chars(haystack, chars, &needle, &needle_chars).map(|score| (idx, score))
194 })
195 .collect();
196 scored.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
199 self.visible = scored.into_iter().map(|(idx, _)| idx).collect();
200 self.selected = 0;
201 }
202
203 pub fn select_next(&mut self) {
205 if self.visible.is_empty() {
206 return;
207 }
208 self.selected = (self.selected + 1) % self.visible.len();
209 }
210
211 pub fn select_prev(&mut self) {
213 if self.visible.is_empty() {
214 return;
215 }
216 if self.selected == 0 {
217 self.selected = self.visible.len() - 1;
218 } else {
219 self.selected -= 1;
220 }
221 }
222
223 pub fn note_flip(&self, flipped: bool) {
226 self.flipped.set(flipped);
227 }
228
229 pub fn is_flipped(&self) -> bool {
231 self.flipped.get()
232 }
233
234 pub fn cycle_down(&mut self) {
239 if self.flipped.get() {
240 self.select_prev();
241 } else {
242 self.select_next();
243 }
244 }
245
246 pub fn cycle_up(&mut self) {
249 if self.flipped.get() {
250 self.select_next();
251 } else {
252 self.select_prev();
253 }
254 }
255
256 pub fn selected_item(&self) -> Option<&CompletionItem> {
258 self.visible
259 .get(self.selected)
260 .and_then(|&idx| self.all_items.get(idx))
261 }
262
263 pub fn is_empty(&self) -> bool {
265 self.visible.is_empty()
266 }
267}
268
269impl Default for Completion {
270 fn default() -> Self {
271 Self::new(0, 0, Vec::new())
272 }
273}
274
275fn match_score_chars(
293 haystack: &str,
294 h: &[char],
295 needle: &str,
296 needle_chars: &[char],
297) -> Option<i32> {
298 if needle.is_empty() {
299 return Some(0);
300 }
301
302 let mut needle_iter = needle.chars();
303 let mut want = needle_iter.next();
304 let mut score: i32 = 0;
305 let mut first_match: Option<usize> = None;
306 let mut prev_match: Option<usize> = None;
307
308 for (i, &hc) in h.iter().enumerate() {
309 let Some(nc) = want else { break };
310 if hc == nc {
311 if first_match.is_none() {
312 first_match = Some(i);
313 }
314 match prev_match {
315 Some(p) if p + 1 == i => score += 15, Some(p) => score -= (i - p - 1) as i32, None => {}
318 }
319 if i == 0 || h[i - 1] == '_' {
321 score += 10;
322 }
323 prev_match = Some(i);
324 want = needle_iter.next();
325 }
326 }
327
328 if want.is_some() {
330 return None;
331 }
332
333 if let Some(f) = first_match {
334 score -= f as i32; }
336 if h == needle_chars {
337 score += 1000; } else if haystack.starts_with(needle) {
339 score += 100; }
341 score -= (h.len() as i32) / 4; Some(score)
343}
344
345pub fn kind_from_lsp(k: Option<lsp_types::CompletionItemKind>) -> CompletionKind {
349 use lsp_types::CompletionItemKind as K;
350 match k {
351 Some(K::FUNCTION) => CompletionKind::Function,
352 Some(K::METHOD) => CompletionKind::Method,
353 Some(K::VARIABLE) => CompletionKind::Variable,
354 Some(K::FIELD) => CompletionKind::Field,
355 Some(K::CLASS) => CompletionKind::Class,
356 Some(K::MODULE) => CompletionKind::Module,
357 Some(K::INTERFACE) => CompletionKind::Interface,
358 Some(K::ENUM) => CompletionKind::Enum,
359 Some(K::CONSTANT) | Some(K::ENUM_MEMBER) => CompletionKind::Constant,
360 Some(K::PROPERTY) => CompletionKind::Property,
361 Some(K::SNIPPET) => CompletionKind::Snippet,
362 Some(K::KEYWORD) => CompletionKind::Keyword,
363 Some(K::FILE) => CompletionKind::File,
364 Some(K::FOLDER) => CompletionKind::Folder,
365 _ => CompletionKind::Other,
366 }
367}
368
369pub fn item_from_lsp(src: lsp_types::CompletionItem) -> CompletionItem {
371 let insert_text = match src.text_edit.as_ref() {
372 Some(lsp_types::CompletionTextEdit::Edit(te)) => te.new_text.clone(),
373 Some(lsp_types::CompletionTextEdit::InsertAndReplace(ite)) => ite.new_text.clone(),
374 None => src.insert_text.clone().unwrap_or_else(|| src.label.clone()),
375 };
376 CompletionItem {
377 label: src.label.clone(),
378 detail: src.detail.clone(),
379 kind: kind_from_lsp(src.kind),
380 insert_text,
381 filter_text: src.filter_text,
382 }
383}
384
385#[cfg(test)]
388mod tests {
389 use super::*;
390
391 fn make_item(label: &str) -> CompletionItem {
392 CompletionItem {
393 label: label.to_string(),
394 detail: None,
395 kind: CompletionKind::Other,
396 insert_text: label.to_string(),
397 filter_text: None,
398 }
399 }
400
401 fn popup(labels: &[&str]) -> Completion {
402 Completion::new(0, 0, labels.iter().map(|l| make_item(l)).collect())
403 }
404
405 #[test]
406 fn set_prefix_filters_with_subseq_match() {
407 let mut c = popup(&["foo_bar", "foobar", "baz"]);
408 c.set_prefix("fb");
409 assert_eq!(c.visible.len(), 2, "visible: {:?}", c.visible);
411 }
412
413 #[test]
414 fn set_prefix_case_insensitive() {
415 let mut c = popup(&["FooBar", "foobar"]);
416 c.set_prefix("FB");
417 assert_eq!(c.visible.len(), 2);
418 }
419
420 #[test]
421 fn set_prefix_ranks_exact_match_first() {
422 let mut c = popup(&["STATUS_LINE_HEIGHT", "letter", "let", "delete"]);
425 c.set_prefix("let");
426 let ranked: Vec<&str> = c
427 .visible
428 .iter()
429 .map(|&i| c.all_items[i].label.as_str())
430 .collect();
431 assert_eq!(ranked.first(), Some(&"let"), "ranked: {ranked:?}");
432 let letter_pos = ranked.iter().position(|&l| l == "letter").unwrap();
434 let status_pos = ranked
435 .iter()
436 .position(|&l| l == "STATUS_LINE_HEIGHT")
437 .unwrap();
438 assert!(
439 letter_pos < status_pos,
440 "prefix match must rank above scattered: {ranked:?}"
441 );
442 }
443
444 #[test]
445 fn set_prefix_prefers_shorter_on_prefix_tie() {
446 let mut c = popup(&["instantiate", "in"]);
448 c.set_prefix("in");
449 let first = c.all_items[c.visible[0]].label.as_str();
450 assert_eq!(first, "in");
451 }
452
453 #[test]
454 fn set_prefix_empty_resets_to_all_items() {
455 let mut c = popup(&["alpha", "beta", "gamma"]);
456 c.set_prefix("alp");
457 assert_eq!(c.visible.len(), 1);
458 c.set_prefix("");
459 assert_eq!(c.visible.len(), 3);
460 }
461
462 #[test]
463 fn set_prefix_cache_preserves_results_and_invalidates_on_growth() {
464 let mut c = popup(&["foo_bar", "foobar", "baz", "FooBar"]);
465 c.set_prefix("fb");
466 let first: Vec<String> = c
467 .visible
468 .iter()
469 .map(|&i| c.all_items[i].label.clone())
470 .collect();
471 assert_eq!(first, vec!["foo_bar", "foobar", "FooBar"]);
472
473 c.set_prefix("baz");
476 let baz: Vec<String> = c
477 .visible
478 .iter()
479 .map(|&i| c.all_items[i].label.clone())
480 .collect();
481 assert_eq!(baz, vec!["baz"]);
482
483 c.set_prefix("fb");
485 let second: Vec<String> = c
486 .visible
487 .iter()
488 .map(|&i| c.all_items[i].label.clone())
489 .collect();
490 assert_eq!(first, second);
491
492 c.all_items.push(make_item("foo_bar2"));
495 c.set_prefix("fb");
496 let third: Vec<String> = c
497 .visible
498 .iter()
499 .map(|&i| c.all_items[i].label.clone())
500 .collect();
501 assert_eq!(third, vec!["foo_bar", "foo_bar2", "foobar", "FooBar"]);
502 }
503
504 #[test]
505 fn select_next_wraps_at_end() {
506 let mut c = popup(&["a", "b", "c"]);
507 c.selected = 2;
508 c.select_next();
509 assert_eq!(c.selected, 0);
510 }
511
512 #[test]
513 fn select_prev_wraps_at_start() {
514 let mut c = popup(&["a", "b", "c"]);
515 c.selected = 0;
516 c.select_prev();
517 assert_eq!(c.selected, 2);
518 }
519
520 #[test]
521 fn cycle_matches_logical_direction_when_not_flipped() {
522 let mut c = popup(&["a", "b", "c"]);
523 c.note_flip(false);
524 assert_eq!(c.selected, 0);
525 c.cycle_down(); assert_eq!(c.selected, 1);
527 c.cycle_up();
528 assert_eq!(c.selected, 0);
529 }
530
531 #[test]
532 fn cycle_inverts_logical_direction_when_flipped() {
533 let mut c = popup(&["a", "b", "c"]);
537 c.note_flip(true);
538 assert_eq!(c.selected, 0);
539 c.cycle_up(); assert_eq!(c.selected, 1);
541 c.cycle_up();
542 assert_eq!(c.selected, 2);
543 c.cycle_down(); assert_eq!(c.selected, 1);
545 c.selected = 0;
547 c.cycle_down();
548 assert_eq!(c.selected, 2);
549 }
550
551 #[test]
552 fn is_empty_after_no_match_filter() {
553 let mut c = popup(&["alpha", "beta"]);
554 c.set_prefix("xyz");
555 assert!(c.is_empty());
556 }
557
558 #[test]
559 fn selected_item_returns_correct_item() {
560 let mut c = popup(&["alpha", "beta", "gamma"]);
561 c.set_prefix("bet");
562 assert_eq!(c.visible.len(), 1);
564 assert_eq!(c.selected_item().map(|i| i.label.as_str()), Some("beta"));
565 }
566
567 #[test]
568 fn default_completion_is_empty() {
569 let c = Completion::default();
570 assert!(c.is_empty());
571 assert_eq!(c.anchor_row, 0);
572 assert_eq!(c.anchor_col, 0);
573 }
574
575 #[test]
576 fn completion_item_new_sets_insert_text_from_label() {
577 let item = CompletionItem::new("my_fn");
578 assert_eq!(item.label, "my_fn");
579 assert_eq!(item.insert_text, "my_fn");
580 assert!(matches!(item.kind, CompletionKind::Other));
581 }
582
583 #[test]
584 fn completion_kind_icon_coverage() {
585 assert_eq!(CompletionKind::Function.icon(), '\u{0192}');
586 assert_eq!(CompletionKind::Snippet.icon(), '\u{25C6}');
587 assert_eq!(CompletionKind::Other.icon(), '\u{00B7}');
588 }
589}