Unnamed repository; edit this file 'description' to name the repository.
implement incomplete completion requests
Pascal Kuthe 2025-02-02
parent 4e0fc0e · commit 5c1f3f8
-rw-r--r--helix-term/src/handlers.rs6
-rw-r--r--helix-term/src/handlers/completion.rs392
-rw-r--r--helix-term/src/handlers/completion/item.rs85
-rw-r--r--helix-term/src/handlers/completion/path.rs40
-rw-r--r--helix-term/src/handlers/completion/request.rs368
-rw-r--r--helix-term/src/ui/completion.rs155
-rw-r--r--helix-term/src/ui/editor.rs9
-rw-r--r--helix-term/src/ui/menu.rs72
-rw-r--r--helix-view/src/handlers.rs16
9 files changed, 702 insertions, 441 deletions
diff --git a/helix-term/src/handlers.rs b/helix-term/src/handlers.rs
index 31e15330..b580e678 100644
--- a/helix-term/src/handlers.rs
+++ b/helix-term/src/handlers.rs
@@ -6,10 +6,8 @@ use helix_event::AsyncHook;
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 @@ mod snippet;
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,
};
diff --git a/helix-term/src/handlers/completion.rs b/helix-term/src/handlers/completion.rs
index 4e03a063..046cfab7 100644
--- a/helix-term/src/handlers/completion.rs
+++ b/helix-term/src/handlers/completion.rs
@@ -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;
- }
- 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,
+async fn replace_completions(
handle: TaskHandle,
- editor: &mut Editor,
- compositor: &mut Compositor,
+ mut requests: JoinSet<CompletionResponse>,
+ is_incomplete: bool,
) {
- 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,
- }
- } 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,
- }
- }
+ 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;
};
-
- 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)
+ if handle.is_canceled() {
+ log::error!("dropping outdated completion response");
+ return;
}
- .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)
+ 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 {
+ editor
+ .handlers
+ .completions
+ .active_completions
+ .insert(response.provider, response.context);
+ }
})
- .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 @@ fn show_completion(
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 @@ fn show_completion(
}
}
-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 @@ pub fn trigger_auto_completion(
#[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 @@ pub fn trigger_auto_completion(
.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 clear_completions(cx: &mut commands::Context) {
}
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 @@ fn completion_post_command_hook(
MappableCommand::Static {
name: "delete_char_backward",
..
- } => update_completions(cx, None),
+ } => update_completion_filter(cx, None),
_ => clear_completions(cx),
}
} else {
@@ -460,33 +234,35 @@ fn completion_post_command_hook(
} => 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(())
});
diff --git a/helix-term/src/handlers/completion/item.rs b/helix-term/src/handlers/completion/item.rs
index bcd35cd5..7a473b02 100644
--- a/helix-term/src/handlers/completion/item.rs
+++ b/helix-term/src/handlers/completion/item.rs
@@ -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 @@ pub enum CompletionItem {
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 PartialEq<CompletionItem> for helix_core::CompletionItem {
}
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),
diff --git a/helix-term/src/handlers/completion/path.rs b/helix-term/src/handlers/completion/path.rs
index db04c456..21dc9c31 100644
--- a/helix-term/src/handlers/completion/path.rs
+++ b/helix-term/src/handlers/completion/path.rs
@@ -3,22 +3,23 @@ use std::{
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 @@ pub(crate) fn path_completion(
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 @@ pub(crate) fn path_completion(
.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 @@ pub(crate) fn path_completion(
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)]
diff --git a/helix-term/src/handlers/completion/request.rs b/helix-term/src/handlers/completion/request.rs
new file mode 100644
index 00000000..3d2a158e
--- /dev/null
+++ b/helix-term/src/handlers/completion/request.rs
@@ -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));
+ }
+}
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 3e86237d..be78dd08 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -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 std::{borrow::Cow, sync::Arc};
-
-use helix_core::{
- self as core, chars,
- snippets::{ActiveSnippet, RenderedSnippet, Snippet},
- Change, Transaction,
-};
use helix_view::{graphics::Rect, Document, Editor};
+use nucleo::{
+ pattern::{Atom, AtomKind, CaseMatching, Normalization},
+ Config, Utf32Str,
+};
+use tui::text::Spans;
+use tui::{buffer::Buffer as Surface, text::Span};
-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 @@ pub struct Completion {
#[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 @@ impl Completion {
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 @@ impl Completion {
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 @@ impl Completion {
}
// 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 @@ impl Completion {
};
// 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 @@ impl Completion {
}
}
}
- 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 {
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 5d028415..6fecd512 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -24,14 +24,14 @@ use helix_core::{
};
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 @@ impl EditorView {
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 @@ impl EditorView {
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 => (),
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index 612832ce..76e50229 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -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 @@ pub trait Item: Sync + Send + 'static {
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 @@ impl<T: Item> Menu<T> {
}
}
- 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();
diff --git a/helix-view/src/handlers.rs b/helix-view/src/handlers.rs
index e2f95ded..a26c4ddb 100644
--- a/helix-view/src/handlers.rs
+++ b/helix-view/src/handlers.rs
@@ -1,3 +1,4 @@
+use completion::{CompletionEvent, CompletionHandler};
use helix_event::send_blocking;
use tokio::sync::mpsc::Sender;
@@ -17,7 +18,7 @@ pub enum AutoSaveEvent {
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 @@ pub struct Handlers {
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) {