gpui_component/input/lsp/
completions.rs1use anyhow::Result;
2use gpui::{Context, EntityInputHandler, Task, Window};
3use lsp_types::{
4 CompletionContext, CompletionItem, CompletionResponse, InlineCompletionContext,
5 InlineCompletionItem, InlineCompletionResponse, InlineCompletionTriggerKind,
6 request::Completion,
7};
8use ropey::Rope;
9use std::{cell::RefCell, ops::Range, rc::Rc, time::Duration};
10
11use crate::input::{
12 InputState,
13 popovers::{CompletionMenu, ContextMenu},
14};
15
16const DEFAULT_INLINE_COMPLETION_DEBOUNCE: Duration = Duration::from_millis(300);
18
19pub trait CompletionProvider {
21 fn completions(
29 &self,
30 text: &Rope,
31 offset: usize,
32 trigger: CompletionContext,
33 window: &mut Window,
34 cx: &mut Context<InputState>,
35 ) -> Task<Result<CompletionResponse>>;
36
37 fn inline_completion(
52 &self,
53 _rope: &Rope,
54 _offset: usize,
55 _trigger: InlineCompletionContext,
56 _window: &mut Window,
57 _cx: &mut Context<InputState>,
58 ) -> Task<Result<InlineCompletionResponse>> {
59 Task::ready(Ok(InlineCompletionResponse::Array(vec![])))
60 }
61
62 #[inline]
66 fn inline_completion_debounce(&self) -> Duration {
67 DEFAULT_INLINE_COMPLETION_DEBOUNCE
68 }
69
70 fn resolve_completions(
71 &self,
72 _completion_indices: Vec<usize>,
73 _completions: Rc<RefCell<Box<[Completion]>>>,
74 _: &mut Context<InputState>,
75 ) -> Task<Result<bool>> {
76 Task::ready(Ok(false))
77 }
78
79 fn is_completion_trigger(
83 &self,
84 offset: usize,
85 new_text: &str,
86 cx: &mut Context<InputState>,
87 ) -> bool;
88}
89
90pub(crate) struct InlineCompletion {
91 pub(crate) item: Option<InlineCompletionItem>,
93 pub(crate) task: Task<Result<InlineCompletionResponse>>,
95}
96
97impl Default for InlineCompletion {
98 fn default() -> Self {
99 Self {
100 item: None,
101 task: Task::ready(Ok(InlineCompletionResponse::Array(vec![]))),
102 }
103 }
104}
105
106impl InputState {
107 pub(crate) fn handle_completion_trigger(
108 &mut self,
109 range: &Range<usize>,
110 new_text: &str,
111 window: &mut Window,
112 cx: &mut Context<Self>,
113 ) {
114 if self.completion_inserting {
115 return;
116 }
117
118 let Some(provider) = self.lsp.completion_provider.clone() else {
119 return;
120 };
121
122 self.schedule_inline_completion(window, cx);
125
126 let start = range.end;
127 let new_offset = self.cursor();
128
129 if !provider.is_completion_trigger(start, new_text, cx) {
130 return;
131 }
132
133 let menu = match self.context_menu.as_ref() {
134 Some(ContextMenu::Completion(menu)) => Some(menu),
135 _ => None,
136 };
137
138 let menu = match menu {
140 Some(menu) => menu.clone(),
141 None => {
142 let menu = CompletionMenu::new(cx.entity(), window, cx);
143 self.context_menu = Some(ContextMenu::Completion(menu.clone()));
144 menu
145 }
146 };
147
148 let start_offset = menu.read(cx).trigger_start_offset.unwrap_or(start);
149 if new_offset < start_offset {
150 return;
151 }
152
153 let query = self
154 .text_for_range(
155 self.range_to_utf16(&(start_offset..new_offset)),
156 &mut None,
157 window,
158 cx,
159 )
160 .map(|s| s.trim().to_string())
161 .unwrap_or_default();
162 _ = menu.update(cx, |menu, _| {
163 menu.update_query(start_offset, query.clone());
164 });
165
166 let completion_context = CompletionContext {
167 trigger_kind: lsp_types::CompletionTriggerKind::TRIGGER_CHARACTER,
168 trigger_character: Some(query),
169 };
170
171 let provider_responses =
172 provider.completions(&self.text, new_offset, completion_context, window, cx);
173 self._context_menu_task = cx.spawn_in(window, async move |editor, cx| {
174 let mut completions: Vec<CompletionItem> = vec![];
175 if let Some(provider_responses) = provider_responses.await.ok() {
176 match provider_responses {
177 CompletionResponse::Array(items) => completions.extend(items),
178 CompletionResponse::List(list) => completions.extend(list.items),
179 }
180 }
181
182 if completions.is_empty() {
183 _ = menu.update(cx, |menu, cx| {
184 menu.hide(cx);
185 cx.notify();
186 });
187
188 return Ok(());
189 }
190
191 editor
192 .update_in(cx, |editor, window, cx| {
193 if !editor.focus_handle.is_focused(window) {
194 return;
195 }
196
197 _ = menu.update(cx, |menu, cx| {
198 menu.show(new_offset, completions, window, cx);
199 });
200
201 cx.notify();
202 })
203 .ok();
204
205 Ok(())
206 });
207 }
208
209 pub(crate) fn schedule_inline_completion(
211 &mut self,
212 window: &mut Window,
213 cx: &mut Context<Self>,
214 ) {
215 self.clear_inline_completion(cx);
217
218 let Some(provider) = self.lsp.completion_provider.clone() else {
219 return;
220 };
221
222 let offset = self.cursor();
223 let text = self.text.clone();
224 let debounce = provider.inline_completion_debounce();
225
226 self.inline_completion.task = cx.spawn_in(window, async move |editor, cx| {
227 smol::Timer::after(debounce).await;
229
230 let task = editor.update_in(cx, |editor, window, cx| {
232 if editor.cursor() != offset {
234 return None;
235 }
236
237 if editor.is_context_menu_open(cx) {
239 return None;
240 }
241
242 let trigger = InlineCompletionContext {
243 trigger_kind: InlineCompletionTriggerKind::Automatic,
244 selected_completion_info: None,
245 };
246
247 Some(provider.inline_completion(&text, offset, trigger, window, cx))
248 })?;
249
250 let Some(task) = task else {
251 return Ok(InlineCompletionResponse::Array(vec![]));
252 };
253
254 let response = task.await?;
255
256 editor.update_in(cx, |editor, _window, cx| {
257 if editor.cursor() != offset {
259 return;
260 }
261
262 if editor.is_context_menu_open(cx) {
264 return;
265 }
266
267 if let Some(item) = match response.clone() {
268 InlineCompletionResponse::Array(items) => items.into_iter().next(),
269 InlineCompletionResponse::List(comp_list) => comp_list.items.into_iter().next(),
270 } {
271 editor.inline_completion.item = Some(item);
272 cx.notify();
273 }
274 })?;
275
276 Ok(response)
277 });
278 }
279
280 #[inline]
282 pub(crate) fn has_inline_completion(&self) -> bool {
283 self.inline_completion.item.is_some()
284 }
285
286 pub(crate) fn clear_inline_completion(&mut self, cx: &mut Context<Self>) {
288 self.inline_completion = InlineCompletion::default();
289 cx.notify();
290 }
291
292 pub(crate) fn accept_inline_completion(
295 &mut self,
296 window: &mut Window,
297 cx: &mut Context<Self>,
298 ) -> bool {
299 let Some(completion_item) = self.inline_completion.item.take() else {
300 return false;
301 };
302
303 let cursor = self.cursor();
304 let range_utf16 = self.range_to_utf16(&(cursor..cursor));
305 let completion_text = completion_item.insert_text;
306 self.replace_text_in_range_silent(Some(range_utf16), &completion_text, window, cx);
307 true
308 }
309}