use anyhow::Result;
use gpui::{Context, EntityInputHandler, Task, Window};
use lsp_types::{request::Completion, CompletionContext, CompletionItem, CompletionResponse};
use ropey::Rope;
use std::{cell::RefCell, ops::Range, rc::Rc};
use crate::input::{
popovers::{CompletionMenu, ContextMenu},
InputState,
};
pub trait CompletionProvider {
fn completions(
&self,
text: &Rope,
offset: usize,
trigger: CompletionContext,
window: &mut Window,
cx: &mut Context<InputState>,
) -> Task<Result<CompletionResponse>>;
fn resolve_completions(
&self,
_completion_indices: Vec<usize>,
_completions: Rc<RefCell<Box<[Completion]>>>,
_: &mut Context<InputState>,
) -> Task<Result<bool>> {
Task::ready(Ok(false))
}
fn is_completion_trigger(
&self,
offset: usize,
new_text: &str,
cx: &mut Context<InputState>,
) -> bool;
}
impl InputState {
pub(crate) fn handle_completion_trigger(
&mut self,
range: &Range<usize>,
new_text: &str,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.completion_inserting {
return;
}
let Some(provider) = self.lsp.completion_provider.clone() else {
return;
};
let start = range.end;
let new_offset = self.cursor();
if !provider.is_completion_trigger(start, new_text, cx) {
return;
}
let menu = match self.context_menu.as_ref() {
Some(ContextMenu::Completion(menu)) => Some(menu),
_ => None,
};
let menu = match menu {
Some(menu) => menu.clone(),
None => {
let menu = CompletionMenu::new(cx.entity(), window, cx);
self.context_menu = Some(ContextMenu::Completion(menu.clone()));
menu
}
};
let start_offset = menu.read(cx).trigger_start_offset.unwrap_or(start);
if new_offset < start_offset {
return;
}
let query = self
.text_for_range(
self.range_to_utf16(&(start_offset..new_offset)),
&mut None,
window,
cx,
)
.map(|s| s.trim().to_string())
.unwrap_or_default();
_ = menu.update(cx, |menu, _| {
menu.update_query(start_offset, query.clone());
});
let completion_context = CompletionContext {
trigger_kind: lsp_types::CompletionTriggerKind::TRIGGER_CHARACTER,
trigger_character: Some(query),
};
let provider_responses =
provider.completions(&self.text, new_offset, completion_context, window, cx);
self._context_menu_task = cx.spawn_in(window, async move |editor, cx| {
let mut completions: Vec<CompletionItem> = vec![];
if let Some(provider_responses) = provider_responses.await.ok() {
match provider_responses {
CompletionResponse::Array(items) => completions.extend(items),
CompletionResponse::List(list) => completions.extend(list.items),
}
}
if completions.is_empty() {
_ = menu.update(cx, |menu, cx| {
menu.hide(cx);
cx.notify();
});
return Ok(());
}
editor
.update_in(cx, |editor, window, cx| {
if !editor.focus_handle.is_focused(window) {
return;
}
_ = menu.update(cx, |menu, cx| {
menu.show(new_offset, completions, window, cx);
});
cx.notify();
})
.ok();
Ok(())
});
}
}