From a5bf443ef68382222a5bd64f3f10ffdcf37246a7 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 12 May 2026 09:29:16 +0300 Subject: [PATCH] Add text icons for completion items (#56396) Part of https://github.com/zed-industries/zed/issues/4943 Based on the last discussion, Zed does not want to have icons there, so technically closes the issue? Helix: Screenshot 2026-05-11 at 12 47 36 VSCode: Screenshot 2026-05-11 at 12 49 00 Screenshot 2026-05-11 at 12 49 04 RustRover: Screenshot 2026-05-11 at 12 49 34 Screenshot 2026-05-11 at 12 49 42 Zed: Screenshot 2026-05-11 at 12 54 17 Screenshot 2026-05-11 at 12 54 29 (tooltip on hover) image Disabled by default, use `"completion_menu_item_kind": "symbol"` to enable. Release Notes: - Added text icons for completion items (disabled by default, use `"completion_menu_item_kind": "symbol"` to enable) --- assets/settings/default.json | 8 ++ crates/editor/src/code_context_menus.rs | 157 ++++++++++++++++++++++-- crates/editor/src/editor.rs | 6 +- crates/editor/src/editor_settings.rs | 11 +- crates/settings/src/vscode_import.rs | 1 + crates/settings_content/src/editor.rs | 31 +++++ crates/settings_ui/src/page_data.rs | 17 ++- crates/settings_ui/src/settings_ui.rs | 1 + docs/src/visual-customization.md | 5 + 9 files changed, 222 insertions(+), 15 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 3e3db5a8100..ce9d4fde850 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -315,6 +315,14 @@ "completion_menu_scrollbar": "never", // Whether to align detail text in code completions context menus left or right. "completion_detail_alignment": "left", + // How to display the LSP item kind (function, method, variable, etc.) + // of each entry in the completions menu. + // + // 1. Do not display item kinds: + // "off" (default) + // 2. Display a single-letter badge, colorized based on the active syntax theme: + // "symbol" + "completion_menu_item_kind": "off", // How to display diffs in the editor. // // Default: split diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 904ebb1f810..e2cdf9c1203 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -8,7 +8,7 @@ use gpui::{ use itertools::Itertools; use language::CodeLabel; use language::{Buffer, LanguageName, LanguageRegistry}; -use lsp::CompletionItemTag; +use lsp::{CompletionItemKind, CompletionItemTag}; use markdown::{CopyButtonVisibility, Markdown, MarkdownElement}; use multi_buffer::Anchor; use ordered_float::OrderedFloat; @@ -29,8 +29,8 @@ use std::{ }; use task::ResolvedTask; use ui::{ - Color, IntoElement, ListItem, Pixels, Popover, ScrollAxes, Scrollbars, Styled, WithScrollbar, - prelude::*, + Color, IntoElement, ListItem, Pixels, Popover, ScrollAxes, Scrollbars, Styled, Tooltip, + WithScrollbar, prelude::*, }; use util::ResultExt; @@ -43,7 +43,7 @@ use crate::{ }; use crate::{CodeActionSource, EditorSettings}; use collections::{HashSet, VecDeque}; -use settings::{CompletionDetailAlignment, Settings, SnippetSortOrder}; +use settings::{CompletionDetailAlignment, CompletionMenuItemKind, Settings, SnippetSortOrder}; pub const MENU_GAP: Pixels = px(4.); pub const MENU_ASIDE_X_PADDING: Pixels = px(16.); @@ -782,8 +782,9 @@ impl CompletionsMenu { cx: &mut Context, ) -> AnyElement { let show_completion_documentation = self.show_completion_documentation; - let completion_detail_alignment = - EditorSettings::get_global(cx).completion_detail_alignment; + let editor_settings = EditorSettings::get_global(cx); + let completion_detail_alignment = editor_settings.completion_detail_alignment; + let completion_menu_item_kind = editor_settings.completion_menu_item_kind; let widest_completion_ix = if self.display_options.dynamic_width { let completions = self.completions.borrow(); let widest_completion_ix = self @@ -952,7 +953,7 @@ impl CompletionsMenu { _ => None, }; - let start_slot = completion + let icon_or_color_slot = completion .color() .map(|color| { div() @@ -971,6 +972,27 @@ impl CompletionsMenu { }) }); + let kind_letter_slot = match completion_menu_item_kind { + CompletionMenuItemKind::Off => None, + CompletionMenuItemKind::Symbol => Some(render_completion_kind_letter( + completion.kind(), + item_ix, + &style, + )), + }; + + let start_slot = match (kind_letter_slot, icon_or_color_slot) { + (Some(letter), Some(icon_or_color)) => Some( + h_flex() + .gap_0p5() + .child(letter) + .child(icon_or_color) + .into_any_element(), + ), + (Some(letter), None) => Some(letter), + (None, slot) => slot, + }; + div() .min_w(COMPLETION_MENU_MIN_WIDTH) .max_w(COMPLETION_MENU_MAX_WIDTH) @@ -1385,6 +1407,127 @@ impl CompletionsMenu { } } +fn render_completion_kind_letter( + kind: Option, + item_ix: usize, + style: &EditorStyle, +) -> AnyElement { + let badge = div() + .flex_none() + .w(IconSize::XSmall.rems()) + .text_center() + .text_size(rems_from_px(11.)) + .line_height(rems_from_px(14.)); + + let Some(kind) = kind else { + return badge.into_any_element(); + }; + let Some(letter) = completion_kind_letter(kind) else { + return badge.into_any_element(); + }; + + let color = completion_kind_highlight_name(kind) + .and_then(|name| { + style.syntax.style_for_name(name).or_else(|| { + let (parent, _) = name.rsplit_once('.')?; + style.syntax.style_for_name(parent) + }) + }) + .and_then(|hl| hl.color); + + badge + .id(("completion-kind", item_ix)) + .tooltip(Tooltip::text(completion_kind_name(kind))) + .child(letter) + .when_some(color, |element, color| element.text_color(color)) + .into_any_element() +} + +fn completion_kind_name(kind: CompletionItemKind) -> &'static str { + match kind { + CompletionItemKind::TEXT => "Text", + CompletionItemKind::METHOD => "Method", + CompletionItemKind::FUNCTION => "Function", + CompletionItemKind::CONSTRUCTOR => "Constructor", + CompletionItemKind::FIELD => "Field", + CompletionItemKind::VARIABLE => "Variable", + CompletionItemKind::CLASS => "Class", + CompletionItemKind::INTERFACE => "Interface", + CompletionItemKind::MODULE => "Module", + CompletionItemKind::PROPERTY => "Property", + CompletionItemKind::UNIT => "Unit", + CompletionItemKind::VALUE => "Value", + CompletionItemKind::ENUM => "Enum", + CompletionItemKind::KEYWORD => "Keyword", + CompletionItemKind::SNIPPET => "Snippet", + CompletionItemKind::COLOR => "Color", + CompletionItemKind::FILE => "File", + CompletionItemKind::REFERENCE => "Reference", + CompletionItemKind::FOLDER => "Folder", + CompletionItemKind::ENUM_MEMBER => "Enum Member", + CompletionItemKind::CONSTANT => "Constant", + CompletionItemKind::STRUCT => "Struct", + CompletionItemKind::EVENT => "Event", + CompletionItemKind::OPERATOR => "Operator", + CompletionItemKind::TYPE_PARAMETER => "Type Parameter", + _ => "Unknown", + } +} + +fn completion_kind_letter(kind: CompletionItemKind) -> Option<&'static str> { + Some(match kind { + CompletionItemKind::TEXT => "t", + CompletionItemKind::METHOD => "m", + CompletionItemKind::FUNCTION => "f", + CompletionItemKind::CONSTRUCTOR => "C", + CompletionItemKind::FIELD => "f", + CompletionItemKind::VARIABLE => "v", + CompletionItemKind::CLASS => "c", + CompletionItemKind::INTERFACE => "i", + CompletionItemKind::MODULE => "M", + CompletionItemKind::PROPERTY => "p", + CompletionItemKind::UNIT => "u", + CompletionItemKind::VALUE => "v", + CompletionItemKind::ENUM => "e", + CompletionItemKind::KEYWORD => "k", + CompletionItemKind::SNIPPET => "s", + CompletionItemKind::COLOR => "c", + CompletionItemKind::FILE => "F", + CompletionItemKind::REFERENCE => "r", + CompletionItemKind::FOLDER => "D", + CompletionItemKind::ENUM_MEMBER => "e", + CompletionItemKind::CONSTANT => "c", + CompletionItemKind::STRUCT => "S", + CompletionItemKind::EVENT => "E", + CompletionItemKind::OPERATOR => "o", + CompletionItemKind::TYPE_PARAMETER => "T", + _ => return None, + }) +} + +fn completion_kind_highlight_name(kind: CompletionItemKind) -> Option<&'static str> { + Some(match kind { + CompletionItemKind::CLASS => "type", + CompletionItemKind::CONSTANT => "constant", + CompletionItemKind::CONSTRUCTOR => "constructor", + CompletionItemKind::ENUM => "enum", + CompletionItemKind::ENUM_MEMBER => "variant", + CompletionItemKind::FIELD => "property", + CompletionItemKind::FUNCTION => "function", + CompletionItemKind::INTERFACE => "type", + CompletionItemKind::METHOD => "function.method", + CompletionItemKind::MODULE => "namespace", + CompletionItemKind::OPERATOR => "operator", + CompletionItemKind::PROPERTY => "property", + CompletionItemKind::STRUCT => "type", + CompletionItemKind::TYPE_PARAMETER => "type", + CompletionItemKind::VARIABLE => "variable", + CompletionItemKind::KEYWORD => "keyword", + CompletionItemKind::SNIPPET => "string", + _ => return None, + }) +} + fn exact_case_match_count(query: &str, string_match: &StringMatch) -> usize { let mut exact_matches = 0; let mut query_chars = query.chars(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4bb88de555e..b8ff734b43e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -80,9 +80,9 @@ pub use display_map::{ }; pub use edit_prediction_types::Direction; pub use editor_settings::{ - CompletionDetailAlignment, CurrentLineHighlight, DiffViewStyle, DocumentColorsRenderMode, - EditorSettings, EditorSettingsScrollbarProxy, ScrollBeyondLastLine, ScrollbarAxes, - SearchSettings, ShowMinimap, ui_scrollbar_settings_from_raw, + CompletionDetailAlignment, CompletionMenuItemKind, CurrentLineHighlight, DiffViewStyle, + DocumentColorsRenderMode, EditorSettings, EditorSettingsScrollbarProxy, ScrollBeyondLastLine, + ScrollbarAxes, SearchSettings, ShowMinimap, ui_scrollbar_settings_from_raw, }; pub use element::{ CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 0c39196062b..9953f657bc8 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -4,10 +4,11 @@ use gpui::App; use language::CursorShape; use project::project_settings::DiagnosticSeverity; pub use settings::{ - CodeLens, CompletionDetailAlignment, CurrentLineHighlight, DelayMs, DiffViewStyle, DisplayIn, - DocumentColorsRenderMode, DoubleClickInMultibuffer, GoToDefinitionFallback, - GoToDefinitionScrollStrategy, MinimapThumb, MinimapThumbBorder, MultiCursorModifier, - ScrollBeyondLastLine, ScrollbarDiagnostics, SeedQuerySetting, ShowMinimap, SnippetSortOrder, + CodeLens, CompletionDetailAlignment, CompletionMenuItemKind, CurrentLineHighlight, DelayMs, + DiffViewStyle, DisplayIn, DocumentColorsRenderMode, DoubleClickInMultibuffer, + GoToDefinitionFallback, GoToDefinitionScrollStrategy, MinimapThumb, MinimapThumbBorder, + MultiCursorModifier, ScrollBeyondLastLine, ScrollbarDiagnostics, SeedQuerySetting, ShowMinimap, + SnippetSortOrder, }; use settings::{RegisterSetting, RelativeLineNumbers, Settings}; use ui::scrollbars::ShowScrollbar; @@ -63,6 +64,7 @@ pub struct EditorSettings { pub minimum_contrast_for_highlights: f32, pub completion_menu_scrollbar: ShowScrollbar, pub completion_detail_alignment: CompletionDetailAlignment, + pub completion_menu_item_kind: CompletionMenuItemKind, pub diff_view_style: DiffViewStyle, pub minimum_split_diff_width: f32, } @@ -304,6 +306,7 @@ impl Settings for EditorSettings { .map(ui_scrollbar_settings_from_raw) .unwrap(), completion_detail_alignment: editor.completion_detail_alignment.unwrap(), + completion_menu_item_kind: editor.completion_menu_item_kind.unwrap(), diff_view_style: editor.diff_view_style.unwrap(), minimum_split_diff_width: editor.minimum_split_diff_width.unwrap(), } diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index db7f847b234..003923fdb90 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -315,6 +315,7 @@ impl VsCodeSettings { vertical_scroll_margin: self.read_f32("editor.cursorSurroundingLines"), completion_menu_scrollbar: None, completion_detail_alignment: None, + completion_menu_item_kind: None, diff_view_style: None, minimum_split_diff_width: None, } diff --git a/crates/settings_content/src/editor.rs b/crates/settings_content/src/editor.rs index 6a36d0c524a..5cd64b044a1 100644 --- a/crates/settings_content/src/editor.rs +++ b/crates/settings_content/src/editor.rs @@ -245,6 +245,16 @@ pub struct EditorSettingsContent { /// Default: left pub completion_detail_alignment: Option, + /// How to display the LSP item kind (function, method, variable, etc.) + /// of each entry in the completions menu. + /// + /// - "off": do not display item kinds (default). + /// - "symbol": display a single-letter badge, colorized based on the + /// active syntax theme. + /// + /// Default: off + pub completion_menu_item_kind: Option, + /// How to display diffs in the editor. /// /// Default: split @@ -300,6 +310,27 @@ pub enum CompletionDetailAlignment { Right, } +#[derive( + Debug, + Default, + Clone, + Copy, + Serialize, + Deserialize, + JsonSchema, + MergeFrom, + PartialEq, + Eq, + strum::VariantArray, + strum::VariantNames, +)] +#[serde(rename_all = "snake_case")] +pub enum CompletionMenuItemKind { + #[default] + Off, + Symbol, +} + impl RelativeLineNumbers { pub fn enabled(&self) -> bool { match self { diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index acb9f53a675..e3fc2fce14c 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -8500,7 +8500,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { ] } - fn completions_section() -> [SettingsPageItem; 7] { + fn completions_section() -> [SettingsPageItem; 8] { [ SettingsPageItem::SectionHeader("Completions"), SettingsPageItem::SettingItem(SettingItem { @@ -8612,6 +8612,21 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { metadata: None, files: USER, }), + SettingsPageItem::SettingItem(SettingItem { + title: "Completion Menu Item Kind", + description: "How to display the LSP item kind (function, method, variable, etc.) of each entry in the completions menu.", + field: Box::new(SettingField { + json_path: Some("editor.completion_menu_item_kind"), + pick: |settings_content| { + settings_content.editor.completion_menu_item_kind.as_ref() + }, + write: |settings_content, value, _| { + settings_content.editor.completion_menu_item_kind = value; + }, + }), + metadata: None, + files: USER, + }), ] } diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index a5c36671ea0..fcc3cbecc85 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -500,6 +500,7 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 7c5b9f000e6..6baa5cb2724 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -384,6 +384,11 @@ TBD: Centered layout related settings "lsp_document_colors": "inlay", // none, inlay, border, background // When to show the scrollbar in the completion menu. "completion_menu_scrollbar": "never", // auto, system, always, never + + // How to display the LSP item kind (function, method, variable, etc.) + // of each entry in the completions menu. "symbol" shows a syntax-colored + // single-letter badge. + "completion_menu_item_kind": "off", // off, symbol // Turn on colorization of brackets in editors (configurable per language) "colorize_brackets": true, ```