reovim_plugin_cmdline_completion/
window.rs1use std::sync::Arc;
6
7use reovim_core::{
8 frame::FrameBuffer,
9 highlight::{Style, Theme},
10 plugin::{EditorContext, PluginStateRegistry, PluginWindow, Rect, WindowConfig},
11 sys::style::Color,
12};
13
14use crate::cache::{CmdlineCompletionCache, CmdlineCompletionKind};
15
16fn kind_color(kind: CmdlineCompletionKind) -> Color {
18 match kind {
19 CmdlineCompletionKind::Command => Color::Magenta,
20 CmdlineCompletionKind::File => Color::Cyan,
21 CmdlineCompletionKind::Directory => Color::Yellow,
22 CmdlineCompletionKind::Option => Color::Green,
23 CmdlineCompletionKind::Subcommand => Color::Blue,
24 }
25}
26
27pub struct CmdlineCompletionWindow {
29 cache: Arc<CmdlineCompletionCache>,
30}
31
32impl CmdlineCompletionWindow {
33 #[must_use]
35 pub fn new(cache: Arc<CmdlineCompletionCache>) -> Self {
36 Self { cache }
37 }
38}
39
40impl PluginWindow for CmdlineCompletionWindow {
41 #[allow(clippy::cast_possible_truncation)]
42 fn window_config(
43 &self,
44 _state: &Arc<PluginStateRegistry>,
45 ctx: &EditorContext,
46 ) -> Option<WindowConfig> {
47 let snapshot = self.cache.load();
48
49 if !snapshot.active || snapshot.items.is_empty() {
50 return None;
51 }
52
53 let max_items = 10.min(snapshot.items.len());
55 let popup_height = max_items as u16;
56
57 let icon_width = 2_usize; let max_label = snapshot
61 .items
62 .iter()
63 .take(max_items)
64 .map(|i| i.label.len())
65 .max()
66 .unwrap_or(10);
67 let max_desc = snapshot
68 .items
69 .iter()
70 .take(max_items)
71 .map(|i| i.description.len())
72 .max()
73 .unwrap_or(20);
74
75 let popup_width =
77 (1 + icon_width + max_label.min(30) + 2 + max_desc.min(25) + 1).min(60) as u16;
78
79 let popup_y = ctx
82 .screen_height
83 .saturating_sub(1)
84 .saturating_sub(popup_height);
85
86 let popup_x =
88 (1 + snapshot.replace_start as u16).min(ctx.screen_width.saturating_sub(popup_width));
89
90 Some(WindowConfig {
91 bounds: Rect::new(popup_x, popup_y, popup_width, popup_height),
92 z_order: 200, visible: true,
94 })
95 }
96
97 #[allow(clippy::cast_possible_truncation)]
98 fn render(
99 &self,
100 _state: &Arc<PluginStateRegistry>,
101 ctx: &EditorContext,
102 buffer: &mut FrameBuffer,
103 bounds: Rect,
104 theme: &Theme,
105 ) {
106 let snapshot = self.cache.load();
107
108 if !snapshot.active || snapshot.items.is_empty() {
109 return;
110 }
111
112 let popup_x = bounds.x;
113 let popup_y = bounds.y;
114 let popup_width = bounds.width;
115 let max_items = bounds.height as usize;
116
117 let icon_col_width = 2_u16; let desc_col_width = snapshot
120 .items
121 .iter()
122 .take(max_items)
123 .map(|i| i.description.len())
124 .max()
125 .unwrap_or(20)
126 .min(25) as u16;
127 let label_col_width = popup_width
128 .saturating_sub(1) .saturating_sub(icon_col_width)
130 .saturating_sub(2) .saturating_sub(desc_col_width)
132 .saturating_sub(1); for (idx, item) in snapshot.items.iter().take(max_items).enumerate() {
136 let is_selected = idx == snapshot.selected_index;
137 let base_style = if is_selected {
138 &theme.popup.selected
139 } else {
140 &theme.popup.normal
141 };
142
143 let row = popup_y + idx as u16;
144 if row >= ctx.screen_height.saturating_sub(1) {
145 break;
146 }
147
148 let mut col = popup_x;
149
150 buffer.put_char(col, row, ' ', base_style);
152 col += 1;
153
154 let icon_fg = kind_color(item.kind);
156 let icon_style = if is_selected {
157 base_style.clone().fg(icon_fg)
158 } else {
159 Style::new()
160 .fg(icon_fg)
161 .bg(base_style.bg.unwrap_or(Color::Black))
162 };
163 for ch in item.icon.chars() {
164 buffer.put_char(col, row, ch, &icon_style);
165 col += 1;
166 }
167 while col < popup_x + 1 + icon_col_width {
169 buffer.put_char(col, row, ' ', base_style);
170 col += 1;
171 }
172
173 let label_chars: Vec<char> = item.label.chars().collect();
175 for ch in label_chars.iter().take(label_col_width as usize) {
176 buffer.put_char(col, row, *ch, base_style);
177 col += 1;
178 }
179 let label_drawn = label_chars.len().min(label_col_width as usize);
181 for _ in label_drawn..label_col_width as usize {
182 buffer.put_char(col, row, ' ', base_style);
183 col += 1;
184 }
185
186 buffer.put_char(col, row, ' ', base_style);
188 col += 1;
189 buffer.put_char(col, row, ' ', base_style);
190 col += 1;
191
192 let desc_style = if is_selected {
194 base_style.clone().dim()
195 } else {
196 Style::new()
197 .fg(Color::DarkGrey)
198 .bg(base_style.bg.unwrap_or(Color::Black))
199 };
200 for ch in item.description.chars().take(desc_col_width as usize) {
201 buffer.put_char(col, row, ch, &desc_style);
202 col += 1;
203 }
204 let desc_drawn = item.description.len().min(desc_col_width as usize);
206 for _ in desc_drawn..desc_col_width as usize {
207 buffer.put_char(col, row, ' ', base_style);
208 col += 1;
209 }
210
211 buffer.put_char(col, row, ' ', base_style);
213 }
214 }
215}