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;
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 start = range.end;
142        let new_offset = self.cursor();
143
144        if !provider.is_completion_trigger(start, new_text, cx) {
145            return;
146        }
147
148        let start_offset = self
149            .extras
150            .context_menu_content
151            .completion
152            .trigger_start_offset
153            .unwrap_or(start);
154        if new_offset < start_offset {
155            return;
156        }
157
158        let query = self
159            .text_for_range(
160                self.range_to_utf16(&(start_offset..new_offset)),
161                &mut None,
162                window,
163                cx,
164            )
165            .map(|s| s.trim().to_string())
166            .unwrap_or_default();
167        self.extras
168            .context_menu_content
169            .completion
170            .trigger_start_offset = Some(start_offset);
171        self.extras
172            .context_menu_content
173            .completion
174            .query
175            .clone_from(&query);
176
177        let completion_context = CompletionContext {
178            trigger_kind: lsp_types::CompletionTriggerKind::TRIGGER_CHARACTER,
179            trigger_character: Some(query),
180        };
181
182        let provider_responses =
183            provider.completions(&self.text, new_offset, completion_context, window, cx);
184        self.extras.context_menu_task = cx.spawn_in(window, async move |editor, cx| {
185            let mut completions: Vec<CompletionItem> = vec![];
186            if let Some(provider_responses) = provider_responses.await.ok() {
187                match provider_responses {
188                    CompletionResponse::Array(items) => completions.extend(items),
189                    CompletionResponse::List(list) => completions.extend(list.items),
190                }
191            }
192
193            if completions.is_empty() {
194                editor.update(cx, |editor, cx| {
195                    editor.extras.context_menu_content.completion.open = false;
196                    editor.extras.context_menu_content.completion.items.clear();
197                    editor.extras.context_menu_content.completion.bump();
198                    cx.notify();
199                })?;
200                return Ok(());
201            }
202
203            editor
204                .update_in(cx, |editor, window, cx| {
205                    if !editor.focus_handle.is_focused(window) {
206                        return;
207                    }
208
209                    editor.extras.context_menu_content.completion.items = completions;
210                    editor.extras.context_menu_content.completion.open = !editor
211                        .extras
212                        .context_menu_content
213                        .completion
214                        .items
215                        .is_empty();
216                    editor.extras.context_menu_content.completion.bump();
217
218                    cx.notify();
219                })
220                .ok();
221
222            Ok(())
223        });
224    }
225
226    pub(crate) fn hide_context_menu(&mut self, cx: &mut Context<Self>) {
227        self.extras.context_menu_content.completion.open = false;
228        self.extras.context_menu_content.code_action.open = false;
229        self.extras.context_menu_task = Task::ready(Ok(()));
230        cx.notify();
231    }
232
233    pub(crate) fn is_context_menu_open(&self, _cx: &gpui::App) -> bool {
234        self.extras.context_menu_content.completion.open
235            || self.extras.context_menu_content.code_action.open
236    }
237
238    pub(crate) fn handle_action_for_context_menu(
239        &mut self,
240        action: Box<dyn gpui::Action>,
241        window: &mut Window,
242        cx: &mut Context<Self>,
243    ) -> bool {
244        let closes_overlay =
245            crate::input::Enter::is_primary(&*action) || action.partial_eq(&crate::input::Escape);
246        let kind = if self.extras.context_menu_content.completion.open {
247            Some(super::InputOverlayKind::Completion)
248        } else if self.extras.context_menu_content.code_action.open {
249            Some(super::InputOverlayKind::CodeAction)
250        } else {
251            None
252        };
253        let Some((kind, handler)) = kind.zip(self.overlay_action_handler.clone()) else {
254            return false;
255        };
256        let handled = handler(kind, action, window, cx);
257        if handled && closes_overlay {
258            match kind {
259                super::InputOverlayKind::Completion => {
260                    self.extras.context_menu_content.completion.open = false
261                }
262                super::InputOverlayKind::CodeAction => {
263                    self.extras.context_menu_content.code_action.open = false
264                }
265            }
266            cx.notify();
267        }
268        handled
269    }
270
271    /// Schedule an inline completion request after debouncing.
272    pub(crate) fn schedule_inline_completion(
273        &mut self,
274        window: &mut Window,
275        cx: &mut Context<Self>,
276    ) {
277        // Clear any existing inline completion on text change
278        self.clear_inline_completion(cx);
279
280        let Some(provider) = self.extras.lsp.completion_provider.clone() else {
281            return;
282        };
283
284        let offset = self.cursor();
285        let text = self.text.clone();
286        let debounce = provider.inline_completion_debounce();
287        let background_executor = cx.background_executor().clone();
288
289        self.extras.inline_completion.task = cx.spawn_in(window, async move |editor, cx| {
290            // Debounce: wait before fetching to avoid unnecessary requests while typing
291            background_executor.timer(debounce).await;
292
293            // Now fetch the inline completion after the debounce period
294            let task = editor.update_in(cx, |editor, window, cx| {
295                // Check if cursor has moved during debounce
296                if editor.cursor() != offset {
297                    return None;
298                }
299
300                // Don't fetch if completion menu is open
301                if editor.is_context_menu_open(cx) {
302                    return None;
303                }
304
305                let trigger = InlineCompletionContext {
306                    trigger_kind: InlineCompletionTriggerKind::Automatic,
307                    selected_completion_info: None,
308                };
309
310                Some(provider.inline_completion(&text, offset, trigger, window, cx))
311            })?;
312
313            let Some(task) = task else {
314                return Ok(InlineCompletionResponse::Array(vec![]));
315            };
316
317            let response = task.await?;
318
319            editor.update_in(cx, |editor, _window, cx| {
320                // Only apply if cursor still hasn't moved
321                if editor.cursor() != offset {
322                    return;
323                }
324
325                // Don't show if completion menu opened while we were fetching
326                if editor.is_context_menu_open(cx) {
327                    return;
328                }
329
330                if let Some(item) = match response.clone() {
331                    InlineCompletionResponse::Array(items) => items.into_iter().next(),
332                    InlineCompletionResponse::List(comp_list) => comp_list.items.into_iter().next(),
333                } {
334                    editor.extras.inline_completion.item = Some(item);
335                    cx.notify();
336                }
337            })?;
338
339            Ok(response)
340        });
341    }
342
343    /// Check if an inline completion suggestion is currently displayed.
344    #[inline]
345    pub(crate) fn has_inline_completion(&self) -> bool {
346        self.extras.inline_completion.item.is_some()
347    }
348
349    /// Clear the inline completion suggestion.
350    pub(crate) fn clear_inline_completion(&mut self, cx: &mut Context<Self>) {
351        self.extras.inline_completion = InlineCompletion::default();
352        cx.notify();
353    }
354
355    /// Accept the inline completion, inserting it at the cursor position.
356    /// Returns true if a completion was accepted, false if there was none.
357    pub(crate) fn accept_inline_completion(
358        &mut self,
359        window: &mut Window,
360        cx: &mut Context<Self>,
361    ) -> bool {
362        let Some(completion_item) = self.extras.inline_completion.item.take() else {
363            return false;
364        };
365
366        let cursor = self.cursor();
367        let range_utf16 = self.range_to_utf16(&(cursor..cursor));
368        let completion_text = completion_item.insert_text;
369        self.replace_text_in_range_silent(Some(range_utf16), &completion_text, window, cx);
370        true
371    }
372}