gpui_component/input/lsp/
completions.rs

1use anyhow::Result;
2use gpui::{Context, EntityInputHandler, Task, Window};
3use lsp_types::{request::Completion, CompletionContext, CompletionItem, CompletionResponse};
4use ropey::Rope;
5use std::{cell::RefCell, ops::Range, rc::Rc};
6
7use crate::input::{
8    popovers::{CompletionMenu, ContextMenu},
9    InputState,
10};
11
12/// A trait for providing code completions based on the current input state and context.
13pub trait CompletionProvider {
14    /// Fetches completions based on the given byte offset.
15    ///
16    /// - The `offset` is in bytes of current cursor.
17    ///
18    /// textDocument/completion
19    ///
20    /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
21    fn completions(
22        &self,
23        text: &Rope,
24        offset: usize,
25        trigger: CompletionContext,
26        window: &mut Window,
27        cx: &mut Context<InputState>,
28    ) -> Task<Result<CompletionResponse>>;
29
30    fn resolve_completions(
31        &self,
32        _completion_indices: Vec<usize>,
33        _completions: Rc<RefCell<Box<[Completion]>>>,
34        _: &mut Context<InputState>,
35    ) -> Task<Result<bool>> {
36        Task::ready(Ok(false))
37    }
38
39    /// Determines if the completion should be triggered based on the given byte offset.
40    ///
41    /// This is called on the main thread.
42    fn is_completion_trigger(
43        &self,
44        offset: usize,
45        new_text: &str,
46        cx: &mut Context<InputState>,
47    ) -> bool;
48}
49
50impl InputState {
51    pub(crate) fn handle_completion_trigger(
52        &mut self,
53        range: &Range<usize>,
54        new_text: &str,
55        window: &mut Window,
56        cx: &mut Context<Self>,
57    ) {
58        if self.completion_inserting {
59            return;
60        }
61
62        let Some(provider) = self.lsp.completion_provider.clone() else {
63            return;
64        };
65
66        let start = range.end;
67        let new_offset = self.cursor();
68
69        if !provider.is_completion_trigger(start, new_text, cx) {
70            return;
71        }
72
73        let menu = match self.context_menu.as_ref() {
74            Some(ContextMenu::Completion(menu)) => Some(menu),
75            _ => None,
76        };
77
78        // To create or get the existing completion menu.
79        let menu = match menu {
80            Some(menu) => menu.clone(),
81            None => {
82                let menu = CompletionMenu::new(cx.entity(), window, cx);
83                self.context_menu = Some(ContextMenu::Completion(menu.clone()));
84                menu
85            }
86        };
87
88        let start_offset = menu.read(cx).trigger_start_offset.unwrap_or(start);
89        if new_offset < start_offset {
90            return;
91        }
92
93        let query = self
94            .text_for_range(
95                self.range_to_utf16(&(start_offset..new_offset)),
96                &mut None,
97                window,
98                cx,
99            )
100            .map(|s| s.trim().to_string())
101            .unwrap_or_default();
102        _ = menu.update(cx, |menu, _| {
103            menu.update_query(start_offset, query.clone());
104        });
105
106        let completion_context = CompletionContext {
107            trigger_kind: lsp_types::CompletionTriggerKind::TRIGGER_CHARACTER,
108            trigger_character: Some(query),
109        };
110
111        let provider_responses =
112            provider.completions(&self.text, new_offset, completion_context, window, cx);
113        self._context_menu_task = cx.spawn_in(window, async move |editor, cx| {
114            let mut completions: Vec<CompletionItem> = vec![];
115            if let Some(provider_responses) = provider_responses.await.ok() {
116                match provider_responses {
117                    CompletionResponse::Array(items) => completions.extend(items),
118                    CompletionResponse::List(list) => completions.extend(list.items),
119                }
120            }
121
122            if completions.is_empty() {
123                _ = menu.update(cx, |menu, cx| {
124                    menu.hide(cx);
125                    cx.notify();
126                });
127
128                return Ok(());
129            }
130
131            editor
132                .update_in(cx, |editor, window, cx| {
133                    if !editor.focus_handle.is_focused(window) {
134                        return;
135                    }
136
137                    _ = menu.update(cx, |menu, cx| {
138                        menu.show(new_offset, completions, window, cx);
139                    });
140
141                    cx.notify();
142                })
143                .ok();
144
145            Ok(())
146        });
147    }
148}