Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/handlers/completion/request.rs')
| -rw-r--r-- | helix-term/src/handlers/completion/request.rs | 374 |
1 files changed, 0 insertions, 374 deletions
diff --git a/helix-term/src/handlers/completion/request.rs b/helix-term/src/handlers/completion/request.rs deleted file mode 100644 index fd65cd4d..00000000 --- a/helix-term/src/handlers/completion/request.rs +++ /dev/null @@ -1,374 +0,0 @@ -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::config::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, SavePoint}; -use helix_view::handlers::completion::{CompletionEvent, ResponseContext}; -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; - -use super::word; - -#[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 { - /// The currently active trigger which will cause a completion request after the timeout. - trigger: Option<Trigger>, - in_flight: Option<Trigger>, - task_controller: TaskController, - config: Arc<ArcSwap<Config>>, -} - -impl CompletionHandler { - pub fn new(config: Arc<ArcSwap<Config>>) -> 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<Instant>, - ) -> Option<Instant> { - if self.in_flight.is_some() && !self.task_controller.is_running() { - self.in_flight = None; - } - match event { - CompletionEvent::AutoTrigger { - cursor: trigger_pos, - doc, - view, - } => { - // Technically it shouldn't be possible to switch views/documents in insert mode - // but people may create weird keymaps/use the mouse so let's be extra careful. - if self - .trigger - .or(self.in_flight) - .is_none_or(|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_ref!(editor); - - if compositor - .find::<ui::EditorView>() - .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 language server if we type fast - // enough? Yes that is true but it's not actually a problem. The language server will resolve - // the completion to the identifier anyway (in fact sending the later position is necessary to - // get the right results from language servers that provide incomplete completion list). We - // rely on the trigger offset and primary cursor matching for multi-cursor completions so this - // is definitely necessary from our side too. - trigger.pos = cursor; - let doc = doc_mut!(editor, &doc.id()); - let savepoint = doc.savepoint(view); - let text = doc.text(); - 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), - savepoint.clone(), - )); - } - if let Some(path_completion_request) = path_completion( - doc.selection(view.id).clone(), - doc, - handle.clone(), - savepoint.clone(), - ) { - requests.spawn_blocking(path_completion_request); - } - if let Some(word_completion_request) = - word::completion(editor, trigger, handle.clone(), savepoint) - { - requests.spawn_blocking(word_completion_request); - } - - let ui = compositor.find::<ui::EditorView>().unwrap(); - ui.last_insert.1.push(InsertEvent::RequestCompletion); - let handle_ = handle.clone(); - let request_completions = async move { - let mut context = HashMap::new(); - let Some(mut response) = handle_response(&mut requests, false).await else { - return; - }; - - let mut items: Vec<_> = Vec::new(); - response.take_items(&mut items); - context.insert(response.provider, response.context); - let deadline = Instant::now() + Duration::from_millis(100); - loop { - let Some(mut response) = timeout_at(deadline, handle_response(&mut requests, false)) - .await - .ok() - .flatten() - else { - break; - }; - response.take_items(&mut items); - context.insert(response.provider, response.context); - } - dispatch(move |editor, compositor| { - show_completion(editor, compositor, items, context, trigger) - }) - .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, - savepoint: Arc<SavePoint>, -) -> impl Future<Output = CompletionResponse> { - 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 before 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<lsp::CompletionResponse> = completion_response - .await - .inspect_err(|err| log::error!("completion request failed: {err}")) - .ok() - .flatten(); - let (mut items, is_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), - context: ResponseContext { - is_incomplete, - priority, - savepoint, - }, - provider: CompletionProvider::Lsp(provider), - } - } -} - -pub fn request_incomplete_completion_list(editor: &mut Editor, handle: TaskHandle) { - let handler = &mut editor.handlers.completions; - let mut requests = JoinSet::new(); - let mut savepoint = None; - for (&provider, context) in &handler.active_completions { - if !context.is_incomplete { - continue; - } - let CompletionProvider::Lsp(ls_id) = provider else { - log::error!("non-lsp incomplete completion lists"); - continue; - }; - let Some(ls) = editor.language_servers.get_by_id(ls_id) else { - continue; - }; - let (view, doc) = current!(editor); - let savepoint = savepoint.get_or_insert_with(|| doc.savepoint(view)).clone(); - let request = request_completions_from_language_server( - ls, - doc, - view.id, - CompletionContext { - trigger_kind: CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS, - trigger_character: None, - }, - context.priority, - savepoint, - ); - requests.spawn(request); - } - if !requests.is_empty() { - tokio::spawn(replace_completions(handle, requests, true)); - } -} |