diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index cb0af6fc638a..4e111710d802 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -7,14 +7,14 @@ use crate::{ }; use helix_view::{ document::SavePoint, - editor::CompleteAction, + editor::{CompleteAction, CompletionItemKindStyle}, handlers::lsp::SignatureHelpInvoked, theme::{Modifier, Style}, ViewId, }; use tui::{buffer::Buffer as Surface, text::Span}; -use std::{borrow::Cow, sync::Arc}; +use std::{borrow::Cow, collections::HashMap, sync::Arc}; use helix_core::{self as core, chars, Change, Transaction}; use helix_view::{graphics::Rect, Document, Editor}; @@ -23,8 +23,45 @@ use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; use helix_lsp::{lsp, util, OffsetEncoding}; +pub struct CompletionData { + completion_item_kinds: Arc>, + default_style: Style, +} + +const fn completion_item_kind_name(kind: Option) -> Option<&'static str> { + match kind { + Some(lsp::CompletionItemKind::TEXT) => Some("text"), + Some(lsp::CompletionItemKind::METHOD) => Some("method"), + Some(lsp::CompletionItemKind::FUNCTION) => Some("function"), + Some(lsp::CompletionItemKind::CONSTRUCTOR) => Some("constructor"), + Some(lsp::CompletionItemKind::FIELD) => Some("field"), + Some(lsp::CompletionItemKind::VARIABLE) => Some("variable"), + Some(lsp::CompletionItemKind::CLASS) => Some("class"), + Some(lsp::CompletionItemKind::INTERFACE) => Some("interface"), + Some(lsp::CompletionItemKind::MODULE) => Some("module"), + Some(lsp::CompletionItemKind::PROPERTY) => Some("property"), + Some(lsp::CompletionItemKind::UNIT) => Some("unit"), + Some(lsp::CompletionItemKind::VALUE) => Some("value"), + Some(lsp::CompletionItemKind::ENUM) => Some("enum"), + Some(lsp::CompletionItemKind::KEYWORD) => Some("keyword"), + Some(lsp::CompletionItemKind::SNIPPET) => Some("snippet"), + Some(lsp::CompletionItemKind::COLOR) => Some("color"), + Some(lsp::CompletionItemKind::FILE) => Some("file"), + Some(lsp::CompletionItemKind::REFERENCE) => Some("reference"), + Some(lsp::CompletionItemKind::FOLDER) => Some("folder"), + Some(lsp::CompletionItemKind::ENUM_MEMBER) => Some("enum-member"), + Some(lsp::CompletionItemKind::CONSTANT) => Some("constant"), + Some(lsp::CompletionItemKind::STRUCT) => Some("struct"), + Some(lsp::CompletionItemKind::EVENT) => Some("event"), + Some(lsp::CompletionItemKind::OPERATOR) => Some("operator"), + Some(lsp::CompletionItemKind::TYPE_PARAMETER) => Some("type-parameter"), + _ => None, + } +} + impl menu::Item for CompletionItem { - type Data = (); + type Data = CompletionData; + fn sort_text(&self, data: &Self::Data) -> Cow { self.filter_text(data) } @@ -42,7 +79,7 @@ impl menu::Item for CompletionItem { } } - fn format(&self, _data: &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() @@ -58,53 +95,46 @@ impl menu::Item for CompletionItem { CompletionItem::Other(core::CompletionItem { label, .. }) => label, }; - let kind = match self { - CompletionItem::Lsp(LspCompletionItem { item, .. }) => match 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); - "" - } - None => "", + let label_cell = menu::Cell::from(Span::styled( + label, + if deprecated { + Style::default().add_modifier(Modifier::CROSSED_OUT) + } else { + Style::default() }, - CompletionItem::Other(core::CompletionItem { kind, .. }) => kind, - }; + )); - menu::Row::new([ - menu::Cell::from(Span::styled( - label, - if deprecated { - Style::default().add_modifier(Modifier::CROSSED_OUT) + let kind_cell = match self { + CompletionItem::Lsp(LspCompletionItem { item, .. }) => { + // If the user specified a custom kind text, use that. It will cause an allocation + // though it should not have much impact since its pretty short strings + if let Some(kind_style) = item + .kind + // NOTE: This table gets populated with default text as well. + .and_then(|kind| data.completion_item_kinds.get(&kind)) + { + let style = kind_style.style.unwrap_or(data.default_style); + if let Some(text) = kind_style.text.clone() { + menu::Cell::from(Span::styled(text, style)) + } else { + let text = completion_item_kind_name(item.kind).unwrap_or_else(|| { + log::error!("Got invalid LSP completion item kind: {:?}", item.kind); + "" + }); + menu::Cell::from(Span::styled(text, style)) + } } else { - Style::default() - }, - )), - menu::Cell::from(kind), - ]) + let text = completion_item_kind_name(item.kind).unwrap_or_else(|| { + log::error!("Got invalid LSP completion item kind: {:?}", item.kind); + "" + }); + menu::Cell::from(Span::styled(text, data.default_style)) + } + } + CompletionItem::Other(core::CompletionItem { kind, .. }) => menu::Cell::from(&**kind), + }; + + menu::Row::new([label_cell, kind_cell]) } } @@ -130,9 +160,13 @@ impl Completion { 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()); + let data = CompletionData { + completion_item_kinds: Arc::clone(&editor.completion_item_kind_styles), + default_style: editor.theme.get("ui.completion.kind"), + }; // Then create the menu - let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { + let menu = Menu::new(items, data, move |editor: &mut Editor, item, event| { fn lsp_item_to_transaction( doc: &Document, view_id: ViewId, diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index ffe3ebb3cbf5..61d09c49f19c 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -218,6 +218,10 @@ impl Menu { }) } + pub fn set_editor_data(&mut self, editor_data: T::Data) { + self.editor_data = editor_data; + } + pub fn is_empty(&self) -> bool { self.matches.is_empty() } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index c2cbd6bc0e64..b1d94dff595d 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1078,6 +1078,12 @@ pub struct Breakpoint { pub log_message: Option, } +#[derive(Debug, Clone, Default)] +pub struct CompletionItemKindStyle { + pub text: Option, + pub style: Option, +} + use futures_util::stream::{Flatten, Once}; pub struct Editor { @@ -1125,6 +1131,7 @@ pub struct Editor { pub config: Arc>, pub auto_pairs: Option, + pub completion_item_kind_styles: Arc>, pub idle_timer: Pin>, redraw_timer: Pin>, @@ -1230,6 +1237,9 @@ impl Editor { // HAXX: offset the render area height by 1 to account for prompt/commandline area.height -= 1; + let theme = theme_loader.default(); + let completion_item_kind_styles = compute_completion_item_kind_styles(&theme, &conf); + Self { mode: Mode::Normal, tree: Tree::new(area), @@ -1242,7 +1252,7 @@ impl Editor { selected_register: None, macro_recording: None, macro_replaying: Vec::new(), - theme: theme_loader.default(), + theme, language_servers, diagnostics: BTreeMap::new(), diff_providers: DiffProviderRegistry::default(), @@ -1265,6 +1275,7 @@ impl Editor { last_completion: None, config, auto_pairs, + completion_item_kind_styles: Arc::new(completion_item_kind_styles), exit_code: 0, config_events: unbounded_channel(), needs_redraw: false, @@ -1405,6 +1416,10 @@ impl Editor { } } + self.completion_item_kind_styles = Arc::new(compute_completion_item_kind_styles( + &self.theme, + &self.config(), + )); self._refresh(); } @@ -2274,6 +2289,48 @@ fn try_restore_indent(doc: &mut Document, view: &mut View) { } } +fn compute_completion_item_kind_styles( + theme: &Theme, + config: &DynGuard, +) -> HashMap { + let mut ret = HashMap::new(); + for (scope_name, kind) in [ + ("text", lsp::CompletionItemKind::TEXT), + ("method", lsp::CompletionItemKind::METHOD), + ("function", lsp::CompletionItemKind::FUNCTION), + ("constructor", lsp::CompletionItemKind::CONSTRUCTOR), + ("field", lsp::CompletionItemKind::FIELD), + ("variable", lsp::CompletionItemKind::VARIABLE), + ("class", lsp::CompletionItemKind::CLASS), + ("interface", lsp::CompletionItemKind::INTERFACE), + ("module", lsp::CompletionItemKind::MODULE), + ("property", lsp::CompletionItemKind::PROPERTY), + ("unit", lsp::CompletionItemKind::UNIT), + ("value", lsp::CompletionItemKind::VALUE), + ("enum", lsp::CompletionItemKind::ENUM), + ("keyword", lsp::CompletionItemKind::KEYWORD), + ("snippet", lsp::CompletionItemKind::SNIPPET), + ("color", lsp::CompletionItemKind::COLOR), + ("file", lsp::CompletionItemKind::FILE), + ("reference", lsp::CompletionItemKind::REFERENCE), + ("folder", lsp::CompletionItemKind::FOLDER), + ("enum-member", lsp::CompletionItemKind::ENUM_MEMBER), + ("constant", lsp::CompletionItemKind::CONSTANT), + ("struct", lsp::CompletionItemKind::STRUCT), + ("event", lsp::CompletionItemKind::EVENT), + ("operator", lsp::CompletionItemKind::OPERATOR), + ("type-parameter", lsp::CompletionItemKind::TYPE_PARAMETER), + ] { + let style = theme.try_get(&dbg!(format!("ui.completion.kind.{scope_name}"))); + let text = config.completion_item_kinds.get(&kind).cloned(); + if style.is_some() || text.is_some() { + ret.insert(kind, CompletionItemKindStyle { text, style }); + } + } + + ret +} + #[derive(Default)] pub struct CursorCache(Cell>>);