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;
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 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 pub(crate) fn schedule_inline_completion(
273 &mut self,
274 window: &mut Window,
275 cx: &mut Context<Self>,
276 ) {
277 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 background_executor.timer(debounce).await;
292
293 let task = editor.update_in(cx, |editor, window, cx| {
295 if editor.cursor() != offset {
297 return None;
298 }
299
300 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 if editor.cursor() != offset {
322 return;
323 }
324
325 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 #[inline]
345 pub(crate) fn has_inline_completion(&self) -> bool {
346 self.extras.inline_completion.item.is_some()
347 }
348
349 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 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}