Unnamed repository; edit this file 'description' to name the repository.
implement incomplete completion requests
Pascal Kuthe 2024-12-08
parent fc9968b · commit 9a06aea
-rw-r--r--helix-core/src/completion.rs14
-rw-r--r--helix-lsp/src/client.rs106
-rw-r--r--helix-term/src/handlers.rs1
-rw-r--r--helix-term/src/handlers/completion.rs337
-rw-r--r--helix-term/src/handlers/completion/item.rs84
-rw-r--r--helix-term/src/handlers/completion/path.rs33
-rw-r--r--helix-term/src/handlers/completion/request.rs373
-rw-r--r--helix-term/src/ui/completion.rs141
-rw-r--r--helix-term/src/ui/editor.rs12
-rw-r--r--helix-term/src/ui/menu.rs72
-rw-r--r--helix-view/Cargo.toml3
-rw-r--r--helix-view/src/document.rs25
12 files changed, 770 insertions, 431 deletions
diff --git a/helix-core/src/completion.rs b/helix-core/src/completion.rs
index 0bd111eb..c024f954 100644
--- a/helix-core/src/completion.rs
+++ b/helix-core/src/completion.rs
@@ -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)
+ }
}
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index cc1c4ce8..28a1bd09 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -426,29 +426,32 @@ impl Client {
let server_tx = self.server_tx.clone();
let id = self.next_request_id();
- let params = serde_json::to_value(params);
+ // 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),
+ };
+ 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;
-
- let request = jsonrpc::MethodCall {
- jsonrpc: Some(jsonrpc::Version::V2),
- id: id.clone(),
- method: R::METHOD.to_string(),
- params: Self::value_into_params(params?),
- };
-
- let (tx, mut rx) = channel::<Result<Value>>(1);
-
- server_tx
- .send(Payload::Request {
- chan: tx,
- value: request,
- })
- .map_err(|e| Error::Other(e.into()))?;
-
// 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,21 +468,25 @@ impl Client {
{
let server_tx = self.server_tx.clone();
- async move {
- let params = serde_json::to_value(params)?;
-
- let notification = jsonrpc::Notification {
- jsonrpc: Some(jsonrpc::Version::V2),
- 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(())
- }
+ // 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 {
+ jsonrpc: Some(jsonrpc::Version::V2),
+ method: R::METHOD.to_string(),
+ params: Self::value_into_params(params),
+ };
+ server_tx
+ .send(Payload::Notification(notification))
+ .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 @@ impl Client {
let server_tx = self.server_tx.clone();
- async move {
- let output = match result {
- Ok(result) => Output::Success(Success {
- jsonrpc: Some(Version::V2),
- id,
- result: serde_json::to_value(result)?,
- }),
- Err(error) => Output::Failure(Failure {
+ let output = match result {
+ Ok(result) => serde_json::to_value(result).map(|result| {
+ Output::Success(Success {
jsonrpc: Some(Version::V2),
id,
- error,
- }),
- };
+ result,
+ })
+ }),
+ 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 }
}
// -------------------------------------------------------------------------------------------
diff --git a/helix-term/src/handlers.rs b/helix-term/src/handlers.rs
index b27e34e2..e667f789 100644
--- a/helix-term/src/handlers.rs
+++ b/helix-term/src/handlers.rs
@@ -9,7 +9,6 @@ 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;
diff --git a/helix-term/src/handlers/completion.rs b/helix-term/src/handlers/completion.rs
index f3223487..41ada7b4 100644
--- a/helix-term/src/handlers/completion.rs
+++ b/helix-term/src/handlers/completion.rs
@@ -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,
-}
-
-#[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>,
+ incomplete: bool,
+) -> Option<CompletionResponse> {
+ loop {
+ let response = requests.join_next().await?.unwrap();
+ if !incomplete && !response.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>,
+ 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,
- }
- }
+ while let Some(response) = handle_response(&mut requests, incomplete).await {
+ let handle = handle.clone();
+ dispatch(move |editor, compositor| {
+ let editor_view = compositor.find::<ui::EditorView>().unwrap();
+ let Some(completion) = &mut editor_view.completion else {
+ return;
};
-
- let completion_response = ls.completion(doc_id, pos, None, context).unwrap();
- async move {
- let json = completion_response.await?;
- let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
- let items = match response {
- Some(lsp::CompletionResponse::Array(items)) => items,
- // TODO: do something with is_incomplete
- Some(lsp::CompletionResponse::List(lsp::CompletionList {
- is_incomplete: _is_incomplete,
- items,
- })) => items,
- None => Vec::new(),
- }
- .into_iter()
- .map(|item| {
- CompletionItem::Lsp(LspCompletionItem {
- item,
- provider: language_server_id,
- resolved: false,
- })
- })
- .collect();
- anyhow::Ok(items)
+ if handle.is_canceled() {
+ log::error!("dropping outdated completion response");
+ return;
+ }
+ 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);
}
- .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;
- };
- dispatch(move |editor, compositor| {
- show_completion(editor, compositor, items, trigger, savepoint);
- drop(handle)
})
- .await
- });
+ .await;
+ }
}
fn show_completion(
editor: &mut Editor,
compositor: &mut Compositor,
items: Vec<CompletionItem>,
+ 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);
}
diff --git a/helix-term/src/handlers/completion/item.rs b/helix-term/src/handlers/completion/item.rs
index bcd35cd5..de08d0db 100644
--- a/helix-term/src/handlers/completion/item.rs
+++ b/helix-term/src/handlers/completion/item.rs
@@ -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 @@ impl PartialEq<CompletionItem> for helix_core::CompletionItem {
}
impl CompletionItem {
+ pub fn provider_priority(&self) -> i8 {
+ match self {
+ CompletionItem::Lsp(item) => item.provider_priority,
+ // sorting path completions after LSP for now
+ CompletionItem::Other(_) => 1,
+ }
+ }
+
+ pub fn provider(&self) -> CompletionProvider {
+ match self {
+ CompletionItem::Lsp(item) => CompletionProvider::Lsp(item.provider),
+ CompletionItem::Other(item) => item.provider,
+ }
+ }
+
pub fn preselect(&self) -> bool {
match self {
CompletionItem::Lsp(LspCompletionItem { item, .. }) => item.preselect.unwrap_or(false),
diff --git a/helix-term/src/handlers/completion/path.rs b/helix-term/src/handlers/completion/path.rs
index e92be51c..a16a0d5c 100644
--- a/helix-term/src/handlers/completion/path.rs
+++ b/helix-term/src/handlers/completion/path.rs
@@ -5,22 +5,21 @@ use std::{
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)]
diff --git a/helix-term/src/handlers/completion/request.rs b/helix-term/src/handlers/completion/request.rs
new file mode 100644
index 00000000..9c37b7e3
--- /dev/null
+++ b/helix-term/src/handlers/completion/request.rs
@@ -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));
+}
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index cb0af6fc..8784a1b7 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -1,10 +1,16 @@
+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::{
+ self as core, chars, completion::CompletionProvider, fuzzy::MATCHER, Change, Transaction,
+};
+use helix_event::TaskController;
+use helix_lsp::{lsp, util, OffsetEncoding};
use helix_view::{
document::SavePoint,
editor::CompleteAction,
@@ -12,35 +18,17 @@ use helix_view::{
theme::{Modifier, Style},
ViewId,
};
-use tui::{buffer::Buffer as Surface, text::Span};
-
-use std::{borrow::Cow, sync::Arc};
-
-use helix_core::{self as core, chars, Change, Transaction};
use helix_view::{graphics::Rect, Document, Editor};
+use nucleo::{
+ pattern::{Atom, AtomKind, CaseMatching, Normalization},
+ Config, Utf32Str,
+};
+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, collections::HashMap, sync::Arc};
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 {
@@ -115,6 +103,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 {
@@ -123,13 +114,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| {
@@ -390,17 +380,77 @@ impl Completion {
// 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(
@@ -442,7 +492,28 @@ 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: 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 {
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 5179be4f..f8a715d8 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -14,6 +14,7 @@ use crate::{
};
use helix_core::{
+ completion::CompletionProvider,
diagnostic::NumberOrString,
graphemes::{next_grapheme_boundary, prev_grapheme_boundary},
movement::Direction,
@@ -31,7 +32,7 @@ use helix_view::{
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};
@@ -1031,10 +1032,17 @@ impl EditorView {
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
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index 612832ce..76e50229 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -1,12 +1,7 @@
-use std::{borrow::Cow, cmp::Reverse};
-
use crate::{
compositor::{Callback, Component, Compositor, Context, Event, EventResult},
ctrl, key, shift,
};
-use helix_core::fuzzy::MATCHER;
-use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization};
-use nucleo::{Config, Utf32Str};
use tui::{buffer::Buffer as Surface, widgets::Table};
pub use tui::widgets::{Cell, Row};
@@ -19,16 +14,6 @@ pub trait Item: Sync + Send + 'static {
type Data: Sync + Send + 'static;
fn format(&self, data: &Self::Data) -> Row;
-
- fn sort_text(&self, data: &Self::Data) -> Cow<str> {
- let label: String = self.format(data).cell_text().collect();
- label.into()
- }
-
- fn filter_text(&self, data: &Self::Data) -> Cow<str> {
- let label: String = self.format(data).cell_text().collect();
- label.into()
- }
}
pub type MenuCallback<T> = Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>;
@@ -77,49 +62,30 @@ impl<T: Item> Menu<T> {
}
}
- pub fn score(&mut self, pattern: &str, incremental: bool) {
- let mut matcher = MATCHER.lock();
- matcher.config = Config::DEFAULT;
- let pattern = Atom::new(
- pattern,
- CaseMatching::Ignore,
- Normalization::Smart,
- AtomKind::Fuzzy,
- false,
- );
- let mut buf = Vec::new();
- if incremental {
- self.matches.retain_mut(|(index, score)| {
- let option = &self.options[*index as usize];
- let text = option.filter_text(&self.editor_data);
- let new_score = pattern.score(Utf32Str::new(&text, &mut buf), &mut matcher);
- match new_score {
- Some(new_score) => {
- *score = new_score as u32;
- true
- }
- None => false,
- }
- })
- } else {
- self.matches.clear();
- let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
- let text = option.filter_text(&self.editor_data);
- pattern
- .score(Utf32Str::new(&text, &mut buf), &mut matcher)
- .map(|score| (i as u32, score as u32))
- });
- self.matches.extend(matches);
- }
- self.matches
- .sort_unstable_by_key(|&(i, score)| (Reverse(score), i));
-
- // reset cursor position
+ pub fn reset_cursor(&mut self) {
self.cursor = None;
self.scroll = 0;
self.recalculate = true;
}
+ pub fn update_options(&mut self) -> (&mut Vec<(u32, u32)>, &mut Vec<T>) {
+ self.recalculate = true;
+ (&mut self.matches, &mut self.options)
+ }
+
+ pub fn ensure_cursor_in_bounds(&mut self) {
+ if self.matches.is_empty() {
+ self.cursor = None;
+ self.scroll = 0;
+ } else {
+ self.scroll = 0;
+ self.recalculate = true;
+ if let Some(cursor) = &mut self.cursor {
+ *cursor = (*cursor).min(self.matches.len() - 1)
+ }
+ }
+ }
+
pub fn clear(&mut self) {
self.matches.clear();
diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml
index 6f71fa05..f69f2982 100644
--- a/helix-view/Cargo.toml
+++ b/helix-view/Cargo.toml
@@ -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"] }
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index fa089cda..12712161 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -1430,16 +1430,12 @@ impl Document {
// 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);
- }
}
}
@@ -1756,6 +1752,25 @@ impl Document {
})
}
+ 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)
}