reovim_plugin_completion/
window.rs1use std::{collections::HashSet, sync::Arc};
8
9use reovim_core::{
10 completion::CompletionKind,
11 frame::FrameBuffer,
12 highlight::{Style, Theme},
13 plugin::{EditorContext, PluginStateRegistry, PluginWindow, Rect, WindowConfig},
14 sys::style::Color,
15};
16
17use crate::state::SharedCompletionManager;
18
19fn kind_abbrev(kind: CompletionKind) -> &'static str {
21 match kind {
22 CompletionKind::Text => "txt",
23 CompletionKind::Method => "fn",
24 CompletionKind::Function => "fn",
25 CompletionKind::Constructor => "new",
26 CompletionKind::Field => "fld",
27 CompletionKind::Variable => "var",
28 CompletionKind::Class => "cls",
29 CompletionKind::Interface => "int",
30 CompletionKind::Module => "mod",
31 CompletionKind::Property => "prp",
32 CompletionKind::Unit => "unt",
33 CompletionKind::Value => "val",
34 CompletionKind::Enum => "enm",
35 CompletionKind::Keyword => "kw",
36 CompletionKind::Snippet => "snp",
37 CompletionKind::Color => "clr",
38 CompletionKind::File => "fil",
39 CompletionKind::Reference => "ref",
40 CompletionKind::Folder => "dir",
41 CompletionKind::EnumMember => "enm",
42 CompletionKind::Constant => "cst",
43 CompletionKind::Struct => "st",
44 CompletionKind::Event => "evt",
45 CompletionKind::Operator => "op",
46 CompletionKind::TypeParameter => "typ",
47 }
48}
49
50fn kind_color(kind: CompletionKind) -> Color {
52 match kind {
53 CompletionKind::Function | CompletionKind::Method | CompletionKind::Constructor => {
54 Color::Magenta
55 }
56 CompletionKind::Variable | CompletionKind::Field | CompletionKind::Property => Color::Cyan,
57 CompletionKind::Module | CompletionKind::Class | CompletionKind::Interface => Color::Yellow,
58 CompletionKind::Struct | CompletionKind::Enum | CompletionKind::EnumMember => Color::Green,
59 CompletionKind::Keyword => Color::Blue,
60 CompletionKind::Snippet => Color::Red,
61 CompletionKind::Constant => Color::Cyan,
62 _ => Color::White,
63 }
64}
65
66pub struct CompletionPluginWindow {
70 manager: Arc<SharedCompletionManager>,
71}
72
73impl CompletionPluginWindow {
74 #[must_use]
76 pub fn new(manager: Arc<SharedCompletionManager>) -> Self {
77 Self { manager }
78 }
79}
80
81impl PluginWindow for CompletionPluginWindow {
82 #[allow(clippy::cast_possible_truncation)]
83 fn window_config(
84 &self,
85 _state: &Arc<PluginStateRegistry>,
86 ctx: &EditorContext,
87 ) -> Option<WindowConfig> {
88 let snapshot = self.manager.snapshot();
89
90 if !snapshot.active || snapshot.items.is_empty() {
91 return None;
92 }
93
94 let display_row = (snapshot.cursor_row as u16).saturating_sub(ctx.active_window_scroll_y);
97 let screen_y = ctx.active_window_anchor_y + display_row;
98 let screen_x = ctx
99 .active_window_anchor_x
100 .saturating_add(ctx.active_window_gutter_width)
101 .saturating_add(snapshot.word_start_col as u16);
102
103 let max_items = 10.min(snapshot.items.len());
104
105 let kind_width = 4_usize; let max_label_width = snapshot
109 .items
110 .iter()
111 .take(max_items)
112 .map(|i| i.label.len())
113 .max()
114 .unwrap_or(8);
115 let max_source_width = snapshot
116 .items
117 .iter()
118 .take(max_items)
119 .map(|i| i.source.len())
120 .max()
121 .unwrap_or(6);
122
123 let popup_width =
125 (1 + kind_width + max_label_width.min(30) + 1 + max_source_width.min(12) + 1).min(60)
126 as u16;
127
128 let (popup_x, popup_y, _, popup_height) =
130 ctx.dropdown(screen_x, screen_y, popup_width, max_items as u16);
131
132 Some(WindowConfig {
133 bounds: Rect::new(popup_x, popup_y, popup_width, popup_height),
134 z_order: 200, visible: true,
136 })
137 }
138
139 #[allow(clippy::cast_possible_truncation)]
140 fn render(
141 &self,
142 _state: &Arc<PluginStateRegistry>,
143 ctx: &EditorContext,
144 buffer: &mut FrameBuffer,
145 bounds: Rect,
146 theme: &Theme,
147 ) {
148 let snapshot = self.manager.snapshot();
149
150 if !snapshot.active || snapshot.items.is_empty() {
151 return;
152 }
153
154 let popup_x = bounds.x;
155 let popup_y = bounds.y;
156 let popup_width = bounds.width;
157 let max_items = bounds.height as usize;
158
159 let kind_col_width = 4_u16; let source_col_width = snapshot
162 .items
163 .iter()
164 .take(max_items)
165 .map(|i| i.source.len())
166 .max()
167 .unwrap_or(6)
168 .min(12) as u16;
169 let label_col_width = popup_width
170 .saturating_sub(1) .saturating_sub(kind_col_width)
172 .saturating_sub(1) .saturating_sub(source_col_width)
174 .saturating_sub(1); for (idx, item) in snapshot.items.iter().take(max_items).enumerate() {
178 let is_selected = idx == snapshot.selected_index;
179 let base_style = if is_selected {
180 &theme.popup.selected
181 } else {
182 &theme.popup.normal
183 };
184
185 let row = popup_y + idx as u16;
186 if row >= ctx.screen_height.saturating_sub(1) {
187 break;
188 }
189
190 let mut col = popup_x;
191
192 buffer.put_char(col, row, ' ', base_style);
194 col += 1;
195
196 let kind_abbr = kind_abbrev(item.kind);
198 let kind_fg = kind_color(item.kind);
199 let kind_style = if is_selected {
200 base_style.clone().fg(kind_fg)
201 } else {
202 Style::new()
203 .fg(kind_fg)
204 .bg(base_style.bg.unwrap_or(Color::Black))
205 };
206 for ch in kind_abbr.chars() {
207 buffer.put_char(col, row, ch, &kind_style);
208 col += 1;
209 }
210 for _ in kind_abbr.len()..kind_col_width as usize {
212 buffer.put_char(col, row, ' ', base_style);
213 col += 1;
214 }
215
216 let matched_set: HashSet<u32> = item.match_indices.iter().copied().collect();
218 let label_chars: Vec<char> = item.label.chars().collect();
219 for (i, &ch) in label_chars
220 .iter()
221 .take(label_col_width as usize)
222 .enumerate()
223 {
224 let char_style = if matched_set.contains(&(i as u32)) {
225 let match_fg_color = theme.popup.match_fg.fg.unwrap_or(Color::Yellow);
226 if is_selected {
227 base_style.clone().fg(match_fg_color)
228 } else {
229 let bg_color = base_style.bg.unwrap_or(Color::Black);
230 theme.popup.match_fg.clone().bg(bg_color)
231 }
232 } else {
233 base_style.clone()
234 };
235 buffer.put_char(col, row, ch, &char_style);
236 col += 1;
237 }
238 for _ in label_chars.len().min(label_col_width as usize)..label_col_width as usize {
240 buffer.put_char(col, row, ' ', base_style);
241 col += 1;
242 }
243
244 buffer.put_char(col, row, ' ', base_style);
246 col += 1;
247
248 let source_style = if is_selected {
250 base_style.clone().dim()
251 } else {
252 Style::new()
253 .fg(Color::DarkGrey)
254 .bg(base_style.bg.unwrap_or(Color::Black))
255 };
256 for ch in item.source.chars().take(source_col_width as usize) {
257 buffer.put_char(col, row, ch, &source_style);
258 col += 1;
259 }
260 for _ in item.source.len().min(source_col_width as usize)..source_col_width as usize {
262 buffer.put_char(col, row, ' ', base_style);
263 col += 1;
264 }
265
266 buffer.put_char(col, row, ' ', base_style);
268 }
269
270 self.render_ghost_text(&snapshot, ctx, buffer, bounds);
272 }
273}
274
275impl CompletionPluginWindow {
276 #[allow(clippy::cast_possible_truncation)]
278 fn render_ghost_text(
279 &self,
280 snapshot: &crate::cache::CompletionSnapshot,
281 ctx: &EditorContext,
282 buffer: &mut FrameBuffer,
283 _bounds: Rect,
284 ) {
285 let Some(item) = snapshot.selected_item() else {
287 return;
288 };
289
290 let ghost_text = if item.insert_text.len() > snapshot.prefix.len()
293 && item.insert_text.starts_with(&snapshot.prefix)
294 {
295 &item.insert_text[snapshot.prefix.len()..]
296 } else if item.label.len() > snapshot.prefix.len()
297 && item
298 .label
299 .to_lowercase()
300 .starts_with(&snapshot.prefix.to_lowercase())
301 {
302 &item.label[snapshot.prefix.len()..]
304 } else {
305 return;
306 };
307
308 if ghost_text.is_empty() {
309 return;
310 }
311
312 let ghost_style = Style::new().fg(Color::DarkGrey).dim();
314
315 let ghost_y = ctx.cursor_screen_y();
318 let ghost_x = ctx.cursor_screen_x();
319
320 let max_width = ctx.screen_width.saturating_sub(ghost_x);
322
323 for (i, ch) in ghost_text.chars().take(max_width as usize).enumerate() {
324 buffer.put_char(ghost_x + i as u16, ghost_y, ch, &ghost_style);
325 }
326 }
327}