Skip to content

Commit

Permalink
helix-term/view: Implement custom completion item kind text/style
Browse files Browse the repository at this point in the history
  • Loading branch information
nferhat committed Nov 29, 2024
1 parent b093290 commit 4303494
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 50 deletions.
132 changes: 83 additions & 49 deletions helix-term/src/ui/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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<HashMap<lsp::CompletionItemKind, 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 = ();
type Data = CompletionData;

fn sort_text(&self, data: &Self::Data) -> Cow<str> {
self.filter_text(data)
}
Expand All @@ -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()
Expand All @@ -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])
}
}

Expand All @@ -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,
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
59 changes: 58 additions & 1 deletion helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,12 @@ pub struct Breakpoint {
pub log_message: Option<String>,
}

#[derive(Debug, Clone, Default)]
pub struct CompletionItemKindStyle {
pub text: Option<String>,
pub style: Option<theme::Style>,
}

use futures_util::stream::{Flatten, Once};

pub struct Editor {
Expand Down Expand Up @@ -1125,6 +1131,7 @@ pub struct Editor {

pub config: Arc<dyn DynAccess<Config>>,
pub auto_pairs: Option<AutoPairs>,
pub completion_item_kind_styles: Arc<HashMap<lsp::CompletionItemKind, CompletionItemKindStyle>>,

pub idle_timer: Pin<Box<Sleep>>,
redraw_timer: Pin<Box<Sleep>>,
Expand Down Expand Up @@ -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),
Expand All @@ -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(),
Expand All @@ -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,
Expand Down Expand Up @@ -1405,6 +1416,10 @@ impl Editor {
}
}

self.completion_item_kind_styles = Arc::new(compute_completion_item_kind_styles(
&self.theme,
&self.config(),
));
self._refresh();
}

Expand Down Expand Up @@ -2274,6 +2289,48 @@ fn try_restore_indent(doc: &mut Document, view: &mut View) {
}
}

fn compute_completion_item_kind_styles(
theme: &Theme,
config: &DynGuard<Config>,
) -> HashMap<lsp::CompletionItemKind, CompletionItemKindStyle> {
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<Option<Option<Position>>>);

Expand Down

0 comments on commit 4303494

Please sign in to comment.