implement incomplete completion requests
This commit is contained in:
parent
4e0fc0efc6
commit
5c1f3f814f
9 changed files with 705 additions and 444 deletions
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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)
|
||||
}
|
||||
.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;
|
||||
};
|
||||
while let Some(mut response) = handle_response(&mut requests, is_incomplete).await {
|
||||
let handle = handle.clone();
|
||||
dispatch(move |editor, compositor| {
|
||||
show_completion(editor, compositor, items, trigger, savepoint);
|
||||
drop(handle)
|
||||
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;
|
||||
}
|
||||
|
||||
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(())
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)]
|
||||
|
|
368
helix-term/src/handlers/completion/request.rs
Normal file
368
helix-term/src/handlers/completion/request.rs
Normal file
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
// if more text was entered, remove it
|
||||
doc.restore(view, &savepoint, false);
|
||||
// always present here
|
||||
let item = item.unwrap();
|
||||
let context = &editor.handlers.completions.active_completions[&item.provider()];
|
||||
// if more text was entered, remove it
|
||||
doc.restore(view, &context.savepoint, false);
|
||||
// always present here
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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 => (),
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Reference in a new issue