changeset 6511:98535093f2d1

implement incomplete completion requests
author Pascal Kuthe <pascalkuthe@pm.me>
date Sat, 01 Feb 2025 15:48:42 -0500
parents c90368b2941f
children 06d6e39d6425
files helix-term/src/handlers.rs helix-term/src/handlers/completion.rs helix-term/src/handlers/completion/item.rs helix-term/src/handlers/completion/path.rs helix-term/src/handlers/completion/request.rs helix-term/src/ui/completion.rs helix-term/src/ui/editor.rs helix-term/src/ui/menu.rs helix-view/src/handlers.rs
diffstat 9 files changed, 703 insertions(+), 442 deletions(-) [+]
line wrap: on
line diff
--- a/helix-term/src/handlers.rs	Sat Feb 01 13:12:57 2025 -0500
+++ b/helix-term/src/handlers.rs	Sat Feb 01 15:48:42 2025 -0500
@@ -6,10 +6,8 @@
 use crate::config::Config;
 use crate::events;
 use crate::handlers::auto_save::AutoSaveHandler;
-use crate::handlers::completion::CompletionHandler;
 use crate::handlers::signature_help::SignatureHelpHandler;
 
-pub use completion::trigger_auto_completion;
 pub use helix_view::handlers::Handlers;
 
 mod auto_save;
@@ -21,12 +19,12 @@
 pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
     events::register();
 
-    let completions = CompletionHandler::new(config).spawn();
+    let event_tx = completion::CompletionHandler::new(config).spawn();
     let signature_hints = SignatureHelpHandler::new().spawn();
     let auto_save = AutoSaveHandler::new().spawn();
 
     let handlers = Handlers {
-        completions,
+        completions: helix_view::handlers::completion::CompletionHandler::new(event_tx),
         signature_hints,
         auto_save,
     };
--- a/helix-term/src/handlers/completion.rs	Sat Feb 01 13:12:57 2025 -0500
+++ b/helix-term/src/handlers/completion.rs	Sat Feb 01 15:48:42 2025 -0500
@@ -1,310 +1,90 @@
-use std::collections::HashSet;
-use std::sync::Arc;
-use std::time::Duration;
+use std::collections::HashMap;
 
-use arc_swap::ArcSwap;
-use futures_util::stream::FuturesUnordered;
-use futures_util::FutureExt;
 use helix_core::chars::char_is_word;
+use helix_core::completion::CompletionProvider;
 use helix_core::syntax::LanguageServerFeature;
-use helix_event::{cancelable_future, register_hook, send_blocking, TaskController, TaskHandle};
+use helix_event::{register_hook, TaskHandle};
 use helix_lsp::lsp;
-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;
-use helix_view::{DocumentId, Editor, ViewId};
-use path::path_completion;
-use tokio::sync::mpsc::Sender;
-use tokio::time::Instant;
-use tokio_stream::StreamExt as _;
+use helix_view::document::Mode;
+use helix_view::handlers::completion::{CompletionEvent, ResponseContext};
+use helix_view::Editor;
+use tokio::task::JoinSet;
 
 use crate::commands;
 use crate::compositor::Compositor;
-use crate::config::Config;
 use crate::events::{OnModeSwitch, PostCommand, PostInsertChar};
-use crate::job::{dispatch, dispatch_blocking};
+use crate::handlers::completion::request::{request_incomplete_completion_list, Trigger};
+use crate::job::dispatch;
 use crate::keymap::MappableCommand;
-use crate::ui::editor::InsertEvent;
 use crate::ui::lsp::signature_help::SignatureHelp;
 use crate::ui::{self, Popup};
 
 use super::Handlers;
-pub use item::{CompletionItem, LspCompletionItem};
+
+pub use item::{CompletionItem, CompletionItems, CompletionResponse, LspCompletionItem};
+pub use request::CompletionHandler;
 pub use resolve::ResolveHandler;
+
 mod item;
 mod path;
