mirror of
https://github.com/helix-editor/helix.git
synced 2025-01-18 21:17:08 +04:00
Merge branch 'incomplete_completion' into batteries
This commit is contained in:
commit
9edd87bdbb
@ -1,5 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::diagnostic::LanguageServerId;
|
||||
use crate::Transaction;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
@ -9,4 +10,17 @@ pub struct CompletionItem {
|
||||
pub kind: Cow<'static, str>,
|
||||
/// Containing Markdown
|
||||
pub documentation: String,
|
||||
pub provider: CompletionProvider,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub enum CompletionProvider {
|
||||
Lsp(LanguageServerId),
|
||||
PathCompletions,
|
||||
}
|
||||
|
||||
impl From<LanguageServerId> for CompletionProvider {
|
||||
fn from(id: LanguageServerId) -> Self {
|
||||
CompletionProvider::Lsp(id)
|
||||
}
|
||||
}
|
||||
|
@ -426,29 +426,32 @@ fn call_with_timeout<R: lsp::request::Request>(
|
||||
let server_tx = self.server_tx.clone();
|
||||
let id = self.next_request_id();
|
||||
|
||||
let params = serde_json::to_value(params);
|
||||
async move {
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
// it' important this is not part of the future so that it gets
|
||||
// executed right away so that the request order stays concisents
|
||||
let rx = serde_json::to_value(params)
|
||||
.map_err(Error::from)
|
||||
.and_then(|params| {
|
||||
let request = jsonrpc::MethodCall {
|
||||
jsonrpc: Some(jsonrpc::Version::V2),
|
||||
id: id.clone(),
|
||||
method: R::METHOD.to_string(),
|
||||
params: Self::value_into_params(params?),
|
||||
params: Self::value_into_params(params),
|
||||
};
|
||||
|
||||
let (tx, mut rx) = channel::<Result<Value>>(1);
|
||||
|
||||
let (tx, rx) = channel::<Result<Value>>(1);
|
||||
server_tx
|
||||
.send(Payload::Request {
|
||||
chan: tx,
|
||||
value: request,
|
||||
})
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
Ok(rx)
|
||||
});
|
||||
|
||||
async move {
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
// TODO: delay other calls until initialize success
|
||||
timeout(Duration::from_secs(timeout_secs), rx.recv())
|
||||
timeout(Duration::from_secs(timeout_secs), rx?.recv())
|
||||
.await
|
||||
.map_err(|_| Error::Timeout(id))? // return Timeout
|
||||
.ok_or(Error::StreamClosed)?
|
||||
@ -465,7 +468,11 @@ pub fn notify<R: lsp::notification::Notification>(
|
||||
{
|
||||
let server_tx = self.server_tx.clone();
|
||||
|
||||
async move {
|
||||
// it' important this is not part of the future so that it gets
|
||||
// executed right away so that the request order stays consisents
|
||||
let res = serde_json::to_value(params)
|
||||
.map_err(Error::from)
|
||||
.and_then(|params| {
|
||||
let params = serde_json::to_value(params)?;
|
||||
|
||||
let notification = jsonrpc::Notification {
|
||||
@ -473,13 +480,13 @@ pub fn notify<R: lsp::notification::Notification>(
|
||||
method: R::METHOD.to_string(),
|
||||
params: Self::value_into_params(params),
|
||||
};
|
||||
|
||||
server_tx
|
||||
.send(Payload::Notification(notification))
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.map_err(|e| Error::Other(e.into()))
|
||||
});
|
||||
// TODO: this function is not async and never should have been
|
||||
// but turning it into non-async function is a big refactor
|
||||
async move { res }
|
||||
}
|
||||
|
||||
/// Reply to a language server RPC call.
|
||||
@ -492,26 +499,27 @@ pub fn reply(
|
||||
|
||||
let server_tx = self.server_tx.clone();
|
||||
|
||||
async move {
|
||||
let output = match result {
|
||||
Ok(result) => Output::Success(Success {
|
||||
Ok(result) => serde_json::to_value(result).map(|result| {
|
||||
Output::Success(Success {
|
||||
jsonrpc: Some(Version::V2),
|
||||
id,
|
||||
result: serde_json::to_value(result)?,
|
||||
result,
|
||||
})
|
||||
}),
|
||||
Err(error) => Output::Failure(Failure {
|
||||
Err(error) => Ok(Output::Failure(Failure {
|
||||
jsonrpc: Some(Version::V2),
|
||||
id,
|
||||
error,
|
||||
}),
|
||||
})),
|
||||
};
|
||||
|
||||
let res = output.map_err(Error::from).and_then(|output| {
|
||||
server_tx
|
||||
.send(Payload::Response(output))
|
||||
.map_err(|e| Error::Other(e.into()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.map_err(|e| Error::Other(e.into()))
|
||||
});
|
||||
async move { res }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
|
@ -9,7 +9,6 @@
|
||||
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;
|
||||
|
@ -1,307 +1,86 @@
|
||||
use std::collections::HashSet;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use futures_util::stream::FuturesUnordered;
|
||||
use futures_util::FutureExt;
|
||||
use anyhow::Result;
|
||||
|
||||
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, send_blocking, 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::lsp::CompletionEvent;
|
||||
use helix_view::{DocumentId, Editor, ViewId};
|
||||
use path::path_completion;
|
||||
use helix_view::Editor;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::time::Instant;
|
||||
use tokio_stream::StreamExt as _;
|
||||
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::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,
|
||||
async fn handle_response(
|
||||
requests: &mut JoinSet<CompletionResponse>,
|
||||
incomplete: bool,
|
||||
) -> Option<CompletionResponse> {
|
||||
loop {
|
||||
let response = requests.join_next().await?.unwrap();
|
||||
if !incomplete && !response.incomplete && response.items.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
});
|
||||
return Some(response);
|
||||
}
|
||||
}
|
||||
|
||||
fn request_completion(
|
||||
mut trigger: Trigger,
|
||||
async fn replace_completions(
|
||||
handle: TaskHandle,
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
mut requests: JoinSet<CompletionResponse>,
|
||||
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 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 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(cursor, text.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(response) = handle_response(&mut requests, 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(response);
|
||||
if completion.is_empty() {
|
||||
editor_view.clear_completion(editor);
|
||||
// clearing completions might mean we want to immediately rerequest them (usually
|
||||
// this occurs if typing a trigger char)
|
||||
trigger_auto_completion(&editor.handlers.completions, editor, false);
|
||||
}
|
||||
})
|
||||
.await
|
||||
});
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
fn show_completion(
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
items: Vec<CompletionItem>,
|
||||
incomplete_completion_lists: HashMap<CompletionProvider, i8>,
|
||||
trigger: Trigger,
|
||||
savepoint: Arc<SavePoint>,
|
||||
) {
|
||||
@ -321,7 +100,14 @@ fn show_completion(
|
||||
return;
|
||||
}
|
||||
|
||||
let completion_area = ui.set_completion(editor, savepoint, items, trigger.pos, size);
|
||||
let completion_area = ui.set_completion(
|
||||
editor,
|
||||
savepoint,
|
||||
items,
|
||||
incomplete_completion_lists,
|
||||
trigger.pos,
|
||||
size,
|
||||
);
|
||||
let signature_help_area = compositor
|
||||
.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID)
|
||||
.map(|signature_help| signature_help.area(size, editor));
|
||||
@ -395,18 +181,21 @@ pub fn trigger_auto_completion(
|
||||
}
|
||||
}
|
||||
|
||||
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 let Some(ui) = &mut editor_view.completion {
|
||||
ui.update_filter(c);
|
||||
if ui.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);
|
||||
}
|
||||
} else {
|
||||
let handle = ui.incomplete_list_controller.restart();
|
||||
request_incomplete_completion_list(cx.editor, ui, handle)
|
||||
}
|
||||
}
|
||||
}))
|
||||
@ -422,7 +211,7 @@ fn clear_completions(cx: &mut commands::Context) {
|
||||
fn completion_post_command_hook(
|
||||
tx: &Sender<CompletionEvent>,
|
||||
PostCommand { command, cx }: &mut PostCommand<'_, '_>,
|
||||
) -> anyhow::Result<()> {
|
||||
) -> Result<()> {
|
||||
if cx.editor.mode == Mode::Insert {
|
||||
if cx.editor.last_completion.is_some() {
|
||||
match command {
|
||||
@ -433,7 +222,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 {
|
||||
@ -483,7 +272,7 @@ pub(super) fn register_hooks(handlers: &Handlers) {
|
||||
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);
|
||||
}
|
||||
|
@ -1,10 +1,69 @@
|
||||
use helix_core::completion::CompletionProvider;
|
||||
use helix_lsp::{lsp, LanguageServerId};
|
||||
|
||||
pub struct CompletionResponse {
|
||||
pub items: CompletionItems,
|
||||
pub incomplete: bool,
|
||||
pub provider: CompletionProvider,
|
||||
pub priority: i8,
|
||||
}
|
||||
|
||||
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 into_items(self, dst: &mut Vec<CompletionItem>) {
|
||||
match self.items {
|
||||
CompletionItems::Lsp(items) => dst.extend(items.into_iter().map(|item| {
|
||||
CompletionItem::Lsp(LspCompletionItem {
|
||||
item,
|
||||
provider: match self.provider {
|
||||
CompletionProvider::Lsp(provider) => provider,
|
||||
CompletionProvider::PathCompletions => unreachable!(),
|
||||
},
|
||||
resolved: false,
|
||||
provider_priority: self.priority,
|
||||
})
|
||||
})),
|
||||
CompletionItems::Other(items) if dst.is_empty() => *dst = items,
|
||||
CompletionItems::Other(mut items) => dst.append(&mut 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 incomplete_completion_list: bool,
|
||||
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 +72,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 +101,21 @@ fn eq(&self, other: &CompletionItem) -> bool {
|
||||
}
|
||||
|
||||
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),
|
||||
|
@ -5,22 +5,21 @@
|
||||
str::FromStr as _,
|
||||
};
|
||||
|
||||
use futures_util::{future::BoxFuture, FutureExt as _};
|
||||
use helix_core as core;
|
||||
use helix_core::Transaction;
|
||||
use helix_core::{self as core, completion::CompletionProvider};
|
||||
use helix_event::TaskHandle;
|
||||
use helix_stdx::path::{self, canonicalize, fold_home_dir, get_path_suffix};
|
||||
use helix_view::Document;
|
||||
use url::Url;
|
||||
|
||||
use super::item::CompletionItem;
|
||||
use crate::handlers::completion::{item::CompletionResponse, CompletionItem, CompletionItems};
|
||||
|
||||
pub(crate) fn path_completion(
|
||||
cursor: usize,
|
||||
text: core::Rope,
|
||||
doc: &Document,
|
||||
handle: TaskHandle,
|
||||
) -> Option<BoxFuture<'static, anyhow::Result<Vec<CompletionItem>>>> {
|
||||
) -> Option<impl Fn() -> CompletionResponse> {
|
||||
if !doc.path_completion_enabled() {
|
||||
return None;
|
||||
}
|
||||
@ -67,12 +66,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()),
|
||||
incomplete: false,
|
||||
provider: CompletionProvider::PathCompletions,
|
||||
priority: PRIORITY, // TODO: hand
|
||||
};
|
||||
};
|
||||
|
||||
read_dir
|
||||
let res: Vec<_> = read_dir
|
||||
.filter_map(Result::ok)
|
||||
.filter_map(|dir_entry| {
|
||||
dir_entry
|
||||
@ -103,12 +109,19 @@ pub(crate) fn path_completion(
|
||||
label: file_name.into(),
|
||||
transaction,
|
||||
documentation,
|
||||
provider: CompletionProvider::PathCompletions,
|
||||
}))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
.collect();
|
||||
CompletionResponse {
|
||||
items: CompletionItems::Other(res),
|
||||
incomplete: false,
|
||||
provider: CompletionProvider::PathCompletions,
|
||||
priority: PRIORITY, // TODO: hand
|
||||
}
|
||||
};
|
||||
|
||||
Some(async move { Ok(future.await?) }.boxed())
|
||||
Some(future)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
|
373
helix-term/src/handlers/completion/request.rs
Normal file
373
helix-term/src/handlers/completion/request.rs
Normal file
@ -0,0 +1,373 @@
|
||||
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;
|
||||
use helix_view::handlers::lsp::CompletionEvent;
|
||||
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 {
|
||||
/// 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,
|
||||
} => {
|
||||
// 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_completions(trigger, handle, editor, compositor)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn request_completions(
|
||||
mut trigger: Trigger,
|
||||
handle: TaskHandle,
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
) {
|
||||
let (view, doc) = current!(editor);
|
||||
|
||||
if compositor
|
||||
.find::<ui::EditorView>()
|
||||
.unwrap()
|
||||
.completion
|
||||
.is_some()
|
||||
|| editor.mode != Mode::Insert
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let text = doc.text();
|
||||
let 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 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 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),
|
||||
));
|
||||
}
|
||||
if let Some(path_completion_request) =
|
||||
path_completion(cursor, text.clone(), doc, handle.clone())
|
||||
{
|
||||
requests.spawn_blocking(path_completion_request);
|
||||
}
|
||||
|
||||
let savepoint = doc.savepoint(view);
|
||||
|
||||
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 incomplete_completion_lists = HashMap::new();
|
||||
let Some(response) = handle_response(&mut requests, false).await else {
|
||||
return;
|
||||
};
|
||||
|
||||
if response.incomplete {
|
||||
incomplete_completion_lists.insert(response.provider, response.priority);
|
||||
}
|
||||
let mut items: Vec<_> = Vec::new();
|
||||
response.into_items(&mut items);
|
||||
let deadline = Instant::now() + Duration::from_millis(100);
|
||||
loop {
|
||||
let Some(response) = timeout_at(deadline, handle_response(&mut requests, false))
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
else {
|
||||
break;
|
||||
};
|
||||
if response.incomplete {
|
||||
incomplete_completion_lists.insert(response.provider, response.priority);
|
||||
}
|
||||
response.into_items(&mut items);
|
||||
}
|
||||
dispatch(move |editor, compositor| {
|
||||
show_completion(
|
||||
editor,
|
||||
compositor,
|
||||
items,
|
||||
incomplete_completion_lists,
|
||||
trigger,
|
||||
savepoint,
|
||||
)
|
||||
})
|
||||
.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,
|
||||
) -> 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 berofe 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, 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),
|
||||
incomplete,
|
||||
provider: CompletionProvider::Lsp(provider),
|
||||
priority,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_incomplete_completion_list(
|
||||
editor: &mut Editor,
|
||||
ui: &mut ui::Completion,
|
||||
handle: TaskHandle,
|
||||
) {
|
||||
if ui.incomplete_completion_lists.is_empty() {
|
||||
return;
|
||||
}
|
||||
let (view, doc) = current_ref!(editor);
|
||||
let mut requests = JoinSet::new();
|
||||
log::error!("request incomplete completions");
|
||||
ui.incomplete_completion_lists
|
||||
.retain(|&provider, &mut priority| {
|
||||
let CompletionProvider::Lsp(ls_id) = provider else {
|
||||
unimplemented!("non-lsp incomplete completion lists")
|
||||
};
|
||||
let Some(ls) = editor.language_server_by_id(ls_id) else {
|
||||
return false;
|
||||
};
|
||||
log::error!("request incomplete completions2");
|
||||
let request = request_completions_from_language_server(
|
||||
ls,
|
||||
doc,
|
||||
view.id,
|
||||
CompletionContext {
|
||||
trigger_kind: CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS,
|
||||
trigger_character: None,
|
||||
},
|
||||
priority,
|
||||
);
|
||||
requests.spawn(request);
|
||||
true
|
||||
});
|
||||
tokio::spawn(replace_completions(handle, requests, true));
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
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_event::TaskController;
|
||||
use helix_view::{
|
||||
document::SavePoint,
|
||||
editor::CompleteAction,
|
||||
@ -12,12 +13,18 @@
|
||||
theme::{Modifier, Style},
|
||||
ViewId,
|
||||
};
|
||||
use nucleo::{
|
||||
pattern::{Atom, AtomKind, CaseMatching, Normalization},
|
||||
Config, Utf32Str,
|
||||
};
|
||||
use tui::{buffer::Buffer as Surface, text::Span};
|
||||
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use std::{cmp::Reverse, collections::HashMap, sync::Arc};
|
||||
|
||||
use helix_core::{
|
||||
self as core, chars,
|
||||
completion::CompletionProvider,
|
||||
fuzzy::MATCHER,
|
||||
snippets::{ActiveSnippet, RenderedSnippet, Snippet},
|
||||
Change, Transaction,
|
||||
};
|
||||
@ -29,22 +36,6 @@
|
||||
|
||||
impl menu::Item for CompletionItem {
|
||||
type Data = ();
|
||||
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, _data: &Self::Data) -> menu::Row {
|
||||
let deprecated = match self {
|
||||
@ -119,6 +110,9 @@ pub struct Completion {
|
||||
trigger_offset: usize,
|
||||
filter: String,
|
||||
resolve_handler: ResolveHandler,
|
||||
pub incomplete_completion_lists: HashMap<CompletionProvider, i8>,
|
||||
// controller for requesting updates for incomplete completion lists
|
||||
pub incomplete_list_controller: TaskController,
|
||||
}
|
||||
|
||||
impl Completion {
|
||||
@ -127,13 +121,12 @@ impl Completion {
|
||||
pub fn new(
|
||||
editor: &Editor,
|
||||
savepoint: Arc<SavePoint>,
|
||||
mut items: Vec<CompletionItem>,
|
||||
items: Vec<CompletionItem>,
|
||||
incomplete_completion_lists: HashMap<CompletionProvider, i8>,
|
||||
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());
|
||||
|
||||
// Then create the menu
|
||||
let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
|
||||
@ -309,17 +302,77 @@ macro_rules! language_server {
|
||||
// and avoid allocation during matching
|
||||
filter: String::from(fragment),
|
||||
resolve_handler: ResolveHandler::new(),
|
||||
incomplete_completion_lists,
|
||||
incomplete_list_controller: TaskController::new(),
|
||||
};
|
||||
|
||||
// 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))
|
||||
}));
|
||||
}
|
||||
// nuclueo is meant as an fzf-like fuzzy matcher and only hides
|
||||
// matches that are truely impossible (as in the sequence of char
|
||||
// just doens't appeart) that doesn't work well for completions
|
||||
// with multi lsps where all completions of the next lsp are below
|
||||
// the current one (so you would good suggestions from the second lsp 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 dervied from nucleo internal
|
||||
// constants and 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(
|
||||
@ -361,7 +414,28 @@ pub fn update_filter(&mut self, c: Option<char>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
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: CompletionResponse) {
|
||||
let menu = self.popup.contents_mut();
|
||||
let (_, options) = menu.update_options();
|
||||
if self
|
||||
.incomplete_completion_lists
|
||||
.remove(&response.provider)
|
||||
.is_some()
|
||||
{
|
||||
options.retain(|item| item.provider() != response.provider)
|
||||
}
|
||||
if response.incomplete {
|
||||
self.incomplete_completion_lists
|
||||
.insert(response.provider, response.priority);
|
||||
}
|
||||
response.into_items(options);
|
||||
self.score(false);
|
||||
let menu = self.popup.contents_mut();
|
||||
menu.ensure_cursor_in_bounds();
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
|
@ -14,6 +14,7 @@
|
||||
};
|
||||
|
||||
use helix_core::{
|
||||
completion::CompletionProvider,
|
||||
diagnostic::NumberOrString,
|
||||
graphemes::{next_grapheme_boundary, prev_grapheme_boundary},
|
||||
movement::Direction,
|
||||
@ -31,7 +32,7 @@
|
||||
keyboard::{KeyCode, KeyModifiers},
|
||||
Document, Editor, Theme, View,
|
||||
};
|
||||
use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
|
||||
use std::{collections::HashMap, mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
|
||||
|
||||
use tui::{buffer::Buffer as Surface, text::Span};
|
||||
|
||||
@ -1057,10 +1058,17 @@ pub fn set_completion(
|
||||
editor: &mut Editor,
|
||||
savepoint: Arc<SavePoint>,
|
||||
items: Vec<CompletionItem>,
|
||||
incomplete_completion_lists: HashMap<CompletionProvider, i8>,
|
||||
trigger_offset: usize,
|
||||
size: Rect,
|
||||
) -> Option<Rect> {
|
||||
let mut completion = Completion::new(editor, savepoint, items, trigger_offset);
|
||||
let mut completion = Completion::new(
|
||||
editor,
|
||||
savepoint,
|
||||
items,
|
||||
incomplete_completion_lists,
|
||||
trigger_offset,
|
||||
);
|
||||
|
||||
if completion.is_empty() {
|
||||
// skip if we got no completion results
|
||||
|
@ -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 @@ pub fn new(
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
|
@ -40,8 +40,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std
|
||||
tokio-stream = "0.1"
|
||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||
|
||||
slotmap = "1"
|
||||
|
||||
slotmap.workspace = true
|
||||
chardetng = "0.1"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
@ -1435,16 +1435,12 @@ fn apply_impl(
|
||||
// TODO: move to hook
|
||||
// emit lsp notification
|
||||
for language_server in self.language_servers() {
|
||||
let notify = language_server.text_document_did_change(
|
||||
let _ = language_server.text_document_did_change(
|
||||
self.versioned_identifier(),
|
||||
&old_doc,
|
||||
self.text(),
|
||||
changes,
|
||||
);
|
||||
|
||||
if let Some(notify) = notify {
|
||||
tokio::spawn(notify);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1761,6 +1757,25 @@ pub fn language_servers_with_feature(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn language_servers_with_feature_owned(
|
||||
&self,
|
||||
feature: LanguageServerFeature,
|
||||
) -> impl Iterator<Item = Arc<helix_lsp::Client>> + '_ {
|
||||
self.language_config().into_iter().flat_map(move |config| {
|
||||
config.language_servers.iter().filter_map(move |features| {
|
||||
let ls = self.language_servers.get(&features.name)?.clone();
|
||||
if ls.is_initialized()
|
||||
&& ls.supports_feature(feature)
|
||||
&& features.has_feature(feature)
|
||||
{
|
||||
Some(ls)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn supports_language_server(&self, id: LanguageServerId) -> bool {
|
||||
self.language_servers().any(|l| l.id() == id)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user