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.rs374
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));
- }
-}