+mod request;
 mod resolve;
 
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-enum TriggerKind {
-    Auto,
-    TriggerChar,
-    Manual,
-}
-
-#[derive(Debug, Clone, Copy)]
-struct Trigger {
-    pos: usize,
-    view: ViewId,
-    doc: DocumentId,
-    kind: TriggerKind,
-}
-
-#[derive(Debug)]
-pub(super) struct CompletionHandler {
-    /// 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,
+async fn handle_response(
+    requests: &mut JoinSet<CompletionResponse>,
+    is_incomplete: bool,
+) -> Option<CompletionResponse> {
+    loop {
+        let response = requests.join_next().await?.unwrap();
+        if !is_incomplete && !response.context.is_incomplete && response.items.is_empty() {
+            continue;
         }
+        return Some(response);
     }
 }
 
-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,
-            } => {
-                // 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;
+async fn replace_completions(
+    handle: TaskHandle,
+    mut requests: JoinSet<CompletionResponse>,
+    is_incomplete: bool,
+) {
+    while let Some(mut response) = handle_response(&mut requests, is_incomplete).await {
+        let handle = handle.clone();
+        dispatch(move |editor, compositor| {
+            let editor_view = compositor.find::<ui::EditorView>().unwrap();
+            let Some(completion) = &mut editor_view.completion else {
+                return;
+            };
+            if handle.is_canceled() {
+                log::error!("dropping outdated completion response");
+                return;
             }
-            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_completion(trigger, handle, editor, compositor)
-        });
-    }
-}
-
-fn request_completion(
-    mut trigger: Trigger,
-    handle: TaskHandle,
-    editor: &mut Editor,
-    compositor: &mut Compositor,
-) {
-    let (view, doc) = current!(editor);
-
-    if compositor
-        .find::<ui::EditorView>()
-        .unwrap()
-        .completion
-        .is_some()
-        || editor.mode != Mode::Insert
-    {
-        return;
-    }
 
-    let text = doc.text();
-    let selection = doc.selection(view.id);
-    let cursor = selection.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 mut futures: FuturesUnordered<_> = doc
-        .language_servers_with_feature(LanguageServerFeature::Completion)
-        .filter(|ls| seen_language_servers.insert(ls.id()))
-        .map(|ls| {
-            let language_server_id = ls.id();
-            let offset_encoding = ls.offset_encoding();
-            let pos = pos_to_lsp_pos(text, cursor, offset_encoding);
-            let doc_id = doc.identifier();
-            let context = if trigger.kind == TriggerKind::Manual {
-                lsp::CompletionContext {
-                    trigger_kind: lsp::CompletionTriggerKind::INVOKED,
-                    trigger_character: None,
-                }
+            completion.replace_provider_completions(&mut response, is_incomplete);
+            if completion.is_empty() {
+                editor_view.clear_completion(editor);
+                // clearing completions might mean we want to immediately re-request them (usually
+                // this occurs if typing a trigger char)
+                trigger_auto_completion(editor, false);
             } 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,
-                    }
-                }
-            };
-
-            let completion_response = ls.completion(doc_id, pos, None, context).unwrap();
-            async move {
-                let json = completion_response.await?;
-                let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
-                let items = match response {
-                    Some(lsp::CompletionResponse::Array(items)) => items,
-                    // TODO: do something with is_incomplete
-                    Some(lsp::CompletionResponse::List(lsp::CompletionList {
-                        is_incomplete: _is_incomplete,
-                        items,
-                    })) => items,
-                    None => Vec::new(),
-                }
-                .into_iter()
-                .map(|item| {
-                    CompletionItem::Lsp(LspCompletionItem {
-                        item,
-                        provider: language_server_id,
-                        resolved: false,
-                    })
-                })
-                .collect();
-                anyhow::Ok(items)
+                editor
+                    .handlers
+                    .completions
+                    .active_completions
+                    .insert(response.provider, response.context);
             }
-            .boxed()
         })
-        .chain(path_completion(selection.clone(), doc, handle.clone()))
-        .collect();
-
-    let future = async move {
-        let mut items = Vec::new();
-        while let Some(lsp_items) = futures.next().await {
-            match lsp_items {
-                Ok(mut lsp_items) => items.append(&mut lsp_items),
-                Err(err) => {
-                    log::debug!("completion request failed: {err:?}");
-                }
-            };
-        }
-        items
-    };
-
-    let savepoint = doc.savepoint(view);
-
-    let ui = compositor.find::<ui::EditorView>().unwrap();
-    ui.last_insert.1.push(InsertEvent::RequestCompletion);
-    tokio::spawn(async move {
-        let items = cancelable_future(future, &handle).await;
-        let Some(items) = items.filter(|items| !items.is_empty()) else {
-            return;
-        };
-        dispatch(move |editor, compositor| {
-            show_completion(editor, compositor, items, trigger, savepoint);
-            drop(handle)
-        })
-        .await
-    });
+        .await;
+    }
 }
 
 fn show_completion(
     editor: &mut Editor,
     compositor: &mut Compositor,
     items: Vec<CompletionItem>,
+    context: HashMap<CompletionProvider, ResponseContext>,
     trigger: Trigger,
-    savepoint: Arc<SavePoint>,
 ) {
     let (view, doc) = current_ref!(editor);
     // check if the completion request is stale.
@@ -321,8 +101,9 @@
     if ui.completion.is_some() {
         return;
     }
+    editor.handlers.completions.active_completions = context;
 
-    let completion_area = ui.set_completion(editor, savepoint, items, trigger.pos, size);
+    let completion_area = ui.set_completion(editor, items, trigger.pos, size);
     let signature_help_area = compositor
         .find_id::<Popup<SignatureHelp>>(SignatureHelp::ID)
         .map(|signature_help| signature_help.area(size, editor));
@@ -332,11 +113,7 @@
     }
 }
 
