diff --git a/book/src/editor.md b/book/src/editor.md index feec09fd0e34..a8055b8d7230 100644 --- a/book/src/editor.md +++ b/book/src/editor.md @@ -1,6 +1,7 @@ ## Editor - [`[editor]` Section](#editor-section) +- [`[editor.completion-item-kinds]` Section](#editorcompletion-item-kinds-section) - [`[editor.statusline]` Section](#editorstatusline-section) - [`[editor.lsp]` Section](#editorlsp-section) - [`[editor.cursor-shape]` Section](#editorcursor-shape-section) @@ -56,6 +57,91 @@ | `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"` | `end-of-line-diagnostics` | Minimum severity of diagnostics to render at the end of the line. Set to `disable` to disable entirely. Refer to the setting about `inline-diagnostics` for more details | "disable" | `clipboard-provider` | Which API to use for clipboard interaction. One of `pasteboard` (MacOS), `wayland`, `x-clip`, `x-sel`, `win-32-yank`, `termux`, `tmux`, `windows`, `termcode`, `none`, or a custom command set. | Platform and environment specific. | +| `completion-item-kinds` | Text or symbol to display for the completion menu item kind. By default, Helix displays the kind in `kebab-case` | `{}` (empty) | + +### `[editor.completion-item-kinds]` Section + +These are used to override completion item kinds text. The same text that displays +what kind of completion is inside the completion menu. + +
+Here are the completion item kinds for the LSP completion menu: +- `text` +- `method` +- `function` +- `constructor` +- `field` +- `variable` +- `class` +- `interface` +- `module` +- `property` +- `unit` +- `value` +- `enum` +- `keyword` +- `snippet` +- `color` +- `file` +- `reference` +- `folder` +- `enum-member` +- `constant` +- `struct` +- `event` +- `operator` +- `type-parameter` +
+ +
+Here are the completion item kinds for the path completion menu: +- `file` +- `folder` +- `link` +
+ +
+ +By default, these values are used as text within the completion menu. +However, you can replace them with custom text or symbols as shown in the following example: + + +
+ +Example configuration with Nerd Fonts + +```toml +[editor.completion-item-kinds] +text = "" +method = "󰆧" +function = "󰊕" +constructor = "" +field = "󰇽" +variable = "󰂡" +class = "󰠱" +interface = "" +module = "" +property = "󰜢" +unit = "" +value = "󰎠" +enum = "" +keyword = "󰌋" +snippet = "" +color = "󰏘" +file = "󰈙" +reference = "" +folder = "󰉋" +link = "󱧮" +enum-member = "" +constant = "󰏿" +struct = "" +event = "" +operator = "󰆕" +type-parameter = "󰅲" + +``` + +
### `[editor.clipboard-provider]` Section diff --git a/book/src/themes.md b/book/src/themes.md index 1d0ba151773d..40c781f22590 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -317,6 +317,8 @@ These scopes are used for theming the editor interface: | `ui.menu` | Code and command completion menus | | `ui.menu.selected` | Selected autocomplete item | | `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar | +| `ui.completion.kind` | Default completion menu item kind color | +| `ui.completion.kind.{kind}` | Completion menu item kind for `kind`. These are the same as [completion item kinds][cik] | | `ui.selection` | For selections in the editing area | | `ui.selection.primary` | | | `ui.highlight` | Highlighted lines in the picker preview | @@ -338,3 +340,4 @@ These scopes are used for theming the editor interface: | `diagnostic.deprecated` | Diagnostics with deprecated tag (editing area) | [editor-section]: ./configuration.md#editor-section +[cik]: ./editor.md#editorcompletion-item-kinds-section diff --git a/helix-lsp-types/src/completion.rs b/helix-lsp-types/src/completion.rs index 7c006bdb62ad..c3151313864e 100644 --- a/helix-lsp-types/src/completion.rs +++ b/helix-lsp-types/src/completion.rs @@ -22,7 +22,7 @@ impl InsertTextFormat { } /// The kind of a completion entry. -#[derive(Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] +#[derive(Eq, PartialEq, Clone, Copy, Serialize, Deserialize, Hash)] #[serde(transparent)] pub struct CompletionItemKind(i32); lsp_enum! { diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index adacfad330f4..cdf5bae67daa 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -7,7 +7,7 @@ use crate::{ }; use helix_view::{ document::SavePoint, - editor::CompleteAction, + editor::{CompleteAction, CompletionItemKindStyle}, handlers::lsp::SignatureHelpInvoked, theme::{Color, Modifier, Style}, ViewId, @@ -17,7 +17,7 @@ use tui::{ text::{Span, Spans}, }; -use std::{borrow::Cow, sync::Arc}; +use std::{borrow::Cow, collections::HashMap, sync::Arc}; use helix_core::{ self as core, chars, @@ -30,8 +30,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 = Style; + type Data = CompletionData; + fn sort_text(&self, data: &Self::Data) -> Cow { self.filter_text(data) } @@ -49,7 +86,7 @@ impl menu::Item for CompletionItem { } } - 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() @@ -65,71 +102,91 @@ 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".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, - }; - Color::from_hex(text) - }) - .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(kind) => { - log::error!("Received unknown completion item kind: {:?}", kind); - "".into() - } - None => "".into(), - }, - CompletionItem::Other(core::CompletionItem { kind, .. }) => kind.as_ref().into(), - }; - - let label = Span::styled( + let label_cell = menu::Cell::from(Span::styled( label, if deprecated { Style::default().add_modifier(Modifier::CROSSED_OUT) - } else if kind.0[0].content == "folder" { - *dir_style } else { Style::default() }, - ); + )); + + let kind_cell = match self { + // Special case: Handle COLOR completion item kind by putting a preview of the color + // provided by the lsp server. For example colors given by the tailwind LSP server + // + // We just add a little square previewing the color. + CompletionItem::Lsp(LspCompletionItem { item, .. }) + if item.kind == Some(lsp::CompletionItemKind::COLOR) => + { + menu::Cell::from( + item.documentation + .as_ref() + .and_then(|docs| { + let text = match docs { + lsp::Documentation::String(text) => text, + lsp::Documentation::MarkupContent(lsp::MarkupContent { + value, + .. + }) => value, + }; + Color::from_hex(text) + }) + .map_or("color".into(), |color| { + Spans::from(vec![ + Span::raw("color "), + Span::styled("■", Style::default().fg(color)), + ]) + }), + ) + } + // Otherwise, handle the styling of the item kind as usual. + 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 + let kind_name = completion_item_kind_name(item.kind).unwrap_or_else(|| { + log::error!("Got invalid LSP completion item kind: {:?}", item.kind); + "" + }); + + if let Some(kind_style) = data.completion_item_kinds.get(kind_name) { + let style = kind_style.style.unwrap_or(data.default_style); + if let Some(text) = kind_style.text.as_ref() { + menu::Cell::from(Span::styled(text.clone(), style)) + } else { + menu::Cell::from(Span::styled(kind_name, style)) + } + } else { + menu::Cell::from(Span::styled(kind_name, data.default_style)) + } + } + CompletionItem::Other(core::CompletionItem { kind, .. }) => { + let kind = match kind.as_ref() { + // This is for path completion source. + // Got this from helix-term/src/handlers/completion/path.rs + // On unix these are all just **file** descriptors + #[cfg(unix)] + "block" | "socket" | "char_device" | "fifo" => "file", + + // NOTE: Whenever you add a new completion source, you may want to add overrides + // here if you wish to. + x => x, // otherwise keep untouched. + }; - menu::Row::new([menu::Cell::from(label), menu::Cell::from(kind)]) + if let Some(kind_style) = data.completion_item_kinds.get(kind) { + let style = kind_style.style.unwrap_or(data.default_style); + if let Some(text) = kind_style.text.as_ref() { + menu::Cell::from(Span::styled(text.clone(), style)) + } else { + menu::Cell::from(Span::styled(kind, style)) + } + } else { + menu::Cell::from(Span::styled(kind, data.default_style)) + } + } + }; + + menu::Row::new([label_cell, kind_cell]) } } @@ -155,11 +212,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 dir_style = editor.theme.get("ui.text.directory"); + 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, dir_style, move |editor: &mut Editor, item, event| { + let menu = Menu::new(items, data, move |editor: &mut Editor, item, event| { let (view, doc) = current!(editor); macro_rules! language_server { diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 612832ce1221..b153dbb38cdf 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 6c585a8a7f2c..f644461eee52 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -306,6 +306,9 @@ pub struct Config { /// Whether to instruct the LSP to replace the entire word when applying a completion /// or to only insert new text pub completion_replace: bool, + /// The completion item kind text to display in the completion menu. Leave kind empty to use + /// the kind's name. + pub completion_item_kinds: HashMap, /// `true` if helix should automatically add a line comment token if you're currently in a comment /// and press `enter`. pub continue_comments: bool, @@ -968,6 +971,7 @@ impl Default for Config { auto_save: AutoSave::default(), idle_timeout: Duration::from_millis(250), completion_timeout: Duration::from_millis(250), + completion_item_kinds: HashMap::new(), preview_completion_insert: true, completion_trigger_len: 2, auto_info: true, @@ -1027,6 +1031,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 { @@ -1074,6 +1084,7 @@ pub struct Editor { pub config: Arc>, pub auto_pairs: Option, + pub completion_item_kind_styles: Arc>, pub idle_timer: Pin>, redraw_timer: Pin>, @@ -1181,6 +1192,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), @@ -1193,7 +1207,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(), @@ -1217,6 +1231,7 @@ impl Editor { last_cwd: None, config, auto_pairs, + completion_item_kind_styles: Arc::new(completion_item_kind_styles), exit_code: 0, config_events: unbounded_channel(), needs_redraw: false, @@ -1263,6 +1278,10 @@ impl Editor { pub fn refresh_config(&mut self) { let config = self.config(); self.auto_pairs = (&config.auto_pairs).into(); + self.completion_item_kind_styles = Arc::new(compute_completion_item_kind_styles( + &self.theme, + &self.config(), + )); self.reset_idle_timer(); self._refresh(); } @@ -1357,6 +1376,10 @@ impl Editor { } } + self.completion_item_kind_styles = Arc::new(compute_completion_item_kind_styles( + &self.theme, + &self.config(), + )); self._refresh(); } @@ -2246,6 +2269,38 @@ fn try_restore_indent(doc: &mut Document, view: &mut View) { } } +// FIXME: This is an ugly hack since the completion menu does not know all the sources we support +// Don't know... +#[rustfmt::skip] +const ALL_KINDS: &[&str] = &[ + // All of these are LSP item kinds. + // It happens that file and folder are also here. + "text", "method", "function", "constructor", "field", "variable", + "class", "interface", "module", "property", "unit", "value", "enum", + "keyword", "snippet", "color", "file", "reference", "folder", + "enum-member", "constant", "struct", "event", "operator", + "type-parameter", + // The following are specific to path completion source + // We ignore the other linux-specific ones (block, socket, etc...) + "link" +]; + +fn compute_completion_item_kind_styles( + theme: &Theme, + config: &DynGuard, +) -> HashMap<&'static str, CompletionItemKindStyle> { + let mut ret = HashMap::new(); + for &name in ALL_KINDS { + let style = theme.try_get(&format!("ui.completion.kind.{name}")); + let text = config.completion_item_kinds.get(name).cloned(); + if style.is_some() || text.is_some() { + ret.insert(name, CompletionItemKindStyle { text, style }); + } + } + + ret +} + #[derive(Default)] pub struct CursorCache(Cell>>);