|
|
@ -1,7 +1,9 @@
|
|
|
|
use crate::{
|
|
|
|
use crate::{
|
|
|
|
compositor::{Component, Context, Event, EventResult},
|
|
|
|
compositor::{Component, Context, Event, EventResult},
|
|
|
|
handlers::trigger_auto_completion,
|
|
|
|
handlers::trigger_auto_completion,
|
|
|
|
|
|
|
|
job,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
use helix_event::AsyncHook;
|
|
|
|
use helix_view::{
|
|
|
|
use helix_view::{
|
|
|
|
document::SavePoint,
|
|
|
|
document::SavePoint,
|
|
|
|
editor::CompleteAction,
|
|
|
|
editor::CompleteAction,
|
|
|
@ -10,14 +12,14 @@ use helix_view::{
|
|
|
|
theme::{Modifier, Style},
|
|
|
|
theme::{Modifier, Style},
|
|
|
|
ViewId,
|
|
|
|
ViewId,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
use tokio::time::Instant;
|
|
|
|
use tui::{buffer::Buffer as Surface, text::Span};
|
|
|
|
use tui::{buffer::Buffer as Surface, text::Span};
|
|
|
|
|
|
|
|
|
|
|
|
use std::{borrow::Cow, sync::Arc};
|
|
|
|
use std::{borrow::Cow, sync::Arc, time::Duration};
|
|
|
|
|
|
|
|
|
|
|
|
use helix_core::{chars, Change, Transaction};
|
|
|
|
use helix_core::{chars, Change, Transaction};
|
|
|
|
use helix_view::{graphics::Rect, Document, Editor};
|
|
|
|
use helix_view::{graphics::Rect, Document, Editor};
|
|
|
|
|
|
|
|
|
|
|
|
use crate::commands;
|
|
|
|
|
|
|
|
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
|
|
|
|
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
|
|
|
|
|
|
|
|
|
|
|
|
use helix_lsp::{lsp, util, OffsetEncoding};
|
|
|
|
use helix_lsp::{lsp, util, OffsetEncoding};
|
|
|
@ -102,6 +104,7 @@ pub struct Completion {
|
|
|
|
#[allow(dead_code)]
|
|
|
|
#[allow(dead_code)]
|
|
|
|
trigger_offset: usize,
|
|
|
|
trigger_offset: usize,
|
|
|
|
filter: String,
|
|
|
|
filter: String,
|
|
|
|
|
|
|
|
resolve_handler: tokio::sync::mpsc::Sender<CompletionItem>,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Completion {
|
|
|
|
impl Completion {
|
|
|
@ -368,6 +371,7 @@ impl Completion {
|
|
|
|
// TODO: expand nucleo api to allow moving straight to a Utf32String here
|
|
|
|
// TODO: expand nucleo api to allow moving straight to a Utf32String here
|
|
|
|
// and avoid allocation during matching
|
|
|
|
// and avoid allocation during matching
|
|
|
|
filter: String::from(fragment),
|
|
|
|
filter: String::from(fragment),
|
|
|
|
|
|
|
|
resolve_handler: ResolveHandler::default().spawn(),
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// need to recompute immediately in case start_offset != trigger_offset
|
|
|
|
// need to recompute immediately in case start_offset != trigger_offset
|
|
|
@ -379,6 +383,8 @@ impl Completion {
|
|
|
|
completion
|
|
|
|
completion
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Synchronously resolve the given completion item. This is used when
|
|
|
|
|
|
|
|
/// accepting a completion.
|
|
|
|
fn resolve_completion_item(
|
|
|
|
fn resolve_completion_item(
|
|
|
|
language_server: &helix_lsp::Client,
|
|
|
|
language_server: &helix_lsp::Client,
|
|
|
|
completion_item: lsp::CompletionItem,
|
|
|
|
completion_item: lsp::CompletionItem,
|
|
|
@ -386,7 +392,7 @@ impl Completion {
|
|
|
|
let future = language_server.resolve_completion_item(completion_item)?;
|
|
|
|
let future = language_server.resolve_completion_item(completion_item)?;
|
|
|
|
let response = helix_lsp::block_on(future);
|
|
|
|
let response = helix_lsp::block_on(future);
|
|
|
|
match response {
|
|
|
|
match response {
|
|
|
|
Ok(value) => serde_json::from_value(value).ok(),
|
|
|
|
Ok(item) => Some(item),
|
|
|
|
Err(err) => {
|
|
|
|
Err(err) => {
|
|
|
|
log::error!("Failed to resolve completion item: {}", err);
|
|
|
|
log::error!("Failed to resolve completion item: {}", err);
|
|
|
|
None
|
|
|
|
None
|
|
|
@ -420,62 +426,6 @@ impl Completion {
|
|
|
|
self.popup.contents_mut().replace_option(old_item, new_item);
|
|
|
|
self.popup.contents_mut().replace_option(old_item, new_item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Asynchronously requests that the currently selection completion item is
|
|
|
|
|
|
|
|
/// resolved through LSP `completionItem/resolve`.
|
|
|
|
|
|
|
|
pub fn ensure_item_resolved(&mut self, cx: &mut commands::Context) -> bool {
|
|
|
|
|
|
|
|
// > If computing full completion items is expensive, servers can additionally provide a
|
|
|
|
|
|
|
|
// > handler for the completion item resolve request. ...
|
|
|
|
|
|
|
|
// > A typical use case is for example: the `textDocument/completion` request doesn't fill
|
|
|
|
|
|
|
|
// > in the `documentation` property for returned completion items since it is expensive
|
|
|
|
|
|
|
|
// > to compute. When the item is selected in the user interface then a
|
|
|
|
|
|
|
|
// > 'completionItem/resolve' request is sent with the selected completion item as a parameter.
|
|
|
|
|
|
|
|
// > The returned completion item should have the documentation property filled in.
|
|
|
|
|
|
|
|
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
|
|
|
|
|
|
|
|
let current_item = match self.popup.contents().selection() {
|
|
|
|
|
|
|
|
Some(item) if !item.resolved => item.clone(),
|
|
|
|
|
|
|
|
_ => return false,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let Some(language_server) = cx
|
|
|
|
|
|
|
|
.editor
|
|
|
|
|
|
|
|
.language_server_by_id(current_item.language_server_id)
|
|
|
|
|
|
|
|
else {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// This method should not block the compositor so we handle the response asynchronously.
|
|
|
|
|
|
|
|
let Some(future) = language_server.resolve_completion_item(current_item.item.clone())
|
|
|
|
|
|
|
|
else {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cx.callback(
|
|
|
|
|
|
|
|
future,
|
|
|
|
|
|
|
|
move |_editor, compositor, response: Option<lsp::CompletionItem>| {
|
|
|
|
|
|
|
|
let resolved_item = match response {
|
|
|
|
|
|
|
|
Some(item) => item,
|
|
|
|
|
|
|
|
None => return,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(completion) = &mut compositor
|
|
|
|
|
|
|
|
.find::<crate::ui::EditorView>()
|
|
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
|
|
.completion
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
let resolved_item = CompletionItem {
|
|
|
|
|
|
|
|
item: resolved_item,
|
|
|
|
|
|
|
|
language_server_id: current_item.language_server_id,
|
|
|
|
|
|
|
|
resolved: true,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
completion.replace_item(current_item, resolved_item);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect {
|
|
|
|
pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect {
|
|
|
|
self.popup.area(viewport, editor)
|
|
|
|
self.popup.area(viewport, editor)
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -498,6 +448,9 @@ impl Component for Completion {
|
|
|
|
Some(option) => option,
|
|
|
|
Some(option) => option,
|
|
|
|
None => return,
|
|
|
|
None => return,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
if !option.resolved {
|
|
|
|
|
|
|
|
helix_event::send_blocking(&self.resolve_handler, option.clone());
|
|
|
|
|
|
|
|
}
|
|
|
|
// need to render:
|
|
|
|
// need to render:
|
|
|
|
// option.detail
|
|
|
|
// option.detail
|
|
|
|
// ---
|
|
|
|
// ---
|
|
|
@ -599,3 +552,88 @@ impl Component for Completion {
|
|
|
|
markdown_doc.render(doc_area, surface, cx);
|
|
|
|
markdown_doc.render(doc_area, surface, cx);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// A hook for resolving incomplete completion items.
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// From the [LSP spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion):
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// > If computing full completion items is expensive, servers can additionally provide a
|
|
|
|
|
|
|
|
/// > handler for the completion item resolve request. ...
|
|
|
|
|
|
|
|
/// > A typical use case is for example: the `textDocument/completion` request doesn't fill
|
|
|
|
|
|
|
|
/// > in the `documentation` property for returned completion items since it is expensive
|
|
|
|
|
|
|
|
/// > to compute. When the item is selected in the user interface then a
|
|
|
|
|
|
|
|
/// > 'completionItem/resolve' request is sent with the selected completion item as a parameter.
|
|
|
|
|
|
|
|
/// > The returned completion item should have the documentation property filled in.
|
|
|
|
|
|
|
|
#[derive(Debug, Default)]
|
|
|
|
|
|
|
|
struct ResolveHandler {
|
|
|
|
|
|
|
|
trigger: Option<CompletionItem>,
|
|
|
|
|
|
|
|
request: Option<helix_event::CancelTx>,
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl AsyncHook for ResolveHandler {
|
|
|
|
|
|
|
|
type Event = CompletionItem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn handle_event(
|
|
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
|
|
item: Self::Event,
|
|
|
|
|
|
|
|
timeout: Option<tokio::time::Instant>,
|
|
|
|
|
|
|
|
) -> Option<tokio::time::Instant> {
|
|
|
|
|
|
|
|
if self
|
|
|
|
|
|
|
|
.trigger
|
|
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
|
|
.is_some_and(|trigger| trigger == &item)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
timeout
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
self.trigger = Some(item);
|
|
|
|
|
|
|
|
self.request = None;
|
|
|
|
|
|
|
|
Some(Instant::now() + Duration::from_millis(150))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn finish_debounce(&mut self) {
|
|
|
|
|
|
|
|
let Some(item) = self.trigger.take() else { return };
|
|
|
|
|
|
|
|
let (tx, rx) = helix_event::cancelation();
|
|
|
|
|
|
|
|
self.request = Some(tx);
|
|
|
|
|
|
|
|
job::dispatch_blocking(move |editor, _| resolve_completion_item(editor, item, rx))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn resolve_completion_item(
|
|
|
|
|
|
|
|
editor: &mut Editor,
|
|
|
|
|
|
|
|
item: CompletionItem,
|
|
|
|
|
|
|
|
cancel: helix_event::CancelRx,
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
let Some(language_server) = editor.language_server_by_id(item.language_server_id) else {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let Some(future) = language_server.resolve_completion_item(item.item.clone()) else {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
|
|
|
match helix_event::cancelable_future(future, cancel).await {
|
|
|
|
|
|
|
|
Some(Ok(resolved_item)) => {
|
|
|
|
|
|
|
|
job::dispatch(move |_, compositor| {
|
|
|
|
|
|
|
|
if let Some(completion) = &mut compositor
|
|
|
|
|
|
|
|
.find::<crate::ui::EditorView>()
|
|
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
|
|
.completion
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
let resolved_item = CompletionItem {
|
|
|
|
|
|
|
|
item: resolved_item,
|
|
|
|
|
|
|
|
language_server_id: item.language_server_id,
|
|
|
|
|
|
|
|
resolved: true,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
completion.replace_item(item, resolved_item);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.await
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
Some(Err(err)) => log::error!("completion resolve request failed: {err}"),
|
|
|
|
|
|
|
|
None => (),
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|