diff --git a/helix-term/src/handlers/completion.rs b/helix-term/src/handlers/completion.rs index 68956c85f504..aaee61aeb213 100644 --- a/helix-term/src/handlers/completion.rs +++ b/helix-term/src/handlers/completion.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::Duration; @@ -9,8 +9,9 @@ use helix_core::syntax::LanguageServerFeature; use helix_event::{ cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx, }; -use helix_lsp::lsp; +use helix_lsp::lsp::{self, CompletionList}; use helix_lsp::util::pos_to_lsp_pos; +use helix_lsp::LanguageServerId; use helix_stdx::rope::RopeSliceExt; use helix_view::document::{Mode, SavePoint}; use helix_view::handlers::lsp::CompletionEvent; @@ -27,7 +28,7 @@ use crate::job::{dispatch, dispatch_blocking}; use crate::keymap::MappableCommand; use crate::ui::editor::InsertEvent; use crate::ui::lsp::SignatureHelp; -use crate::ui::{self, CompletionItem, Popup}; +use crate::ui::{self, CompletionDetails, CompletionItem, Popup}; use super::Handlers; pub use resolve::ResolveHandler; @@ -171,12 +172,16 @@ fn request_completion( ) { let (view, doc) = current!(editor); - if compositor + + + + let completion = &compositor .find::() .unwrap() - .completion - .is_some() - || editor.mode != Mode::Insert + .completion; + + if completion.as_ref().is_some_and(|completion| completion.incomplete_ids().is_empty()) + || editor.mode != Mode::Insert { return; } @@ -197,10 +202,22 @@ fn request_completion( trigger.pos = cursor; let trigger_text = text.slice(..cursor); + let ls_filter: Box bool> = match completion { + None => Box::from(|_: LanguageServerId| true), + Some(completion) => { + let is_incomplete_ids = completion.incomplete_ids(); + + Box::from(move |ls: LanguageServerId| { + is_incomplete_ids.contains(&ls) + }) + } + }; + 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())) + .filter(|ls| ls_filter(ls.id())) .map(|ls| { let language_server_id = ls.id(); let offset_encoding = ls.offset_encoding(); @@ -241,38 +258,38 @@ fn request_completion( async move { let json = completion_response.await?; let response: Option = 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 { - item, - provider: language_server_id, - resolved: false, - }) - .collect(); - anyhow::Ok(items) + let response = response + .map(|response| match response { // (completion items, (id, is_incomplete)) to be later collected into HashMap + lsp::CompletionResponse::Array(items) => (items, (language_server_id, CompletionDetails::default())), + lsp::CompletionResponse::List(CompletionList { is_incomplete, items }) => (items, (language_server_id, CompletionDetails {is_incomplete})) + }) + .map(|(items, comp_type)| ( + items.into_iter().map(|item| CompletionItem {item, provider: language_server_id, resolved: false}).collect::>(), + comp_type + )); + + anyhow::Ok(response) } }) .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), + let mut cmp_is_incomplete: HashMap = HashMap::new(); + + while let Some(response) = futures.next().await { + match response { + Ok(Some((mut lsp_items, lsp_type_pair))) => { + items.append(&mut lsp_items); + cmp_is_incomplete.insert(lsp_type_pair.0, lsp_type_pair.1); + }, Err(err) => { log::debug!("completion request failed: {err:?}"); - } + }, + Ok(None) => (), }; } - items + (items, cmp_is_incomplete) }; let savepoint = doc.savepoint(view); @@ -280,12 +297,13 @@ fn request_completion( let ui = compositor.find::().unwrap(); ui.last_insert.1.push(InsertEvent::RequestCompletion); tokio::spawn(async move { - let items = cancelable_future(future, cancel).await.unwrap_or_default(); + let (items, lsp_cmp_details) = cancelable_future(future, cancel).await.unwrap_or_default(); + if items.is_empty() { return; } dispatch(move |editor, compositor| { - show_completion(editor, compositor, items, trigger, savepoint) + show_completion(editor, compositor, items, lsp_cmp_details, trigger, savepoint) }) .await }); @@ -295,6 +313,7 @@ fn show_completion( editor: &mut Editor, compositor: &mut Compositor, items: Vec, + lsp_cmp_details: HashMap, trigger: Trigger, savepoint: Arc, ) { @@ -310,11 +329,39 @@ fn show_completion( let size = compositor.size(); let ui = compositor.find::().unwrap(); - if ui.completion.is_some() { - return; - } + + // Persist old completions and completion window offset on is_incomplete + let completion_area = match &ui.completion { + Some(completion) => { + let offset = completion.trigger_offset(); + + println!("offset: {offset}"); + + let complete_items = completion.complete_items(); + + let all_items = complete_items + .map(|item| item.clone()) // TODO: Workaround + .chain(items.into_iter()) + .collect::>(); + + // TODO: how to align the new completion menu with the old one? I am trying to set the offset but + // it is not working + let area = ui.set_completion(editor, savepoint, all_items, lsp_cmp_details, offset, size); + + + // TODO: do we need to rerank? and Would the completion menu change? + // if let Some(completion) = &compositor.find::().unwrap().completion { + // completion.rerank + // } + + area + + + + }, + None => ui.set_completion(editor, savepoint, items, lsp_cmp_details, trigger.pos, size) + }; - let completion_area = ui.set_completion(editor, savepoint, items, trigger.pos, size); let signature_help_area = compositor .find_id::>(SignatureHelp::ID) .map(|signature_help| signature_help.area(size, editor)); @@ -383,6 +430,17 @@ fn update_completions(cx: &mut commands::Context, c: Option) { let editor_view = compositor.find::().unwrap(); if let Some(completion) = &mut editor_view.completion { completion.update_filter(c); + + + // Handle completions with is_incomplete + let ids = completion.incomplete_ids(); + if !ids.is_empty() { + + trigger_auto_completion(&cx.editor.handlers.completions, cx.editor, false); + + } + + if completion.is_empty() { editor_view.clear_completion(cx.editor); // clearing completions might mean we want to immediately rerequest them (usually diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 372e3e5ef4e4..83a763670ed1 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -11,7 +11,7 @@ use helix_view::{ }; use tui::{buffer::Buffer as Surface, text::Span}; -use std::{borrow::Cow, sync::Arc}; +use std::{borrow::Cow, collections::{HashMap, HashSet}, sync::Arc}; use helix_core::{chars, Change, Transaction}; use helix_view::{graphics::Rect, Document, Editor}; @@ -94,6 +94,19 @@ pub struct CompletionItem { pub resolved: bool, } +#[derive(Debug, PartialEq, Clone)] +pub struct CompletionDetails { + pub is_incomplete: bool +} + +impl Default for CompletionDetails { + fn default() -> Self { + Self { + is_incomplete: false + } + } +} + /// Wraps a Menu. pub struct Completion { popup: Popup>, @@ -101,6 +114,7 @@ pub struct Completion { trigger_offset: usize, filter: String, resolve_handler: ResolveHandler, + lsp_cmp_details: HashMap } impl Completion { @@ -110,6 +124,7 @@ impl Completion { editor: &Editor, savepoint: Arc, mut items: Vec, + lsp_cmp_details: HashMap, trigger_offset: usize, ) -> Self { let preview_completion_insert = editor.config().preview_completion_insert; @@ -351,6 +366,8 @@ impl Completion { let mut completion = Self { popup, trigger_offset, + lsp_cmp_details, + // TODO: expand nucleo api to allow moving straight to a Utf32String here // and avoid allocation during matching filter: String::from(fragment), @@ -421,6 +438,28 @@ impl Completion { pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect { self.popup.area(viewport, editor) } + + pub fn incomplete_ids(&self) -> HashSet { + self.lsp_cmp_details.iter() + .flat_map(|(&id, details)| match details { + CompletionDetails {is_incomplete: true} => Some(id), + _ => None + }) + .collect() + } + + pub fn trigger_offset(&self) -> usize { + self.trigger_offset + } + + pub fn complete_items(&self) -> impl Iterator { + let incomplete_ids = self.incomplete_ids(); + + self.popup.contents().items() + .iter() + .filter(move |item| !incomplete_ids.contains(&item.provider)) + + } } impl Component for Completion { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 97f90f625549..dc09aa6e7795 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -19,6 +19,7 @@ use helix_core::{ unicode::width::UnicodeWidthStr, visual_offset_from_block, Change, Position, Range, Selection, Transaction, }; +use helix_lsp::LanguageServerId; use helix_view::{ document::{Mode, SavePoint, SCRATCH_BUFFER_NAME}, editor::{CompleteAction, CursorShapeConfig}, @@ -27,11 +28,11 @@ 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}; -use super::document::LineDecoration; +use super::{completion::CompletionDetails, document::LineDecoration}; use super::{completion::CompletionItem, statusline}; pub struct EditorView { @@ -1019,10 +1020,11 @@ impl EditorView { editor: &mut Editor, savepoint: Arc, items: Vec, + lsp_cmp_details: HashMap, trigger_offset: usize, size: Rect, ) -> Option { - let mut completion = Completion::new(editor, savepoint, items, trigger_offset); + let mut completion = Completion::new(editor, savepoint, items, lsp_cmp_details, 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 c5006f9580af..a028b280d128 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -231,6 +231,10 @@ impl Menu { pub fn len(&self) -> usize { self.matches.len() } + + pub fn items(&self) -> &Vec { + &self.options + } } impl Menu { diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 5211c2e272ef..424be3e2a967 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -16,7 +16,7 @@ mod text; use crate::compositor::Compositor; use crate::filter_picker_entry; use crate::job::{self, Callback}; -pub use completion::{Completion, CompletionItem}; +pub use completion::{Completion, CompletionItem, CompletionDetails}; pub use editor::EditorView; use helix_stdx::rope; pub use markdown::Markdown; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 5540c5182944..9c90a0860ee4 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -885,7 +885,7 @@ impl Default for Config { idle_timeout: Duration::from_millis(250), completion_timeout: Duration::from_millis(250), preview_completion_insert: true, - completion_trigger_len: 2, + completion_trigger_len: 1, auto_info: true, file_picker: FilePickerConfig::default(), statusline: StatusLineConfig::default(),