use crate::editor::Cursor;
use crate::lsp::CompletionItem;
pub const AUTO_TRIGGER_MIN_PREFIX_LEN: usize = 2;
pub struct CompletionState {
pub prefix_start: Cursor,
pub items: Vec<CompletionItem>,
pub filtered: Vec<usize>,
pub selected: usize,
pub resolved: Vec<bool>,
pub selecting: bool,
pub min_prefix_len: usize,
}
impl CompletionState {
pub fn new(prefix_start: Cursor, items: Vec<CompletionItem>, prefix: &str) -> Self {
let resolved = vec![false; items.len()];
let min_prefix_len = prefix.chars().count().min(AUTO_TRIGGER_MIN_PREFIX_LEN);
let mut s = Self {
prefix_start,
items,
filtered: Vec::new(),
selected: 0,
resolved,
selecting: false,
min_prefix_len,
};
s.refilter(prefix);
s
}
pub fn current_index(&self) -> Option<usize> {
self.filtered.get(self.selected).copied()
}
pub fn merge_resolved(&mut self, idx: usize, resolved: &CompletionItem) {
if let Some(item) = self.items.get_mut(idx) {
if resolved.detail.as_ref().is_some_and(|s| !s.is_empty()) {
item.resolved_detail = resolved.detail.clone();
}
if !resolved.additional_text_edits.is_empty() {
item.additional_text_edits = resolved.additional_text_edits.clone();
}
}
if let Some(flag) = self.resolved.get_mut(idx) {
*flag = true;
}
}
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);
}
self.selecting = false;
}
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,
resolved_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), "");
}
}