use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::Duration; use arc_swap::ArcSwap; use futures_util::Future; use helix_core::completion::CompletionProvider; use helix_core::syntax::LanguageServerFeature; use helix_event::{cancelable_future, TaskController, TaskHandle}; use helix_lsp::lsp; use helix_lsp::lsp::{CompletionContext, CompletionTriggerKind}; use helix_lsp::util::pos_to_lsp_pos; use helix_stdx::rope::RopeSliceExt; use helix_view::document::Mode; use helix_view::handlers::lsp::CompletionEvent; use helix_view::{Document, DocumentId, Editor, ViewId}; use tokio::task::JoinSet; use tokio::time::{timeout_at, Instant}; use crate::compositor::Compositor; use crate::config::Config; use crate::handlers::completion::item::CompletionResponse; use crate::handlers::completion::path::path_completion; use crate::handlers::completion::{ handle_response, replace_completions, show_completion, CompletionItems, }; use crate::job::{dispatch, dispatch_blocking}; use crate::ui; use crate::ui::editor::InsertEvent; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub(super) enum TriggerKind { Auto, TriggerChar, Manual, } #[derive(Debug, Clone, Copy)] pub(super) struct Trigger { pub(super) pos: usize, pub(super) view: ViewId, pub(super) doc: DocumentId, pub(super) kind: TriggerKind, } #[derive(Debug)] pub struct CompletionHandler { /// currently active trigger which will cause a /// completion request after the timeout trigger: Option, in_flight: Option, task_controller: TaskController, config: Arc>, } impl CompletionHandler { pub fn new(config: Arc>) -> CompletionHandler { Self { config, task_controller: TaskController::new(), trigger: None, in_flight: None, } } } impl helix_event::AsyncHook for CompletionHandler { type Event = CompletionEvent; fn handle_event( &mut self, event: Self::Event, _old_timeout: Option, ) -> Option { if self.in_flight.is_some() && !self.task_controller.is_running() { self.in_flight = None; } match event { CompletionEvent::AutoTrigger { cursor: trigger_pos, doc, view, } => { // techically it shouldn't be possible to switch views/documents in insert mode // but people may create weird keymaps/use the mouse so lets be extra careful if self .trigger .or(self.in_flight) .map_or(true, |trigger| trigger.doc != doc || trigger.view != view) { self.trigger = Some(Trigger { pos: trigger_pos, view, doc, kind: TriggerKind::Auto, }); } } CompletionEvent::TriggerChar { cursor, doc, view } => { // immediately request completions and drop all auto completion requests self.task_controller.cancel(); self.trigger = Some(Trigger { pos: cursor, view, doc, kind: TriggerKind::TriggerChar, }); } CompletionEvent::ManualTrigger { cursor, doc, view } => { // immediately request completions and drop all auto completion requests self.trigger = Some(Trigger { pos: cursor, view, doc, kind: TriggerKind::Manual, }); // stop debouncing immediately and request the completion self.finish_debounce(); return None; } CompletionEvent::Cancel => { self.trigger = None; self.task_controller.cancel(); } CompletionEvent::DeleteText { cursor } => { // if we deleted the original trigger, abort the completion if matches!(self.trigger.or(self.in_flight), Some(Trigger{ pos, .. }) if cursor < pos) { self.trigger = None; self.task_controller.cancel(); } } } self.trigger.map(|trigger| { // if the current request was closed forget about it // otherwise immediately restart the completion request let timeout = if trigger.kind == TriggerKind::Auto { self.config.load().editor.completion_timeout } else { // we want almost instant completions for trigger chars // and restarting completion requests. The small timeout here mainly // serves to better handle cases where the completion handler // may fall behind (so multiple events in the channel) and macros Duration::from_millis(5) }; Instant::now() + timeout }) } fn finish_debounce(&mut self) { let trigger = self.trigger.take().expect("debounce always has a trigger"); self.in_flight = Some(trigger); let handle = self.task_controller.restart(); dispatch_blocking(move |editor, compositor| { request_completions(trigger, handle, editor, compositor) }); } } fn request_completions( mut trigger: Trigger, handle: TaskHandle, editor: &mut Editor, compositor: &mut Compositor, ) { let (view, doc) = current!(editor); if compositor .find::() .unwrap() .completion .is_some() || editor.mode != Mode::Insert { return; } let text = doc.text(); let cursor = doc.selection(view.id).primary().cursor(text.slice(..)); if trigger.view != view.id || trigger.doc != doc.id() || cursor < trigger.pos { return; } // this looks odd... Why are we not using the trigger position from // the `trigger` here? Won't that mean that the trigger char doesn't get // send to the LS if we type fast enougn? Yes that is true but it's // not actually a problem. The LSP will resolve the completion to the identifier // anyway (in fact sending the later position is necessary to get the right results // from LSPs that provide incomplete completion list). We rely on trigger offset // and primary cursor matching for multi-cursor completions so this is definitely // necessary from our side too. trigger.pos = cursor; let trigger_text = text.slice(..cursor); let mut seen_language_servers = HashSet::new(); let language_servers: Vec<_> = doc .language_servers_with_feature(LanguageServerFeature::Completion) .filter(|ls| seen_language_servers.insert(ls.id())) .collect(); let mut requests = JoinSet::new(); for (priority, ls) in language_servers.iter().enumerate() { let context = if trigger.kind == TriggerKind::Manual { lsp::CompletionContext { trigger_kind: lsp::CompletionTriggerKind::INVOKED, trigger_character: None, } } else { let trigger_char = ls.capabilities() .completion_provider .as_ref() .and_then(|provider| { provider .trigger_characters .as_deref()? .iter() .find(|&trigger| trigger_text.ends_with(trigger)) }); if trigger_char.is_some() { lsp::CompletionContext { trigger_kind: lsp::CompletionTriggerKind::TRIGGER_CHARACTER, trigger_character: trigger_char.cloned(), } } else { lsp::CompletionContext { trigger_kind: lsp::CompletionTriggerKind::INVOKED, trigger_character: None, } } }; requests.spawn(request_completions_from_language_server( ls, doc, view.id, context, -(priority as i8), )); } if let Some(path_completion_request) = path_completion(cursor, text.clone(), doc, handle.clone()) { requests.spawn_blocking(path_completion_request); } let savepoint = doc.savepoint(view); let ui = compositor.find::().unwrap(); ui.last_insert.1.push(InsertEvent::RequestCompletion); let handle_ = handle.clone(); let request_completions = async move { let mut incomplete_completion_lists = HashMap::new(); let Some(response) = handle_response(&mut requests, false).await else { return; }; if response.incomplete { incomplete_completion_lists.insert(response.provider, response.priority); } let mut items: Vec<_> = Vec::new(); response.into_items(&mut items); let deadline = Instant::now() + Duration::from_millis(100); loop { let Some(response) = timeout_at(deadline, handle_response(&mut requests, false)) .await .ok() .flatten() else { break; }; if response.incomplete { incomplete_completion_lists.insert(response.provider, response.priority); } response.into_items(&mut items); } dispatch(move |editor, compositor| { show_completion( editor, compositor, items, incomplete_completion_lists, trigger, savepoint, ) }) .await; if !requests.is_empty() { replace_completions(handle_, requests, false).await; } }; tokio::spawn(cancelable_future(request_completions, handle)); } fn request_completions_from_language_server( ls: &helix_lsp::Client, doc: &Document, view: ViewId, context: lsp::CompletionContext, priority: i8, ) -> impl Future { let provider = ls.id(); let offset_encoding = ls.offset_encoding(); let text = doc.text(); let cursor = doc.selection(view).primary().cursor(text.slice(..)); let pos = pos_to_lsp_pos(text, cursor, offset_encoding); let doc_id = doc.identifier(); // it's important that this is berofe the async block (and that this is not an async function) // to ensure the request is dispatched right away before any new edit notifications let completion_response = ls.completion(doc_id, pos, None, context).unwrap(); async move { let response: Option = completion_response .await .and_then(|json| serde_json::from_value(json).map_err(helix_lsp::Error::Parse)) .inspect_err(|err| log::error!("completion request failed: {err}")) .ok() .flatten(); let (mut items, incomplete) = match response { Some(lsp::CompletionResponse::Array(items)) => (items, false), Some(lsp::CompletionResponse::List(lsp::CompletionList { is_incomplete, items, })) => (items, is_incomplete), None => (Vec::new(), false), }; items.sort_by(|item1, item2| { let sort_text1 = item1.sort_text.as_deref().unwrap_or(&item1.label); let sort_text2 = item2.sort_text.as_deref().unwrap_or(&item2.label); sort_text1.cmp(sort_text2) }); CompletionResponse { items: CompletionItems::Lsp(items), incomplete, provider: CompletionProvider::Lsp(provider), priority, } } } pub fn request_incomplete_completion_list( editor: &mut Editor, ui: &mut ui::Completion, handle: TaskHandle, ) { if ui.incomplete_completion_lists.is_empty() { return; } let (view, doc) = current_ref!(editor); let mut requests = JoinSet::new(); log::error!("request incomplete completions"); ui.incomplete_completion_lists .retain(|&provider, &mut priority| { let CompletionProvider::Lsp(ls_id) = provider else { unimplemented!("non-lsp incomplete completion lists") }; let Some(ls) = editor.language_server_by_id(ls_id) else { return false; }; log::error!("request incomplete completions2"); let request = request_completions_from_language_server( ls, doc, view.id, CompletionContext { trigger_kind: CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS, trigger_character: None, }, priority, ); requests.spawn(request); true }); tokio::spawn(replace_completions(handle, requests, true)); }