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 pub fn set_items(&mut self, items: Vec<CompletionItem>, word: &str, anchor: u32) {
136 self.set_from_items(items, word, anchor);
137 }
138
139 fn set_from_items(&mut self, items: Vec<CompletionItem>, word: &str, anchor: u32) {
142 if items.is_empty() {
143 self.state = CompletionState::Closed;
144 return;
145 }
146 let mut list = PopupList { items, filtered: Vec::new(), selected: 0, anchor };
147 list.refilter(word);
148 self.state = if list.filtered.is_empty() {
149 CompletionState::Closed
150 } else {
151 CompletionState::Open(list)
152 };
153 }
154
155 pub fn on_boundary(&mut self) {
158 if matches!(self.state, CompletionState::DismissedUntilBoundary) {
159 self.state = CompletionState::Closed;
160 }
161 }
162
163 pub fn set_selected(&mut self, index: u32) {
166 if let CompletionState::Open(list) = &mut self.state {
167 let n = list.filtered.len() as u32;
168 if n > 0 {
169 list.selected = index.min(n - 1);
170 }
171 }
172 }
173
174 pub fn move_selection(&mut self, down: bool) {
176 if let CompletionState::Open(list) = &mut self.state {
177 let n = list.filtered.len() as u32;
178 if n == 0 {
179 return;
180 }
181 list.selected = if down {
182 (list.selected + 1) % n
183 } else {
184 (list.selected + n - 1) % n
185 };
186 }
187 }
188
189 pub fn escape(&mut self) -> bool {
192 if matches!(self.state, CompletionState::Open(_)) {
193 self.state = CompletionState::DismissedUntilBoundary;
194 true
195 } else {
196 false
197 }
198 }
199
200 pub fn accept(&mut self) -> Option<CompletionItem> {
205 let item = match &self.state {
206 CompletionState::Open(list) => {
207 list.filtered.get(list.selected as usize).map(|&i| list.items[i as usize].clone())
208 }
209 _ => None,
210 };
211 if item.is_some() {
212 self.state = CompletionState::Closed;
213 }
214 item
215 }
216
217 pub fn close(&mut self) {
220 self.state = CompletionState::Closed;
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use crate::intel::providers::CompletionKind;
228 use crate::Point;
229
230 fn kw(label: &str, sort: &str) -> CompletionItem {
231 CompletionItem::plain(label, CompletionKind::Keyword).with_sort_key(sort)
232 }
233
234 struct Stub {
235 items: Vec<CompletionItem>,
236 calls: u32,
237 }
238 impl Completions for Stub {
239 fn complete(&mut self, _cx: &CompletionCx) -> Vec<CompletionItem> {
240 self.calls += 1;
241 self.items.clone()
242 }
243 }
244
245 fn cx(word: &str, trigger: CompletionTrigger) -> CompletionCx {
246 let doc = crate::Buffer::new("").unwrap().doc_id();
247 CompletionCx {
248 doc,
249 revision: 0,
250 position: Point::new(0, word.len() as u32),
251 word: 0..word.len() as u32,
252 lookback: word.to_string(),
253 trigger,
254 }
255 }
256
257 fn labels(c: &CompletionController) -> Vec<String> {
258 match c.state() {
259 CompletionState::Open(list) => {
260 list.filtered.iter().map(|&i| list.items[i as usize].label.clone()).collect()
261 }
262 _ => vec![],
263 }
264 }
265
266 #[test]
267 fn word_char_opens_from_closed_and_prefix_filters() {
268 let mut stub = Stub { items: vec![kw("send", "3_send"), kw("set", "3_set"), kw("let", "3_let")], calls: 0 };
269 let mut c = CompletionController::new();
270 c.on_input(&cx("s", CompletionTrigger::Typed('s')), "s", &mut stub);
271 assert!(c.is_open());
272 assert_eq!(labels(&c), ["send", "set"], "prefix 's' in sort_key order");
273 assert_eq!(stub.calls, 1);
274 }
275
276 #[test]
277 fn open_word_char_refilters_without_a_provider_call() {
278 let mut stub = Stub { items: vec![kw("send", "3_send"), kw("set", "3_set")], calls: 0 };
279 let mut c = CompletionController::new();
280 c.on_input(&cx("s", CompletionTrigger::Typed('s')), "s", &mut stub);
281 assert_eq!(stub.calls, 1);
282 c.on_input(&cx("se", CompletionTrigger::Typed('e')), "se", &mut stub);
284 assert_eq!(stub.calls, 1, "Open × word char must not call the provider");
285 assert_eq!(labels(&c), ["send", "set"]);
286 c.on_input(&cx("set", CompletionTrigger::Typed('t')), "set", &mut stub);
287 assert_eq!(labels(&c), ["set"]);
288 c.on_input(&cx("setx", CompletionTrigger::Typed('x')), "setx", &mut stub);
290 assert!(!c.is_open());
291 }
292
293 #[test]
294 fn empty_provider_result_closes() {
295 let mut stub = Stub { items: vec![], calls: 0 };
296 let mut c = CompletionController::new();
297 c.on_input(&cx("s", CompletionTrigger::Typed('s')), "s", &mut stub);
298 assert!(!c.is_open());
299 }
300
301 #[test]
302 fn escape_is_sticky_until_a_boundary_but_a_trigger_char_reopens() {
303 let mut stub = Stub { items: vec![kw("send", "3_send")], calls: 0 };
304 let mut c = CompletionController::new();
305 c.on_input(&cx("s", CompletionTrigger::Typed('s')), "s", &mut stub);
306 assert!(c.escape(), "Escape while open is captured");
307 assert!(matches!(c.state(), CompletionState::DismissedUntilBoundary));
308 c.on_input(&cx("se", CompletionTrigger::Typed('e')), "se", &mut stub);
310 assert!(!c.is_open());
311 assert_eq!(stub.calls, 1);
312 c.on_input(&cx("", CompletionTrigger::TriggerChar('(')), "", &mut stub);
314 assert!(c.is_open());
315 assert_eq!(stub.calls, 2);
316 c.escape();
318 c.on_boundary();
319 assert!(matches!(c.state(), CompletionState::Closed));
320 c.on_input(&cx("s", CompletionTrigger::Typed('s')), "s", &mut stub);
321 assert!(c.is_open());
322 }
323
324 #[test]
325 fn selection_wraps_and_accept_returns_it_then_closes() {
326 let mut stub = Stub { items: vec![kw("a", "1"), kw("b", "2"), kw("c", "3")], calls: 0 };
327 let mut c = CompletionController::new();
328 c.on_input(&cx("", CompletionTrigger::Manual), "", &mut stub);
329 assert_eq!(labels(&c), ["a", "b", "c"]);
330 c.move_selection(true); c.move_selection(true); c.move_selection(true); c.move_selection(false); let accepted = c.accept().expect("open → accepts");
335 assert_eq!(accepted.label, "c");
336 assert!(matches!(c.state(), CompletionState::Closed));
337 assert!(c.accept().is_none(), "closed → nothing to accept");
338 }
339
340 #[test]
341 fn set_selected_selects_a_row_and_clamps() {
342 let mut stub = Stub { items: vec![kw("a", "1"), kw("b", "2"), kw("c", "3")], calls: 0 };
343 let mut c = CompletionController::new();
344 c.on_input(&cx("", CompletionTrigger::Manual), "", &mut stub);
345 c.set_selected(1);
346 assert_eq!(c.accept().unwrap().label, "b", "clicked row 1");
347 c.on_input(&cx("", CompletionTrigger::Manual), "", &mut stub);
349 c.set_selected(99);
350 assert_eq!(c.accept().unwrap().label, "c", "clamped to the last row");
351 }
352
353 #[test]
354 fn caret_move_closes_and_escape_when_closed_is_not_captured() {
355 let mut stub = Stub { items: vec![kw("a", "1")], calls: 0 };
356 let mut c = CompletionController::new();
357 c.on_input(&cx("a", CompletionTrigger::Typed('a')), "a", &mut stub);
358 assert!(c.is_open());
359 c.close();
360 assert!(matches!(c.state(), CompletionState::Closed));
361 assert!(!c.escape(), "Escape while closed is not captured");
362 }
363}