use std::sync::Arc;
use crate::{
ActiveTheme as _, Anchor, App, Context, Entity, InteractiveElement, IntoElement, ParentElement,
Pixels, Render, RenderOnce, StatefulInteractiveElement, Styled, StyledExt, Window, anchored,
deferred, div, h_flex, prelude::FluentBuilder as _, px,
};
use super::completions::{Completion, CompletionMenuOptions, CompletionState};
#[derive(Default)]
pub struct CompletionPopupState {
pub completions: Vec<Completion>,
pub selected_index: usize,
pub visible: bool,
pub position: crate::Point<Pixels>,
pub options: CompletionMenuOptions,
}
impl CompletionPopupState {
pub fn update_from_state(&mut self, state: &CompletionState) {
self.completions = state.completions.clone();
self.selected_index = state.selected_index;
self.visible = state.visible;
self.options = state.options;
}
pub fn clear(&mut self) {
self.completions.clear();
self.selected_index = 0;
self.visible = false;
}
pub fn select_next(&mut self) {
if self.completions.is_empty() {
return;
}
self.selected_index = (self.selected_index + 1) % self.completions.len();
}
pub fn select_previous(&mut self) {
if self.completions.is_empty() {
return;
}
if self.selected_index == 0 {
self.selected_index = self.completions.len() - 1;
} else {
self.selected_index -= 1;
}
}
pub fn selected(&self) -> Option<&Completion> {
self.completions.get(self.selected_index)
}
}
#[derive(IntoElement)]
pub struct CompletionPopup {
state: Entity<CompletionPopupState>,
on_change: Option<Arc<dyn Fn(usize, &mut Window, &mut App) + Send + Sync>>,
}
impl CompletionPopup {
pub fn new(state: Entity<CompletionPopupState>) -> Self {
Self {
state,
on_change: None,
}
}
pub fn on_change(
mut self,
handler: impl Fn(usize, &mut Window, &mut App) + Send + Sync + 'static,
) -> Self {
self.on_change = Some(Arc::new(handler));
self
}
}
impl RenderOnce for CompletionPopup {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let state = self.state.read(cx);
let on_change = self.on_change.clone();
if !state.visible || state.completions.is_empty() {
return div().into_any_element();
}
let max_width = state.options.max_width;
let max_items = state.options.max_visible_items.min(state.completions.len());
let anchor_position = state.position;
let items: Vec<_> = state
.completions
.iter()
.take(max_items)
.enumerate()
.map(|(i, completion)| {
let is_selected = i == state.selected_index;
let on_change = on_change.clone();
let kind_label = completion.kind.map(|k| {
let name = match k {
lsp_types::CompletionItemKind::FUNCTION => "fn",
lsp_types::CompletionItemKind::METHOD => "fn",
lsp_types::CompletionItemKind::STRUCT => "struct",
lsp_types::CompletionItemKind::ENUM => "enum",
lsp_types::CompletionItemKind::MODULE => "mod",
lsp_types::CompletionItemKind::VARIABLE => "let",
lsp_types::CompletionItemKind::FIELD => "field",
lsp_types::CompletionItemKind::KEYWORD => "kw",
lsp_types::CompletionItemKind::VALUE => "val",
lsp_types::CompletionItemKind::CLASS => "class",
lsp_types::CompletionItemKind::INTERFACE => "trait",
lsp_types::CompletionItemKind::TYPE_PARAMETER => "type",
_ => "?",
};
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(name)
});
let label = div()
.text_sm()
.text_color(cx.theme().popover_foreground)
.child(completion.label.clone());
let detail = if state.options.show_detail {
completion.detail.as_ref().map(|d| {
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.ml_auto()
.child(d.clone())
})
} else {
None
};
let row = h_flex()
.id(("completion-item", i))
.w_full()
.px_2()
.py_1()
.rounded(cx.theme().radius.min(px(6.)))
.gap_2()
.items_center()
.text_color(cx.theme().popover_foreground)
.when(is_selected, |this| {
this.bg(cx.theme().tokens.accent)
.text_color(cx.theme().accent_foreground)
})
.hover(|s| s.bg(cx.theme().tokens.accent.opacity(0.5)))
.children(kind_label)
.child(label)
.children(detail);
if let Some(on_change) = on_change {
row.cursor_pointer().on_click(move |_, window, cx| {
on_change(i, window, cx);
})
} else {
row
}
})
.collect();
deferred(
anchored()
.position(anchor_position)
.anchor(Anchor::TopLeft)
.snap_to_window_with_margin(px(8.))
.child(
div()
.w(max_width)
.max_h(px(300.))
.popover_style(cx)
.p_1()
.flex()
.flex_col()
.gap_y_0p5()
.occlude()
.children(items),
),
)
.with_priority(1)
.into_any_element()
}
}
pub struct CompletionIcon {
kind: lsp_types::CompletionItemKind,
}
impl CompletionIcon {
pub fn new(kind: lsp_types::CompletionItemKind) -> Self {
Self { kind }
}
}
impl Render for CompletionIcon {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let (symbol, color) = match self.kind {
lsp_types::CompletionItemKind::FUNCTION | lsp_types::CompletionItemKind::METHOD => {
("f", crate::blue_400())
}
lsp_types::CompletionItemKind::STRUCT => ("S", crate::green_400()),
lsp_types::CompletionItemKind::ENUM => ("E", crate::yellow_400()),
lsp_types::CompletionItemKind::MODULE => ("M", crate::purple_400()),
lsp_types::CompletionItemKind::VARIABLE | lsp_types::CompletionItemKind::FIELD => {
("v", crate::cyan_400())
}
lsp_types::CompletionItemKind::KEYWORD => ("k", crate::red_400()),
lsp_types::CompletionItemKind::VALUE => ("V", crate::orange_400()),
lsp_types::CompletionItemKind::CLASS | lsp_types::CompletionItemKind::INTERFACE => {
("T", crate::teal_400())
}
lsp_types::CompletionItemKind::TYPE_PARAMETER => ("T", crate::pink_400()),
_ => ("?", crate::gray_400()),
};
div()
.w_4()
.h_4()
.flex()
.items_center()
.justify_center()
.text_xs()
.font_bold()
.text_color(color)
.child(symbol)
}
}