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 last_unset_value: Option<String>,
75}
76
77impl VariableReplacementComponent {
78 pub fn new(
80 service: IntelliShellService,
81 theme: Theme,
82 inline: bool,
83 execute_mode: bool,
84 replace_process: bool,
85 command: CommandTemplate,
86 cancellation_token: CancellationToken,
87 ) -> Self {
88 let command = CommandTemplateWidget::new(&theme, inline, command);
89
90 let suggestions = CustomList::new(theme.clone(), inline, Vec::new());
91
92 let error = ErrorPopup::empty(&theme);
93
94 let layout = if inline {
95 Layout::vertical([Constraint::Length(1), Constraint::Min(3)])
96 } else {
97 Layout::vertical([Constraint::Length(3), Constraint::Min(5)]).margin(1)
98 };
99
100 Self {
101 theme,
102 inline,
103 service,
104 layout,
105 execute_mode,
106 replace_process,
107 cancellation_token: Arc::new(Mutex::new(None)),
108 global_cancellation_token: cancellation_token,
109 state: Arc::new(RwLock::new(VariableReplacementComponentState {
110 template: command,
111 current_variable_ctx: (String::new(), true),
112 variable_suggestions: Vec::new(),
113 suggestions,
114 error,
115 loading: None,
116 last_unset_value: None,
117 })),
118 }
119 }
120}
121
122#[async_trait]
123impl Component for VariableReplacementComponent {
124 fn name(&self) -> &'static str {
125 "VariableReplacementComponent"
126 }
127
128 fn min_inline_height(&self) -> u16 {
129 1 + 5
131 }
132
133 #[instrument(skip_all)]
134 async fn init_and_peek(&mut self) -> Result<Action> {
135 self.update_variable_context(true).await
136 }
137
138 #[instrument(skip_all)]
139 fn render(&mut self, frame: &mut Frame, area: Rect) {
140 let [cmd_area, suggestions_area] = self.layout.areas(area);
142
143 let mut state = self.state.write();
144
145 frame.render_widget(&state.template, cmd_area);
147
148 frame.render_widget(&mut state.suggestions, suggestions_area);
150
151 if let Some(new_version) = self.service.poll_new_version() {
153 NewVersionBanner::new(&self.theme, new_version).render_in(frame, area);
154 }
155 state.error.render_in(frame, area);
156 if let Some(loading) = &state.loading {
158 let loading_area = if self.inline {
159 Rect {
160 x: suggestions_area.x,
161 y: suggestions_area.y + suggestions_area.height.saturating_sub(1),
162 width: 1,
163 height: 1,
164 }
165 } else {
166 Rect {
167 x: suggestions_area.x.saturating_add(1),
168 y: suggestions_area.y + suggestions_area.height.saturating_sub(2),
169 width: 1,
170 height: 1,
171 }
172 };
173 loading.render_in(frame, loading_area);
174 }
175 }
176
177 fn tick(&mut self) -> Result<Action> {
178 let mut state = self.state.write();
179 state.error.tick();
180 if let Some(loading) = &mut state.loading {
181 loading.tick();
182 }
183
184 Ok(Action::NoOp)
185 }
186
187 fn exit(&mut self) -> Result<Action> {
188 {
189 let mut token_guard = self.cancellation_token.lock().unwrap();
190 if let Some(token) = token_guard.take() {
191 token.cancel();
192 }
193 }
194 let mut state = self.state.write();
195 if let Some(VariableSuggestionItem::Existing { editing, .. }) = state.suggestions.selected_mut()
196 && editing.is_some()
197 {
198 tracing::debug!("Closing variable value edit mode: user request");
199 *editing = None;
200 Ok(Action::NoOp)
201 } else {
202 tracing::info!("User requested to exit");
203 Ok(Action::Quit(
204 ProcessOutput::success().fileout(state.template.to_string()),
205 ))
206 }
207 }
208
209 fn process_mouse_event(&mut self, mouse: MouseEvent) -> Result<Action> {
210 match mouse.kind {
211 MouseEventKind::ScrollDown => Ok(self.move_next()?),
212 MouseEventKind::ScrollUp => Ok(self.move_prev()?),
213 _ => Ok(Action::NoOp),
214 }
215 }
216
217 fn move_up(&mut self) -> Result<Action> {
218 let mut state = self.state.write();
219 match state.suggestions.selected() {
220 Some(VariableSuggestionItem::Existing { editing: Some(_), .. }) => (),
221 _ => state.suggestions.select_prev(),
222 }
223 Ok(Action::NoOp)
224 }
225
226 fn move_down(&mut self) -> Result<Action> {
227 let mut state = self.state.write();
228 match state.suggestions.selected() {
229 Some(VariableSuggestionItem::Existing { editing: Some(_), .. }) => (),
230 _ => state.suggestions.select_next(),
231 }
232 Ok(Action::NoOp)
233 }
234
235 fn move_left(&mut self, word: bool) -> Result<Action> {
236 let mut state = self.state.write();
237 match state.suggestions.selected_mut() {
238 Some(VariableSuggestionItem::New { textarea, .. }) => {
239 textarea.move_cursor_left(word);
240 }
241 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
242 ta.move_cursor_left(word);
243 }
244 _ => (),
245 }
246 Ok(Action::NoOp)
247 }
248
249 fn move_right(&mut self, word: bool) -> Result<Action> {
250 let mut state = self.state.write();
251 match state.suggestions.selected_mut() {
252 Some(VariableSuggestionItem::New { textarea, .. }) => {
253 textarea.move_cursor_right(word);
254 }
255 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
256 ta.move_cursor_right(word);
257 }
258 _ => (),
259 }
260 Ok(Action::NoOp)
261 }
262
263 fn move_prev(&mut self) -> Result<Action> {
264 self.move_up()
265 }
266
267 fn move_next(&mut self) -> Result<Action> {
268 self.move_down()
269 }
270
271 fn move_home(&mut self, absolute: bool) -> Result<Action> {
272 let mut state = self.state.write();
273 match state.suggestions.selected_mut() {
274 Some(VariableSuggestionItem::New { textarea, .. }) => {
275 textarea.move_home(absolute);
276 }
277 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
278 ta.move_home(absolute);
279 }
280 _ => state.suggestions.select_first(),
281 }
282 Ok(Action::NoOp)
283 }
284
285 fn move_end(&mut self, absolute: bool) -> Result<Action> {
286 let mut state = self.state.write();
287 match state.suggestions.selected_mut() {
288 Some(VariableSuggestionItem::New { textarea, .. }) => {
289 textarea.move_end(absolute);
290 }
291 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
292 ta.move_end(absolute);
293 }
294 _ => state.suggestions.select_last(),
295 }
296 Ok(Action::NoOp)
297 }
298
299 fn undo(&mut self) -> Result<Action> {
300 let mut state = self.state.write();
301 match state.suggestions.selected_mut() {
302 Some(VariableSuggestionItem::New { textarea, .. }) => {
303 textarea.undo();
304 let query = textarea.lines_as_string();
305 state.filter_suggestions(&query);
306 }
307 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
308 ta.undo();
309 }
310 _ => {
311 if let Some(unset_value) = state.template.unset_last_variable() {
312 state.last_unset_value = Some(unset_value);
313 self.debounced_update_variable_context();
314 }
315 }
316 }
317 Ok(Action::NoOp)
318 }
319
320 fn redo(&mut self) -> Result<Action> {
321 let mut state = self.state.write();
322 match state.suggestions.selected_mut() {
323 Some(VariableSuggestionItem::New { textarea, .. }) => {
324 textarea.redo();
325 let query = textarea.lines_as_string();
326 state.filter_suggestions(&query);
327 }
328 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
329 ta.redo();
330 }
331 _ => {
332 if let Some(value_to_set) = state.last_unset_value.take() {
333 state.template.set_next_variable(value_to_set);
334 self.debounced_update_variable_context();
335 }
336 }
337 }
338 Ok(Action::NoOp)
339 }
340
341 fn insert_text(&mut self, mut text: String) -> Result<Action> {
342 let mut state = self.state.write();
343 if let Some(variable) = state.template.current_variable() {
344 text = variable.apply_functions_to(text);
345 }
346 match state.suggestions.selected_mut() {
347 Some(VariableSuggestionItem::New { textarea, .. }) => {
348 textarea.insert_str(text);
349 let query = textarea.lines_as_string();
350 state.filter_suggestions(&query);
351 }
352 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
353 ta.insert_str(text);
354 }
355 _ => (),
356 }
357 Ok(Action::NoOp)
358 }
359
360 fn insert_char(&mut self, c: char) -> Result<Action> {
361 let mut state = self.state.write();
362 let maybe_replacement = state
363 .template
364 .current_variable()
365 .and_then(|variable| variable.check_functions_char(c));
366 let insert_content = |ta: &mut CustomTextArea<'_>| {
367 if let Some(r) = &maybe_replacement {
368 ta.insert_str(r);
369 } else {
370 ta.insert_char(c);
371 }
372 };
373 match state.suggestions.selected_mut() {
374 Some(VariableSuggestionItem::New { textarea, .. }) => {
375 insert_content(textarea);
376 let query = textarea.lines_as_string();
377 state.filter_suggestions(&query);
378 }
379 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
380 insert_content(ta);
381 }
382 _ => {
383 if let Some(VariableSuggestionItem::New { .. }) = state.suggestions.items().iter().next() {
384 state.suggestions.select_first();
385 if let Some(VariableSuggestionItem::New { textarea, .. }) = state.suggestions.selected_mut() {
386 insert_content(textarea);
387 let query = textarea.lines_as_string();
388 state.filter_suggestions(&query);
389 }
390 }
391 }
392 }
393 Ok(Action::NoOp)
394 }
395
396 fn delete(&mut self, backspace: bool, word: bool) -> Result<Action> {
397 let mut state = self.state.write();
398 match state.suggestions.selected_mut() {
399 Some(VariableSuggestionItem::New { textarea, .. }) => {
400 textarea.delete(backspace, word);
401 let query = textarea.lines_as_string();
402 state.filter_suggestions(&query);
403 }
404 Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
405 ta.delete(backspace, word);
406 }
407 _ => (),
408 }
409 Ok(Action::NoOp)
410 }
411
412 #[instrument(skip_all)]
413 async fn selection_delete(&mut self) -> Result<Action> {
414 let deleted_id = {
415 let mut state = self.state.write();
416 match state.suggestions.selected_mut() {
417 Some(VariableSuggestionItem::New { .. }) => return Ok(Action::NoOp),
418 Some(VariableSuggestionItem::Existing {
419 value: VariableValue { id: Some(id), .. },
420 editing,
421 ..
422 }) => {
423 if editing.is_none() {
424 let id = *id;
425 state.suggestions.delete_selected();
426 id
427 } else {
428 return Ok(Action::NoOp);
429 }
430 }
431 _ => {
432 state.error.set_temp_message("This value is not yet stored");
433 return Ok(Action::NoOp);
434 }
435 }
436 };
437
438 self.service
439 .delete_variable_value(deleted_id)
440 .await
441 .map_err(AppError::into_report)?;
442
443 self.state
444 .write()
445 .variable_suggestions
446 .retain(|s| !matches!(s, VariableSuggestionItem::Existing { value, .. } if value.id == Some(deleted_id)));
447
448 Ok(Action::NoOp)
449 }
450
451 #[instrument(skip_all)]
452 async fn selection_update(&mut self) -> Result<Action> {
453 let mut state = self.state.write();
454
455 match state.suggestions.selected_mut() {
456 Some(VariableSuggestionItem::New { .. }) => (),
457 Some(i @ VariableSuggestionItem::Existing { .. }) => {
458 if let VariableSuggestionItem::Existing { value, editing, .. } = i {
459 if let Some(id) = value.id {
460 if editing.is_none() {
461 tracing::debug!("Entering edit mode for existing variable value: {id}");
462 i.enter_edit_mode();
463 }
464 } else {
465 state.error.set_temp_message("This value is not yet stored");
466 }
467 }
468 }
469 _ => state.error.set_temp_message("This value is not yet stored"),
470 }
471 Ok(Action::NoOp)
472 }
473
474 async fn selection_confirm(&mut self) -> Result<Action> {
475 {
476 let mut token_guard = self.cancellation_token.lock().unwrap();
477 if let Some(token) = token_guard.take() {
478 token.cancel();
479 }
480 }
481
482 enum NextAction {
484 NoOp,
485 ConfirmNewSecret(String),
486 ConfirmNewRegular(String),
487 ConfirmExistingEdition(VariableValue, String),
488 ConfirmExistingValue(VariableValue),
489 ConfirmLiteral(String, bool),
490 }
491
492 let next_action = {
493 let mut state = self.state.write();
494 let (_, is_secret) = state.current_variable_ctx;
495 match state.suggestions.selected_mut() {
496 None => NextAction::NoOp,
497 Some(VariableSuggestionItem::New {
498 textarea,
499 is_secret: true,
500 ..
501 }) => NextAction::ConfirmNewSecret(textarea.lines_as_string()),
502 Some(VariableSuggestionItem::New {
503 textarea,
504 is_secret: false,
505 ..
506 }) => NextAction::ConfirmNewRegular(textarea.lines_as_string()),
507 Some(VariableSuggestionItem::Existing { value, editing, .. }) => match editing.take() {
508 Some(ta) => NextAction::ConfirmExistingEdition(value.clone(), ta.lines_as_string()),
509 None => NextAction::ConfirmExistingValue(value.clone()),
510 },
511 Some(VariableSuggestionItem::Environment {
512 content,
513 is_value: false,
514 ..
515 }) => NextAction::ConfirmLiteral(content.clone(), false),
516 Some(VariableSuggestionItem::Environment {
517 content: value,
518 is_value: true,
519 ..
520 })
521 | Some(VariableSuggestionItem::Previous { value, .. })
522 | Some(VariableSuggestionItem::Completion { value, .. })
523 | Some(VariableSuggestionItem::Derived { value, .. }) => {
524 NextAction::ConfirmLiteral(value.clone(), !is_secret)
525 }
526 }
527 };
528
529 match next_action {
530 NextAction::NoOp => Ok(Action::NoOp),
531 NextAction::ConfirmNewSecret(value) => self.confirm_new_secret_value(value).await,
532 NextAction::ConfirmNewRegular(value) => self.confirm_new_regular_value(value).await,
533 NextAction::ConfirmExistingEdition(val, new_val) => self.confirm_existing_edition(val, new_val).await,
534 NextAction::ConfirmExistingValue(val) => self.confirm_existing_value(val, false).await,
535 NextAction::ConfirmLiteral(val, is_value) => self.confirm_literal_value(val, is_value).await,
536 }
537 }
538
539 async fn selection_execute(&mut self) -> Result<Action> {
540 self.selection_confirm().await
541 }
542}
543
544impl<'a> VariableReplacementComponentState<'a> {
545 fn filter_suggestions(&mut self, query: &str) {
547 tracing::debug!("Filtering suggestions for: {query}");
548 let mut filtered_suggestions = self.variable_suggestions.clone();
550 filtered_suggestions.retain(|s| match s {
551 VariableSuggestionItem::New { .. } => false,
552 VariableSuggestionItem::Existing { value, .. } => value_matches_filter_query(&value.value, query),
553 VariableSuggestionItem::Previous { value, .. }
554 | VariableSuggestionItem::Environment { content: value, .. }
555 | VariableSuggestionItem::Completion { value, .. }
556 | VariableSuggestionItem::Derived { value, .. } => value_matches_filter_query(value, query),
557 });
558
559 let new_row = self
561 .suggestions
562 .items()
563 .iter()
564 .find(|s| matches!(s, VariableSuggestionItem::New { .. }));
565 if let Some(new_row) = new_row.cloned() {
566 filtered_suggestions.insert(0, new_row);
567 }
568 let selected_id = self.suggestions.selected().map(|s| s.identifier());
570 self.suggestions.update_items(filtered_suggestions, false);
572 if let Some(selected_id) = selected_id {
574 self.suggestions.select_matching(|i| i.identifier() == selected_id);
575 }
576 }
577
578 fn merge_completions(&mut self, score_boost: f64, completion_suggestions: Vec<String>) {
580 let master_suggestions = &mut self.variable_suggestions;
582
583 let completion_set = completion_suggestions.iter().collect::<HashSet<_>>();
585 master_suggestions.retain_mut(|item| {
586 !matches!(
587 item,
588 VariableSuggestionItem::Derived { value, .. }
589 if completion_set.contains(value)
590 )
591 });
592
593 for suggestion in completion_suggestions {
595 let mut skip_completion = false;
597 for item in master_suggestions.iter_mut() {
598 match item {
599 VariableSuggestionItem::New { .. } => (),
601 VariableSuggestionItem::Derived { .. } => (),
603 VariableSuggestionItem::Previous { value, .. } => {
605 if value == &suggestion {
606 skip_completion = true;
607 break;
608 }
609 }
610 VariableSuggestionItem::Environment { content, is_value, .. } => {
612 if *is_value && content == &suggestion {
613 skip_completion = true;
614 break;
615 }
616 }
617 VariableSuggestionItem::Existing {
619 value,
620 score,
621 completion_merged,
622 ..
623 } => {
624 if value.value == suggestion {
625 if !*completion_merged {
626 *score += score_boost;
627 *completion_merged = true;
628 }
629 skip_completion = true;
630 break;
631 }
632 }
633 VariableSuggestionItem::Completion { value, score, .. } => {
635 if value == &suggestion {
636 *score = score.max(score_boost);
637 skip_completion = true;
638 break;
639 }
640 }
641 }
642 }
643 if skip_completion {
644 continue;
645 }
646
647 master_suggestions.push(VariableSuggestionItem::Completion {
649 sort_index: 3,
650 value: suggestion,
651 score: score_boost,
652 });
653 }
654
655 master_suggestions.sort_by(|a, b| {
657 a.sort_index()
658 .cmp(&b.sort_index())
659 .then_with(|| b.score().partial_cmp(&a.score()).unwrap_or(Ordering::Equal))
660 });
661
662 let query = self
664 .suggestions
665 .items()
666 .iter()
667 .find_map(|s| match s {
668 VariableSuggestionItem::New { textarea, .. } => Some(textarea.lines_as_string()),
669 _ => None,
670 })
671 .unwrap_or_default();
672 self.filter_suggestions(&query);
673 }
674}
675
676impl VariableReplacementComponent {
677 fn debounced_update_variable_context(&self) {
679 let this = self.clone();
680 tokio::spawn(async move {
681 if let Err(err) = this.update_variable_context(false).await {
682 tracing::error!("Error updating variable context: {err:?}");
683 }
684 });
685 }
686
687 async fn update_variable_context(&self, peek: bool) -> Result<Action> {
689 let cancellation_token = {
691 let mut token_guard = self.cancellation_token.lock().unwrap();
692 if let Some(token) = token_guard.take() {
693 token.cancel();
694 }
695 let new_token = CancellationToken::new();
696 *token_guard = Some(new_token.clone());
697 new_token
698 };
699
700 let (flat_root_cmd, previous_values, current_variable, context) = {
702 let state = self.state.read();
703 match state.template.current_variable().cloned() {
704 Some(variable) => (
705 state.template.flat_root_cmd.clone(),
706 state.template.previous_values_for(&variable.flat_name),
707 variable,
708 state.template.current_variable_context(),
709 ),
710 None => {
711 if peek {
712 tracing::info!("There are no variables to replace");
713 } else {
714 tracing::info!("There are no more variables");
715 }
716 return self.quit_action(peek, state.template.to_string());
717 }
718 }
719 };
720
721 let (initial_suggestions, completion_stream) = self
723 .service
724 .search_variable_suggestions(&flat_root_cmd, ¤t_variable, previous_values, context)
725 .await
726 .map_err(AppError::into_report)?;
727
728 {
730 let mut state = self.state.write();
731 let suggestions = initial_suggestions
732 .into_iter()
733 .map(VariableSuggestionItem::from)
734 .collect::<Vec<_>>();
735 state.current_variable_ctx = (current_variable.flat_name.clone(), current_variable.secret);
736 state.variable_suggestions = suggestions.clone();
737 state.suggestions.update_items(suggestions, false);
738 }
739
740 let remaining_stream = if let Some(mut stream) = completion_stream {
742 let sleep = tokio::time::sleep(INITIAL_COMPLETION_WAIT);
743 tokio::pin!(sleep);
744
745 let mut has_more_items = true;
746
747 loop {
748 tokio::select! {
749 biased;
750 _ = &mut sleep => {
751 tracing::debug!(
752 "There are pending completions after initial {}ms wait, spawning a background task",
753 INITIAL_COMPLETION_WAIT.as_millis()
754 );
755 break;
756 }
757 item = stream.next() => {
758 if let Some((score_boost, result)) = item {
759 match result {
760 Err(err) => {
762 if let Some(line) = err.lines().next() {
763 self.state.write().error.set_temp_message(line.to_string());
764 }
765 }
766 Ok(completion_suggestions) => {
768 self.state.write().merge_completions(score_boost, completion_suggestions);
769 }
770 }
771 } else {
772 tracing::debug!(
774 "All completions were resolved on the initial {}ms window",
775 INITIAL_COMPLETION_WAIT.as_millis()
776 );
777 has_more_items = false;
778 break;
779 }
780 }
781 }
782 }
783 if has_more_items { Some(stream) } else { None }
784 } else {
785 None
786 };
787
788 {
790 let mut state = self.state.write();
791 if let Some(idx) = state.suggestions.items().iter().position(|s| {
792 !matches!(
793 s,
794 VariableSuggestionItem::New { .. } | VariableSuggestionItem::Derived { .. }
795 )
796 }) {
797 state.suggestions.select(idx);
798 }
799 }
800
801 if let Some(mut stream) = remaining_stream {
803 let token = cancellation_token.clone();
804 let global_token = self.global_cancellation_token.clone();
805 let state_clone = self.state.clone();
806
807 self.state.write().loading = Some(LoadingSpinner::new(&self.theme));
809
810 tokio::spawn(async move {
812 while let Some((score_boost, result)) = tokio::select! {
813 biased;
814 _ = token.cancelled() => None,
815 _ = global_token.cancelled() => None,
816 item = stream.next() => item,
817 } {
818 match result {
819 Err(err) => {
821 if let Some(line) = err.lines().next() {
822 state_clone.write().error.set_temp_message(line.to_string());
823 }
824 }
825 Ok(completion_suggestions) => {
827 state_clone
828 .write()
829 .merge_completions(score_boost, completion_suggestions);
830 }
831 }
832 }
833 state_clone.write().loading = None;
834 });
835 }
836
837 Ok(Action::NoOp)
838 }
839
840 #[instrument(skip_all)]
841 async fn confirm_new_secret_value(&mut self, value: String) -> Result<Action> {
842 tracing::debug!("Secret variable value selected");
843 self.state.write().template.set_next_variable(value);
844 self.update_variable_context(false).await
845 }
846
847 #[instrument(skip_all)]
848 async fn confirm_new_regular_value(&mut self, value: String) -> Result<Action> {
849 if !value.trim().is_empty() {
850 let variable_value = {
851 let state = self.state.read();
852 let (flat_variable_name, _) = &state.current_variable_ctx;
853 state.template.new_variable_value_for(flat_variable_name, &value)
854 };
855 match self.service.insert_variable_value(variable_value).await {
856 Ok(v) => {
857 tracing::debug!("New variable value stored");
858 self.confirm_existing_value(v, true).await
859 }
860 Err(AppError::UserFacing(err)) => {
861 tracing::warn!("{err}");
862 self.state.write().error.set_temp_message(err.to_string());
863 Ok(Action::NoOp)
864 }
865 Err(AppError::Unexpected(report)) => Err(report),
866 }
867 } else {
868 tracing::debug!("New empty variable value selected");
869 self.state.write().template.set_next_variable(value);
870 self.update_variable_context(false).await
871 }
872 }
873
874 #[instrument(skip_all)]
875 async fn confirm_existing_edition(&mut self, mut value: VariableValue, new_value: String) -> Result<Action> {
876 value.value = new_value;
877 match self.service.update_variable_value(value).await {
878 Ok(v) => {
879 let mut state = self.state.write();
880 if let VariableSuggestionItem::Existing { value, .. } = state.suggestions.selected_mut().unwrap() {
881 *value = v;
882 };
883 Ok(Action::NoOp)
884 }
885 Err(AppError::UserFacing(err)) => {
886 tracing::warn!("{err}");
887 self.state.write().error.set_temp_message(err.to_string());
888 Ok(Action::NoOp)
889 }
890 Err(AppError::Unexpected(report)) => Err(report),
891 }
892 }
893
894 #[instrument(skip_all)]
895 async fn confirm_existing_value(&mut self, mut value: VariableValue, new: bool) -> Result<Action> {
896 let value_id = match value.id {
897 Some(id) => id,
898 None => {
899 value = self
900 .service
901 .insert_variable_value(value)
902 .await
903 .map_err(AppError::into_report)?;
904 value.id.expect("just inserted")
905 }
906 };
907 let context = self.state.read().template.current_variable_context();
908 match self
909 .service
910 .increment_variable_value_usage(value_id, context)
911 .await
912 .map_err(AppError::into_report)
913 {
914 Ok(_) => {
915 if !new {
916 tracing::debug!("Existing variable value selected");
917 }
918 self.state.write().template.set_next_variable(value.value);
919 self.update_variable_context(false).await
920 }
921 Err(report) => Err(report),
922 }
923 }
924
925 #[instrument(skip_all)]
926 async fn confirm_literal_value(&mut self, value: String, store: bool) -> Result<Action> {
927 if store && !value.trim().is_empty() {
928 let variable_value = {
929 let state = self.state.read();
930 let (flat_variable_name, _) = &state.current_variable_ctx;
931 state.template.new_variable_value_for(flat_variable_name, &value)
932 };
933 match self.service.insert_variable_value(variable_value).await {
934 Ok(v) => {
935 tracing::debug!("Literal variable value selected and stored");
936 self.confirm_existing_value(v, true).await
937 }
938 Err(AppError::UserFacing(err)) => {
939 tracing::debug!("Literal variable value selected but couldn't be stored: {err}");
940 self.state.write().template.set_next_variable(value);
941 self.update_variable_context(false).await
942 }
943 Err(AppError::Unexpected(report)) => Err(report),
944 }
945 } else {
946 tracing::debug!("Literal variable value selected");
947 self.state.write().template.set_next_variable(value);
948 self.update_variable_context(false).await
949 }
950 }
951
952 fn quit_action(&self, peek: bool, cmd: String) -> Result<Action> {
954 if self.execute_mode {
955 Ok(Action::Quit(ProcessOutput::execute(cmd)))
956 } else if self.replace_process && peek {
957 Ok(Action::Quit(
958 ProcessOutput::success()
959 .stderr(format_msg!(self.theme, "There are no variables to replace"))
960 .stdout(&cmd)
961 .fileout(cmd),
962 ))
963 } else {
964 Ok(Action::Quit(ProcessOutput::success().stdout(&cmd).fileout(cmd)))
965 }
966 }
967}
968
969fn value_matches_filter_query(value: &str, query: &str) -> bool {
983 let mut search_offset = 0;
985 query.split_whitespace().all(|word| {
986 if let Some(relative_pos) = value[search_offset..].find(word) {
988 search_offset += relative_pos + 1;
990 true
991 } else {
992 false
994 }
995 })
996}