Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/ui/completion.rs')
| -rw-r--r-- | helix-term/src/ui/completion.rs | 306 |
1 files changed, 198 insertions, 108 deletions
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index c0d3294f..93b6a753 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -1,41 +1,49 @@ -use crate::handlers::completion::LspCompletionItem; -use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; use crate::{ compositor::{Component, Context, Event, EventResult}, handlers::completion::{ - trigger_auto_completion, CompletionItem, CompletionResponse, ResolveHandler, + trigger_auto_completion, CompletionItem, CompletionResponse, LspCompletionItem, + ResolveHandler, }, }; -use helix_core::snippets::{ActiveSnippet, RenderedSnippet, Snippet}; -use helix_core::{self as core, chars, fuzzy::MATCHER, Change, Transaction}; -use helix_lsp::{lsp, util, OffsetEncoding}; +use helix_event::TaskController; use helix_view::{ + document::SavePoint, editor::CompleteAction, handlers::lsp::SignatureHelpInvoked, - theme::{Color, Modifier, Style}, + theme::{Modifier, Style}, ViewId, }; -use helix_view::{graphics::Rect, Document, Editor}; use nucleo::{ pattern::{Atom, AtomKind, CaseMatching, Normalization}, Config, Utf32Str, }; -use tui::text::Spans; use tui::{buffer::Buffer as Surface, text::Span}; -use std::cmp::Reverse; +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, +}; +use helix_view::{graphics::Rect, Document, Editor}; + +use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; + +use helix_lsp::{lsp, util, OffsetEncoding}; impl menu::Item for CompletionItem { - type Data = Style; + type Data = (); - fn format(&self, dir_style: &Self::Data) -> menu::Row<'_> { + fn format(&self, _data: &Self::Data) -> menu::Row { let deprecated = match self { CompletionItem::Lsp(LspCompletionItem { item, .. }) => { item.deprecated.unwrap_or_default() - || item - .tags - .as_ref() - .is_some_and(|tags| tags.contains(&lsp::CompletionItemTag::DEPRECATED)) + || item.tags.as_ref().map_or(false, |tags| { + tags.contains(&lsp::CompletionItemTag::DEPRECATED) + }) } CompletionItem::Other(_) => false, }; @@ -47,72 +55,51 @@ impl menu::Item for CompletionItem { let kind = match self { CompletionItem::Lsp(LspCompletionItem { item, .. }) => match item.kind { - Some(lsp::CompletionItemKind::TEXT) => "text".into(), - Some(lsp::CompletionItemKind::METHOD) => "method".into(), - Some(lsp::CompletionItemKind::FUNCTION) => "function".into(), - Some(lsp::CompletionItemKind::CONSTRUCTOR) => "constructor".into(), - Some(lsp::CompletionItemKind::FIELD) => "field".into(), - Some(lsp::CompletionItemKind::VARIABLE) => "variable".into(), - Some(lsp::CompletionItemKind::CLASS) => "class".into(), - Some(lsp::CompletionItemKind::INTERFACE) => "interface".into(), - Some(lsp::CompletionItemKind::MODULE) => "module".into(), - Some(lsp::CompletionItemKind::PROPERTY) => "property".into(), - Some(lsp::CompletionItemKind::UNIT) => "unit".into(), - Some(lsp::CompletionItemKind::VALUE) => "value".into(), - Some(lsp::CompletionItemKind::ENUM) => "enum".into(), - Some(lsp::CompletionItemKind::KEYWORD) => "keyword".into(), - Some(lsp::CompletionItemKind::SNIPPET) => "snippet".into(), - Some(lsp::CompletionItemKind::COLOR) => item - .documentation - .as_ref() - .and_then(|docs| { - let text = match docs { - lsp::Documentation::String(text) => text, - lsp::Documentation::MarkupContent(lsp::MarkupContent { - value, .. - }) => value, - }; - // Language servers which send Color completion items tend to include a 6 - // digit hex code at the end for the color. The extra 1 digit is for the '#' - text.get(text.len().checked_sub(7)?..) - }) - .and_then(Color::from_hex) - .map_or("color".into(), |color| { - Spans::from(vec![ - Span::raw("color "), - Span::styled("■", Style::default().fg(color)), - ]) - }), - Some(lsp::CompletionItemKind::FILE) => "file".into(), - Some(lsp::CompletionItemKind::REFERENCE) => "reference".into(), - Some(lsp::CompletionItemKind::FOLDER) => "folder".into(), - Some(lsp::CompletionItemKind::ENUM_MEMBER) => "enum_member".into(), - Some(lsp::CompletionItemKind::CONSTANT) => "constant".into(), - Some(lsp::CompletionItemKind::STRUCT) => "struct".into(), - Some(lsp::CompletionItemKind::EVENT) => "event".into(), - Some(lsp::CompletionItemKind::OPERATOR) => "operator".into(), - Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param".into(), + Some(lsp::CompletionItemKind::TEXT) => "text", + Some(lsp::CompletionItemKind::METHOD) => "method", + Some(lsp::CompletionItemKind::FUNCTION) => "function", + Some(lsp::CompletionItemKind::CONSTRUCTOR) => "constructor", + Some(lsp::CompletionItemKind::FIELD) => "field", + Some(lsp::CompletionItemKind::VARIABLE) => "variable", + Some(lsp::CompletionItemKind::CLASS) => "class", + Some(lsp::CompletionItemKind::INTERFACE) => "interface", + Some(lsp::CompletionItemKind::MODULE) => "module", + Some(lsp::CompletionItemKind::PROPERTY) => "property", + Some(lsp::CompletionItemKind::UNIT) => "unit", + Some(lsp::CompletionItemKind::VALUE) => "value", + Some(lsp::CompletionItemKind::ENUM) => "enum", + Some(lsp::CompletionItemKind::KEYWORD) => "keyword", + Some(lsp::CompletionItemKind::SNIPPET) => "snippet", + Some(lsp::CompletionItemKind::COLOR) => "color", + Some(lsp::CompletionItemKind::FILE) => "file", + Some(lsp::CompletionItemKind::REFERENCE) => "reference", + Some(lsp::CompletionItemKind::FOLDER) => "folder", + Some(lsp::CompletionItemKind::ENUM_MEMBER) => "enum_member", + Some(lsp::CompletionItemKind::CONSTANT) => "constant", + Some(lsp::CompletionItemKind::STRUCT) => "struct", + Some(lsp::CompletionItemKind::EVENT) => "event", + Some(lsp::CompletionItemKind::OPERATOR) => "operator", + Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param", Some(kind) => { log::error!("Received unknown completion item kind: {:?}", kind); - "".into() + "" } - None => "".into(), + None => "", }, - CompletionItem::Other(core::CompletionItem { kind, .. }) => kind.as_ref().into(), + CompletionItem::Other(core::CompletionItem { kind, .. }) => kind, }; - let label = Span::styled( - label, - if deprecated { - Style::default().add_modifier(Modifier::CROSSED_OUT) - } else if kind.0[0].content == "folder" { - *dir_style - } else { - Style::default() - }, - ); - - menu::Row::new([menu::Cell::from(label), menu::Cell::from(kind)]) + menu::Row::new([ + menu::Cell::from(Span::styled( + label, + if deprecated { + Style::default().add_modifier(Modifier::CROSSED_OUT) + } else { + Style::default() + }, + )), + menu::Cell::from(kind), + ]) } } @@ -122,21 +109,27 @@ pub struct Completion { #[allow(dead_code)] trigger_offset: usize, filter: String, - // TODO: move to helix-view/central handler struct in the future resolve_handler: ResolveHandler, + pub incomplete_completion_lists: HashMap<CompletionProvider, i8>, + // controller for requesting updates for incomplete completion lists + pub incomplete_list_controller: TaskController, } impl Completion { pub const ID: &'static str = "completion"; - pub fn new(editor: &Editor, items: Vec<CompletionItem>, trigger_offset: usize) -> Self { + pub fn new( + editor: &Editor, + savepoint: Arc<SavePoint>, + 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; - let dir_style = editor.theme.get("ui.text.directory"); - // Then create the menu - let menu = Menu::new(items, dir_style, move |editor: &mut Editor, item, event| { + let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { let (view, doc) = current!(editor); macro_rules! language_server { @@ -175,11 +168,10 @@ impl Completion { savepoint: doc.savepoint(view), }) } - let item = item.unwrap(); - let context = &editor.handlers.completions.active_completions[&item.provider()]; // if more text was entered, remove it - doc.restore(view, &context.savepoint, false); + doc.restore(view, &savepoint, false); // always present here + let item = item.unwrap(); match item { CompletionItem::Lsp(item) => { @@ -206,15 +198,13 @@ impl Completion { doc.restore(view, &savepoint, false); } - let item = item.unwrap(); - let context = &editor.handlers.completions.active_completions[&item.provider()]; // if more text was entered, remove it - doc.restore(view, &context.savepoint, true); + doc.restore(view, &savepoint, true); // save an undo checkpoint before the completion doc.append_changes_to_history(view); // item always present here - let (transaction, additional_edits, snippet) = match item.clone() { + let (transaction, additional_edits, snippet) = match item.unwrap().clone() { CompletionItem::Lsp(mut item) => { let language_server = language_server!(item); @@ -278,7 +268,7 @@ impl Completion { } // we could have just inserted a trigger char (like a `crate::` completion for rust // so we want to retrigger immediately when accepting a completion. - trigger_auto_completion(editor, true); + trigger_auto_completion(&editor.handlers.completions, editor, true); } }; @@ -312,6 +302,8 @@ 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 @@ -357,15 +349,17 @@ impl Completion { .map(|score| (i as u32, score as u32 / 3)) })); } - // Nucleo is meant as an FZF-like fuzzy matcher and only hides matches that are truly - // impossible - as in the sequence of characters just doesn't appear. That doesn't work - // well for completions with multiple language servers where all completions of the next - // server are below the current one (so you would get good suggestions from the second - // server below those of the first). Setting a reasonable cutoff below which to move bad - // completions out of the way helps with that. + // 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 derived from Nucleo internal constants that may - // move upstream in the future. I want to test this out here to settle on a good number. + // 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]; @@ -424,17 +418,21 @@ impl Completion { self.popup.contents_mut().reset_cursor(); } - pub fn replace_provider_completions( - &mut self, - response: &mut CompletionResponse, - is_incomplete: bool, - ) { + pub fn replace_provider_completions(&mut self, response: CompletionResponse) { let menu = self.popup.contents_mut(); let (_, options) = menu.update_options(); - if is_incomplete { + if self + .incomplete_completion_lists + .remove(&response.provider) + .is_some() + { options.retain(|item| item.provider() != response.provider) } - response.take_items(options); + 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(); @@ -523,10 +521,7 @@ impl Component for Completion { None => return, }, CompletionItem::Other(option) => { - let Some(doc) = option.documentation.as_deref() else { - return; - }; - markdowned(language, None, Some(doc)) + markdowned(language, None, Some(&option.documentation)) } }; @@ -662,3 +657,98 @@ fn completion_changes(transaction: &Transaction, trigger_offset: usize) -> Vec<C .filter(|(start, end, _)| (*start..=*end).contains(&trigger_offset)) .collect() } + +// fn lsp_item_to_transaction( +// doc: &Document, +// view_id: ViewId, +// item: &lsp::CompletionItem, +// offset_encoding: OffsetEncoding, +// trigger_offset: usize, +// include_placeholder: bool, +// replace_mode: bool, +// ) -> Transaction { +// use helix_lsp::snippet; +// let selection = doc.selection(view_id); +// let text = doc.text().slice(..); +// let primary_cursor = selection.primary().cursor(text); + +// let (edit_offset, new_text) = if let Some(edit) = &item.text_edit { +// let edit = match edit { +// lsp::CompletionTextEdit::Edit(edit) => edit.clone(), +// lsp::CompletionTextEdit::InsertAndReplace(item) => { +// let range = if replace_mode { +// item.replace +// } else { +// item.insert +// }; +// lsp::TextEdit::new(range, item.new_text.clone()) +// } +// }; + +// let Some(range) = +// util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) +// else { +// return Transaction::new(doc.text()); +// }; + +// let start_offset = range.anchor as i128 - primary_cursor as i128; +// let end_offset = range.head as i128 - primary_cursor as i128; + +// (Some((start_offset, end_offset)), edit.new_text) +// } else { +// let new_text = item +// .insert_text +// .clone() +// .unwrap_or_else(|| item.label.clone()); +// // check that we are still at the correct savepoint +// // we can still generate a transaction regardless but if the +// // document changed (and not just the selection) then we will +// // likely delete the wrong text (same if we applied an edit sent by the LS) +// debug_assert!(primary_cursor == trigger_offset); +// (None, new_text) +// }; + +// if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET)) +// || matches!( +// item.insert_text_format, +// Some(lsp::InsertTextFormat::SNIPPET) +// ) +// { +// match snippet::parse(&new_text) { +// Ok(snippet) => util::generate_transaction_from_snippet( +// doc.text(), +// selection, +// edit_offset, +// replace_mode, +// snippet, +// doc.line_ending.as_str(), +// include_placeholder, +// doc.tab_width(), +// doc.indent_width(), +// ), +// Err(err) => { +// log::error!( +// "Failed to parse snippet: {:?}, remaining output: {}", +// &new_text, +// err +// ); +// Transaction::new(doc.text()) +// } +// } +// } else { +// util::generate_transaction_from_completion_edit( +// doc.text(), +// selection, +// edit_offset, +// replace_mode, +// new_text, +// ) +// } +// } + +// fn completion_changes(transaction: &Transaction, trigger_offset: usize) -> Vec<Change> { +// transaction +// .changes_iter() +// .filter(|(start, end, _)| (*start..=*end).contains(&trigger_offset)) +// .collect() +// } |