#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum CompletionKind {
Function,
Method,
Variable,
Field,
Class,
Module,
Interface,
Enum,
Constant,
Property,
Snippet,
Keyword,
File,
Folder,
#[default]
Other,
}
impl CompletionKind {
pub fn icon(self) -> char {
match self {
Self::Function | Self::Method => '\u{0192}', Self::Variable => 'v',
Self::Field | Self::Property => '\u{00B7}', Self::Class | Self::Interface => 'C',
Self::Module => 'M',
Self::Enum => 'E',
Self::Constant => 'k',
Self::Snippet => '\u{25C6}', Self::Keyword => 'K',
Self::File | Self::Folder => '\u{25F0}', Self::Other => '\u{00B7}', }
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct CompletionItem {
pub label: String,
pub detail: Option<String>,
pub kind: CompletionKind,
pub insert_text: String,
pub filter_text: Option<String>,
}
impl CompletionItem {
pub fn new(label: impl Into<String>) -> Self {
let label = label.into();
let insert_text = label.clone();
Self {
label,
detail: None,
kind: CompletionKind::Other,
insert_text,
filter_text: None,
}
}
}
impl Default for CompletionItem {
fn default() -> Self {
Self::new("")
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Completion {
pub anchor_row: usize,
pub anchor_col: usize,
pub all_items: Vec<CompletionItem>,
pub visible: Vec<usize>,
pub selected: usize,
pub prefix: String,
flipped: std::cell::Cell<bool>,
lower_cache: Vec<(String, Vec<char>)>,
lower_cache_len: usize,
}
impl Completion {
pub fn new(anchor_row: usize, anchor_col: usize, items: Vec<CompletionItem>) -> Self {
let visible: Vec<usize> = (0..items.len()).collect();
Self {
anchor_row,
anchor_col,
all_items: items,
visible,
selected: 0,
prefix: String::new(),
flipped: std::cell::Cell::new(false),
lower_cache: Vec::new(),
lower_cache_len: 0,
}
}
pub fn set_prefix(&mut self, prefix: &str) {
self.prefix = prefix.to_string();
if self.lower_cache_len != self.all_items.len() {
self.lower_cache = self
.all_items
.iter()
.map(|item| {
let haystack = item
.filter_text
.as_deref()
.unwrap_or(&item.label)
.to_lowercase();
let chars: Vec<char> = haystack.chars().collect();
(haystack, chars)
})
.collect();
self.lower_cache_len = self.all_items.len();
}
let needle = prefix.to_lowercase();
let needle_chars: Vec<char> = needle.chars().collect();
let mut scored: Vec<(usize, i32)> = (0..self.all_items.len())
.filter_map(|idx| {
let (haystack, chars) = &self.lower_cache[idx];
match_score_chars(haystack, chars, &needle, &needle_chars).map(|score| (idx, score))
})
.collect();
scored.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
self.visible = scored.into_iter().map(|(idx, _)| idx).collect();
self.selected = 0;
}
pub fn select_next(&mut self) {
if self.visible.is_empty() {
return;
}
self.selected = (self.selected + 1) % self.visible.len();
}
pub fn select_prev(&mut self) {
if self.visible.is_empty() {
return;
}
if self.selected == 0 {
self.selected = self.visible.len() - 1;
} else {
self.selected -= 1;
}
}
pub fn note_flip(&self, flipped: bool) {
self.flipped.set(flipped);
}
pub fn is_flipped(&self) -> bool {
self.flipped.get()
}
pub fn cycle_down(&mut self) {
if self.flipped.get() {
self.select_prev();
} else {
self.select_next();
}
}
pub fn cycle_up(&mut self) {
if self.flipped.get() {
self.select_next();
} else {
self.select_prev();
}
}
pub fn selected_item(&self) -> Option<&CompletionItem> {
self.visible
.get(self.selected)
.and_then(|&idx| self.all_items.get(idx))
}
pub fn is_empty(&self) -> bool {
self.visible.is_empty()
}
}
impl Default for Completion {
fn default() -> Self {
Self::new(0, 0, Vec::new())
}
}
fn match_score_chars(
haystack: &str,
h: &[char],
needle: &str,
needle_chars: &[char],
) -> Option<i32> {
if needle.is_empty() {
return Some(0);
}
let mut needle_iter = needle.chars();
let mut want = needle_iter.next();
let mut score: i32 = 0;
let mut first_match: Option<usize> = None;
let mut prev_match: Option<usize> = None;
for (i, &hc) in h.iter().enumerate() {
let Some(nc) = want else { break };
if hc == nc {
if first_match.is_none() {
first_match = Some(i);
}
match prev_match {
Some(p) if p + 1 == i => score += 15, Some(p) => score -= (i - p - 1) as i32, None => {}
}
if i == 0 || h[i - 1] == '_' {
score += 10;
}
prev_match = Some(i);
want = needle_iter.next();
}
}
if want.is_some() {
return None;
}
if let Some(f) = first_match {
score -= f as i32; }
if h == needle_chars {
score += 1000; } else if haystack.starts_with(needle) {
score += 100; }
score -= (h.len() as i32) / 4; Some(score)
}
pub fn kind_from_lsp(k: Option<lsp_types::CompletionItemKind>) -> CompletionKind {
use lsp_types::CompletionItemKind as K;
match k {
Some(K::FUNCTION) => CompletionKind::Function,
Some(K::METHOD) => CompletionKind::Method,
Some(K::VARIABLE) => CompletionKind::Variable,
Some(K::FIELD) => CompletionKind::Field,
Some(K::CLASS) => CompletionKind::Class,
Some(K::MODULE) => CompletionKind::Module,
Some(K::INTERFACE) => CompletionKind::Interface,
Some(K::ENUM) => CompletionKind::Enum,
Some(K::CONSTANT) | Some(K::ENUM_MEMBER) => CompletionKind::Constant,
Some(K::PROPERTY) => CompletionKind::Property,
Some(K::SNIPPET) => CompletionKind::Snippet,
Some(K::KEYWORD) => CompletionKind::Keyword,
Some(K::FILE) => CompletionKind::File,
Some(K::FOLDER) => CompletionKind::Folder,
_ => CompletionKind::Other,
}
}
pub fn item_from_lsp(src: lsp_types::CompletionItem) -> CompletionItem {
let insert_text = match src.text_edit.as_ref() {
Some(lsp_types::CompletionTextEdit::Edit(te)) => te.new_text.clone(),
Some(lsp_types::CompletionTextEdit::InsertAndReplace(ite)) => ite.new_text.clone(),
None => src.insert_text.clone().unwrap_or_else(|| src.label.clone()),
};
CompletionItem {
label: src.label.clone(),
detail: src.detail.clone(),
kind: kind_from_lsp(src.kind),
insert_text,
filter_text: src.filter_text,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_item(label: &str) -> CompletionItem {
CompletionItem {
label: label.to_string(),
detail: None,
kind: CompletionKind::Other,
insert_text: label.to_string(),
filter_text: None,
}
}
fn popup(labels: &[&str]) -> Completion {
Completion::new(0, 0, labels.iter().map(|l| make_item(l)).collect())
}
#[test]
fn set_prefix_filters_with_subseq_match() {
let mut c = popup(&["foo_bar", "foobar", "baz"]);
c.set_prefix("fb");
assert_eq!(c.visible.len(), 2, "visible: {:?}", c.visible);
}
#[test]
fn set_prefix_case_insensitive() {
let mut c = popup(&["FooBar", "foobar"]);
c.set_prefix("FB");
assert_eq!(c.visible.len(), 2);
}
#[test]
fn set_prefix_ranks_exact_match_first() {
let mut c = popup(&["STATUS_LINE_HEIGHT", "letter", "let", "delete"]);
c.set_prefix("let");
let ranked: Vec<&str> = c
.visible
.iter()
.map(|&i| c.all_items[i].label.as_str())
.collect();
assert_eq!(ranked.first(), Some(&"let"), "ranked: {ranked:?}");
let letter_pos = ranked.iter().position(|&l| l == "letter").unwrap();
let status_pos = ranked
.iter()
.position(|&l| l == "STATUS_LINE_HEIGHT")
.unwrap();
assert!(
letter_pos < status_pos,
"prefix match must rank above scattered: {ranked:?}"
);
}
#[test]
fn set_prefix_prefers_shorter_on_prefix_tie() {
let mut c = popup(&["instantiate", "in"]);
c.set_prefix("in");
let first = c.all_items[c.visible[0]].label.as_str();
assert_eq!(first, "in");
}
#[test]
fn set_prefix_empty_resets_to_all_items() {
let mut c = popup(&["alpha", "beta", "gamma"]);
c.set_prefix("alp");
assert_eq!(c.visible.len(), 1);
c.set_prefix("");
assert_eq!(c.visible.len(), 3);
}
#[test]
fn set_prefix_cache_preserves_results_and_invalidates_on_growth() {
let mut c = popup(&["foo_bar", "foobar", "baz", "FooBar"]);
c.set_prefix("fb");
let first: Vec<String> = c
.visible
.iter()
.map(|&i| c.all_items[i].label.clone())
.collect();
assert_eq!(first, vec!["foo_bar", "foobar", "FooBar"]);
c.set_prefix("baz");
let baz: Vec<String> = c
.visible
.iter()
.map(|&i| c.all_items[i].label.clone())
.collect();
assert_eq!(baz, vec!["baz"]);
c.set_prefix("fb");
let second: Vec<String> = c
.visible
.iter()
.map(|&i| c.all_items[i].label.clone())
.collect();
assert_eq!(first, second);
c.all_items.push(make_item("foo_bar2"));
c.set_prefix("fb");
let third: Vec<String> = c
.visible
.iter()
.map(|&i| c.all_items[i].label.clone())
.collect();
assert_eq!(third, vec!["foo_bar", "foo_bar2", "foobar", "FooBar"]);
}
#[test]
fn select_next_wraps_at_end() {
let mut c = popup(&["a", "b", "c"]);
c.selected = 2;
c.select_next();
assert_eq!(c.selected, 0);
}
#[test]
fn select_prev_wraps_at_start() {
let mut c = popup(&["a", "b", "c"]);
c.selected = 0;
c.select_prev();
assert_eq!(c.selected, 2);
}
#[test]
fn cycle_matches_logical_direction_when_not_flipped() {
let mut c = popup(&["a", "b", "c"]);
c.note_flip(false);
assert_eq!(c.selected, 0);
c.cycle_down(); assert_eq!(c.selected, 1);
c.cycle_up();
assert_eq!(c.selected, 0);
}
#[test]
fn cycle_inverts_logical_direction_when_flipped() {
let mut c = popup(&["a", "b", "c"]);
c.note_flip(true);
assert_eq!(c.selected, 0);
c.cycle_up(); assert_eq!(c.selected, 1);
c.cycle_up();
assert_eq!(c.selected, 2);
c.cycle_down(); assert_eq!(c.selected, 1);
c.selected = 0;
c.cycle_down();
assert_eq!(c.selected, 2);
}
#[test]
fn is_empty_after_no_match_filter() {
let mut c = popup(&["alpha", "beta"]);
c.set_prefix("xyz");
assert!(c.is_empty());
}
#[test]
fn selected_item_returns_correct_item() {
let mut c = popup(&["alpha", "beta", "gamma"]);
c.set_prefix("bet");
assert_eq!(c.visible.len(), 1);
assert_eq!(c.selected_item().map(|i| i.label.as_str()), Some("beta"));
}
#[test]
fn default_completion_is_empty() {
let c = Completion::default();
assert!(c.is_empty());
assert_eq!(c.anchor_row, 0);
assert_eq!(c.anchor_col, 0);
}
#[test]
fn completion_item_new_sets_insert_text_from_label() {
let item = CompletionItem::new("my_fn");
assert_eq!(item.label, "my_fn");
assert_eq!(item.insert_text, "my_fn");
assert!(matches!(item.kind, CompletionKind::Other));
}
#[test]
fn completion_kind_icon_coverage() {
assert_eq!(CompletionKind::Function.icon(), '\u{0192}');
assert_eq!(CompletionKind::Snippet.icon(), '\u{25C6}');
assert_eq!(CompletionKind::Other.icon(), '\u{00B7}');
}
}