1use std::sync::Arc;
2
3use async_trait::async_trait;
4use color_eyre::Result;
5use crossterm::event::{MouseEvent, MouseEventKind};
6use itertools::Itertools;
7use parking_lot::RwLock;
8use ratatui::{
9 Frame,
10 layout::{Constraint, Layout, Rect},
11 widgets::{Block, Borders, Paragraph, Wrap},
12};
13use tracing::instrument;
14
15use super::{
16 Component,
17 completion_edit::{EditCompletionComponent, EditCompletionComponentMode},
18};
19use crate::{
20 app::Action,
21 config::{Config, Theme},
22 errors::AppError,
23 format_msg,
24 model::{SOURCE_WORKSPACE, VariableCompletion},
25 process::ProcessOutput,
26 service::IntelliShellService,
27 utils::resolve_completion,
28 widgets::{CustomList, ErrorPopup, NewVersionBanner},
29};
30
31const GLOBAL_ROOT_CMD: &str = "[GLOBAL]";
32const EMPTY_STORAGE_MESSAGE: &str = "There are no stored variable completions!";
33
34#[derive(Clone)]
36pub struct CompletionListComponent {
37 theme: Theme,
39 inline: bool,
41 service: IntelliShellService,
43 layout: Layout,
45 state: Arc<RwLock<CompletionListComponentState<'static>>>,
47}
48struct CompletionListComponentState<'a> {
49 initial_root_cmd: Option<String>,
51 active_list: ActiveList,
53 root_cmds: CustomList<'a, String>,
55 completions: CustomList<'a, VariableCompletion>,
57 preview: Option<Result<String, String>>,
59 error: ErrorPopup<'a>,
61}
62
63#[derive(Copy, Clone, PartialEq, Eq)]
65enum ActiveList {
66 RootCmds,
67 Completions,
68}
69
70impl CompletionListComponent {
71 pub fn new(service: IntelliShellService, config: Config, inline: bool, root_cmd: Option<String>) -> Self {
73 let root_cmds = CustomList::new(config.theme.clone(), inline, Vec::new()).title(" Commands ");
74 let completions = CustomList::new(config.theme.clone(), inline, Vec::new()).title(" Completions ");
75
76 let error = ErrorPopup::empty(&config.theme);
77
78 let layout = if inline {
79 Layout::horizontal([Constraint::Fill(1), Constraint::Fill(3), Constraint::Fill(2)])
80 } else {
81 Layout::horizontal([Constraint::Fill(1), Constraint::Fill(3), Constraint::Fill(2)]).margin(1)
82 };
83
84 let mut state = CompletionListComponentState {
85 initial_root_cmd: root_cmd,
86 active_list: ActiveList::RootCmds,
87 root_cmds,
88 completions,
89 preview: None,
90 error,
91 };
92 state.update_active_list(ActiveList::RootCmds);
93
94 Self {
95 theme: config.theme,
96 inline,
97 service,
98 layout,
99 state: Arc::new(RwLock::new(state)),
100 }
101 }
102}
103impl<'a> CompletionListComponentState<'a> {
104 fn update_active_list(&mut self, active: ActiveList) {
106 self.active_list = active;
107
108 self.root_cmds.set_focus(active == ActiveList::RootCmds);
109 self.completions.set_focus(active == ActiveList::Completions);
110 }
111}
112
113#[async_trait]
114impl Component for CompletionListComponent {
115 fn name(&self) -> &'static str {
116 "CompletionListComponent"
117 }
118
119 fn min_inline_height(&self) -> u16 {
120 5
121 }
122
123 async fn init_and_peek(&mut self) -> Result<Action> {
124 self.refresh_lists(true).await
125 }
126
127 fn render(&mut self, frame: &mut Frame, area: Rect) {
128 let [root_cmds_area, completions_area, preview_area] = self.layout.areas(area);
130
131 let mut state = self.state.write();
132
133 frame.render_widget(&mut state.root_cmds, root_cmds_area);
135 frame.render_widget(&mut state.completions, completions_area);
136
137 if let Some(res) = &state.preview {
139 let is_err = res.is_err();
140 let (output, style) = match res {
141 Ok(o) => (o, self.theme.secondary),
142 Err(err) => (err, self.theme.error),
143 };
144 let mut preview_paragraph = Paragraph::new(output.as_str()).style(style).wrap(Wrap { trim: is_err });
145 if !self.inline {
146 preview_paragraph =
147 preview_paragraph.block(Block::default().borders(Borders::ALL).title(" Preview ").style(style));
148 }
149 frame.render_widget(preview_paragraph, preview_area);
150 }
151
152 if let Some(new_version) = self.service.poll_new_version() {
154 NewVersionBanner::new(&self.theme, new_version).render_in(frame, area);
155 }
156 state.error.render_in(frame, area);
157 }
158
159 fn tick(&mut self) -> Result<Action> {
160 let mut state = self.state.write();
161 state.error.tick();
162 Ok(Action::NoOp)
163 }
164
165 fn exit(&mut self) -> Result<Action> {
166 let mut state = self.state.write();
167 match &state.active_list {
168 ActiveList::RootCmds => Ok(Action::Quit(ProcessOutput::success())),
169 ActiveList::Completions => {
170 state.update_active_list(ActiveList::RootCmds);
171 Ok(Action::NoOp)
172 }
173 }
174 }
175
176 fn process_mouse_event(&mut self, mouse: MouseEvent) -> Result<Action> {
177 match mouse.kind {
178 MouseEventKind::ScrollDown => Ok(self.move_down()?),
179 MouseEventKind::ScrollUp => Ok(self.move_up()?),
180 _ => Ok(Action::NoOp),
181 }
182 }
183
184 fn move_up(&mut self) -> Result<Action> {
185 let mut state = self.state.write();
186 match &state.active_list {
187 ActiveList::RootCmds => state.root_cmds.select_prev(),
188 ActiveList::Completions => state.completions.select_prev(),
189 }
190 self.debounced_refresh_lists();
191 Ok(Action::NoOp)
192 }
193
194 fn move_down(&mut self) -> Result<Action> {
195 let mut state = self.state.write();
196 match &state.active_list {
197 ActiveList::RootCmds => state.root_cmds.select_next(),
198 ActiveList::Completions => state.completions.select_next(),
199 }
200 self.debounced_refresh_lists();
201
202 Ok(Action::NoOp)
203 }
204
205 fn move_left(&mut self, _word: bool) -> Result<Action> {
206 let mut state = self.state.write();
207 match &state.active_list {
208 ActiveList::RootCmds => (),
209 ActiveList::Completions => state.update_active_list(ActiveList::RootCmds),
210 }
211 Ok(Action::NoOp)
212 }
213
214 fn move_right(&mut self, _word: bool) -> Result<Action> {
215 let mut state = self.state.write();
216 match &state.active_list {
217 ActiveList::RootCmds => state.update_active_list(ActiveList::Completions),
218 ActiveList::Completions => (),
219 }
220 Ok(Action::NoOp)
221 }
222
223 fn move_prev(&mut self) -> Result<Action> {
224 self.move_up()
225 }
226
227 fn move_next(&mut self) -> Result<Action> {
228 self.move_down()
229 }
230
231 fn move_home(&mut self, absolute: bool) -> Result<Action> {
232 if absolute {
233 let mut state = self.state.write();
234 match &state.active_list {
235 ActiveList::RootCmds => state.root_cmds.select_first(),
236 ActiveList::Completions => state.completions.select_first(),
237 }
238 self.debounced_refresh_lists();
239 }
240 Ok(Action::NoOp)
241 }
242
243 fn move_end(&mut self, absolute: bool) -> Result<Action> {
244 if absolute {
245 let mut state = self.state.write();
246 match &state.active_list {
247 ActiveList::RootCmds => state.root_cmds.select_last(),
248 ActiveList::Completions => state.completions.select_last(),
249 }
250 self.debounced_refresh_lists();
251 }
252 Ok(Action::NoOp)
253 }
254
255 #[instrument(skip_all)]
256 async fn selection_delete(&mut self) -> Result<Action> {
257 let data = {
258 let mut state = self.state.write();
259 if state.active_list == ActiveList::Completions
260 && let Some(selected) = state.completions.selected()
261 {
262 if selected.source != SOURCE_WORKSPACE {
263 state
264 .completions
265 .delete_selected()
266 .map(|(_, c)| (c, state.completions.is_empty()))
267 } else {
268 state.error.set_temp_message("Workspace completions can't be deleted");
269 return Ok(Action::NoOp);
270 }
271 } else {
272 None
273 }
274 };
275 if let Some((completion, is_now_empty)) = data {
276 self.service
277 .delete_variable_completion(completion.id)
278 .await
279 .map_err(AppError::into_report)?;
280 if is_now_empty {
281 self.state.write().update_active_list(ActiveList::RootCmds);
282 }
283 return self.refresh_lists(false).await;
284 }
285
286 Ok(Action::NoOp)
287 }
288
289 #[instrument(skip_all)]
290 async fn selection_update(&mut self) -> Result<Action> {
291 let completion = {
292 let state = self.state.read();
293 if state.active_list == ActiveList::Completions {
294 state.completions.selected().cloned()
295 } else {
296 None
297 }
298 };
299 if let Some(completion) = completion {
300 if completion.source != SOURCE_WORKSPACE {
301 tracing::info!("Entering completion update for: {completion}");
302 Ok(Action::SwitchComponent(Box::new(EditCompletionComponent::new(
303 self.service.clone(),
304 self.theme.clone(),
305 self.inline,
306 completion,
307 EditCompletionComponentMode::Edit {
308 parent: Box::new(self.clone()),
309 },
310 ))))
311 } else {
312 self.state
313 .write()
314 .error
315 .set_temp_message("Workspace completions can't be updated");
316 Ok(Action::NoOp)
317 }
318 } else {
319 Ok(Action::NoOp)
320 }
321 }
322
323 #[instrument(skip_all)]
324 async fn selection_confirm(&mut self) -> Result<Action> {
325 self.move_right(false)
326 }
327
328 async fn selection_execute(&mut self) -> Result<Action> {
329 self.selection_confirm().await
330 }
331}
332
333impl CompletionListComponent {
334 fn debounced_refresh_lists(&self) {
336 let this = self.clone();
337 tokio::spawn(async move {
338 if let Err(err) = this.refresh_lists(false).await {
339 panic!("Error refreshing lists: {err:?}");
340 }
341 });
342 }
343
344 #[instrument(skip_all)]
346 async fn refresh_lists(&self, init: bool) -> Result<Action> {
347 let root_cmds = self
349 .service
350 .list_variable_completion_root_cmds()
351 .await
352 .map_err(AppError::into_report)?
353 .into_iter()
354 .map(|r| {
355 if r.trim().is_empty() {
356 GLOBAL_ROOT_CMD.to_string()
357 } else {
358 r
359 }
360 })
361 .collect::<Vec<_>>();
362 if root_cmds.is_empty() && init {
363 return Ok(Action::Quit(
364 ProcessOutput::success().stderr(format_msg!(self.theme, "{EMPTY_STORAGE_MESSAGE}")),
365 ));
366 } else if root_cmds.is_empty() {
367 return Ok(Action::Quit(ProcessOutput::success()));
368 }
369 let root_cmd = {
370 let mut state = self.state.write();
371 state.root_cmds.update_items(root_cmds, true);
372 if init && let Some(root_cmd) = state.initial_root_cmd.take() {
373 let mut irc = root_cmd.as_str();
374 if irc.is_empty() {
375 irc = GLOBAL_ROOT_CMD;
376 }
377 if state.root_cmds.select_matching(|rc| rc == irc) {
378 state.update_active_list(ActiveList::Completions);
379 }
380 }
381 let Some(root_cmd) = state.root_cmds.selected().cloned() else {
382 return Ok(Action::Quit(ProcessOutput::success()));
383 };
384 root_cmd
385 };
386
387 let root_cmd_filter = if root_cmd.is_empty() || root_cmd == GLOBAL_ROOT_CMD {
389 Some("")
390 } else {
391 Some(root_cmd.as_str())
392 };
393 let completions = self
394 .service
395 .list_variable_completions(root_cmd_filter)
396 .await
397 .map_err(AppError::into_report)?;
398 let completion = {
399 let mut state = self.state.write();
400 state.completions.update_items(completions, true);
401 let Some(completion) = state.completions.selected().cloned() else {
402 return Ok(Action::NoOp);
403 };
404 completion
405 };
406
407 self.state.write().preview = match resolve_completion(&completion, None).await {
409 Ok(suggestions) if suggestions.is_empty() => {
410 let msg = "... empty output ...";
411 Some(Ok(msg.to_string()))
412 }
413 Ok(suggestions) => Some(Ok(suggestions.iter().join("\n"))),
414 Err(err) => Some(Err(err)),
415 };
416
417 Ok(Action::NoOp)
418 }
419}