use crate::editor::Cursor;
use crate::lsp::CompletionItem;
pub struct CompletionState {
pub prefix_start: Cursor,
pub items: Vec<CompletionItem>,
pub filtered: Vec<usize>,
pub selected: usize,
}
impl CompletionState {
pub fn new(prefix_start: Cursor, items: Vec<CompletionItem>, prefix: &str) -> Self {
let mut s = Self {
prefix_start,
items,
filtered: Vec::new(),
selected: 0,
};
s.refilter(prefix);
s
}
pub fn refilter(&mut self, prefix: &str) {
let needle = prefix.to_lowercase();
let mut scored: Vec<(usize, usize, &str)> = self
.items
.iter()
.enumerate()
.filter_map(|(i, it)| {
let hay = it.filter_text.as_deref().unwrap_or(&it.label).to_lowercase();
let pos = if needle.is_empty() {
Some(0)
} else {
hay.find(&needle)
};
pos.map(|p| {
let sort = it.sort_text.as_deref().unwrap_or(&it.label);
(i, p, sort)
})
})
.collect();
scored.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.2.cmp(b.2)));
self.filtered = scored.into_iter().map(|(i, _, _)| i).collect();
if self.selected >= self.filtered.len() {
self.selected = self.filtered.len().saturating_sub(1);
}
}
pub fn is_empty(&self) -> bool {
self.filtered.is_empty()
}
pub fn current(&self) -> Option<&CompletionItem> {
self.filtered
.get(self.selected)
.and_then(|i| self.items.get(*i))
}
pub fn move_selection(&mut self, delta: isize) {
if self.filtered.is_empty() {
return;
}
let len = self.filtered.len() as isize;
let next = (self.selected as isize + delta).rem_euclid(len);
self.selected = next as usize;
}
}
pub fn identifier_prefix_start(line: &str, cursor_col: usize) -> usize {
let chars: Vec<char> = line.chars().collect();
let mut col = cursor_col.min(chars.len());
while col > 0 {
let c = chars[col - 1];
if is_ident_continue(c) {
col -= 1;
} else {
break;
}
}
col
}
pub fn is_ident_continue(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
pub fn prefix_slice(line: &str, prefix_start_col: usize, cursor_col: usize) -> String {
if cursor_col <= prefix_start_col {
return String::new();
}
line.chars()
.skip(prefix_start_col)
.take(cursor_col - prefix_start_col)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn item(label: &str) -> CompletionItem {
CompletionItem {
label: label.to_string(),
kind: 0,
text_edit: None,
insert_text: None,
filter_text: None,
sort_text: None,
detail: None,
additional_text_edits: Vec::new(),
raw: serde_json::Value::Null,
source: String::new(),
}
}
#[test]
fn prefix_start_walks_back_over_ident_chars() {
let line = "let foo_bar";
assert_eq!(identifier_prefix_start(line, line.chars().count()), 4);
assert_eq!(identifier_prefix_start("let x", 4), 4);
assert_eq!(identifier_prefix_start("abc", 0), 0);
}
#[test]
fn refilter_prefers_prefix_matches() {
let items = vec![item("into_vec"), item("vec"), item("vector")];
let mut s = CompletionState::new(Cursor { row: 0, col: 0 }, items, "vec");
let order: Vec<&str> = s.filtered.iter().map(|i| s.items[*i].label.as_str()).collect();
assert_eq!(order, vec!["vec", "vector", "into_vec"]);
s.refilter("");
assert_eq!(s.filtered.len(), 3);
}
#[test]
fn refilter_clamps_selection() {
let items = vec![item("aaa"), item("aab"), item("aac")];
let mut s = CompletionState::new(Cursor { row: 0, col: 0 }, items, "");
s.selected = 2;
s.refilter("aac");
assert_eq!(s.filtered.len(), 1);
assert_eq!(s.selected, 0);
}
#[test]
fn move_selection_wraps() {
let items = vec![item("a"), item("b"), item("c")];
let mut s = CompletionState::new(Cursor { row: 0, col: 0 }, items, "");
s.move_selection(1);
assert_eq!(s.selected, 1);
s.move_selection(-2);
assert_eq!(s.selected, 2); s.move_selection(1);
assert_eq!(s.selected, 0); }
#[test]
fn prefix_slice_extracts_typed_chars() {
assert_eq!(prefix_slice("let foo_bar", 4, 7), "foo");
assert_eq!(prefix_slice("xy", 2, 1), "");
assert_eq!(prefix_slice("xy", 2, 2), "");
}
}