diff --git a/book/src/configuration.md b/book/src/configuration.md index 83ab3f6b9d69..bb952c4546bc 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -56,6 +56,8 @@ Its settings will be merged with the configuration directory `config.toml` and t | `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` | | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` | | `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` | +| `completion-item-kinds` | Custom kind text for completion items | `{}` | +| `completion-item-columns` | Which columns should be displayed on each completion menu row | `["name", "kind"]` | | `auto-info` | Whether to display info boxes | `true` | | `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative | `false` | | `undercurl` | Set to `true` to override automatic detection of terminal undercurl support in the event of a false negative | `false` | diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 6cbb5b1095f4..f745abe3f03f 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -6,7 +6,7 @@ use crate::{ use helix_event::AsyncHook; use helix_view::{ document::SavePoint, - editor::CompleteAction, + editor::{completion_item_kind_name, CompleteAction, CompletionItemColumn}, graphics::Margin, handlers::lsp::SignatureHelpInvoked, theme::{Modifier, Style}, @@ -24,8 +24,70 @@ use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; use helix_lsp::{lsp, util, OffsetEncoding}; +pub struct RenderContext { + completion_item_kinds: Vec<(lsp::CompletionItemKind, String, Style)>, + completion_item_columns: Vec, +} + +impl RenderContext { + fn new(editor: &Editor) -> Self { + let completion_item_columns = editor.config().completion_item_columns.clone(); + + let mut completion_item_kinds = Vec::new(); + let mut insert_item_kind = |kind: lsp::CompletionItemKind| { + let name = completion_item_kind_name(kind); + let style = if name == "unknown" { + editor.theme.get("ui.menu.kind") + } else { + editor.theme.get(&format!("ui.menu.kind.{name}")) + }; + let text = editor + .config() + .completion_item_kinds + .iter() + .find(|(k, _)| *k == kind) + .map(|(_, text)| text.clone()) // FIXME: No alloc? + .unwrap_or_else(|| name.to_string()); + + completion_item_kinds.push((kind, text, style)); + }; + + insert_item_kind(lsp::CompletionItemKind::TEXT); + insert_item_kind(lsp::CompletionItemKind::METHOD); + insert_item_kind(lsp::CompletionItemKind::FUNCTION); + insert_item_kind(lsp::CompletionItemKind::CONSTRUCTOR); + insert_item_kind(lsp::CompletionItemKind::FIELD); + insert_item_kind(lsp::CompletionItemKind::VARIABLE); + insert_item_kind(lsp::CompletionItemKind::CLASS); + insert_item_kind(lsp::CompletionItemKind::INTERFACE); + insert_item_kind(lsp::CompletionItemKind::MODULE); + insert_item_kind(lsp::CompletionItemKind::PROPERTY); + insert_item_kind(lsp::CompletionItemKind::UNIT); + insert_item_kind(lsp::CompletionItemKind::VALUE); + insert_item_kind(lsp::CompletionItemKind::ENUM); + insert_item_kind(lsp::CompletionItemKind::KEYWORD); + insert_item_kind(lsp::CompletionItemKind::SNIPPET); + insert_item_kind(lsp::CompletionItemKind::COLOR); + insert_item_kind(lsp::CompletionItemKind::FILE); + insert_item_kind(lsp::CompletionItemKind::REFERENCE); + insert_item_kind(lsp::CompletionItemKind::FOLDER); + insert_item_kind(lsp::CompletionItemKind::ENUM_MEMBER); + insert_item_kind(lsp::CompletionItemKind::CONSTANT); + insert_item_kind(lsp::CompletionItemKind::STRUCT); + insert_item_kind(lsp::CompletionItemKind::EVENT); + insert_item_kind(lsp::CompletionItemKind::OPERATOR); + insert_item_kind(lsp::CompletionItemKind::TYPE_PARAMETER); + + Self { + completion_item_kinds, + completion_item_columns, + } + } +} + impl menu::Item for CompletionItem { - type Data = (); + type Data = RenderContext; + fn sort_text(&self, data: &Self::Data) -> Cow { self.filter_text(data) } @@ -40,54 +102,40 @@ impl menu::Item for CompletionItem { .into() } - fn format(&self, _data: &Self::Data) -> menu::Row { + fn format(&self, data: &Self::Data) -> menu::Row { let deprecated = self.item.deprecated.unwrap_or_default() || self.item.tags.as_ref().map_or(false, |tags| { tags.contains(&lsp::CompletionItemTag::DEPRECATED) }); - menu::Row::new(vec![ - menu::Cell::from(Span::styled( - self.item.label.as_str(), - if deprecated { - Style::default().add_modifier(Modifier::CROSSED_OUT) - } else { - Style::default() - }, - )), - menu::Cell::from(match self.item.kind { - 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); - "" + let columns = data + .completion_item_columns + .iter() + .map(|column_type| match column_type { + CompletionItemColumn::Name => menu::Cell::from(Span::styled( + self.item.label.as_str(), + if deprecated { + Style::default().add_modifier(Modifier::CROSSED_OUT) + } else { + Style::default() + }, + )), + CompletionItemColumn::Kind => { + let (content, style) = self + .item + .kind + .and_then(|kind| { + data.completion_item_kinds + .iter() + .find(|(k, _, _)| *k == kind) + .map(|(_, text, style)| (text.clone(), *style)) + }) + .unwrap_or_else(|| (String::new(), Style::default())); + menu::Cell::from(Span::styled(content, style)) } - None => "", - }), - ]) + }); + + menu::Row::new(columns) } } @@ -103,6 +151,7 @@ pub struct Completion { popup: Popup>, #[allow(dead_code)] trigger_offset: usize, + item_offset: usize, filter: String, resolve_handler: tokio::sync::mpsc::Sender, } @@ -121,106 +170,127 @@ impl Completion { // Sort completion items according to their preselect status (given by the LSP server) items.sort_by_key(|item| !item.item.preselect.unwrap_or(false)); + let render_context = RenderContext::new(editor); + let item_offset = if render_context.completion_item_columns.first() + == Some(&CompletionItemColumn::Kind) + { + // offset only if the kind icons are the first, otherwise the popup is fine where it is. + render_context + .completion_item_kinds + .iter() + .map(|(_, text, _)| text.len()) + .max() + .unwrap_or_default() + } else { + 0 + }; + // Then create the menu - let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { - fn 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 menu = Menu::new( + items, + render_context, + move |editor: &mut Editor, item, event| { + fn 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) = + 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) - }; + 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( + 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, - 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()) - } + new_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 { - transaction - .changes_iter() - .filter(|(start, end, _)| (*start..=*end).contains(&trigger_offset)) - .collect() - } + fn completion_changes( + transaction: &Transaction, + trigger_offset: usize, + ) -> Vec { + transaction + .changes_iter() + .filter(|(start, end, _)| (*start..=*end).contains(&trigger_offset)) + .collect() + } - let (view, doc) = current!(editor); + let (view, doc) = current!(editor); - macro_rules! language_server { + macro_rules! language_server { ($item:expr) => { match editor .language_servers @@ -237,111 +307,112 @@ impl Completion { }; } - match event { - PromptEvent::Abort => {} - PromptEvent::Update if preview_completion_insert => { - // Update creates "ghost" transactions which are not sent to the - // lsp server to avoid messing up re-requesting completions. Once a - // completion has been selected (with tab, c-n or c-p) it's always accepted whenever anything - // is typed. The only way to avoid that is to explicitly abort the completion - // with c-c. This will remove the "ghost" transaction. - // - // The ghost transaction is modeled with a transaction that is not sent to the LS. - // (apply_temporary) and a savepoint. It's extremely important this savepoint is restored - // (also without sending the transaction to the LS) *before any further transaction is applied*. - // Otherwise incremental sync breaks (since the state of the LS doesn't match the state the transaction - // is applied to). - if matches!(editor.last_completion, Some(CompleteAction::Triggered)) { - editor.last_completion = Some(CompleteAction::Selected { - savepoint: doc.savepoint(view), - }) - } - // if more text was entered, remove it - doc.restore(view, &savepoint, false); - // always present here - let item = item.unwrap(); - - let transaction = item_to_transaction( - doc, - view.id, - &item.item, - language_server!(item).offset_encoding(), - trigger_offset, - true, - replace_mode, - ); - doc.apply_temporary(&transaction, view.id); - } - PromptEvent::Update => {} - PromptEvent::Validate => { - if let Some(CompleteAction::Selected { savepoint }) = - editor.last_completion.take() - { + match event { + PromptEvent::Abort => {} + PromptEvent::Update if preview_completion_insert => { + // Update creates "ghost" transactions which are not sent to the + // lsp server to avoid messing up re-requesting completions. Once a + // completion has been selected (with tab, c-n or c-p) it's always accepted whenever anything + // is typed. The only way to avoid that is to explicitly abort the completion + // with c-c. This will remove the "ghost" transaction. + // + // The ghost transaction is modeled with a transaction that is not sent to the LS. + // (apply_temporary) and a savepoint. It's extremely important this savepoint is restored + // (also without sending the transaction to the LS) *before any further transaction is applied*. + // Otherwise incremental sync breaks (since the state of the LS doesn't match the state the transaction + // is applied to). + if matches!(editor.last_completion, Some(CompleteAction::Triggered)) { + editor.last_completion = Some(CompleteAction::Selected { + savepoint: doc.savepoint(view), + }) + } + // if more text was entered, remove it doc.restore(view, &savepoint, false); + // always present here + let item = item.unwrap(); + + let transaction = item_to_transaction( + doc, + view.id, + &item.item, + language_server!(item).offset_encoding(), + trigger_offset, + true, + replace_mode, + ); + doc.apply_temporary(&transaction, view.id); } - // always present here - let mut item = item.unwrap().clone(); - - let language_server = language_server!(item); - let offset_encoding = language_server.offset_encoding(); - - let language_server = editor - .language_servers - .get_by_id(item.language_server_id) - .unwrap(); - - // resolve item if not yet resolved - if !item.resolved { - if let Some(resolved) = - Self::resolve_completion_item(language_server, item.item.clone()) + PromptEvent::Update => {} + PromptEvent::Validate => { + if let Some(CompleteAction::Selected { savepoint }) = + editor.last_completion.take() { - item.item = resolved; + doc.restore(view, &savepoint, false); } - }; - // if more text was entered, remove it - doc.restore(view, &savepoint, true); - // save an undo checkpoint before the completion - doc.append_changes_to_history(view); - let transaction = item_to_transaction( - doc, - view.id, - &item.item, - offset_encoding, - trigger_offset, - false, - replace_mode, - ); - doc.apply(&transaction, view.id); - - editor.last_completion = Some(CompleteAction::Applied { - trigger_offset, - changes: completion_changes(&transaction, trigger_offset), - }); - - // TODO: add additional _edits to completion_changes? - if let Some(additional_edits) = item.item.additional_text_edits { - if !additional_edits.is_empty() { - let transaction = util::generate_transaction_from_edits( - doc.text(), - additional_edits, - offset_encoding, // TODO: should probably transcode in Client - ); - doc.apply(&transaction, view.id); + // always present here + let mut item = item.unwrap().clone(); + + let language_server = language_server!(item); + let offset_encoding = language_server.offset_encoding(); + + let language_server = editor + .language_servers + .get_by_id(item.language_server_id) + .unwrap(); + + // resolve item if not yet resolved + if !item.resolved { + if let Some(resolved) = + Self::resolve_completion_item(language_server, item.item.clone()) + { + item.item = resolved; + } + }; + // if more text was entered, remove it + doc.restore(view, &savepoint, true); + // save an undo checkpoint before the completion + doc.append_changes_to_history(view); + let transaction = item_to_transaction( + doc, + view.id, + &item.item, + offset_encoding, + trigger_offset, + false, + replace_mode, + ); + doc.apply(&transaction, view.id); + + editor.last_completion = Some(CompleteAction::Applied { + trigger_offset, + changes: completion_changes(&transaction, trigger_offset), + }); + + // TODO: add additional _edits to completion_changes? + if let Some(additional_edits) = item.item.additional_text_edits { + if !additional_edits.is_empty() { + let transaction = util::generate_transaction_from_edits( + doc.text(), + additional_edits, + offset_encoding, // TODO: should probably transcode in Client + ); + doc.apply(&transaction, view.id); + } } + // 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.handlers.completions, editor, true); } - // 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.handlers.completions, editor, true); - } - }; + }; - // In case the popup was deleted because of an intersection w/ the auto-complete menu. - if event != PromptEvent::Update { - editor - .handlers - .trigger_signature_help(SignatureHelpInvoked::Automatic, editor); - } - }); + // In case the popup was deleted because of an intersection w/ the auto-complete menu. + if event != PromptEvent::Update { + editor + .handlers + .trigger_signature_help(SignatureHelpInvoked::Automatic, editor); + } + }, + ); let margin = if editor.menu_border() { Margin::vertical(1) @@ -368,6 +439,7 @@ impl Completion { let mut completion = Self { popup, trigger_offset, + item_offset, // TODO: expand nucleo api to allow moving straight to a Utf32String here // and avoid allocation during matching filter: String::from(fragment), @@ -427,6 +499,10 @@ impl Completion { } pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect { + // 1st step: calculate real area. + let mut position = editor.cursor().0.unwrap_or_default(); + position.col = position.col.saturating_sub(self.item_offset); + self.popup.set_position(position); self.popup.area(viewport, editor) } } diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index b38b8b6e3fbc..0dda697704e7 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -61,6 +61,10 @@ impl Popup { self.position } + pub fn set_position(&mut self, position: Position) { + self.position = Some(position); + } + /// Set the popup to prefer to render above or below the anchor position. /// /// This preference will be ignored if the viewport doesn't have enough space in the diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index dd360a78ebdd..0bc00568e07d 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -229,6 +229,97 @@ where Ok(chars) } +pub fn completion_item_kind_name(kind: lsp::CompletionItemKind) -> &'static str { + match kind { + lsp::CompletionItemKind::TEXT => "text", + lsp::CompletionItemKind::METHOD => "method", + lsp::CompletionItemKind::FUNCTION => "function", + lsp::CompletionItemKind::CONSTRUCTOR => "constructor", + lsp::CompletionItemKind::FIELD => "field", + lsp::CompletionItemKind::VARIABLE => "variable", + lsp::CompletionItemKind::CLASS => "class", + lsp::CompletionItemKind::INTERFACE => "interface", + lsp::CompletionItemKind::MODULE => "module", + lsp::CompletionItemKind::PROPERTY => "property", + lsp::CompletionItemKind::UNIT => "unit", + lsp::CompletionItemKind::VALUE => "value", + lsp::CompletionItemKind::ENUM => "enum", + lsp::CompletionItemKind::KEYWORD => "keyword", + lsp::CompletionItemKind::SNIPPET => "snippet", + lsp::CompletionItemKind::COLOR => "color", + lsp::CompletionItemKind::FILE => "file", + lsp::CompletionItemKind::REFERENCE => "reference", + lsp::CompletionItemKind::FOLDER => "folder", + lsp::CompletionItemKind::ENUM_MEMBER => "enum_member", + lsp::CompletionItemKind::CONSTANT => "constant", + lsp::CompletionItemKind::STRUCT => "struct", + lsp::CompletionItemKind::EVENT => "event", + lsp::CompletionItemKind::OPERATOR => "operator", + lsp::CompletionItemKind::TYPE_PARAMETER => "type_parameter", + _ => "invalid", // invalid, but should never get one. + } +} + +fn name_to_completion_item_kind(name: &str) -> Option { + match name { + "text" => Some(lsp::CompletionItemKind::TEXT), + "method" => Some(lsp::CompletionItemKind::METHOD), + "function" => Some(lsp::CompletionItemKind::FUNCTION), + "constructor" => Some(lsp::CompletionItemKind::CONSTRUCTOR), + "field" => Some(lsp::CompletionItemKind::FIELD), + "variable" => Some(lsp::CompletionItemKind::VARIABLE), + "class" => Some(lsp::CompletionItemKind::CLASS), + "interface" => Some(lsp::CompletionItemKind::INTERFACE), + "module" => Some(lsp::CompletionItemKind::MODULE), + "property" => Some(lsp::CompletionItemKind::PROPERTY), + "unit" => Some(lsp::CompletionItemKind::UNIT), + "value" => Some(lsp::CompletionItemKind::VALUE), + "enum" => Some(lsp::CompletionItemKind::ENUM), + "keyword" => Some(lsp::CompletionItemKind::KEYWORD), + "snippet" => Some(lsp::CompletionItemKind::SNIPPET), + "color" => Some(lsp::CompletionItemKind::COLOR), + "file" => Some(lsp::CompletionItemKind::FILE), + "reference" => Some(lsp::CompletionItemKind::REFERENCE), + "folder" => Some(lsp::CompletionItemKind::FOLDER), + "enum_member" => Some(lsp::CompletionItemKind::ENUM_MEMBER), + "constant" => Some(lsp::CompletionItemKind::CONSTANT), + "struct" => Some(lsp::CompletionItemKind::STRUCT), + "event" => Some(lsp::CompletionItemKind::EVENT), + "operator" => Some(lsp::CompletionItemKind::OPERATOR), + "type_parameter" => Some(lsp::CompletionItemKind::TYPE_PARAMETER), + _ => None, // should never get one. + } +} + +fn deserialize_completion_item_kinds<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let kind_names = HashMap::::deserialize(deserializer)?; + let mut res = vec![]; + + for (name, text) in kind_names.into_iter() { + let Some(kind) = name_to_completion_item_kind(&name) else { + return Err(::invalid_value(serde::de::Unexpected + ::Str(&name), &"No such item kind!")); + }; + + res.push((kind, text)); + } + + Ok(res) +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub enum CompletionItemColumn { + #[default] + Name, + Kind, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct Config { @@ -279,6 +370,10 @@ pub struct Config { /// Whether to insert the completion suggestion on hover. Defaults to true. pub preview_completion_insert: bool, pub completion_trigger_len: u8, + /// Custom text for completion item kinds. + #[serde(deserialize_with = "deserialize_completion_item_kinds")] + pub completion_item_kinds: Vec<(lsp::CompletionItemKind, String)>, + pub completion_item_columns: Vec, /// Whether to instruct the LSP to replace the entire word when applying a completion /// or to only insert new text pub completion_replace: bool, @@ -875,6 +970,8 @@ impl Default for Config { completion_timeout: Duration::from_millis(250), preview_completion_insert: true, completion_trigger_len: 2, + completion_item_kinds: vec![], + completion_item_columns: vec![CompletionItemColumn::Name, CompletionItemColumn::Kind], auto_info: true, file_picker: FilePickerConfig::default(), statusline: StatusLineConfig::default(),