-pub fn trigger_auto_completion(
-    tx: &Sender<CompletionEvent>,
-    editor: &Editor,
-    trigger_char_only: bool,
-) {
+pub fn trigger_auto_completion(editor: &Editor, trigger_char_only: bool) {
     let config = editor.config.load();
     if !config.auto_completion {
         return;
@@ -364,15 +141,13 @@
     #[cfg(not(windows))]
     let is_path_completion_trigger = matches!(cursor_char, Some(b'/'));
 
+    let handler = &editor.handlers.completions;
     if is_trigger_char || (is_path_completion_trigger && doc.path_completion_enabled()) {
-        send_blocking(
-            tx,
-            CompletionEvent::TriggerChar {
-                cursor,
-                doc: doc.id(),
-                view: view.id,
-            },
-        );
+        handler.event(CompletionEvent::TriggerChar {
+            cursor,
+            doc: doc.id(),
+            view: view.id,
+        });
         return;
     }
 
@@ -385,29 +160,29 @@
             .all(char_is_word);
 
     if is_auto_trigger {
-        send_blocking(
-            tx,
-            CompletionEvent::AutoTrigger {
-                cursor,
-                doc: doc.id(),
-                view: view.id,
-            },
-        );
+        handler.event(CompletionEvent::AutoTrigger {
+            cursor,
+            doc: doc.id(),
+            view: view.id,
+        });
     }
 }
 
-fn update_completions(cx: &mut commands::Context, c: Option<char>) {
+fn update_completion_filter(cx: &mut commands::Context, c: Option<char>) {
     cx.callback.push(Box::new(move |compositor, cx| {
         let editor_view = compositor.find::<ui::EditorView>().unwrap();
         if let Some(completion) = &mut editor_view.completion {
             completion.update_filter(c);
-            if completion.is_empty() {
+            if completion.is_empty() || c.is_some_and(|c| !char_is_word(c)) {
                 editor_view.clear_completion(cx.editor);
                 // clearing completions might mean we want to immediately rerequest them (usually
                 // this occurs if typing a trigger char)
                 if c.is_some() {
-                    trigger_auto_completion(&cx.editor.handlers.completions, cx.editor, false);
+                    trigger_auto_completion(cx.editor, false);
                 }
+            } else {
+                let handle = cx.editor.handlers.completions.request_controller.restart();
+                request_incomplete_completion_list(cx.editor, handle)
             }
         }
     }))
@@ -421,7 +196,6 @@
 }
 
 fn completion_post_command_hook(
-    tx: &Sender<CompletionEvent>,
     PostCommand { command, cx }: &mut PostCommand<'_, '_>,
 ) -> anyhow::Result<()> {
     if cx.editor.mode == Mode::Insert {
@@ -434,7 +208,7 @@
                 MappableCommand::Static {
                     name: "delete_char_backward",
                     ..
-                } => update_completions(cx, None),
+                } => update_completion_filter(cx, None),
                 _ => clear_completions(cx),
             }
         } else {
@@ -460,33 +234,35 @@
                 } => return Ok(()),
                 _ => CompletionEvent::Cancel,
             };
-            send_blocking(tx, event);
+            cx.editor.handlers.completions.event(event);
         }
     }
     Ok(())
 }
 
-pub(super) fn register_hooks(handlers: &Handlers) {
-    let tx = handlers.completions.clone();
-    register_hook!(move |event: &mut PostCommand<'_, '_>| completion_post_command_hook(&tx, event));
+pub(super) fn register_hooks(_handlers: &Handlers) {
+    register_hook!(move |event: &mut PostCommand<'_, '_>| completion_post_command_hook(event));
 
-    let tx = handlers.completions.clone();
     register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
         if event.old_mode == Mode::Insert {
-            send_blocking(&tx, CompletionEvent::Cancel);
+            event
+                .cx
+                .editor
+                .handlers
+                .completions
+                .event(CompletionEvent::Cancel);
             clear_completions(event.cx);
         } else if event.new_mode == Mode::Insert {
-            trigger_auto_completion(&tx, event.cx.editor, false)
+            trigger_auto_completion(event.cx.editor, false)
         }
         Ok(())
     });
 
