1use crate::input::EditorMode;
2use anyhow::Result;
3use gpui::{App, Context, EntityInputHandler, Pixels, Task, Window, px};
4use lsp_types::{
5 CompletionContext, CompletionItem, CompletionResponse, InlineCompletionContext,
6 InlineCompletionItem, InlineCompletionResponse, InlineCompletionTriggerKind,
7 request::Completion,
8};
9use ropey::Rope;
10use std::{cell::RefCell, ops::Range, rc::Rc, time::Duration};
11
12use crate::input::{InputBaseState, ShowCompletions};
13
14const DEFAULT_INLINE_COMPLETION_DEBOUNCE: Duration = Duration::from_millis(300);
16
17pub type CompletionHighlighter = dyn Fn(&str, &CompletionItem) -> Vec<Range<usize>>;
22
23pub fn highlight_prefix(query: &str, item: &CompletionItem) -> Vec<Range<usize>> {
29 let matched_len = item
30 .filter_text
31 .as_ref()
32 .map(|text| text.len())
33 .unwrap_or(query.len())
34 .min(item.label.len());
35 let end = item.label.floor_char_boundary(matched_len);
36 if end == 0 { Vec::new() } else { vec![0..end] }
37}
38
39pub fn highlight_contains(query: &str, item: &CompletionItem) -> Vec<Range<usize>> {
44 let query: Vec<char> = query.chars().collect();
45 if query.is_empty() {
46 return Vec::new();
47 }
48
49 let label: Vec<(usize, char)> = item.label.char_indices().collect();
50 let Some(last_start) = label.len().checked_sub(query.len()) else {
51 return Vec::new();
52 };
53
54 for start in 0..=last_start {
55 let matched = query
56 .iter()
57 .zip(label[start..].iter())
58 .all(|(query_char, (_, label_char))| chars_eq_ignore_case(*query_char, *label_char));
59 if !matched {
60 continue;
61 }
62
63 let byte_start = label[start].0;
64 let (last_index, last_char) = label[start + query.len() - 1];
65 return vec![byte_start..last_index + last_char.len_utf8()];
66 }
67 Vec::new()
68}
69
70pub fn highlight_characters(query: &str, item: &CompletionItem) -> Vec<Range<usize>> {
76 if query.is_empty() {
77 return Vec::new();
78 }
79
80 let mut ranges: Vec<Range<usize>> = Vec::new();
81 let mut rest = item.label.as_str();
82 let mut base = 0usize;
83 for query_char in query.chars() {
84 let Some((index, matched)) = rest
85 .char_indices()
86 .find(|(_, label_char)| chars_eq_ignore_case(query_char, *label_char))
87 else {
88 return Vec::new();
89 };
90
91 let start = base + index;
92 let end = start + matched.len_utf8();
93 let next = index + matched.len_utf8();
94 base += next;
95 rest = &rest[next..];
96 if let Some(last) = ranges.last_mut() {
97 if last.end == start {
98 last.end = end;
99 continue;
100 }
101 }
102 ranges.push(start..end);
103 }
104 ranges
105}
106
107fn chars_eq_ignore_case(left: char, right: char) -> bool {
108 left == right
109 || left.eq_ignore_ascii_case(&right)
110 || left.to_lowercase().eq(right.to_lowercase())
111}
112
113#[derive(Debug, Clone, Copy)]
128pub struct CompletionMenuOptions {
129 pub max_width: Pixels,
135}
136
137impl Default for CompletionMenuOptions {
138 fn default() -> Self {
139 Self {
140 max_width: px(320.),
141 }
142 }
143}
144
145pub trait CompletionProvider {
147 fn completions(
155 &self,
156 text: &Rope,
157 offset: usize,
158 trigger: CompletionContext,
159 window: &mut Window,
160 cx: &mut App,
161 ) -> Task<Result<CompletionResponse>>;
162
163 fn inline_completion(
178 &self,
179 _rope: &Rope,
180 _offset: usize,
181 _trigger: InlineCompletionContext,
182 _window: &mut Window,
183 _cx: &mut App,
184 ) -> Task<Result<InlineCompletionResponse>> {
185 Task::ready(Ok(InlineCompletionResponse::Array(vec![])))
186 }
187
188 #[inline]
192 fn inline_completion_debounce(&self) -> Duration {
193 DEFAULT_INLINE_COMPLETION_DEBOUNCE
194 }
195
196 fn resolve_completions(
197 &self,
198 _completion_indices: Vec<usize>,
199 _completions: Rc<RefCell<Box<[Completion]>>>,
200 _: &mut App,
201 ) -> Task<Result<bool>> {
202 Task::ready(Ok(false))
203 }
204
205 fn is_completion_trigger(&self, offset: usize, new_text: &str, cx: &mut App) -> bool;
209}
210
211pub(crate) struct InlineCompletion {
212 pub(crate) item: Option<InlineCompletionItem>,
214 pub(crate) task: Task<Result<InlineCompletionResponse>>,
216}
217
218impl Default for InlineCompletion {
219 fn default() -> Self {
220 Self {
221 item: None,
222 task: Task::ready(Ok(InlineCompletionResponse::Array(vec![]))),
223 }
224 }
225}
226
227impl InputBaseState<EditorMode> {
228 pub(crate) fn handle_completion_trigger(
229 &mut self,
230 _range: &Range<usize>,
231 new_text: &str,
232 window: &mut Window,
233 cx: &mut Context<Self>,
234 ) {
235 if self.completion_inserting {
236 return;
237 }
238
239 let Some(provider) = self.extras.lsp.completion_provider.clone() else {
240 return;
241 };
242
243 self.schedule_inline_completion(window, cx);
246
247 let new_offset = self.cursor();
248 let Some(start) = new_offset.checked_sub(new_text.len()) else {
253 return;
254 };
255
256 if !provider.is_completion_trigger(start, new_text, cx) {
257 return;
258 }
259
260 let completion = &self.extras.context_menu_content.completion;
269 let latched = completion.trigger_start_offset.filter(|&latched| {
270 latched <= start
271 && start <= latched + completion.query.len()
272 && self.text.is_char_boundary(latched)
273 && self.text.is_char_boundary(start)
274 && completion
275 .query
276 .starts_with(self.text.slice(latched..start).to_string().as_str())
277 });
278 let start_offset = latched.unwrap_or(start);
279 if new_offset < start_offset {
280 return;
281 }
282
283 let query = self
284 .text_for_range(
285 self.range_to_utf16(&(start_offset..new_offset)),
286 &mut None,
287 window,
288 cx,
289 )
290 .map(|s| s.trim().to_string())
291 .unwrap_or_default();
292 self.request_completions(
293 provider,
294 start_offset,
295 query.clone(),
296 CompletionContext {
297 trigger_kind: lsp_types::CompletionTriggerKind::TRIGGER_CHARACTER,
298 trigger_character: Some(query),
299 },
300 window,
301 cx,
302 );
303 }
304
305 pub(crate) fn on_action_show_completions(
306 &mut self,
307 _: &ShowCompletions,
308 window: &mut Window,
309 cx: &mut Context<Self>,
310 ) {
311 self.show_completions(window, cx);
312 }
313
314 pub fn show_completions(&mut self, window: &mut Window, cx: &mut Context<Self>) {
325 if self.disabled
326 || self.readonly
327 || self.completion_inserting
328 || self.ime_marked_range.is_some()
329 || self.extras.context_menu_content.completion.open
330 {
331 return;
332 }
333 let Some(provider) = self.extras.lsp.completion_provider.clone() else {
334 return;
335 };
336
337 let offset = self.cursor();
338 let word_len: usize = self
339 .text
340 .chars_at(offset)
341 .reversed()
342 .take_while(|ch| ch.is_alphanumeric() || *ch == '_')
343 .map(char::len_utf8)
344 .sum();
345 let start_offset = offset - word_len;
346 let query = self.text.slice(start_offset..offset).to_string();
347
348 self.hide_context_menu(cx);
349 self.clear_inline_completion(cx);
350 self.focus(window, cx);
351
352 self.request_completions(
353 provider,
354 start_offset,
355 query,
356 CompletionContext {
357 trigger_kind: lsp_types::CompletionTriggerKind::INVOKED,
358 trigger_character: None,
359 },
360 window,
361 cx,
362 );
363 }
364
365 fn request_completions(
367 &mut self,
368 provider: Rc<dyn CompletionProvider>,
369 start_offset: usize,
370 query: String,
371 completion_context: CompletionContext,
372 window: &mut Window,
373 cx: &mut Context<Self>,
374 ) {
375 let new_offset = self.cursor();
376 let revision = self.document_revision;
377 self.extras.context_menu_task = Task::ready(Ok(()));
378 self.extras.context_menu_content.code_action.open = false;
379 self.extras
380 .context_menu_content
381 .completion
382 .trigger_start_offset = Some(start_offset);
383 self.extras
384 .context_menu_content
385 .completion
386 .query
387 .clone_from(&query);
388
389 let provider_responses =
390 provider.completions(&self.text, new_offset, completion_context, window, cx);
391 self.extras.context_menu_task = cx.spawn_in(window, async move |editor, cx| {
392 let mut completions: Vec<CompletionItem> = vec![];
393 if let Some(provider_responses) = provider_responses.await.ok() {
394 match provider_responses {
395 CompletionResponse::Array(items) => completions.extend(items),
396 CompletionResponse::List(list) => completions.extend(list.items),
397 }
398 }
399
400 editor
401 .update_in(cx, |editor, window, cx| {
402 if !editor.focus_handle.is_focused(window)
403 || editor.document_revision != revision
404 || editor.cursor() != new_offset
405 || editor.ime_marked_range.is_some()
406 || editor.disabled
407 || editor.readonly
408 {
409 return;
410 }
411
412 editor.extras.context_menu_content.completion.open = !completions.is_empty();
413 editor.extras.context_menu_content.completion.items = completions;
414 editor.extras.context_menu_content.completion.bump();
415 cx.notify();
416 })
417 .ok();
418
419 Ok(())
420 });
421 }
422
423 pub(crate) fn hide_context_menu(&mut self, cx: &mut Context<Self>) {
424 self.extras.context_menu_content.completion.open = false;
425 self.extras.context_menu_content.code_action.open = false;
426 self.extras.context_menu_task = Task::ready(Ok(()));
427 cx.notify();
428 }
429
430 pub(crate) fn is_context_menu_open(&self, _cx: &gpui::App) -> bool {
431 self.extras.context_menu_content.completion.open
432 || self.extras.context_menu_content.code_action.open
433 }
434
435 pub(crate) fn handle_action_for_context_menu(
436 &mut self,
437 action: Box<dyn gpui::Action>,
438 window: &mut Window,
439 cx: &mut Context<Self>,
440 ) -> bool {
441 let closes_overlay =
442 crate::input::Enter::is_primary(&*action) || action.partial_eq(&crate::input::Escape);
443 let kind = if self.extras.context_menu_content.completion.open {
444 Some(super::InputOverlayKind::Completion)
445 } else if self.extras.context_menu_content.code_action.open {
446 Some(super::InputOverlayKind::CodeAction)
447 } else {
448 None
449 };
450 let Some((kind, handler)) = kind.zip(self.overlay_action_handler.clone()) else {
451 return false;
452 };
453 let handled = handler(kind, action, window, cx);
454 if handled && closes_overlay {
455 self.hide_context_menu(cx);
456 }
457 handled
458 }
459
460 pub(crate) fn schedule_inline_completion(
462 &mut self,
463 window: &mut Window,
464 cx: &mut Context<Self>,
465 ) {
466 self.clear_inline_completion(cx);
468
469 let Some(provider) = self.extras.lsp.completion_provider.clone() else {
470 return;
471 };
472
473 let offset = self.cursor();
474 let text = self.text.clone();
475 let debounce = provider.inline_completion_debounce();
476 let background_executor = cx.background_executor().clone();
477
478 self.extras.inline_completion.task = cx.spawn_in(window, async move |editor, cx| {
479 background_executor.timer(debounce).await;
481
482 let task = editor.update_in(cx, |editor, window, cx| {
484 if editor.cursor() != offset {
486 return None;
487 }
488
489 if editor.is_context_menu_open(cx) {
491 return None;
492 }
493
494 let trigger = InlineCompletionContext {
495 trigger_kind: InlineCompletionTriggerKind::Automatic,
496 selected_completion_info: None,
497 };
498
499 Some(provider.inline_completion(&text, offset, trigger, window, cx))
500 })?;
501
502 let Some(task) = task else {
503 return Ok(InlineCompletionResponse::Array(vec![]));
504 };
505
506 let response = task.await?;
507
508 editor.update_in(cx, |editor, _window, cx| {
509 if editor.cursor() != offset {
511 return;
512 }
513
514 if editor.is_context_menu_open(cx) {
516 return;
517 }
518
519 if let Some(item) = match response.clone() {
520 InlineCompletionResponse::Array(items) => items.into_iter().next(),
521 InlineCompletionResponse::List(comp_list) => comp_list.items.into_iter().next(),
522 } {
523 editor.extras.inline_completion.item = Some(item);
524 cx.notify();
525 }
526 })?;
527
528 Ok(response)
529 });
530 }
531
532 #[inline]
534 pub(crate) fn has_inline_completion(&self) -> bool {
535 self.extras.inline_completion.item.is_some()
536 }
537
538 pub(crate) fn clear_inline_completion(&mut self, cx: &mut Context<Self>) {
540 self.extras.inline_completion = InlineCompletion::default();
541 cx.notify();
542 }
543
544 pub(crate) fn accept_inline_completion(
547 &mut self,
548 window: &mut Window,
549 cx: &mut Context<Self>,
550 ) -> bool {
551 let Some(completion_item) = self.extras.inline_completion.item.take() else {
552 return false;
553 };
554
555 let cursor = self.cursor();
556 let range_utf16 = self.range_to_utf16(&(cursor..cursor));
557 let completion_text = completion_item.insert_text;
558 self.replace_text_in_range_silent(Some(range_utf16), &completion_text, window, cx);
559 true
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566
567 fn item(label: &str, filter_text: Option<&str>) -> CompletionItem {
568 CompletionItem {
569 label: label.to_string(),
570 filter_text: filter_text.map(str::to_string),
571 ..Default::default()
572 }
573 }
574
575 #[test]
576 fn prefix_uses_the_query_length_unless_filter_text_is_set() {
577 assert_eq!(highlight_prefix("fl", &item("flag", None)), vec![0..2]);
578 assert_eq!(highlight_prefix("fl", &item("flag", Some("f"))), vec![0..1]);
579 assert_eq!(
580 highlight_prefix("flagpole", &item("flag", None)),
581 vec![0..4]
582 );
583 assert!(highlight_prefix("", &item("flag", None)).is_empty());
584 }
585
586 #[test]
587 fn prefix_does_not_split_a_character() {
588 assert_eq!(highlight_prefix("你", &item("你好", None)), vec![0..3]);
589 assert!(highlight_prefix("a", &item("你", None)).is_empty());
590 }
591
592 #[test]
593 fn contains_highlights_the_first_contiguous_match() {
594 assert_eq!(
596 highlight_contains("log", &item("dialog_log", None)),
597 vec![3..6]
598 );
599 assert_eq!(
600 highlight_contains("LOG", &item("dialog_log", None)),
601 vec![3..6]
602 );
603 assert_eq!(
604 highlight_contains("log", &item("catalog", None)),
605 vec![4..7]
606 );
607 assert_eq!(
608 highlight_contains("好世", &item("你好世界", None)),
609 vec![3..9]
610 );
611 assert!(highlight_contains("log", &item("flag", None)).is_empty());
612 assert!(highlight_contains("", &item("flag", None)).is_empty());
613 }
614
615 #[test]
616 fn characters_highlight_gaps_and_merge_adjacent_hits() {
617 assert_eq!(highlight_characters("fl", &item("flag", None)), vec![0..2]);
618 assert_eq!(
619 highlight_characters("fl", &item("file_loader", None)),
620 vec![0..1, 2..3]
621 );
622 assert_eq!(
623 highlight_characters("你世", &item("你好世界", None)),
624 vec![0..3, 6..9]
625 );
626 assert!(highlight_characters("fx", &item("flag", None)).is_empty());
627 assert!(highlight_characters("", &item("flag", None)).is_empty());
628 }
629}