gpui_base/input/editor/lsp/
completions.rs1use 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
17#[derive(Debug, Clone, Copy)]
22pub struct CompletionMenuOptions {
23 pub max_width: Pixels,
29}
30
31impl Default for CompletionMenuOptions {
32 fn default() -> Self {
33 Self {
34 max_width: px(320.),
35 }
36 }
37}
38
39pub trait CompletionProvider {
41 fn completions(
49 &self,
50 text: &Rope,
51 offset: usize,
52 trigger: CompletionContext,
53 window: &mut Window,
54 cx: &mut App,
55 ) -> Task<Result<CompletionResponse>>;
56
57 fn inline_completion(
72 &self,
73 _rope: &Rope,
74 _offset: usize,
75 _trigger: InlineCompletionContext,
76 _window: &mut Window,
77 _cx: &mut App,
78 ) -> Task<Result<InlineCompletionResponse>> {
79 Task::ready(Ok(InlineCompletionResponse::Array(vec![])))
80 }
81
82 #[inline]
86 fn inline_completion_debounce(&self) -> Duration {
87 DEFAULT_INLINE_COMPLETION_DEBOUNCE
88 }
89
90 fn resolve_completions(
91 &self,
92 _completion_indices: Vec<usize>,
93 _completions: Rc<RefCell<Box<[Completion]>>>,
94 _: &mut App,
95 ) -> Task<Result<bool>> {
96 Task::ready(Ok(false))
97 }
98
99 fn is_completion_trigger(&self, offset: usize, new_text: &str, cx: &mut App) -> bool;
103}
104
105pub(crate) struct InlineCompletion {
106 pub(crate) item: Option<InlineCompletionItem>,
108 pub(crate) task: Task<Result<InlineCompletionResponse>>,
110}
111
112impl Default for InlineCompletion {
113 fn default() -> Self {
114 Self {
115 item: None,
116 task: Task::ready(Ok(InlineCompletionResponse::Array(vec![]))),
117 }
118 }
119}
120
121impl InputBaseState<EditorMode> {
122 pub(crate) fn handle_completion_trigger(
123 &mut self,
124 _range: &Range<usize>,
125 new_text: &str,
126 window: &mut Window,
127 cx: &mut Context<Self>,
128 ) {
129 if self.completion_inserting {
130 return;
131 }
132
133 let Some(provider) = self.extras.lsp.completion_provider.clone() else {
134 return;
135 };
136
137 self.schedule_inline_completion(window, cx);
140
141 let new_offset = self.cursor();
142 let Some(start) = new_offset.checked_sub(new_text.len()) else {
147 return;
148 };
149
150 if !provider.is_completion_trigger(start, new_text, cx) {
151 return;
152 }
153
154 let completion = &self.extras.context_menu_content.completion;
163 let latched = completion.trigger_start_offset.filter(|&latched| {
164 latched <= start
165 && start <= latched + completion.query.len()
166 && self.text.is_char_boundary(latched)
167 && self.text.is_char_boundary(start)
168 && completion
169 .query
170 .starts_with(self.text.slice(latched..start).to_string().as_str())
171 });
172 let start_offset = latched.unwrap_or(start);
173 if new_offset < start_offset {
174 return;
175 }
176
177 let query = self
178 .text_for_range(
179 self.range_to_utf16(&(start_offset..new_offset)),
180 &mut None,
181 window,
182 cx,
183 )
184 .map(|s| s.trim().to_string())
185 .unwrap_or_default();
186 self.request_completions(
187 provider,
188 start_offset,
189 query.clone(),
190 CompletionContext {
191 trigger_kind: lsp_types::CompletionTriggerKind::TRIGGER_CHARACTER,
192 trigger_character: Some(query),
193 },
194 window,
195 cx,
196 );
197 }
198
199 pub(crate) fn on_action_show_completions(
200 &mut self,
201 _: &ShowCompletions,
202 window: &mut Window,
203 cx: &mut Context<Self>,
204 ) {
205 self.show_completions(window, cx);
206 }
207
208 pub fn show_completions(&mut self, window: &mut Window, cx: &mut Context<Self>) {
219 if self.disabled
220 || self.readonly
221 || self.completion_inserting
222 || self.ime_marked_range.is_some()
223 || self.extras.context_menu_content.completion.open
224 {
225 return;
226 }
227 let Some(provider) = self.extras.lsp.completion_provider.clone() else {
228 return;
229 };
230
231 let offset = self.cursor();
232 let word_len: usize = self
233 .text
234 .chars_at(offset)
235 .reversed()
236 .take_while(|ch| ch.is_alphanumeric() || *ch == '_')
237 .map(char::len_utf8)
238 .sum();
239 let start_offset = offset - word_len;
240 let query = self.text.slice(start_offset..offset).to_string();
241
242 self.hide_context_menu(cx);
243 self.clear_inline_completion(cx);
244 self.focus(window, cx);
245
246 self.request_completions(
247 provider,
248 start_offset,
249 query,
250 CompletionContext {
251 trigger_kind: lsp_types::CompletionTriggerKind::INVOKED,
252 trigger_character: None,
253 },
254 window,
255 cx,
256 );
257 }
258
259 fn request_completions(
261 &mut self,
262 provider: Rc<dyn CompletionProvider>,
263 start_offset: usize,
264 query: String,
265 completion_context: CompletionContext,
266 window: &mut Window,
267 cx: &mut Context<Self>,
268 ) {
269 let new_offset = self.cursor();
270 let revision = self.document_revision;
271 self.extras.context_menu_task = Task::ready(Ok(()));
272 self.extras.context_menu_content.code_action.open = false;
273 self.extras
274 .context_menu_content
275 .completion
276 .trigger_start_offset = Some(start_offset);
277 self.extras
278 .context_menu_content
279 .completion
280 .query
281 .clone_from(&query);
282
283 let provider_responses =
284 provider.completions(&self.text, new_offset, completion_context, window, cx);
285 self.extras.context_menu_task = cx.spawn_in(window, async move |editor, cx| {
286 let mut completions: Vec<CompletionItem> = vec![];
287 if let Some(provider_responses) = provider_responses.await.ok() {
288 match provider_responses {
289 CompletionResponse::Array(items) => completions.extend(items),
290 CompletionResponse::List(list) => completions.extend(list.items),
291 }
292 }
293
294 editor
295 .update_in(cx, |editor, window, cx| {
296 if !editor.focus_handle.is_focused(window)
297 || editor.document_revision != revision
298 || editor.cursor() != new_offset
299 || editor.ime_marked_range.is_some()
300 || editor.disabled
301 || editor.readonly
302 {
303 return;
304 }
305
306 editor.extras.context_menu_content.completion.open = !completions.is_empty();
307 editor.extras.context_menu_content.completion.items = completions;
308 editor.extras.context_menu_content.completion.bump();
309 cx.notify();
310 })
311 .ok();
312
313 Ok(())
314 });
315 }
316
317 pub(crate) fn hide_context_menu(&mut self, cx: &mut Context<Self>) {
318 self.extras.context_menu_content.completion.open = false;
319 self.extras.context_menu_content.code_action.open = false;
320 self.extras.context_menu_task = Task::ready(Ok(()));
321 cx.notify();
322 }
323
324 pub(crate) fn is_context_menu_open(&self, _cx: &gpui::App) -> bool {
325 self.extras.context_menu_content.completion.open
326 || self.extras.context_menu_content.code_action.open
327 }
328
329 pub(crate) fn handle_action_for_context_menu(
330 &mut self,
331 action: Box<dyn gpui::Action>,
332 window: &mut Window,
333 cx: &mut Context<Self>,
334 ) -> bool {
335 let closes_overlay =
336 crate::input::Enter::is_primary(&*action) || action.partial_eq(&crate::input::Escape);
337 let kind = if self.extras.context_menu_content.completion.open {
338 Some(super::InputOverlayKind::Completion)
339 } else if self.extras.context_menu_content.code_action.open {
340 Some(super::InputOverlayKind::CodeAction)
341 } else {
342 None
343 };
344 let Some((kind, handler)) = kind.zip(self.overlay_action_handler.clone()) else {
345 return false;
346 };
347 let handled = handler(kind, action, window, cx);
348 if handled && closes_overlay {
349 self.hide_context_menu(cx);
350 }
351 handled
352 }
353
354 pub(crate) fn schedule_inline_completion(
356 &mut self,
357 window: &mut Window,
358 cx: &mut Context<Self>,
359 ) {
360 self.clear_inline_completion(cx);
362
363 let Some(provider) = self.extras.lsp.completion_provider.clone() else {
364 return;
365 };
366
367 let offset = self.cursor();
368 let text = self.text.clone();
369 let debounce = provider.inline_completion_debounce();
370 let background_executor = cx.background_executor().clone();
371
372 self.extras.inline_completion.task = cx.spawn_in(window, async move |editor, cx| {
373 background_executor.timer(debounce).await;
375
376 let task = editor.update_in(cx, |editor, window, cx| {
378 if editor.cursor() != offset {
380 return None;
381 }
382
383 if editor.is_context_menu_open(cx) {
385 return None;
386 }
387
388 let trigger = InlineCompletionContext {
389 trigger_kind: InlineCompletionTriggerKind::Automatic,
390 selected_completion_info: None,
391 };
392
393 Some(provider.inline_completion(&text, offset, trigger, window, cx))
394 })?;
395
396 let Some(task) = task else {
397 return Ok(InlineCompletionResponse::Array(vec![]));
398 };
399
400 let response = task.await?;
401
402 editor.update_in(cx, |editor, _window, cx| {
403 if editor.cursor() != offset {
405 return;
406 }
407
408 if editor.is_context_menu_open(cx) {
410 return;
411 }
412
413 if let Some(item) = match response.clone() {
414 InlineCompletionResponse::Array(items) => items.into_iter().next(),
415 InlineCompletionResponse::List(comp_list) => comp_list.items.into_iter().next(),
416 } {
417 editor.extras.inline_completion.item = Some(item);
418 cx.notify();
419 }
420 })?;
421
422 Ok(response)
423 });
424 }
425
426 #[inline]
428 pub(crate) fn has_inline_completion(&self) -> bool {
429 self.extras.inline_completion.item.is_some()
430 }
431
432 pub(crate) fn clear_inline_completion(&mut self, cx: &mut Context<Self>) {
434 self.extras.inline_completion = InlineCompletion::default();
435 cx.notify();
436 }
437
438 pub(crate) fn accept_inline_completion(
441 &mut self,
442 window: &mut Window,
443 cx: &mut Context<Self>,
444 ) -> bool {
445 let Some(completion_item) = self.extras.inline_completion.item.take() else {
446 return false;
447 };
448
449 let cursor = self.cursor();
450 let range_utf16 = self.range_to_utf16(&(cursor..cursor));
451 let completion_text = completion_item.insert_text;
452 self.replace_text_in_range_silent(Some(range_utf16), &completion_text, window, cx);
453 true
454 }
455}