-    let tx = handlers.completions.clone();
     register_hook!(move |event: &mut PostInsertChar<'_, '_>| {
         if event.cx.editor.last_completion.is_some() {
-            update_completions(event.cx, Some(event.c))
+            update_completion_filter(event.cx, Some(event.c))
         } else {
-            trigger_auto_completion(&tx, event.cx.editor, false);
+            trigger_auto_completion(event.cx.editor, false);
         }
         Ok(())
     });
--- a/helix-term/src/handlers/completion/item.rs	Sat Feb 01 13:12:57 2025 -0500
+++ b/helix-term/src/handlers/completion/item.rs	Sat Feb 01 15:48:42 2025 -0500
@@ -1,10 +1,70 @@
+use std::mem;
+
+use helix_core::completion::CompletionProvider;
 use helix_lsp::{lsp, LanguageServerId};
+use helix_view::handlers::completion::ResponseContext;
+
+pub struct CompletionResponse {
+    pub items: CompletionItems,
+    pub provider: CompletionProvider,
+    pub context: ResponseContext,
+}
+
+pub enum CompletionItems {
+    Lsp(Vec<lsp::CompletionItem>),
+    Other(Vec<CompletionItem>),
+}
+
+impl CompletionItems {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            CompletionItems::Lsp(items) => items.is_empty(),
+            CompletionItems::Other(items) => items.is_empty(),
+        }
+    }
+}
+
+impl CompletionResponse {
+    pub fn take_items(&mut self, dst: &mut Vec<CompletionItem>) {
+        match &mut self.items {
+            CompletionItems::Lsp(items) => dst.extend(items.drain(..).map(|item| {
+                CompletionItem::Lsp(LspCompletionItem {
+                    item,
+                    provider: match self.provider {
+                        CompletionProvider::Lsp(provider) => provider,
+                        _ => unreachable!(),
+                    },
+                    resolved: false,
+                    provider_priority: self.context.priority,
+                })
+            })),
+            CompletionItems::Other(items) if dst.is_empty() => mem::swap(dst, items),
+            CompletionItems::Other(items) => dst.append(items),
+        }
+    }
+}
 
 #[derive(Debug, PartialEq, Clone)]
 pub struct LspCompletionItem {
     pub item: lsp::CompletionItem,
     pub provider: LanguageServerId,
     pub resolved: bool,
+    // TODO: we should not be filtering and sorting incomplete completion list
+    // according to the spec but vscode does that anyway and most servers (
+    // including rust-analyzer) rely on that.. so we can't do that without
+    // breaking completions.
+    pub provider_priority: i8,
+}
+
+impl LspCompletionItem {
+    #[inline]
+    pub fn filter_text(&self) -> &str {
+        self.item
+            .filter_text
+            .as_ref()
+            .unwrap_or(&self.item.label)
+            .as_str()
+    }
 }
 
 #[derive(Debug, PartialEq, Clone)]
@@ -13,6 +73,16 @@
     Other(helix_core::CompletionItem),
 }
 
