Skip to content

Completion item kinds customization support V2 #12151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
86 changes: 86 additions & 0 deletions book/src/editor.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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.

<details>
<summary>Here are the completion item kinds for the <b>LSP</b> completion menu:</summary>
- `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`
</details>

<details>
<summary>Here are the completion item kinds for the <b>path</b> completion menu:</summary>
- `file`
- `folder`
- `link`
</details>

<br>

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:


<details>

<summary>Example configuration with <a href="https://nerdfonts.com">Nerd Fonts</a> </summary>

```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 = "󰅲"

```

</details>

### `[editor.clipboard-provider]` Section

Expand Down
3 changes: 3 additions & 0 deletions book/src/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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
2 changes: 1 addition & 1 deletion helix-lsp-types/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand Down
189 changes: 124 additions & 65 deletions helix-term/src/ui/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
};
use helix_view::{
document::SavePoint,
editor::CompleteAction,
editor::{CompleteAction, CompletionItemKindStyle},
handlers::lsp::SignatureHelpInvoked,
theme::{Color, Modifier, Style},
ViewId,
Expand All @@ -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,
Expand All @@ -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<HashMap<&'static str, CompletionItemKindStyle>>,
default_style: Style,
}

const fn completion_item_kind_name(kind: Option<lsp::CompletionItemKind>) -> 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<str> {
self.filter_text(data)
}
Expand All @@ -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()
Expand All @@ -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])
}
}

Expand All @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions helix-term/src/ui/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ impl<T: Item> Menu<T> {
})
}

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()
}
Expand Down
Loading