use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::Widget,
};
use crate::operation::LspCompletionItem;
fn kind_icon(kind: Option<u8>) -> char {
match kind {
Some(1) => '⁰', Some(2) => 'ƒ', Some(3) => 'ƒ', Some(4) => 'ƒ', Some(5) => '□', Some(6) => '×', Some(7) => '◇', Some(8) => '◇', Some(9) => '⊕', Some(10) => '□', Some(12) => '□', Some(13) => '◈', Some(14) => '→', Some(15) => '✂', Some(16) => '"', Some(17) => '⊡', Some(18) => '↺', Some(22) => '◇', Some(25) => 'α', _ => '·',
}
}
pub(crate) struct CompletionWidget<'a> {
pub items: &'a [LspCompletionItem],
pub cursor: usize,
pub filter: &'a str,
pub anchor_x: u16,
pub anchor_y: u16,
pub terminal_area: Rect,
pub loading: bool,
}
impl Widget for CompletionWidget<'_> {
fn render(self, _area: Rect, buf: &mut Buffer) {
let max_w: u16 = 52;
let max_h: u16 = 10;
if self.loading {
let term_w = self.terminal_area.width;
let term_h = self.terminal_area.height;
let top = self.anchor_y + 1;
let left = self.anchor_x.min(term_w.saturating_sub(max_w));
let _width = max_w.min(term_w.saturating_sub(left));
if top < term_h {
let loading_text = "Loading...";
let loading_style = Style::default().fg(Color::DarkGray);
for (col_idx, ch) in loading_text.chars().enumerate() {
let x = left + col_idx as u16;
if x >= term_w {
break;
}
if let Some(cell) = buf.cell_mut((x, top)) {
cell.set_char(ch);
cell.set_style(loading_style);
}
}
return;
}
}
let filter_lower = self.filter.to_lowercase();
let visible: Vec<&LspCompletionItem> = self
.items
.iter()
.filter(|item| {
filter_lower.is_empty() || item.label.to_lowercase().contains(&filter_lower)
})
.collect();
if visible.is_empty() {
return;
}
let row_count = (visible.len() as u16).min(max_h);
let term_h = self.terminal_area.height;
let term_w = self.terminal_area.width;
let top = if self.anchor_y + 1 + row_count <= term_h {
self.anchor_y + 1
} else {
self.anchor_y.saturating_sub(row_count)
};
let left = self.anchor_x.min(term_w.saturating_sub(max_w));
let width = max_w.min(term_w.saturating_sub(left));
let scroll_offset = if self.cursor >= max_h as usize {
self.cursor + 1 - max_h as usize
} else {
0
};
for (row_idx, item) in visible
.iter()
.enumerate()
.skip(scroll_offset)
.take(row_count as usize)
{
let y = top + (row_idx - scroll_offset) as u16;
if y >= term_h {
break;
}
let is_selected = row_idx == self.cursor;
let bg = if is_selected {
Color::DarkGray
} else {
Color::Reset
};
let icon = kind_icon(item.kind);
let label_style = if is_selected {
Style::default()
.fg(Color::White)
.bg(bg)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::White).bg(bg)
};
let detail_style = Style::default().fg(Color::DarkGray).bg(bg);
let label_text = format!("{} {}", icon, item.label);
let detail_text = item
.detail
.as_deref()
.map(|d| format!(" {}", d))
.unwrap_or_default();
let row_width = width as usize;
use unicode_width::UnicodeWidthStr;
let label_w = UnicodeWidthStr::width(label_text.as_str());
let (label_part, detail_part) = if label_w >= row_width {
(truncate_to_width(&label_text, row_width), String::new())
} else {
let rem = row_width.saturating_sub(label_w);
(label_text.to_string(), truncate_to_width(&detail_text, rem))
};
let line = Line::from(vec![
Span::styled(label_part, label_style),
Span::styled(detail_part, detail_style),
]);
for (col_idx, (ch, style)) in iter_line_chars(&line).enumerate() {
let x = left + col_idx as u16;
if x >= term_w {
break;
}
if let Some(cell) = buf.cell_mut((x, y)) {
cell.set_char(ch);
cell.set_style(style);
}
}
}
}
}
fn iter_line_chars<'a>(line: &'a Line<'a>) -> impl Iterator<Item = (char, Style)> + 'a {
line.spans.iter().flat_map(|span| {
let style = span.style;
span.content.chars().map(move |c| (c, style))
})
}
fn truncate_to_width(s: &str, max_w: usize) -> String {
let mut acc = String::new();
let mut used = 0usize;
for ch in s.chars() {
let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1);
if used + w > max_w { break; }
acc.push(ch);
used += w;
}
acc
}