+impl CompletionItem {
+    #[inline]
+    pub fn filter_text(&self) -> &str {
+        match self {
+            CompletionItem::Lsp(item) => item.filter_text(),
+            CompletionItem::Other(item) => &item.label,
+        }
+    }
+}
+
 impl PartialEq<CompletionItem> for LspCompletionItem {
     fn eq(&self, other: &CompletionItem) -> bool {
         match other {
@@ -32,6 +102,21 @@
 }
 
 impl CompletionItem {
+    pub fn provider_priority(&self) -> i8 {
+        match self {
+            CompletionItem::Lsp(item) => item.provider_priority,
+            // sorting path completions after LSP for now
+            CompletionItem::Other(_) => 1,
+        }
+    }
+
+    pub fn provider(&self) -> CompletionProvider {
+        match self {
+            CompletionItem::Lsp(item) => CompletionProvider::Lsp(item.provider),
+            CompletionItem::Other(item) => item.provider,
+        }
+    }
+
     pub fn preselect(&self) -> bool {
         match self {
             CompletionItem::Lsp(LspCompletionItem { item, .. }) => item.preselect.unwrap_or(false),
--- a/helix-term/src/handlers/completion/path.rs	Sat Feb 01 13:12:57 2025 -0500
+++ b/helix-term/src/handlers/completion/path.rs	Sat Feb 01 15:48:42 2025 -0500
@@ -3,22 +3,23 @@
     fs,
     path::{Path, PathBuf},
     str::FromStr as _,
+    sync::Arc,
 };
 
-use futures_util::{future::BoxFuture, FutureExt as _};
 use helix_core::{self as core, completion::CompletionProvider, Selection, Transaction};
 use helix_event::TaskHandle;
 use helix_stdx::path::{self, canonicalize, fold_home_dir, get_path_suffix};
-use helix_view::Document;
+use helix_view::{document::SavePoint, handlers::completion::ResponseContext, Document};
 use url::Url;
 
-use super::item::CompletionItem;
+use crate::handlers::completion::{item::CompletionResponse, CompletionItem, CompletionItems};
 
 pub(crate) fn path_completion(
     selection: Selection,
     doc: &Document,
     handle: TaskHandle,
-) -> Option<BoxFuture<'static, anyhow::Result<Vec<CompletionItem>>>> {
+    savepoint: Arc<SavePoint>,
+) -> Option<impl FnOnce() -> CompletionResponse> {
     if !doc.path_completion_enabled() {
         return None;
     }
@@ -67,9 +68,19 @@
         return None;
     }
 
-    let future = tokio::task::spawn_blocking(move || {
+    // TODO: handle properly in the future
+    const PRIORITY: i8 = 1;
+    let future = move || {
         let Ok(read_dir) = std::fs::read_dir(&dir_path) else {
-            return Vec::new();
+            return CompletionResponse {
+                items: CompletionItems::Other(Vec::new()),
+                provider: CompletionProvider::Path,
+                context: ResponseContext {
+                    is_incomplete: false,
+                    priority: PRIORITY,
+                    savepoint,
+                },
+            };
         };
 
         let edit_diff = typed_file_name
@@ -77,7 +88,7 @@
             .map(|s| s.chars().count())
             .unwrap_or_default();
 
-        read_dir
+        let res: Vec<_> = read_dir
             .filter_map(Result::ok)
             .filter_map(|dir_entry| {
                 dir_entry
@@ -106,10 +117,19 @@
                     provider: CompletionProvider::Path,
                 }))
             })
-            .collect::<Vec<_>>()
-    });
+            .collect();
+        CompletionResponse {
+            items: CompletionItems::Other(res),
+            provider: CompletionProvider::Path,
+            context: ResponseContext {
+                is_incomplete: false,
+                priority: PRIORITY,
+                savepoint,
+            },
+        }
+    };
 
-    Some(async move { Ok(future.await?) }.boxed())
+    Some(future)
 }
 
 #[cfg(unix)]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/helix-term/src/handlers/completion/request.rs	Sat Feb 01 15:48:42 2025 -0500
