1use super::providers::{CompletionCx, CompletionItem, CompletionTrigger, Completions};
9
10pub enum CompletionState {
12 Closed,
14 Open(PopupList),
16 DismissedUntilBoundary,
20}
21
22pub struct PopupList {
26 pub items: Vec<CompletionItem>,
28 pub filtered: Vec<u32>,
31 pub selected: u32,
33 pub anchor: u32,
35}
36
37impl PopupList {
38 fn refilter(&mut self, word: &str) {
47 self.filtered.clear();
48 self.filtered.extend(
49 (0..self.items.len() as u32)
50 .filter(|&i| prefix_matches(&self.items[i as usize].label, word)),
51 );
52 self.filtered
53 .sort_by(|&a, &b| self.items[a as usize].sort_key.cmp(&self.items[b as usize].sort_key));
54 self.selected = 0;
55 }
56}
57
58fn prefix_matches(label: &str, prefix: &str) -> bool {
63 let (l, p) = (label.as_bytes(), prefix.as_bytes());
64 l.len() >= p.len() && l[..p.len()].eq_ignore_ascii_case(p)
65}
66
67pub struct CompletionController {
70 state: CompletionState,
71}
72
73impl Default for CompletionController {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl CompletionController {
80 #[must_use]
82 pub fn new() -> Self {
83 Self { state: CompletionState::Closed }
84 }
85
86 #[must_use]
88 pub fn state(&self) -> &CompletionState {
89 &self.state
90 }
91
92 #[must_use]
94 pub fn is_open(&self) -> bool {
95 matches!(self.state, CompletionState::Open(_))
96 }
97
98 pub fn on_input(&mut self, cx: &CompletionCx, word: &str, provider: &mut dyn Completions) {
105 match cx.trigger {
106 CompletionTrigger::Typed(_) => match &mut self.state {
107 CompletionState::Open(list) => {
108 list.refilter(word);
109 if list.filtered.is_empty() {
110 self.state = CompletionState::Closed;
111 }
112 }
113 CompletionState::DismissedUntilBoundary => {} CompletionState::Closed => {
115 let items = provider.complete(cx);
116 self.set_from_items(items, word, cx.word.start);
117 }
118 },
119 CompletionTrigger::TriggerChar(_) | CompletionTrigger::Manual => {
120 let items = provider.complete(cx);
123 self.set_from_items(items, word, cx.word.start);
124 }
125 }
126 }
127
128 fn set_from_items(&mut self, items: Vec<CompletionItem>, word: &str, anchor: u32) {
131 if items.is_empty() {
132 self.state = CompletionState::Closed;
133 return;
134 }
135 let mut list = PopupList { items, filtered: Vec::new(), selected: 0, anchor };
136 list.refilter(word);
137 self.state = if list.filtered.is_empty() {
138 CompletionState::Closed
139 } else {
140 CompletionState::Open(list)
141 };
142 }
143
144 pub fn on_boundary(&mut self) {
147 if matches!(self.state, CompletionState::DismissedUntilBoundary) {
148 self.state = CompletionState::Closed;
149 }
150 }
151
152 pub fn set_selected(&mut self, index: u32) {
155 if let CompletionState::Open(list) = &mut self.state {
156 let n = list.filtered.len() as u32;
157 if n > 0 {
158 list.selected = index.min(n - 1);
159 }
160 }
161 }
162
163 pub fn move_selection(&mut self, down: bool) {
165 if let CompletionState::Open(list) = &mut self.state {
166 let n = list.filtered.len() as u32;
167 if n == 0 {
168 return;
169 }
170 list.selected = if down {
171 (list.selected + 1) % n
172 } else {
173 (list.selected + n - 1) % n
174 };
175 }
176 }
177
178 pub fn escape(&mut self) -> bool {
181 if matches!(self.state, CompletionState::Open(_)) {
182 self.state = CompletionState::DismissedUntilBoundary;
183 true
184 } else {
185 false
186 }
187 }
188
189 pub fn accept(&mut self) -> Option<CompletionItem> {
194 let item = match &self.state {
195 CompletionState::Open(list) => {
196 list.filtered.get(list.selected as usize).map(|&i| list.items[i as usize].clone())
197 }
198 _ => None,
199 };
200 if item.is_some() {
201 self.state = CompletionState::Closed;
202 }
203 item
204 }
205
206 pub fn close(&mut self) {
209 self.state = CompletionState::Closed;
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use crate::intel::providers::CompletionKind;
217 use crate::Point;
218
219 fn kw(label: &str, sort: &str) -> CompletionItem {
220 CompletionItem::plain(label, CompletionKind::Keyword).with_sort_key(sort)
221 }
222
223 struct Stub {
224 items: Vec<CompletionItem>,
225 calls: u32,
226 }
227 impl Completions for Stub {
228 fn complete(&mut self, _cx: &CompletionCx) -> Vec<CompletionItem> {
229 self.calls += 1;
230 self.items.clone()
231 }
232 }
233
234 fn cx(word: &str, trigger: CompletionTrigger) -> CompletionCx {
235 let doc = crate::Buffer::new("").unwrap().doc_id();
236 CompletionCx {
237 doc,
238 revision: 0,
239 position: Point::new(0, word.len() as u32),
240 word: 0..word.len() as u32,
241 lookback: word.to_string(),
242 trigger,
243 }
244 }
245
246 fn labels(c: &CompletionController) -> Vec<String> {
247 match c.state() {
248 CompletionState::Open(list) => {
249 list.filtered.iter().map(|&i| list.items[i as usize].label.clone()).collect()
250 }
251 _ => vec![],
252 }
253 }
254
255 #[test]
256 fn word_char_opens_from_closed_and_prefix_filters() {
257 let mut stub = Stub { items: vec![kw("send", "3_send"), kw("set", "3_set"), kw("let", "3_let")], calls: 0 };
258 let mut c = CompletionController::new();
259 c.on_input(&cx("s", CompletionTrigger::Typed('s')), "s", &mut stub);
260 assert!(c.is_open());
261 assert_eq!(labels(&c), ["send", "set"], "prefix 's' in sort_key order");
262 assert_eq!(stub.calls, 1);
263 }
264
265 #[test]
266 fn open_word_char_refilters_without_a_provider_call() {
267 let mut stub = Stub { items: vec![kw("send", "3_send"), kw("set", "3_set")], calls: 0 };
268 let mut c = CompletionController::new();
269 c.on_input(&cx("s", CompletionTrigger::Typed('s')), "s", &mut stub);
270 assert_eq!(stub.calls, 1);
271 c.on_input(&cx("se", CompletionTrigger::Typed('e')), "se", &mut stub);
273 assert_eq!(stub.calls, 1, "Open × word char must not call the provider");
274 assert_eq!(labels(&c), ["send", "set"]);
275 c.on_input(&cx("set", CompletionTrigger::Typed('t')), "set", &mut stub);
276 assert_eq!(labels(&c), ["set"]);
277 c.on_input(&cx("setx", CompletionTrigger::Typed('x')), "setx", &mut stub);
279 assert!(!c.is_open());
280 }
281
282 #[test]
283 fn empty_provider_result_closes() {
284 let mut stub = Stub { items: vec![], calls: 0 };
285 let mut c = CompletionController::new();
286 c.on_input(&cx("s", CompletionTrigger::Typed('s')), "s", &mut stub);
287 assert!(!c.is_open());
288 }
289
290 #[test]
291 fn escape_is_sticky_until_a_boundary_but_a_trigger_char_reopens() {
292 let mut stub = Stub { items: vec![kw("send", "3_send")], calls: 0 };
293 let mut c = CompletionController::new();
294 c.on_input(&cx("s", CompletionTrigger::Typed('s')), "s", &mut stub);
295 assert!(c.escape(), "Escape while open is captured");
296 assert!(matches!(c.state(), CompletionState::DismissedUntilBoundary));
297 c.on_input(&cx("se", CompletionTrigger::Typed('e')), "se", &mut stub);
299 assert!(!c.is_open());
300 assert_eq!(stub.calls, 1);
301 c.on_input(&cx("", CompletionTrigger::TriggerChar('(')), "", &mut stub);
303 assert!(c.is_open());
304 assert_eq!(stub.calls, 2);
305 c.escape();
307 c.on_boundary();
308 assert!(matches!(c.state(), CompletionState::Closed));
309 c.on_input(&cx("s", CompletionTrigger::Typed('s')), "s", &mut stub);
310 assert!(c.is_open());
311 }
312
313 #[test]
314 fn selection_wraps_and_accept_returns_it_then_closes() {
315 let mut stub = Stub { items: vec![kw("a", "1"), kw("b", "2"), kw("c", "3")], calls: 0 };
316 let mut c = CompletionController::new();
317 c.on_input(&cx("", CompletionTrigger::Manual), "", &mut stub);
318 assert_eq!(labels(&c), ["a", "b", "c"]);
319 c.move_selection(true); c.move_selection(true); c.move_selection(true); c.move_selection(false); let accepted = c.accept().expect("open → accepts");
324 assert_eq!(accepted.label, "c");
325 assert!(matches!(c.state(), CompletionState::Closed));
326 assert!(c.accept().is_none(), "closed → nothing to accept");
327 }
328
329 #[test]
330 fn set_selected_selects_a_row_and_clamps() {
331 let mut stub = Stub { items: vec![kw("a", "1"), kw("b", "2"), kw("c", "3")], calls: 0 };
332 let mut c = CompletionController::new();
333 c.on_input(&cx("", CompletionTrigger::Manual), "", &mut stub);
334 c.set_selected(1);
335 assert_eq!(c.accept().unwrap().label, "b", "clicked row 1");
336 c.on_input(&cx("", CompletionTrigger::Manual), "", &mut stub);
338 c.set_selected(99);
339 assert_eq!(c.accept().unwrap().label, "c", "clamped to the last row");
340 }
341
342 #[test]
343 fn caret_move_closes_and_escape_when_closed_is_not_captured() {
344 let mut stub = Stub { items: vec![kw("a", "1")], calls: 0 };
345 let mut c = CompletionController::new();
346 c.on_input(&cx("a", CompletionTrigger::Typed('a')), "a", &mut stub);
347 assert!(c.is_open());
348 c.close();
349 assert!(matches!(c.state(), CompletionState::Closed));
350 assert!(!c.escape(), "Escape while closed is not captured");
351 }
352}