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