@@ -0,0 +1,368 @@
+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, 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;
+
+#[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)
+                    .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_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,
+    ) {
+        requests.spawn_blocking(path_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
+            .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, 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));
+    }
+}
--- a/helix-term/src/ui/completion.rs	Sat Feb 01 13:12:57 2025 -0500
+++ b/helix-term/src/ui/completion.rs	Sat Feb 01 15:48:42 2025 -0500
@@ -1,53 +1,32 @@
+use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
 use crate::{
     compositor::{Component, Context, Event, EventResult},
-    handlers::{
-        completion::{CompletionItem, LspCompletionItem, ResolveHandler},
-        trigger_auto_completion,
+    handlers::completion::{
+        trigger_auto_completion, CompletionItem, CompletionResponse, LspCompletionItem,
+        ResolveHandler,
     },
 };
+use helix_core::snippets::{ActiveSnippet, RenderedSnippet, Snippet};
+use helix_core::{self as core, chars, fuzzy::MATCHER, Change, Transaction};
+use helix_lsp::{lsp, util, OffsetEncoding};
 use helix_view::{
-    document::SavePoint,
     editor::CompleteAction,
     handlers::lsp::SignatureHelpInvoked,
     theme::{Color, Modifier, Style},
     ViewId,
 };
-use tui::{
-    buffer::Buffer as Surface,
-    text::{Span, Spans},
+use helix_view::{graphics::Rect, Document, Editor};
+use nucleo::{
+    pattern::{Atom, AtomKind, CaseMatching, Normalization},
+    Config, Utf32Str,
 };
-
-use std::{borrow::Cow, sync::Arc};
+use tui::text::Spans;
+use tui::{buffer::Buffer as Surface, text::Span};
 
-use helix_core::{
-    self as core, chars,
-    snippets::{ActiveSnippet, RenderedSnippet, Snippet},
-    Change, Transaction,
-};
-use helix_view::{graphics::Rect, Document, Editor};
-
-use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
-
-use helix_lsp::{lsp, util, OffsetEncoding};
+use std::cmp::Reverse;
 
 impl menu::Item for CompletionItem {
     type Data = Style;
-    fn sort_text(&self, data: &Self::Data) -> Cow<str> {
-        self.filter_text(data)
-    }
-
-    #[inline]
-    fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
-        match self {
-            CompletionItem::Lsp(LspCompletionItem { item, .. }) => item
-                .filter_text
-                .as_ref()
-                .unwrap_or(&item.label)
-                .as_str()
-                .into(),
-            CompletionItem::Other(core::CompletionItem { label, .. }) => label.clone(),
-        }
-    }
 
     fn format(&self, dir_style: &Self::Data) -> menu::Row {
         let deprecated = match self {
@@ -143,22 +122,16 @@
     #[allow(dead_code)]
     trigger_offset: usize,
     filter: String,
+    // TODO: move to helix-view/central handler struct in the future
     resolve_handler: ResolveHandler,
 }
 
 impl Completion {
     pub const ID: &'static str = "completion";
 
-    pub fn new(
-        editor: &Editor,
-        savepoint: Arc<SavePoint>,
-        mut items: Vec<CompletionItem>,
-        trigger_offset: usize,
-    ) -> Self {
+    pub fn new(editor: &Editor, items: Vec<CompletionItem>, trigger_offset: usize) -> Self {
         let preview_completion_insert = editor.config().preview_completion_insert;
         let replace_mode = editor.config().completion_replace;
-        // Sort completion items according to their preselect status (given by the LSP server)
-        items.sort_by_key(|item| !item.preselect());
 
         let dir_style = editor.theme.get("ui.text.directory");
 
@@ -202,10 +175,11 @@
                             savepoint: doc.savepoint(view),
                         })
                     }
+                    let item = item.unwrap();
+                    let context = &editor.handlers.completions.active_completions[&item.provider()];
                     // if more text was entered, remove it
-                    doc.restore(view, &savepoint, false);
+                    doc.restore(view, &context.savepoint, false);
                     // always present here
-                    let item = item.unwrap();
 
                     match item {
                         CompletionItem::Lsp(item) => {
@@ -232,13 +206,15 @@
                         doc.restore(view, &savepoint, false);
                     }
 
+                    let item = item.unwrap();
+                    let context = &editor.handlers.completions.active_completions[&item.provider()];
                     // if more text was entered, remove it
-                    doc.restore(view, &savepoint, true);
+                    doc.restore(view, &context.savepoint, true);
                     // save an undo checkpoint before the completion
                     doc.append_changes_to_history(view);
 
                     // item always present here
-                    let (transaction, additional_edits, snippet) = match item.unwrap().clone() {
+                    let (transaction, additional_edits, snippet) = match item.clone() {
                         CompletionItem::Lsp(mut item) => {
                             let language_server = language_server!(item);
 
@@ -302,7 +278,7 @@
                     }
                     // we could have just inserted a trigger char (like a `crate::` completion for rust
                     // so we want to retrigger immediately when accepting a completion.
-                    trigger_auto_completion(&editor.handlers.completions, editor, true);
+                    trigger_auto_completion(editor, true);
                 }
             };
 
@@ -339,14 +315,70 @@
         };
 
         // need to recompute immediately in case start_offset != trigger_offset
-        completion
-            .popup
-            .contents_mut()
-            .score(&completion.filter, false);
+        completion.score(false);
 
         completion
     }
 
+    fn score(&mut self, incremental: bool) {
+        let pattern = &self.filter;
+        let mut matcher = MATCHER.lock();
+        matcher.config = Config::DEFAULT;
+        // slight preference towards prefix matches
+        matcher.config.prefer_prefix = true;
+        let pattern = Atom::new(
+            pattern,
+            CaseMatching::Ignore,
+            Normalization::Smart,
+            AtomKind::Fuzzy,
+            false,
+        );
+        let mut buf = Vec::new();
+        let (matches, options) = self.popup.contents_mut().update_options();
+        if incremental {
+            matches.retain_mut(|(index, score)| {
+                let option = &options[*index as usize];
+                let text = option.filter_text();
+                let new_score = pattern.score(Utf32Str::new(text, &mut buf), &mut matcher);
+                match new_score {
+                    Some(new_score) => {
+                        *score = new_score as u32 / 2;
+                        true
+                    }
+                    None => false,
+                }
+            })
+        } else {
+            matches.clear();
+            matches.extend(options.iter().enumerate().filter_map(|(i, option)| {
+                let text = option.filter_text();
+                pattern
+                    .score(Utf32Str::new(text, &mut buf), &mut matcher)
+                    .map(|score| (i as u32, score as u32 / 3))
+            }));
+        }
+        // Nucleo is meant as an FZF-like fuzzy matcher and only hides matches that are truly
+        // impossible - as in the sequence of characters just doesn't appear. That doesn't work
+        // well for completions with multiple language servers where all completions of the next
+        // server are below the current one (so you would get good suggestions from the second
+        // server below those of the first). Setting a reasonable cutoff below which to move bad
+        // completions out of the way helps with that.
+        //
+        // The score computation is a heuristic derived from Nucleo internal constants that may
+        // move upstream in the future. I want to test this out here to settle on a good number.
+        let min_score = (7 + pattern.needle_text().len() as u32 * 14) / 3;
+        matches.sort_unstable_by_key(|&(i, score)| {
+            let option = &options[i as usize];
+            (
+                score <= min_score,
+                Reverse(option.preselect()),
+                option.provider_priority(),
+                Reverse(score),
+                i,
+            )
+        });
+    }
+
     /// Synchronously resolve the given completion item. This is used when
     /// accepting a completion.
     fn resolve_completion_item(
@@ -388,7 +420,24 @@
                 }
             }
         }
