gpui_component/input/lsp/
completions.rs1use anyhow::Result;
2use gpui::{Context, EntityInputHandler, Task, Window};
3use lsp_types::{request::Completion, CompletionContext, CompletionItem, CompletionResponse};
4use ropey::Rope;
5use std::{cell::RefCell, ops::Range, rc::Rc};
6
7use crate::input::{
8 popovers::{CompletionMenu, ContextMenu},
9 InputState,
10};
11
12pub trait CompletionProvider {
14 fn completions(
22 &self,
23 text: &Rope,
24 offset: usize,
25 trigger: CompletionContext,
26 window: &mut Window,
27 cx: &mut Context<InputState>,
28 ) -> Task<Result<CompletionResponse>>;
29
30 fn resolve_completions(
31 &self,
32 _completion_indices: Vec<usize>,
33 _completions: Rc<RefCell<Box<[Completion]>>>,
34 _: &mut Context<InputState>,
35 ) -> Task<Result<bool>> {
36 Task::ready(Ok(false))
37 }
38
39 fn is_completion_trigger(
43 &self,
44 offset: usize,
45 new_text: &str,
46 cx: &mut Context<InputState>,
47 ) -> bool;
48}
49
50impl InputState {
51 pub(crate) fn handle_completion_trigger(
52 &mut self,
53 range: &Range<usize>,
54 new_text: &str,
55 window: &mut Window,
56 cx: &mut Context<Self>,
57 ) {
58 if self.completion_inserting {
59 return;
60 }
61
62 let Some(provider) = self.lsp.completion_provider.clone() else {
63 return;
64 };
65
66 let start = range.end;
67 let new_offset = self.cursor();
68
69 if !provider.is_completion_trigger(start, new_text, cx) {
70 return;
71 }
72
73 let menu = match self.context_menu.as_ref() {
74 Some(ContextMenu::Completion(menu)) => Some(menu),
75 _ => None,
76 };
77
78 let menu = match menu {
80 Some(menu) => menu.clone(),
81 None => {
82 let menu = CompletionMenu::new(cx.entity(), window, cx);
83 self.context_menu = Some(ContextMenu::Completion(menu.clone()));
84 menu
85 }
86 };
87
88 let start_offset = menu.read(cx).trigger_start_offset.unwrap_or(start);
89 if new_offset < start_offset {
90 return;
91 }
92
93 let query = self
94 .text_for_range(
95 self.range_to_utf16(&(start_offset..new_offset)),
96 &mut None,
97 window,
98 cx,
99 )
100 .map(|s| s.trim().to_string())
101 .unwrap_or_default();
102 _ = menu.update(cx, |menu, _| {
103 menu.update_query(start_offset, query.clone());
104 });
105
106 let completion_context = CompletionContext {
107 trigger_kind: lsp_types::CompletionTriggerKind::TRIGGER_CHARACTER,
108 trigger_character: Some(query),
109 };
110
111 let provider_responses =
112 provider.completions(&self.text, new_offset, completion_context, window, cx);
113 self._context_menu_task = cx.spawn_in(window, async move |editor, cx| {
114 let mut completions: Vec<CompletionItem> = vec![];
115 if let Some(provider_responses) = provider_responses.await.ok() {
116 match provider_responses {
117 CompletionResponse::Array(items) => completions.extend(items),
118 CompletionResponse::List(list) => completions.extend(list.items),
119 }
120 }
121
122 if completions.is_empty() {
123 _ = menu.update(cx, |menu, cx| {
124 menu.hide(cx);
125 cx.notify();
126 });
127
128 return Ok(());
129 }
130
131 editor
132 .update_in(cx, |editor, window, cx| {
133 if !editor.focus_handle.is_focused(window) {
134 return;
135 }
136
137 _ = menu.update(cx, |menu, cx| {
138 menu.show(new_offset, completions, window, cx);
139 });
140
141 cx.notify();
142 })
143 .ok();
144
145 Ok(())
146 });
147 }
148}