Skip to main content

gpui_base/input/editor/lsp/
completions.rs

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
14/// Default debounce duration for inline completions.
15const DEFAULT_INLINE_COMPLETION_DEBOUNCE: Duration = Duration::from_millis(300);
16
17/// Display options for the LSP completion popover.
18///
19/// Accessed through [`super::Lsp::completion_menu`] so embedders can tweak the
20/// popover without growing the [`InputBaseState`] API.
21#[derive(Debug, Clone, Copy)]
22pub struct CompletionMenuOptions {
23    /// Maximum width of the popover.
24    ///
25    /// Defaults to 320 px, which is fine for most identifiers but can
26    /// truncate longer labels. Widen this when hosting an editor that
27    /// surfaces long completion labels.
28    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
39/// A trait for providing code completions based on the current input state and context.
40pub trait CompletionProvider {
41    /// Fetches completions based on the given byte offset.
42    ///
43    /// - The `offset` is in bytes of current cursor.
44    ///
45    /// textDocument/completion
46    ///
47    /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
48    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    /// Fetches an inline completion suggestion for the given position.
58    ///
59    /// This is called after a debounce period when the user stops typing.
60    /// The provider can analyze the text and cursor position to determine
61    /// what inline completion suggestion to show.
62    ///
63    ///
64    /// # Arguments
65    /// * `rope` - The current text content
66    /// * `offset` - The cursor position in bytes
67    ///
68    /// textDocument/inlineCompletion
69    ///
70    /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_inlineCompletion
71    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    /// Returns the debounce duration for inline completions.
83    ///
84    /// Default: 300ms
85    #[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    /// Determines if the completion should be triggered based on the given byte offset.
100    ///
101    /// This is called on the main thread.
102    fn is_completion_trigger(&self, offset: usize, new_text: &str, cx: &mut App) -> bool;
103}
104
105pub(crate) struct InlineCompletion {
106    /// Completion item to display as an inline completion suggestion
107    pub(crate) item: Option<InlineCompletionItem>,
108    /// Task for debouncing inline completion requests
109    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        // Always schedule inline completion (debounced).
138        // It will check if menu is open before showing the suggestion.
139        self.schedule_inline_completion(window, cx);
140
141        let new_offset = self.cursor();
142        // Measure the inserted text in the current document. The replaced range
143        // uses pre-edit coordinates, which preceding multi-cursor edits can
144        // shift. The active caret ends immediately after the normalized input,
145        // including selection replacements and IME commits.
146        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        // `trigger_start_offset` latches where the word a menu was opened for
155        // begins, so later keystrokes refine the same query instead of starting
156        // over at each character. It only describes this edit while the edit
157        // continues that word: the document between the latch and the edit
158        // must still read as a prefix of the last query. Deleting back into
159        // the word keeps it; typing somewhere else, or into a document that
160        // has since been replaced, starts a new query at this edit instead of
161        // handing the provider text the user never typed as a prefix.
162        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    /// Request completions at the caret and focus the editor, without editing text.
209    ///
210    /// The current identifier prefix is used to refine the menu. An empty
211    /// prefix still asks the provider for suggestions. There is no default
212    /// keybinding; applications can dispatch [`ShowCompletions`] in the
213    /// `Input && mode == editor` context or call this method from a command.
214    ///
215    /// Disabled/readonly editors, IME preedit, and an already-open completion
216    /// menu are left alone. A new request cancels the previous overlay request
217    /// and inline suggestion, and closes the code-action menu.
218    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    /// Record the word a menu is for and ask `provider` about the caret.
260    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    /// Schedule an inline completion request after debouncing.
355    pub(crate) fn schedule_inline_completion(
356        &mut self,
357        window: &mut Window,
358        cx: &mut Context<Self>,
359    ) {
360        // Clear any existing inline completion on text change
361        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            // Debounce: wait before fetching to avoid unnecessary requests while typing
374            background_executor.timer(debounce).await;
375
376            // Now fetch the inline completion after the debounce period
377            let task = editor.update_in(cx, |editor, window, cx| {
378                // Check if cursor has moved during debounce
379                if editor.cursor() != offset {
380                    return None;
381                }
382
383                // Don't fetch if completion menu is open
384                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                // Only apply if cursor still hasn't moved
404                if editor.cursor() != offset {
405                    return;
406                }
407
408                // Don't show if completion menu opened while we were fetching
409                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    /// Check if an inline completion suggestion is currently displayed.
427    #[inline]
428    pub(crate) fn has_inline_completion(&self) -> bool {
429        self.extras.inline_completion.item.is_some()
430    }
431
432    /// Clear the inline completion suggestion.
433    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    /// Accept the inline completion, inserting it at the cursor position.
439    /// Returns true if a completion was accepted, false if there was none.
440    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}