-        menu.score(&self.filter, c.is_some());
+        self.score(c.is_some());
+        self.popup.contents_mut().reset_cursor();
+    }
+
+    pub fn replace_provider_completions(
+        &mut self,
+        response: &mut CompletionResponse,
+        is_incomplete: bool,
+    ) {
+        let menu = self.popup.contents_mut();
+        let (_, options) = menu.update_options();
+        if is_incomplete {
+            options.retain(|item| item.provider() != response.provider)
+        }
+        response.take_items(options);
+        self.score(false);
+        let menu = self.popup.contents_mut();
+        menu.ensure_cursor_in_bounds();
     }
 
     pub fn is_empty(&self) -> bool {
--- a/helix-term/src/ui/editor.rs	Sat Feb 01 13:12:57 2025 -0500
+++ b/helix-term/src/ui/editor.rs	Sat Feb 01 15:48:42 2025 -0500
@@ -24,14 +24,14 @@
 };
 use helix_view::{
     annotations::diagnostics::DiagnosticFilter,
-    document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
+    document::{Mode, SCRATCH_BUFFER_NAME},
     editor::{CompleteAction, CursorShapeConfig},
     graphics::{Color, CursorKind, Modifier, Rect, Style},
     input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
     keyboard::{KeyCode, KeyModifiers},
     Document, Editor, Theme, View,
 };
-use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
+use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc};
 
 use tui::{buffer::Buffer as Surface, text::Span};
 
