Skip to main content

gpui_component/input/lsp/
completions.rs

1use 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
16/// Default debounce duration for inline completions.
17const DEFAULT_INLINE_COMPLETION_DEBOUNCE: Duration = Duration::from_millis(300);
18
19/// A trait for providing code completions based on the current input state and context.
20pub trait CompletionProvider {
21    /// Fetches completions based on the given byte offset.
22    ///
23    /// - The `offset` is in bytes of current cursor.
24    ///
25    /// textDocument/completion
26    ///
27    /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
28    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    /// Fetches an inline completion suggestion for the given position.
38    ///
39    /// This is called after a debounce period when the user stops typing.
40    /// The provider can analyze the text and cursor position to determine
41    /// what inline completion suggestion to show.
42    ///
43    ///
44    /// # Arguments
45    /// * `rope` - The current text content
46    /// * `offset` - The cursor position in bytes
47    ///
48    /// textDocument/inlineCompletion
49    ///
50    /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_inlineCompletion
51    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    /// Returns the debounce duration for inline completions.
63    ///
64    /// Default: 300ms
65    #[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    /// Determines if the completion should be triggered based on the given byte offset.
80    ///
81    /// This is called on the main thread.
82    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    /// Completion item to display as an inline completion suggestion
92    pub(crate) item: Option<InlineCompletionItem>,
93    /// Task for debouncing inline completion requests
94    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        // Always schedule inline completion (debounced).
123        // It will check if menu is open before showing the suggestion.
124        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        // To create or get the existing completion menu.
139        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    /// Schedule an inline completion request after debouncing.
210    pub(crate) fn schedule_inline_completion(
211        &mut self,
212        window: &mut Window,
213        cx: &mut Context<Self>,
214    ) {
215        // Clear any existing inline completion on text change
216        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            // Debounce: wait before fetching to avoid unnecessary requests while typing
228            smol::Timer::after(debounce).await;
229
230            // Now fetch the inline completion after the debounce period
231            let task = editor.update_in(cx, |editor, window, cx| {
232                // Check if cursor has moved during debounce
233                if editor.cursor() != offset {
234                    return None;
235                }
236
237                // Don't fetch if completion menu is open
238                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                // Only apply if cursor still hasn't moved
258                if editor.cursor() != offset {
259                    return;
260                }
261
262                // Don't show if completion menu opened while we were fetching
263                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    /// Check if an inline completion suggestion is currently displayed.
281    #[inline]
282    pub(crate) fn has_inline_completion(&self) -> bool {
283        self.inline_completion.item.is_some()
284    }
285
286    /// Clear the inline completion suggestion.
287    pub(crate) fn clear_inline_completion(&mut self, cx: &mut Context<Self>) {
288        self.inline_completion = InlineCompletion::default();
289        cx.notify();
290    }
291
292    /// Accept the inline completion, inserting it at the cursor position.
293    /// Returns true if a completion was accepted, false if there was none.
294    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}