1use std::{
2 cmp::Ordering,
3 collections::HashSet,
4 sync::{Arc, Mutex},
5 time::Duration,
6};
7
8use async_trait::async_trait;
9use color_eyre::Result;
10use crossterm::event::{MouseEvent, MouseEventKind};
11use futures_util::StreamExt;
12use parking_lot::RwLock;
13use ratatui::{
14 Frame,
15 layout::{Constraint, Layout, Rect},
16};
17use tokio_util::sync::CancellationToken;
18use tracing::instrument;
19
20use super::Component;
21use crate::{
22 app::Action,
23 config::Theme,
24 errors::AppError,
25 format_msg,
26 model::{CommandTemplate, VariableValue},
27 process::ProcessOutput,
28 service::IntelliShellService,
29 widgets::{
30 CommandTemplateWidget, CustomList, CustomTextArea, ErrorPopup, LoadingSpinner, NewVersionBanner,
31 items::VariableSuggestionItem,
32 },
33};
34
35const INITIAL_COMPLETION_WAIT: Duration = Duration::from_millis(250);
37
38#[derive(Clone)]
40pub struct VariableReplacementComponent {
41 theme: Theme,
43 inline: bool,
45 service: IntelliShellService,
47 layout: Layout,
49 execute_mode: bool,
51 replace_process: bool,
53 global_cancellation_token: CancellationToken,
55 cancellation_token: Arc<Mutex<Option<CancellationToken>>>,
57 state: Arc<RwLock<VariableReplacementComponentState<'static>>>,
59}
60struct VariableReplacementComponentState<'a> {
61 template: CommandTemplateWidget,
63 current_variable_ctx: (String, bool),
65 variable_suggestions: Vec<VariableSuggestionItem<'static>>,
67 suggestions: CustomList<'a, VariableSuggestionItem<'a>>,
69 error: ErrorPopup<'a>,
71 loading: Option<LoadingSpinner<'a>>,
73 current_variable_index: usize,
75 variable_values: Vec<Option<String>>,
77 confirmed_variables: Vec<usize>,
79 redo_stack: Vec<(usize, String)>,
81}
82
83impl VariableReplacementComponent {
84 pub fn new(
86 service: IntelliShellService,
87 theme: Theme,
88 inline: bool,
89 execute_mode: bool,
90 replace_process: bool,
91 command: CommandTemplate,
92 cancellation_token: CancellationToken,
93 ) -> Self {
94 let command = CommandTemplateWidget::new(&theme, inline, command);
95
96 let suggestions = CustomList::new(theme.clone(), inline, Vec::new());
97
98 let error = ErrorPopup::empty(&theme);
99
100 let layout = if inline {
101 Layout::vertical([Constraint::Length(1), Constraint::Min(3)])
102 } else {
103 Layout::vertical([Constraint::Length(3), Constraint::Min(5)]).margin(1)
104 };
105
106 let total_vars = command.count_variables();
108 let variable_values = vec![None; total_vars];
109
110 Self {
111 theme,
112 inline,
113 service,
114 layout,
115 execute_mode,
116 replace_process,
117 cancellation_token: Arc::new(Mutex::new(None)),
118 global_cancellation_token: cancellation_token,
119 state: Arc::new(RwLock::new(VariableReplacementComponentState {
120 template: command,
121 current_variable_ctx: (String::new(), true),
122 variable_suggestions: Vec::new(),
123 suggestions,
124 error,
125 loading: None,
126 current_variable_index: 0,
127 variable_values,
128 confirmed_variables: Vec::new(),
129 redo_stack: Vec::new(),
130 })),
131 }
132 }
133}
134
135#[async_trait]
136impl Component for VariableReplacementComponent {
137 fn name(&self) -> &'static str {
138 "VariableReplacementComponent"
139 }
140
141 fn min_inline_height(&self) -> u16 {
142 1 + 5
144 }
145
146 #[instrument(skip_all)]
147 async fn init_and_peek(&mut self) -> Result<Action> {
148 self.update_variable_context(true).await
149 }
150
151 #[instrument(skip_all)]
152 fn render(&mut self, frame: &mut Frame, area: Rect) {
153 let [cmd_area, suggestions_area] = self.layout.areas(area);
155
156 let mut state = self.state.write();
157
158 let values = state.variable_values.clone();
160 state.template.set_variable_values(&values);
161
162 state.template.current_variable_index = state.current_variable_index;
164
165 frame.render_widget(&state.template, cmd_area);
167
168 frame.render_widget(&mut state.suggestions, suggestions_area);
170
171 if let Some(new_version) = self.service.poll_new_version() {
173 NewVersionBanner::new(&self.theme, new_version).render_in(frame, area);
174 }
175 state.error.render_in(frame, area);
176 if let Some(loading) = &state.loading {
178 let loading_area = if self.inline {
179 Rect {
180 x: suggestions_area.x,
181 y: suggestions_area.y + suggestions_area.height.saturating_sub(1),
182 width: 1,
183 height: 1,
184 }
185 } else {
186 Rect {
187 x: suggestions_area.x.saturating_add(1),
188 y: suggestions_area.y + suggestions_area.height.saturating_sub(2),
189 width: 1,
190 height: 1,
191 }
192 };
193 loading.render_in(frame, loading_area);
194 }
195 }
196
197 fn tick(&mut self) -> Result<Action> {
198 let mut state = self.state.write();
199 state.error.tick();
200 if let Some(loading) = &mut state.loading {
201 loading.tick();
202 }
203
204 Ok(Action::NoOp)
205 }
206
207 fn exit(&mut self) -> Result<Action> {
208 {
209 let mut token_guard = self.cancellation_token.lock().unwrap();
210 if let Some(token) = token_guard.take() {
211 token.cancel();
212 }
213 }
214 let mut state = self.state.write();
215 if let Some(VariableSuggestionItem::Existing { editing, .. }) = state.suggestions.selected_mut()
216 && editing.is_some()
217 {
218 tracing::debug!("Closing variable value edit mode: user request");
219 *editing = None;
220 Ok(Action::NoOp)
221 } else {
222 tracing::info!("User requested to exit");
223 Ok(Action::Quit(
224 ProcessOutput::success().fileout(state.template.to_string()),
225 ))
226 }
227 }
228
229 fn process_mouse_event(&mut self, mouse: MouseEvent) -> Result<Action> {
230 match mouse.kind {
231 MouseEventKind::ScrollDown => Ok(self.move_next()?),
232 MouseEventKind::ScrollUp => Ok(self.move_prev()?),
233 _ => Ok(Action::NoOp),
234 }
235 }
236
237 fn move_up(&mut self) -> Result<Action> {
238 let mut state = self.state.write();
239 match state.suggestions.selected() {
240 Some(VariableSuggestionItem::Existing { editing: Some(_), .. }) => (),
241 _ => state.suggestions.select_prev(),
242 }
243 Ok(Action::NoOp)
244 }
245
246 fn move_down(&mut self) -> Result<Action> {
247 let mut state = self.state.write();
248 match state.suggestions.selected() {
249 Some(VariableSuggestionItem::Existing { editing: Some(_), .. }) => (),
250 _ => state.suggestions.select_next(),
251 }
252 Ok(Action::NoOp)
253 }
254
255 fn move_left(&mut self, word: bool) -> Result<Action> {
256 let mut state = self.state.write();
257 match state.suggestions.selected_mut() {
258 Some(VariableSuggestionItem::New { textarea, .. }) => {
259 textarea.move_cursor_left(word);
260 }
261 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
262 ta.move_cursor_left(word);
263 }
264 _ => (),
265 }
266 Ok(Action::NoOp)
267 }
268
269 fn move_right(&mut self, word: bool) -> Result<Action> {
270 let mut state = self.state.write();
271 match state.suggestions.selected_mut() {
272 Some(VariableSuggestionItem::New { textarea, .. }) => {
273 textarea.move_cursor_right(word);
274 }
275 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
276 ta.move_cursor_right(word);
277 }
278 _ => (),
279 }
280 Ok(Action::NoOp)
281 }
282
283 fn move_prev(&mut self) -> Result<Action> {
284 self.move_up()
285 }
286
287 fn move_next(&mut self) -> Result<Action> {
288 self.move_down()
289 }
290
291 fn move_prev_variable(&mut self) -> Result<Action> {
292 let mut state = self.state.write();
293
294 if matches!(
296 state.suggestions.selected(),
297 Some(VariableSuggestionItem::Existing { editing: Some(_), .. })
298 ) {
299 return Ok(Action::NoOp);
300 }
301
302 let total_vars = state.template.count_variables();
303 if total_vars == 0 {
304 return Ok(Action::NoOp);
305 }
306
307 if state.current_variable_index == 0 {
309 state.current_variable_index = total_vars - 1; } else {
311 state.current_variable_index -= 1;
312 }
313
314 drop(state);
315 self.debounced_update_variable_context();
316 Ok(Action::NoOp)
317 }
318
319 fn move_next_variable(&mut self) -> Result<Action> {
320 let mut state = self.state.write();
321
322 if matches!(
324 state.suggestions.selected(),
325 Some(VariableSuggestionItem::Existing { editing: Some(_), .. })
326 ) {
327 return Ok(Action::NoOp);
328 }
329
330 let total_vars = state.template.count_variables();
331 if total_vars == 0 {
332 return Ok(Action::NoOp);
333 }
334
335 state.current_variable_index += 1;
337 if state.current_variable_index >= total_vars {
338 state.current_variable_index = 0; }
340
341 drop(state);
342 self.debounced_update_variable_context();
343 Ok(Action::NoOp)
344 }
345
346 fn move_home(&mut self, absolute: bool) -> Result<Action> {
347 let mut state = self.state.write();
348 match state.suggestions.selected_mut() {
349 Some(VariableSuggestionItem::New { textarea, .. }) => {
350 textarea.move_home(absolute);
351 }
352 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
353 ta.move_home(absolute);
354 }
355 _ => state.suggestions.select_first(),
356 }
357 Ok(Action::NoOp)
358 }
359
360 fn move_end(&mut self, absolute: bool) -> Result<Action> {
361 let mut state = self.state.write();
362 match state.suggestions.selected_mut() {
363 Some(VariableSuggestionItem::New { textarea, .. }) => {
364 textarea.move_end(absolute);
365 }
366 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
367 ta.move_end(absolute);
368 }
369 _ => state.suggestions.select_last(),
370 }
371 Ok(Action::NoOp)
372 }
373
374 fn undo(&mut self) -> Result<Action> {
375 let mut state = self.state.write();
376 match state.suggestions.selected_mut() {
377 Some(VariableSuggestionItem::New { textarea, .. }) => {
378 textarea.undo();
379 let query = textarea.lines_as_string();
380 state.filter_suggestions(&query);
381 }
382 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
383 ta.undo();
384 }
385 _ => {
386 if let Some(last_index) = state.confirmed_variables.pop()
387 && let Some(value) = state.variable_values[last_index].take()
388 {
389 state.redo_stack.push((last_index, value));
390 state.current_variable_index = last_index;
391 self.debounced_update_variable_context();
392 }
393 }
394 }
395 Ok(Action::NoOp)
396 }
397
398 fn redo(&mut self) -> Result<Action> {
399 let mut state = self.state.write();
400 match state.suggestions.selected_mut() {
401 Some(VariableSuggestionItem::New { textarea, .. }) => {
402 textarea.redo();
403 let query = textarea.lines_as_string();
404 state.filter_suggestions(&query);
405 }
406 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
407 ta.redo();
408 }
409 _ => {
410 if let Some((index, value)) = state.redo_stack.pop() {
411 state.variable_values[index] = Some(value.clone());
412 state.confirmed_variables.push(index);
413 state.current_variable_index = index + 1;
414 self.debounced_update_variable_context();
415 }
416 }
417 }
418 Ok(Action::NoOp)
419 }
420
421 fn insert_text(&mut self, mut text: String) -> Result<Action> {
422 let mut state = self.state.write();
423 let current_index = state.current_variable_index;
424 if let Some(variable) = state.template.variable_at(current_index) {
425 text = variable.apply_functions_to(text);
426 }
427 match state.suggestions.selected_mut() {
428 Some(VariableSuggestionItem::New { textarea, .. }) => {
429 textarea.insert_str(text);
430 let query = textarea.lines_as_string();
431 state.filter_suggestions(&query);
432 }
433 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
434 ta.insert_str(text);
435 }
436 _ => (),
437 }
438 Ok(Action::NoOp)
439 }
440
441 fn insert_char(&mut self, c: char) -> Result<Action> {
442 let mut state = self.state.write();
443 let current_index = state.current_variable_index;
444 let maybe_replacement = state
445 .template
446 .variable_at(current_index)
447 .and_then(|variable| variable.check_functions_char(c));
448 let insert_content = |ta: &mut CustomTextArea<'_>| {
449 if let Some(r) = &maybe_replacement {
450 ta.insert_str(r);
451 } else {
452 ta.insert_char(c);
453 }
454 };
455 match state.suggestions.selected_mut() {
456 Some(VariableSuggestionItem::New { textarea, .. }) => {
457 insert_content(textarea);
458 let query = textarea.lines_as_string();
459 state.filter_suggestions(&query);
460 }
461 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
462 insert_content(ta);
463 }
464 _ => {
465 if let Some(VariableSuggestionItem::New { .. }) = state.suggestions.items().iter().next() {
466 state.suggestions.select_first();
467 if let Some(VariableSuggestionItem::New { textarea, .. }) = state.suggestions.selected_mut() {
468 insert_content(textarea);
469 let query = textarea.lines_as_string();
470 state.filter_suggestions(&query);
471 }
472 }
473 }
474 }
475 Ok(Action::NoOp)
476 }
477
478 fn delete(&mut self, backspace: bool, word: bool) -> Result<Action> {
479 let mut state = self.state.write();
480 match state.suggestions.selected_mut() {
481 Some(VariableSuggestionItem::New { textarea, .. }) => {
482 textarea.delete(backspace, word);
483 let query = textarea.lines_as_string();
484 state.filter_suggestions(&query);
485 }
486 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
487 ta.delete(backspace, word);
488 }
489 _ => (),
490 }
491 Ok(Action::NoOp)
492 }
493
494 #[instrument(skip_all)]
495 async fn selection_delete(&mut self) -> Result<Action> {
496 let deleted_id = {
497 let mut state = self.state.write();
498 match state.suggestions.selected_mut() {
499 Some(VariableSuggestionItem::New { .. }) => return Ok(Action::NoOp),
500 Some(VariableSuggestionItem::Existing {
501 value: VariableValue { id: Some(id), .. },
502 editing,
503 ..
504 }) => {
505 if editing.is_none() {
506 let id = *id;
507 state.suggestions.delete_selected();
508 id
509 } else {
510 return Ok(Action::NoOp);
511 }
512 }
513 _ => {
514 state.error.set_temp_message("This value is not yet stored");
515 return Ok(Action::NoOp);
516 }
517 }
518 };
519
520 self.service
521 .delete_variable_value(deleted_id)
522 .await
523 .map_err(AppError::into_report)?;
524
525 self.state
526 .write()
527 .variable_suggestions
528 .retain(|s| !matches!(s, VariableSuggestionItem::Existing { value, .. } if value.id == Some(deleted_id)));
529
530 Ok(Action::NoOp)
531 }
532
533 #[instrument(skip_all)]
534 async fn selection_update(&mut self) -> Result<Action> {
535 let mut state = self.state.write();
536
537 match state.suggestions.selected_mut() {
538 Some(VariableSuggestionItem::New { .. }) => (),
539 Some(i @ VariableSuggestionItem::Existing { .. }) => {
540 if let VariableSuggestionItem::Existing { value, editing, .. } = i {
541 if let Some(id) = value.id {
542 if editing.is_none() {
543 tracing::debug!("Entering edit mode for existing variable value: {id}");
544 i.enter_edit_mode();
545 }
546 } else {
547 state.error.set_temp_message("This value is not yet stored");
548 }
549 }
550 }
551 _ => state.error.set_temp_message("This value is not yet stored"),
552 }
553 Ok(Action::NoOp)
554 }
555
556 async fn selection_confirm(&mut self) -> Result<Action> {
557 {
558 let mut token_guard = self.cancellation_token.lock().unwrap();
559 if let Some(token) = token_guard.take() {
560 token.cancel();
561 }
562 }
563
564 enum NextAction {
566 NoOp,
567 ConfirmNewSecret(String),
568 ConfirmNewRegular(String),
569 ConfirmExistingEdition(VariableValue, String),
570 ConfirmExistingValue(VariableValue),
571 ConfirmLiteral(String, bool),
572 }
573
574 let next_action = {
575 let mut state = self.state.write();
576 let (_, is_secret) = state.current_variable_ctx;
577 match state.suggestions.selected_mut() {
578 None => NextAction::NoOp,
579 Some(VariableSuggestionItem::New {
580 textarea,
581 is_secret: true,
582 ..
583 }) => NextAction::ConfirmNewSecret(textarea.lines_as_string()),
584 Some(VariableSuggestionItem::New {
585 textarea,
586 is_secret: false,
587 ..
588 }) => NextAction::ConfirmNewRegular(textarea.lines_as_string()),
589 Some(VariableSuggestionItem::Existing { value, editing, .. }) => match editing.take() {
590 Some(ta) => NextAction::ConfirmExistingEdition(value.clone(), ta.lines_as_string()),
591 None => NextAction::ConfirmExistingValue(value.clone()),
592 },
593 Some(VariableSuggestionItem::Environment {
594 content,
595 is_value: false,
596 ..
597 }) => NextAction::ConfirmLiteral(content.clone(), false),
598 Some(VariableSuggestionItem::Environment {
599 content: value,
600 is_value: true,
601 ..
602 })
603 | Some(VariableSuggestionItem::Previous { value, .. })
604 | Some(VariableSuggestionItem::Completion { value, .. })
605 | Some(VariableSuggestionItem::Derived { value, .. }) => {
606 NextAction::ConfirmLiteral(value.clone(), !is_secret)
607 }
608 }
609 };
610
611 match next_action {
612 NextAction::NoOp => Ok(Action::NoOp),
613 NextAction::ConfirmNewSecret(value) => self.confirm_new_secret_value(value).await,
614 NextAction::ConfirmNewRegular(value) => self.confirm_new_regular_value(value).await,
615 NextAction::ConfirmExistingEdition(val, new_val) => self.confirm_existing_edition(val, new_val).await,
616 NextAction::ConfirmExistingValue(val) => self.confirm_existing_value(val, false).await,
617 NextAction::ConfirmLiteral(val, is_value) => self.confirm_literal_value(val, is_value).await,
618 }
619 }
620
621 async fn selection_execute(&mut self) -> Result<Action> {
622 self.selection_confirm().await
623 }
624}
625
626impl<'a> VariableReplacementComponentState<'a> {
627 fn filter_suggestions(&mut self, query: &str) {
629 tracing::debug!("Filtering suggestions for: {query}");
630 let mut filtered_suggestions = self.variable_suggestions.clone();
632 filtered_suggestions.retain(|s| match s {
633 VariableSuggestionItem::New { .. } => false,
634 VariableSuggestionItem::Existing { value, .. } => value_matches_filter_query(&value.value, query),
635 VariableSuggestionItem::Previous { value, .. }
636 | VariableSuggestionItem::Environment { content: value, .. }
637 | VariableSuggestionItem::Completion { value, .. }
638 | VariableSuggestionItem::Derived { value, .. } => value_matches_filter_query(value, query),
639 });
640
641 let new_row = self
643 .suggestions
644 .items()
645 .iter()
646 .find(|s| matches!(s, VariableSuggestionItem::New { .. }));
647 if let Some(new_row) = new_row.cloned() {
648 filtered_suggestions.insert(0, new_row);
649 }
650 let selected_id = self.suggestions.selected().map(|s| s.identifier());
652 self.suggestions.update_items(filtered_suggestions, false);
654 if let Some(selected_id) = selected_id {
656 self.suggestions.select_matching(|i| i.identifier() == selected_id);
657 }
658 }
659
660 fn merge_completions(&mut self, score_boost: f64, completion_suggestions: Vec<String>) {
662 let master_suggestions = &mut self.variable_suggestions;
664
665 let completion_set = completion_suggestions.iter().collect::<HashSet<_>>();
667 master_suggestions.retain_mut(|item| {
668 !matches!(
669 item,
670 VariableSuggestionItem::Derived { value, .. }
671 if completion_set.contains(value)
672 )
673 });
674
675 for suggestion in completion_suggestions {
677 let mut skip_completion = false;
679 for item in master_suggestions.iter_mut() {
680 match item {
681 VariableSuggestionItem::New { .. } => (),
683 VariableSuggestionItem::Derived { .. } => (),
685 VariableSuggestionItem::Previous { value, .. } => {
687 if value == &suggestion {
688 skip_completion = true;
689 break;
690 }
691 }
692 VariableSuggestionItem::Environment { content, is_value, .. } => {
694 if *is_value && content == &suggestion {
695 skip_completion = true;
696 break;
697 }
698 }
699 VariableSuggestionItem::Existing {
701 value,
702 score,
703 completion_merged,
704 ..
705 } => {
706 if value.value == suggestion {
707 if !*completion_merged {
708 *score += score_boost;
709 *completion_merged = true;
710 }
711 skip_completion = true;
712 break;
713 }
714 }
715 VariableSuggestionItem::Completion { value, score, .. } => {
717 if value == &suggestion {
718 *score = score.max(score_boost);
719 skip_completion = true;
720 break;
721 }
722 }
723 }
724 }
725 if skip_completion {
726 continue;
727 }
728
729 master_suggestions.push(VariableSuggestionItem::Completion {
731 sort_index: 3,
732 value: suggestion,
733 score: score_boost,
734 });
735 }
736
737 master_suggestions.sort_by(|a, b| {
739 a.sort_index()
740 .cmp(&b.sort_index())
741 .then_with(|| b.score().partial_cmp(&a.score()).unwrap_or(Ordering::Equal))
742 });
743
744 let query = self
746 .suggestions
747 .items()
748 .iter()
749 .find_map(|s| match s {
750 VariableSuggestionItem::New { textarea, .. } => Some(textarea.lines_as_string()),
751 _ => None,
752 })
753 .unwrap_or_default();
754 self.filter_suggestions(&query);
755 }
756}
757
758impl VariableReplacementComponent {
759 fn debounced_update_variable_context(&self) {
761 let this = self.clone();
762 tokio::spawn(async move {
763 if let Err(err) = this.update_variable_context(false).await {
764 tracing::error!("Error updating variable context: {err:?}");
765 }
766 });
767 }
768
769 fn move_to_next_variable_with_value(&self, value: String) {
772 let mut state = self.state.write();
773
774 let current_index = state.current_variable_index;
776 state.variable_values[current_index] = Some(value);
777 state.confirmed_variables.push(current_index);
778 state.redo_stack.clear();
779
780 state.current_variable_index += 1;
782
783 if state.current_variable_index >= state.variable_values.len() {
785 let has_pending = state.variable_values.iter().any(|v| v.is_none());
787 if has_pending {
788 state.current_variable_index = 0;
790 }
791 }
792 }
793
794 async fn update_variable_context(&self, peek: bool) -> Result<Action> {
796 {
798 let mut state = self.state.write();
799 let values = state.variable_values.clone();
800 state.template.set_variable_values(&values);
801 }
802
803 let cancellation_token = {
805 let mut token_guard = self.cancellation_token.lock().unwrap();
806 if let Some(token) = token_guard.take() {
807 token.cancel();
808 }
809 let new_token = CancellationToken::new();
810 *token_guard = Some(new_token.clone());
811 new_token
812 };
813
814 let (flat_root_cmd, previous_values, current_variable, context, current_stored_value) = {
816 let state = self.state.read();
817 let current_index = state.current_variable_index;
818
819 match state.template.variable_at(current_index).cloned() {
820 Some(variable) => (
821 state.template.flat_root_cmd.clone(),
822 state.template.previous_values_for(&variable.flat_name),
823 variable,
824 state.template.variable_context(),
825 state.variable_values.get(current_index).and_then(|v| v.clone()),
826 ),
827 None => {
828 if peek {
829 tracing::info!("There are no variables to replace");
830 } else {
831 tracing::info!("There are no more variables");
832 }
833 return self.quit_action(peek, state.template.to_string());
834 }
835 }
836 };
837
838 let (initial_suggestions, completion_stream) = self
840 .service
841 .search_variable_suggestions(&flat_root_cmd, ¤t_variable, previous_values, context)
842 .await
843 .map_err(AppError::into_report)?;
844
845 {
847 let mut state = self.state.write();
848 let suggestions = initial_suggestions
849 .into_iter()
850 .map(VariableSuggestionItem::from)
851 .collect::<Vec<_>>();
852 state.current_variable_ctx = (current_variable.flat_name.clone(), current_variable.secret);
853 state.variable_suggestions = suggestions.clone();
854 state.suggestions.update_items(suggestions, false);
855 }
856
857 let remaining_stream = if let Some(mut stream) = completion_stream {
859 let sleep = tokio::time::sleep(INITIAL_COMPLETION_WAIT);
860 tokio::pin!(sleep);
861
862 let mut has_more_items = true;
863
864 loop {
865 tokio::select! {
866 biased;
867 _ = &mut sleep => {
868 tracing::debug!(
869 "There are pending completions after initial {}ms wait, spawning a background task",
870 INITIAL_COMPLETION_WAIT.as_millis()
871 );
872 break;
873 }
874 item = stream.next() => {
875 if let Some((score_boost, result)) = item {
876 match result {
877 Err(err) => {
879 if let Some(line) = err.lines().next() {
880 self.state.write().error.set_temp_message(line.to_string());
881 }
882 }
883 Ok(completion_suggestions) => {
885 self.state.write().merge_completions(score_boost, completion_suggestions);
886 }
887 }
888 } else {
889 tracing::debug!(
891 "All completions were resolved on the initial {}ms window",
892 INITIAL_COMPLETION_WAIT.as_millis()
893 );
894 has_more_items = false;
895 break;
896 }
897 }
898 }
899 }
900 if has_more_items { Some(stream) } else { None }
901 } else {
902 None
903 };
904
905 {
907 let mut state = self.state.write();
908
909 let mut selected = false;
911 if let Some(ref stored_value) = current_stored_value
912 && let Some(idx) = state.suggestions.items().iter().position(|item| match item {
913 VariableSuggestionItem::Existing { value, .. } => &value.value == stored_value,
914 VariableSuggestionItem::Previous { value, .. } => value == stored_value,
915 VariableSuggestionItem::Environment { content, .. } => content == stored_value,
916 VariableSuggestionItem::Completion { value, .. } => value == stored_value,
917 VariableSuggestionItem::Derived { value, .. } => value == stored_value,
918 VariableSuggestionItem::New { .. } => false,
919 })
920 {
921 state.suggestions.select(idx);
922 selected = true;
923 }
924
925 if !selected
927 && let Some(idx) = state.suggestions.items().iter().position(|s| {
928 !matches!(
929 s,
930 VariableSuggestionItem::New { .. } | VariableSuggestionItem::Derived { .. }
931 )
932 })
933 {
934 state.suggestions.select(idx);
935 }
936 }
937
938 if let Some(mut stream) = remaining_stream {
940 let token = cancellation_token.clone();
941 let global_token = self.global_cancellation_token.clone();
942 let state_clone = self.state.clone();
943
944 self.state.write().loading = Some(LoadingSpinner::new(&self.theme));
946
947 tokio::spawn(async move {
949 while let Some((score_boost, result)) = tokio::select! {
950 biased;
951 _ = token.cancelled() => None,
952 _ = global_token.cancelled() => None,
953 item = stream.next() => item,
954 } {
955 match result {
956 Err(err) => {
958 if let Some(line) = err.lines().next() {
959 state_clone.write().error.set_temp_message(line.to_string());
960 }
961 }
962 Ok(completion_suggestions) => {
964 state_clone
965 .write()
966 .merge_completions(score_boost, completion_suggestions);
967 }
968 }
969 }
970 state_clone.write().loading = None;
971 });
972 }
973
974 Ok(Action::NoOp)
975 }
976
977 #[instrument(skip_all)]
978 async fn confirm_new_secret_value(&mut self, value: String) -> Result<Action> {
979 tracing::debug!("Secret variable value selected");
980 self.move_to_next_variable_with_value(value);
981 self.update_variable_context(false).await
982 }
983
984 #[instrument(skip_all)]
985 async fn confirm_new_regular_value(&mut self, value: String) -> Result<Action> {
986 if !value.trim().is_empty() {
987 let variable_value = {
988 let state = self.state.read();
989 let (flat_variable_name, _) = &state.current_variable_ctx;
990 state.template.new_variable_value_for(flat_variable_name, &value)
991 };
992 match self.service.insert_variable_value(variable_value).await {
993 Ok(v) => {
994 tracing::debug!("New variable value stored");
995 self.confirm_existing_value(v, true).await
996 }
997 Err(AppError::UserFacing(err)) => {
998 tracing::warn!("{err}");
999 self.state.write().error.set_temp_message(err.to_string());
1000 Ok(Action::NoOp)
1001 }
1002 Err(AppError::Unexpected(report)) => Err(report),
1003 }
1004 } else {
1005 tracing::debug!("New empty variable value selected");
1006 self.move_to_next_variable_with_value(value);
1007 self.update_variable_context(false).await
1008 }
1009 }
1010
1011 #[instrument(skip_all)]
1012 async fn confirm_existing_edition(&mut self, mut value: VariableValue, new_value: String) -> Result<Action> {
1013 value.value = new_value;
1014 match self.service.update_variable_value(value).await {
1015 Ok(v) => {
1016 let mut state = self.state.write();
1017 if let VariableSuggestionItem::Existing { value, .. } = state.suggestions.selected_mut().unwrap() {
1018 *value = v;
1019 };
1020 Ok(Action::NoOp)
1021 }
1022 Err(AppError::UserFacing(err)) => {
1023 tracing::warn!("{err}");
1024 self.state.write().error.set_temp_message(err.to_string());
1025 Ok(Action::NoOp)
1026 }
1027 Err(AppError::Unexpected(report)) => Err(report),
1028 }
1029 }
1030
1031 #[instrument(skip_all)]
1032 async fn confirm_existing_value(&mut self, mut value: VariableValue, new: bool) -> Result<Action> {
1033 let value_id = match value.id {
1034 Some(id) => id,
1035 None => {
1036 value = self
1037 .service
1038 .insert_variable_value(value)
1039 .await
1040 .map_err(AppError::into_report)?;
1041 value.id.expect("just inserted")
1042 }
1043 };
1044 let context = self.state.read().template.variable_context();
1045 match self
1046 .service
1047 .increment_variable_value_usage(value_id, context)
1048 .await
1049 .map_err(AppError::into_report)
1050 {
1051 Ok(_) => {
1052 if !new {
1053 tracing::debug!("Existing variable value selected");
1054 }
1055 self.move_to_next_variable_with_value(value.value);
1056 self.update_variable_context(false).await
1057 }
1058 Err(report) => Err(report),
1059 }
1060 }
1061
1062 #[instrument(skip_all)]
1063 async fn confirm_literal_value(&mut self, value: String, store: bool) -> Result<Action> {
1064 if store && !value.trim().is_empty() {
1065 let variable_value = {
1066 let state = self.state.read();
1067 let (flat_variable_name, _) = &state.current_variable_ctx;
1068 state.template.new_variable_value_for(flat_variable_name, &value)
1069 };
1070 match self.service.insert_variable_value(variable_value).await {
1071 Ok(v) => {
1072 tracing::debug!("Literal variable value selected and stored");
1073 self.confirm_existing_value(v, true).await
1074 }
1075 Err(AppError::UserFacing(err)) => {
1076 tracing::debug!("Literal variable value selected but couldn't be stored: {err}");
1077 self.move_to_next_variable_with_value(value);
1078 self.update_variable_context(false).await
1079 }
1080 Err(AppError::Unexpected(report)) => Err(report),
1081 }
1082 } else {
1083 tracing::debug!("Literal variable value selected");
1084 self.move_to_next_variable_with_value(value);
1085 self.update_variable_context(false).await
1086 }
1087 }
1088
1089 fn quit_action(&self, peek: bool, cmd: String) -> Result<Action> {
1091 if self.execute_mode {
1092 Ok(Action::Quit(ProcessOutput::execute(cmd)))
1093 } else if self.replace_process && peek {
1094 Ok(Action::Quit(
1095 ProcessOutput::success()
1096 .stderr(format_msg!(self.theme, "There are no variables to replace"))
1097 .stdout(&cmd)
1098 .fileout(cmd),
1099 ))
1100 } else {
1101 Ok(Action::Quit(ProcessOutput::success().stdout(&cmd).fileout(cmd)))
1102 }
1103 }
1104}
1105
1106fn value_matches_filter_query(value: &str, query: &str) -> bool {
1120 let mut search_offset = 0;
1122 query.split_whitespace().all(|word| {
1123 if let Some(relative_pos) = value[search_offset..].find(word) {
1125 search_offset += relative_pos + 1;
1127 true
1128 } else {
1129 false
1131 }
1132 })
1133}