@@ -1049,12 +1049,11 @@
     pub fn set_completion(
         &mut self,
         editor: &mut Editor,
-        savepoint: Arc<SavePoint>,
         items: Vec<CompletionItem>,
         trigger_offset: usize,
         size: Rect,
     ) -> Option<Rect> {
-        let mut completion = Completion::new(editor, savepoint, items, trigger_offset);
+        let mut completion = Completion::new(editor, items, trigger_offset);
 
         if completion.is_empty() {
             // skip if we got no completion results
@@ -1073,6 +1072,8 @@
     pub fn clear_completion(&mut self, editor: &mut Editor) -> Option<OnKeyCallback> {
         self.completion = None;
         let mut on_next_key: Option<OnKeyCallback> = None;
+        editor.handlers.completions.request_controller.restart();
+        editor.handlers.completions.active_completions.clear();
         if let Some(last_completion) = editor.last_completion.take() {
             match last_completion {
                 CompleteAction::Triggered => (),
--- a/helix-term/src/ui/menu.rs	Sat Feb 01 13:12:57 2025 -0500
+++ b/helix-term/src/ui/menu.rs	Sat Feb 01 15:48:42 2025 -0500
@@ -1,12 +1,7 @@
-use std::{borrow::Cow, cmp::Reverse};
-
 use crate::{
     compositor::{Callback, Component, Compositor, Context, Event, EventResult},
     ctrl, key, shift,
 };
-use helix_core::fuzzy::MATCHER;
-use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization};
-use nucleo::{Config, Utf32Str};
 use tui::{buffer::Buffer as Surface, widgets::Table};
 
 pub use tui::widgets::{Cell, Row};
@@ -19,16 +14,6 @@
     type Data: Sync + Send + 'static;
 
     fn format(&self, data: &Self::Data) -> Row;
-
-    fn sort_text(&self, data: &Self::Data) -> Cow<str> {
-        let label: String = self.format(data).cell_text().collect();
-        label.into()
-    }
-
-    fn filter_text(&self, data: &Self::Data) -> Cow<str> {
-        let label: String = self.format(data).cell_text().collect();
-        label.into()
-    }
 }
 
 pub type MenuCallback<T> = Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>;
@@ -77,49 +62,30 @@
         }
     }
 
-    pub fn score(&mut self, pattern: &str, incremental: bool) {
-        let mut matcher = MATCHER.lock();
-        matcher.config = Config::DEFAULT;
-        let pattern = Atom::new(
-            pattern,
-            CaseMatching::Ignore,
-            Normalization::Smart,
-            AtomKind::Fuzzy,
-            false,
-        );
-        let mut buf = Vec::new();
-        if incremental {
-            self.matches.retain_mut(|(index, score)| {
-                let option = &self.options[*index as usize];
-                let text = option.filter_text(&self.editor_data);
-                let new_score = pattern.score(Utf32Str::new(&text, &mut buf), &mut matcher);
-                match new_score {
-                    Some(new_score) => {
-                        *score = new_score as u32;
-                        true
-                    }
-                    None => false,
-                }
-            })
-        } else {
-            self.matches.clear();
-            let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
-                let text = option.filter_text(&self.editor_data);
-                pattern
-                    .score(Utf32Str::new(&text, &mut buf), &mut matcher)
-                    .map(|score| (i as u32, score as u32))
-            });
-            self.matches.extend(matches);
-        }
-        self.matches
-            .sort_unstable_by_key(|&(i, score)| (Reverse(score), i));
-
-        // reset cursor position
+    pub fn reset_cursor(&mut self) {
         self.cursor = None;
         self.scroll = 0;
         self.recalculate = true;
     }
 
+    pub fn update_options(&mut self) -> (&mut Vec<(u32, u32)>, &mut Vec<T>) {
+        self.recalculate = true;
+        (&mut self.matches, &mut self.options)
+    }
+
+    pub fn ensure_cursor_in_bounds(&mut self) {
+        if self.matches.is_empty() {
+            self.cursor = None;
+            self.scroll = 0;
+        } else {
+            self.scroll = 0;
+            self.recalculate = true;
+            if let Some(cursor) = &mut self.cursor {
+                *cursor = (*cursor).min(self.matches.len() - 1)
+            }
+        }
+    }
+
     pub fn clear(&mut self) {
         self.matches.clear();
 
--- a/helix-view/src/handlers.rs	Sat Feb 01 13:12:57 2025 -0500
+++ b/helix-view/src/handlers.rs	Sat Feb 01 15:48:42 2025 -0500
@@ -1,3 +1,4 @@
+use completion::{CompletionEvent, CompletionHandler};
 use helix_event::send_blocking;
 use tokio::sync::mpsc::Sender;
 
@@ -17,7 +18,7 @@
 
 pub struct Handlers {
     // only public because most of the actual implementation is in helix-term right now :/
-    pub completions: Sender<completion::CompletionEvent>,
+    pub completions: CompletionHandler,
     pub signature_hints: Sender<lsp::SignatureHelpEvent>,
     pub auto_save: Sender<AutoSaveEvent>,
 }
@@ -25,14 +26,11 @@
 impl Handlers {
     /// Manually trigger completion (c-x)
     pub fn trigger_completions(&self, trigger_pos: usize, doc: DocumentId, view: ViewId) {
-        send_blocking(
-            &self.completions,
-            completion::CompletionEvent::ManualTrigger {
-                cursor: trigger_pos,
-                doc,
-                view,
-            },
-        );
+        self.completions.event(CompletionEvent::ManualTrigger {
+            cursor: trigger_pos,
+            doc,
+            view,
+        });
     }
 
     pub fn trigger_signature_help(&self, invocation: SignatureHelpInvoked, editor: &Editor) {