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
272 if let Some(item) = state.suggestions.selected_mut()
274 && let VariableSuggestionItem::New { textarea, .. }
275 | VariableSuggestionItem::Existing {
276 editing: Some(textarea),
277 ..
278 } = item
279 {
280 textarea.move_cursor_right(word);
281 return Ok(Action::NoOp);
282 }
283
284 let text_to_copy = match state.suggestions.selected() {
286 None | Some(VariableSuggestionItem::New { .. }) => None,
287 Some(VariableSuggestionItem::Existing { value, .. }) => Some(value.value.clone()),
288 Some(
289 VariableSuggestionItem::Previous { value, .. }
290 | VariableSuggestionItem::Environment { content: value, .. }
291 | VariableSuggestionItem::Completion { value, .. }
292 | VariableSuggestionItem::Derived { value, .. },
293 ) => Some(value.clone()),
294 };
295
296 if let Some(text) = text_to_copy
297 && !text.is_empty()
298 && let Some(VariableSuggestionItem::New { .. }) = state.suggestions.items().iter().next()
299 {
300 state.suggestions.select_first();
301 if let Some(VariableSuggestionItem::New { textarea, .. }) = state.suggestions.selected_mut() {
302 if !textarea.lines_as_string().is_empty() {
303 textarea.select_all();
304 textarea.cut();
305 }
306 textarea.insert_str(&text);
307 state.filter_suggestions(&text);
308 }
309 }
310
311 Ok(Action::NoOp)
312 }
313
314 fn move_prev(&mut self) -> Result<Action> {
315 self.move_up()
316 }
317
318 fn move_next(&mut self) -> Result<Action> {
319 self.move_down()
320 }
321
322 fn move_prev_variable(&mut self) -> Result<Action> {
323 let mut state = self.state.write();
324
325 if matches!(
327 state.suggestions.selected(),
328 Some(VariableSuggestionItem::Existing { editing: Some(_), .. })
329 ) {
330 return Ok(Action::NoOp);
331 }
332
333 let total_vars = state.template.count_variables();
334 if total_vars <= 1 {
335 return Ok(Action::NoOp);
336 }
337
338 if state.current_variable_index == 0 {
340 state.current_variable_index = total_vars - 1; } else {
342 state.current_variable_index -= 1;
343 }
344
345 drop(state);
346 self.debounced_update_variable_context();
347 Ok(Action::NoOp)
348 }
349
350 fn move_next_variable(&mut self) -> Result<Action> {
351 let mut state = self.state.write();
352
353 if matches!(
355 state.suggestions.selected(),
356 Some(VariableSuggestionItem::Existing { editing: Some(_), .. })
357 ) {
358 return Ok(Action::NoOp);
359 }
360
361 let total_vars = state.template.count_variables();
362 if total_vars <= 1 {
363 return Ok(Action::NoOp);
364 }
365
366 state.current_variable_index += 1;
368 if state.current_variable_index >= total_vars {
369 state.current_variable_index = 0; }
371
372 drop(state);
373 self.debounced_update_variable_context();
374 Ok(Action::NoOp)
375 }
376
377 fn move_home(&mut self, absolute: bool) -> Result<Action> {
378 let mut state = self.state.write();
379 match state.suggestions.selected_mut() {
380 Some(VariableSuggestionItem::New { textarea, .. }) => {
381 textarea.move_home(absolute);
382 }
383 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
384 ta.move_home(absolute);
385 }
386 _ => state.suggestions.select_first(),
387 }
388 Ok(Action::NoOp)
389 }
390
391 fn move_end(&mut self, absolute: bool) -> Result<Action> {
392 let mut state = self.state.write();
393 match state.suggestions.selected_mut() {
394 Some(VariableSuggestionItem::New { textarea, .. }) => {
395 textarea.move_end(absolute);
396 }
397 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
398 ta.move_end(absolute);
399 }
400 _ => state.suggestions.select_last(),
401 }
402 Ok(Action::NoOp)
403 }
404
405 fn undo(&mut self) -> Result<Action> {
406 let mut state = self.state.write();
407 match state.suggestions.selected_mut() {
408 Some(VariableSuggestionItem::New { textarea, .. }) => {
409 textarea.undo();
410 let query = textarea.lines_as_string();
411 state.filter_suggestions(&query);
412 }
413 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
414 ta.undo();
415 }
416 _ => {
417 if let Some(last_index) = state.confirmed_variables.pop()
418 && let Some(value) = state.variable_values[last_index].take()
419 {
420 state.redo_stack.push((last_index, value));
421 state.current_variable_index = last_index;
422 self.debounced_update_variable_context();
423 }
424 }
425 }
426 Ok(Action::NoOp)
427 }
428
429 fn redo(&mut self) -> Result<Action> {
430 let mut state = self.state.write();
431 match state.suggestions.selected_mut() {
432 Some(VariableSuggestionItem::New { textarea, .. }) => {
433 textarea.redo();
434 let query = textarea.lines_as_string();
435 state.filter_suggestions(&query);
436 }
437 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
438 ta.redo();
439 }
440 _ => {
441 if let Some((index, value)) = state.redo_stack.pop() {
442 state.variable_values[index] = Some(value.clone());
443 state.confirmed_variables.push(index);
444 state.current_variable_index = index + 1;
445 self.debounced_update_variable_context();
446 }
447 }
448 }
449 Ok(Action::NoOp)
450 }
451
452 fn insert_text(&mut self, mut text: String) -> Result<Action> {
453 let mut state = self.state.write();
454 let current_index = state.current_variable_index;
455 if let Some(variable) = state.template.variable_at(current_index) {
456 text = variable.apply_functions_to(text);
457 }
458 match state.suggestions.selected_mut() {
459 Some(VariableSuggestionItem::New { textarea, .. }) => {
460 textarea.insert_str(text);
461 let query = textarea.lines_as_string();
462 state.filter_suggestions(&query);
463 }
464 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
465 ta.insert_str(text);
466 }
467 _ => (),
468 }
469 Ok(Action::NoOp)
470 }
471
472 fn insert_char(&mut self, c: char) -> Result<Action> {
473 let mut state = self.state.write();
474 let current_index = state.current_variable_index;
475 let maybe_replacement = state
476 .template
477 .variable_at(current_index)
478 .and_then(|variable| variable.check_functions_char(c));
479 let insert_content = |ta: &mut CustomTextArea<'_>| {
480 if let Some(r) = &maybe_replacement {
481 ta.insert_str(r);
482 } else {
483 ta.insert_char(c);
484 }
485 };
486 match state.suggestions.selected_mut() {
487 Some(VariableSuggestionItem::New { textarea, .. }) => {
488 insert_content(textarea);
489 let query = textarea.lines_as_string();
490 state.filter_suggestions(&query);
491 }
492 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
493 insert_content(ta);
494 }
495 _ => {
496 if let Some(VariableSuggestionItem::New { .. }) = state.suggestions.items().iter().next() {
497 state.suggestions.select_first();
498 if let Some(VariableSuggestionItem::New { textarea, .. }) = state.suggestions.selected_mut() {
499 insert_content(textarea);
500 let query = textarea.lines_as_string();
501 state.filter_suggestions(&query);
502 }
503 }
504 }
505 }
506 Ok(Action::NoOp)
507 }
508
509 fn delete(&mut self, backspace: bool, word: bool) -> Result<Action> {
510 let mut state = self.state.write();
511 match state.suggestions.selected_mut() {
512 Some(VariableSuggestionItem::New { textarea, .. }) => {
513 textarea.delete(backspace, word);
514 let query = textarea.lines_as_string();
515 state.filter_suggestions(&query);
516 }
517 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
518 ta.delete(backspace, word);
519 }
520 _ => (),
521 }
522 Ok(Action::NoOp)
523 }
524
525 #[instrument(skip_all)]
526 async fn selection_delete(&mut self) -> Result<Action> {
527 let deleted_id = {
528 let mut state = self.state.write();
529 match state.suggestions.selected_mut() {
530 Some(VariableSuggestionItem::New { .. }) => return Ok(Action::NoOp),
531 Some(VariableSuggestionItem::Existing {
532 value: VariableValue { id: Some(id), .. },
533 editing,
534 ..
535 }) => {
536 if editing.is_none() {
537 let id = *id;
538 state.suggestions.delete_selected();
539 id
540 } else {
541 return Ok(Action::NoOp);
542 }
543 }
544 _ => {
545 state.error.set_temp_message("This value is not yet stored");
546 return Ok(Action::NoOp);
547 }
548 }
549 };
550
551 self.service
552 .delete_variable_value(deleted_id)
553 .await
554 .map_err(AppError::into_report)?;
555
556 self.state
557 .write()
558 .variable_suggestions
559 .retain(|s| !matches!(s, VariableSuggestionItem::Existing { value, .. } if value.id == Some(deleted_id)));
560
561 Ok(Action::NoOp)
562 }
563
564 #[instrument(skip_all)]
565 async fn selection_update(&mut self) -> Result<Action> {
566 let mut state = self.state.write();
567
568 match state.suggestions.selected_mut() {
569 Some(VariableSuggestionItem::New { .. }) => (),
570 Some(i @ VariableSuggestionItem::Existing { .. }) => {
571 if let VariableSuggestionItem::Existing { value, editing, .. } = i {
572 if let Some(id) = value.id {
573 if editing.is_none() {
574 tracing::debug!("Entering edit mode for existing variable value: {id}");
575 i.enter_edit_mode();
576 }
577 } else {
578 state.error.set_temp_message("This value is not yet stored");
579 }
580 }
581 }
582 _ => state.error.set_temp_message("This value is not yet stored"),
583 }
584 Ok(Action::NoOp)
585 }
586
587 async fn selection_confirm(&mut self) -> Result<Action> {
588 {
589 let mut token_guard = self.cancellation_token.lock().unwrap();
590 if let Some(token) = token_guard.take() {
591 token.cancel();
592 }
593 }
594
595 enum NextAction {
597 NoOp,
598 ConfirmNewSecret(String),
599 ConfirmNewRegular(String),
600 ConfirmExistingEdition(VariableValue, String),
601 ConfirmExistingValue(VariableValue),
602 ConfirmLiteral(String, bool),
603 }
604
605 let next_action = {
606 let mut state = self.state.write();
607 let (_, is_secret) = state.current_variable_ctx;
608 match state.suggestions.selected_mut() {
609 None => NextAction::NoOp,
610 Some(VariableSuggestionItem::New {
611 textarea,
612 is_secret: true,
613 ..
614 }) => NextAction::ConfirmNewSecret(textarea.lines_as_string()),
615 Some(VariableSuggestionItem::New {
616 textarea,
617 is_secret: false,
618 ..
619 }) => NextAction::ConfirmNewRegular(textarea.lines_as_string()),
620 Some(VariableSuggestionItem::Existing { value, editing, .. }) => match editing.take() {
621 Some(ta) => NextAction::ConfirmExistingEdition(value.clone(), ta.lines_as_string()),
622 None => NextAction::ConfirmExistingValue(value.clone()),
623 },
624 Some(VariableSuggestionItem::Environment {
625 content,
626 is_value: false,
627 ..
628 }) => NextAction::ConfirmLiteral(content.clone(), false),
629 Some(VariableSuggestionItem::Environment {
630 content: value,
631 is_value: true,
632 ..
633 })
634 | Some(VariableSuggestionItem::Previous { value, .. })
635 | Some(VariableSuggestionItem::Completion { value, .. })
636 | Some(VariableSuggestionItem::Derived { value, .. }) => {
637 NextAction::ConfirmLiteral(value.clone(), !is_secret)
638 }
639 }
640 };
641
642 match next_action {
643 NextAction::NoOp => Ok(Action::NoOp),
644 NextAction::ConfirmNewSecret(value) => self.confirm_new_secret_value(value).await,
645 NextAction::ConfirmNewRegular(value) => self.confirm_new_regular_value(value).await,
646 NextAction::ConfirmExistingEdition(val, new_val) => self.confirm_existing_edition(val, new_val).await,
647 NextAction::ConfirmExistingValue(val) => self.confirm_existing_value(val, false).await,
648 NextAction::ConfirmLiteral(val, is_value) => self.confirm_literal_value(val, is_value).await,
649 }
650 }
651
652 async fn selection_execute(&mut self) -> Result<Action> {
653 self.selection_confirm().await
654 }
655}
656
657impl<'a> VariableReplacementComponentState<'a> {
658 fn filter_suggestions(&mut self, query: &str) {
660 tracing::debug!("Filtering suggestions for: {query}");
661 let mut filtered_suggestions = self.variable_suggestions.clone();
663 filtered_suggestions.retain(|s| match s {
664 VariableSuggestionItem::New { .. } => false,
665 VariableSuggestionItem::Existing { value, .. } => value_matches_filter_query(&value.value, query),
666 VariableSuggestionItem::Previous { value, .. }
667 | VariableSuggestionItem::Environment { content: value, .. }
668 | VariableSuggestionItem::Completion { value, .. }
669 | VariableSuggestionItem::Derived { value, .. } => value_matches_filter_query(value, query),
670 });
671
672 let new_row = self
674 .suggestions
675 .items()
676 .iter()
677 .find(|s| matches!(s, VariableSuggestionItem::New { .. }));
678 if let Some(new_row) = new_row.cloned() {
679 filtered_suggestions.insert(0, new_row);
680 }
681 let selected_id = self.suggestions.selected().map(|s| s.identifier());
683 self.suggestions.update_items(filtered_suggestions, false);
685 if let Some(selected_id) = selected_id {
687 self.suggestions.select_matching(|i| i.identifier() == selected_id);
688 }
689 }
690
691 fn merge_completions(&mut self, score_boost: f64, completion_suggestions: Vec<String>) {
693 let master_suggestions = &mut self.variable_suggestions;
695
696 let completion_set = completion_suggestions.iter().collect::<HashSet<_>>();
698 master_suggestions.retain_mut(|item| {
699 !matches!(
700 item,
701 VariableSuggestionItem::Derived { value, .. }
702 if completion_set.contains(value)
703 )
704 });
705
706 for suggestion in completion_suggestions {
708 let mut skip_completion = false;
710 for item in master_suggestions.iter_mut() {
711 match item {
712 VariableSuggestionItem::New { .. } => (),
714 VariableSuggestionItem::Derived { .. } => (),
716 VariableSuggestionItem::Previous { value, .. } => {
718 if value == &suggestion {
719 skip_completion = true;
720 break;
721 }
722 }
723 VariableSuggestionItem::Environment { content, is_value, .. } => {
725 if *is_value && content == &suggestion {
726 skip_completion = true;
727 break;
728 }
729 }
730 VariableSuggestionItem::Existing {
732 value,
733 score,
734 completion_merged,
735 ..
736 } => {
737 if value.value == suggestion {
738 if !*completion_merged {
739 *score += score_boost;
740 *completion_merged = true;
741 }
742 skip_completion = true;
743 break;
744 }
745 }
746 VariableSuggestionItem::Completion { value, score, .. } => {
748 if value == &suggestion {
749 *score = score.max(score_boost);
750 skip_completion = true;
751 break;
752 }
753 }
754 }
755 }
756 if skip_completion {
757 continue;
758 }
759
760 master_suggestions.push(VariableSuggestionItem::Completion {
762 sort_index: 3,
763 value: suggestion,
764 score: score_boost,
765 });
766 }
767
768 master_suggestions.sort_by(|a, b| {
770 a.sort_index()
771 .cmp(&b.sort_index())
772 .then_with(|| b.score().partial_cmp(&a.score()).unwrap_or(Ordering::Equal))
773 });
774
775 let query = self
777 .suggestions
778 .items()
779 .iter()
780 .find_map(|s| match s {
781 VariableSuggestionItem::New { textarea, .. } => Some(textarea.lines_as_string()),
782 _ => None,
783 })
784 .unwrap_or_default();
785 self.filter_suggestions(&query);
786 }
787}
788
789impl VariableReplacementComponent {
790 fn debounced_update_variable_context(&self) {
792 let this = self.clone();
793 tokio::spawn(async move {
794 if let Err(err) = this.update_variable_context(false).await {
795 tracing::error!("Error updating variable context: {err:?}");
796 }
797 });
798 }
799
800 fn move_to_next_variable_with_value(&self, value: String) {
803 let mut state = self.state.write();
804
805 let current_index = state.current_variable_index;
807 state.variable_values[current_index] = Some(value);
808 state.confirmed_variables.push(current_index);
809 state.redo_stack.clear();
810
811 state.current_variable_index += 1;
813
814 if state.current_variable_index >= state.variable_values.len() {
816 let has_pending = state.variable_values.iter().any(|v| v.is_none());
818 if has_pending {
819 state.current_variable_index = 0;
821 }
822 }
823 }
824
825 async fn update_variable_context(&self, peek: bool) -> Result<Action> {
827 {
829 let mut state = self.state.write();
830 let values = state.variable_values.clone();
831 state.template.set_variable_values(&values);
832 }
833
834 let cancellation_token = {
836 let mut token_guard = self.cancellation_token.lock().unwrap();
837 if let Some(token) = token_guard.take() {
838 token.cancel();
839 }
840 let new_token = CancellationToken::new();
841 *token_guard = Some(new_token.clone());
842 new_token
843 };
844
845 let (flat_root_cmd, previous_values, current_variable, context, current_stored_value) = {
847 let state = self.state.read();
848 let current_index = state.current_variable_index;
849
850 match state.template.variable_at(current_index).cloned() {
851 Some(variable) => (
852 state.template.flat_root_cmd.clone(),
853 state.template.previous_values_for(&variable.flat_name),
854 variable,
855 state.template.variable_context(),
856 state.variable_values.get(current_index).and_then(|v| v.clone()),
857 ),
858 None => {
859 if peek {
860 tracing::info!("There are no variables to replace");
861 } else {
862 tracing::info!("There are no more variables");
863 }
864 return self.quit_action(peek, state.template.to_string());
865 }
866 }
867 };
868
869 let (initial_suggestions, completion_stream) = self
871 .service
872 .search_variable_suggestions(&flat_root_cmd, ¤t_variable, previous_values, context)
873 .await
874 .map_err(AppError::into_report)?;
875
876 {
878 let mut state = self.state.write();
879 let suggestions = initial_suggestions
880 .into_iter()
881 .map(VariableSuggestionItem::from)
882 .collect::<Vec<_>>();
883 state.current_variable_ctx = (current_variable.flat_name.clone(), current_variable.secret);
884 state.variable_suggestions = suggestions.clone();
885 state.suggestions.update_items(suggestions, false);
886 }
887
888 let remaining_stream = if let Some(mut stream) = completion_stream {
890 let sleep = tokio::time::sleep(INITIAL_COMPLETION_WAIT);
891 tokio::pin!(sleep);
892
893 let mut has_more_items = true;
894
895 loop {
896 tokio::select! {
897 biased;
898 _ = &mut sleep => {
899 tracing::debug!(
900 "There are pending completions after initial {}ms wait, spawning a background task",
901 INITIAL_COMPLETION_WAIT.as_millis()
902 );
903 break;
904 }
905 item = stream.next() => {
906 if let Some((score_boost, result)) = item {
907 match result {
908 Err(err) => {
910 if let Some(line) = err.lines().next() {
911 self.state.write().error.set_temp_message(line.to_string());
912 }
913 }
914 Ok(completion_suggestions) => {
916 self.state.write().merge_completions(score_boost, completion_suggestions);
917 }
918 }
919 } else {
920 tracing::debug!(
922 "All completions were resolved on the initial {}ms window",
923 INITIAL_COMPLETION_WAIT.as_millis()
924 );
925 has_more_items = false;
926 break;
927 }
928 }
929 }
930 }
931 if has_more_items { Some(stream) } else { None }
932 } else {
933 None
934 };
935
936 {
938 let mut state = self.state.write();
939
940 let mut selected = false;
942 if let Some(ref stored_value) = current_stored_value
943 && let Some(idx) = state.suggestions.items().iter().position(|item| match item {
944 VariableSuggestionItem::Existing { value, .. } => &value.value == stored_value,
945 VariableSuggestionItem::Previous { value, .. } => value == stored_value,
946 VariableSuggestionItem::Environment { content, .. } => content == stored_value,
947 VariableSuggestionItem::Completion { value, .. } => value == stored_value,
948 VariableSuggestionItem::Derived { value, .. } => value == stored_value,
949 VariableSuggestionItem::New { .. } => false,
950 })
951 {
952 state.suggestions.select(idx);
953 selected = true;
954 }
955
956 if !selected
958 && let Some(idx) = state.suggestions.items().iter().position(|s| {
959 !matches!(
960 s,
961 VariableSuggestionItem::New { .. } | VariableSuggestionItem::Derived { .. }
962 )
963 })
964 {
965 state.suggestions.select(idx);
966 }
967 }
968
969 if let Some(mut stream) = remaining_stream {
971 let token = cancellation_token.clone();
972 let global_token = self.global_cancellation_token.clone();
973 let state_clone = self.state.clone();
974
975 self.state.write().loading = Some(LoadingSpinner::new(&self.theme));
977
978 tokio::spawn(async move {
980 while let Some((score_boost, result)) = tokio::select! {
981 biased;
982 _ = token.cancelled() => None,
983 _ = global_token.cancelled() => None,
984 item = stream.next() => item,
985 } {
986 match result {
987 Err(err) => {
989 if let Some(line) = err.lines().next() {
990 state_clone.write().error.set_temp_message(line.to_string());
991 }
992 }
993 Ok(completion_suggestions) => {
995 state_clone
996 .write()
997 .merge_completions(score_boost, completion_suggestions);
998 }
999 }
1000 }
1001 state_clone.write().loading = None;
1002 });
1003 }
1004
1005 Ok(Action::NoOp)
1006 }
1007
1008 #[instrument(skip_all)]
1009 async fn confirm_new_secret_value(&mut self, value: String) -> Result<Action> {
1010 tracing::debug!("Secret variable value selected");
1011 self.move_to_next_variable_with_value(value);
1012 self.update_variable_context(false).await
1013 }
1014
1015 #[instrument(skip_all)]
1016 async fn confirm_new_regular_value(&mut self, value: String) -> Result<Action> {
1017 if !value.trim().is_empty() {
1018 let variable_value = {
1019 let state = self.state.read();
1020 let (flat_variable_name, _) = &state.current_variable_ctx;
1021 state.template.new_variable_value_for(flat_variable_name, &value)
1022 };
1023 match self.service.insert_variable_value(variable_value).await {
1024 Ok(v) => {
1025 tracing::debug!("New variable value stored");
1026 self.confirm_existing_value(v, true).await
1027 }
1028 Err(AppError::UserFacing(err)) => {
1029 tracing::warn!("{err}");
1030 self.state.write().error.set_temp_message(err.to_string());
1031 Ok(Action::NoOp)
1032 }
1033 Err(AppError::Unexpected(report)) => Err(report),
1034 }
1035 } else {
1036 tracing::debug!("New empty variable value selected");
1037 self.move_to_next_variable_with_value(value);
1038 self.update_variable_context(false).await
1039 }
1040 }
1041
1042 #[instrument(skip_all)]
1043 async fn confirm_existing_edition(&mut self, mut value: VariableValue, new_value: String) -> Result<Action> {
1044 value.value = new_value;
1045 match self.service.update_variable_value(value).await {
1046 Ok(v) => {
1047 let mut state = self.state.write();
1048 if let VariableSuggestionItem::Existing { value, .. } = state.suggestions.selected_mut().unwrap() {
1049 *value = v;
1050 };
1051 Ok(Action::NoOp)
1052 }
1053 Err(AppError::UserFacing(err)) => {
1054 tracing::warn!("{err}");
1055 self.state.write().error.set_temp_message(err.to_string());
1056 Ok(Action::NoOp)
1057 }
1058 Err(AppError::Unexpected(report)) => Err(report),
1059 }
1060 }
1061
1062 #[instrument(skip_all)]
1063 async fn confirm_existing_value(&mut self, mut value: VariableValue, new: bool) -> Result<Action> {
1064 let value_id = match value.id {
1065 Some(id) => id,
1066 None => {
1067 value = self
1068 .service
1069 .insert_variable_value(value)
1070 .await
1071 .map_err(AppError::into_report)?;
1072 value.id.expect("just inserted")
1073 }
1074 };
1075 let context = self.state.read().template.variable_context();
1076 match self
1077 .service
1078 .increment_variable_value_usage(value_id, context)
1079 .await
1080 .map_err(AppError::into_report)
1081 {
1082 Ok(_) => {
1083 if !new {
1084 tracing::debug!("Existing variable value selected");
1085 }
1086 self.move_to_next_variable_with_value(value.value);
1087 self.update_variable_context(false).await
1088 }
1089 Err(report) => Err(report),
1090 }
1091 }
1092
1093 #[instrument(skip_all)]
1094 async fn confirm_literal_value(&mut self, value: String, store: bool) -> Result<Action> {
1095 if store && !value.trim().is_empty() {
1096 let variable_value = {
1097 let state = self.state.read();
1098 let (flat_variable_name, _) = &state.current_variable_ctx;
1099 state.template.new_variable_value_for(flat_variable_name, &value)
1100 };
1101 match self.service.insert_variable_value(variable_value).await {
1102 Ok(v) => {
1103 tracing::debug!("Literal variable value selected and stored");
1104 self.confirm_existing_value(v, true).await
1105 }
1106 Err(AppError::UserFacing(err)) => {
1107 tracing::debug!("Literal variable value selected but couldn't be stored: {err}");
1108 self.move_to_next_variable_with_value(value);
1109 self.update_variable_context(false).await
1110 }
1111 Err(AppError::Unexpected(report)) => Err(report),
1112 }
1113 } else {
1114 tracing::debug!("Literal variable value selected");
1115 self.move_to_next_variable_with_value(value);
1116 self.update_variable_context(false).await
1117 }
1118 }
1119
1120 fn quit_action(&self, peek: bool, cmd: String) -> Result<Action> {
1122 if self.execute_mode {
1123 Ok(Action::Quit(ProcessOutput::execute(cmd)))
1124 } else if self.replace_process && peek {
1125 Ok(Action::Quit(
1126 ProcessOutput::success()
1127 .stderr(format_msg!(self.theme, "There are no variables to replace"))
1128 .stdout(&cmd)
1129 .fileout(cmd),
1130 ))
1131 } else {
1132 Ok(Action::Quit(ProcessOutput::success().stdout(&cmd).fileout(cmd)))
1133 }
1134 }
1135}
1136
1137fn value_matches_filter_query(value: &str, query: &str) -> bool {
1151 let mut search_offset = 0;
1153 query.split_whitespace().all(|word| {
1154 if let Some(relative_pos) = value[search_offset..].find(word) {
1156 search_offset += relative_pos + 1;
1158 true
1159 } else {
1160 false
1162 }
1163 })
1164}