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