intelli_shell/component/
variable.rs

1use std::{
2    cmp::Ordering,
3    collections::HashSet,
4    sync::{Arc, Mutex},
5};
6
7use async_trait::async_trait;
8use color_eyre::Result;
9use crossterm::event::{MouseEvent, MouseEventKind};
10use futures_util::StreamExt;
11use parking_lot::RwLock;
12use ratatui::{
13    Frame,
14    layout::{Constraint, Layout, Rect},
15};
16use tokio_util::sync::CancellationToken;
17use tracing::instrument;
18
19use super::Component;
20use crate::{
21    app::Action,
22    config::Theme,
23    errors::AppError,
24    format_msg,
25    model::{CommandTemplate, VariableValue},
26    process::ProcessOutput,
27    service::IntelliShellService,
28    widgets::{
29        CommandTemplateWidget, CustomList, CustomTextArea, ErrorPopup, LoadingSpinner, NewVersionBanner,
30        items::VariableSuggestionItem,
31    },
32};
33
34/// A component for replacing the variables of a command
35#[derive(Clone)]
36pub struct VariableReplacementComponent {
37    /// Visual theme for styling the component
38    theme: Theme,
39    /// Whether the TUI is in inline mode or not
40    inline: bool,
41    /// Service for interacting with command storage
42    service: IntelliShellService,
43    /// Layout for arranging the input fields
44    layout: Layout,
45    /// Whether the command must be executed after replacing thw variables or just output it
46    execute_mode: bool,
47    /// Whether this component is part of the replace process (or maybe rendered after another process)
48    replace_process: bool,
49    /// Cancellation token for the background completions task
50    cancellation_token: Arc<Mutex<Option<CancellationToken>>>,
51    /// The state of the component
52    state: Arc<RwLock<VariableReplacementComponentState<'static>>>,
53}
54struct VariableReplacementComponentState<'a> {
55    /// The command with variables to be replaced
56    template: CommandTemplateWidget,
57    /// Flat name of the current variable being set
58    flat_variable_name: String,
59    /// Full list of suggestions for the current variable
60    variable_suggestions: Vec<VariableSuggestionItem<'static>>,
61    /// Widget list of filtered suggestions for the variable value
62    suggestions: CustomList<'a, VariableSuggestionItem<'a>>,
63    /// Popup for displaying error messages
64    error: ErrorPopup<'a>,
65    /// A spinner to indicate that completions are being fetched
66    loading: Option<LoadingSpinner<'a>>,
67    /// The last value that was unset via undo, to be used by redo
68    last_unset_value: Option<String>,
69}
70
71impl VariableReplacementComponent {
72    /// Creates a new [`VariableReplacementComponent`]
73    pub fn new(
74        service: IntelliShellService,
75        theme: Theme,
76        inline: bool,
77        execute_mode: bool,
78        replace_process: bool,
79        command: CommandTemplate,
80    ) -> Self {
81        let command = CommandTemplateWidget::new(&theme, inline, command);
82
83        let suggestions = CustomList::new(theme.clone(), inline, Vec::new());
84
85        let error = ErrorPopup::empty(&theme);
86
87        let layout = if inline {
88            Layout::vertical([Constraint::Length(1), Constraint::Min(3)])
89        } else {
90            Layout::vertical([Constraint::Length(3), Constraint::Min(5)]).margin(1)
91        };
92
93        Self {
94            theme,
95            inline,
96            service,
97            layout,
98            execute_mode,
99            replace_process,
100            cancellation_token: Arc::new(Mutex::new(None)),
101            state: Arc::new(RwLock::new(VariableReplacementComponentState {
102                template: command,
103                flat_variable_name: String::new(),
104                variable_suggestions: Vec::new(),
105                suggestions,
106                error,
107                loading: None,
108                last_unset_value: None,
109            })),
110        }
111    }
112}
113
114#[async_trait]
115impl Component for VariableReplacementComponent {
116    fn name(&self) -> &'static str {
117        "VariableReplacementComponent"
118    }
119
120    fn min_inline_height(&self) -> u16 {
121        // Command + Values
122        1 + 5
123    }
124
125    #[instrument(skip_all)]
126    async fn init_and_peek(&mut self) -> Result<Action> {
127        self.update_variable_context(true).await
128    }
129
130    #[instrument(skip_all)]
131    fn render(&mut self, frame: &mut Frame, area: Rect) {
132        // Split the area according to the layout
133        let [cmd_area, suggestions_area] = self.layout.areas(area);
134
135        let mut state = self.state.write();
136
137        // Render the command widget
138        frame.render_widget(&state.template, cmd_area);
139
140        // Render the suggestions
141        frame.render_widget(&mut state.suggestions, suggestions_area);
142
143        // Render the new version banner and error message as an overlay
144        if let Some(new_version) = self.service.check_new_version() {
145            NewVersionBanner::new(&self.theme, new_version).render_in(frame, area);
146        }
147        state.error.render_in(frame, area);
148        // Display the loading spinner, if any
149        if let Some(loading) = &state.loading {
150            let loading_area = if self.inline {
151                Rect {
152                    x: suggestions_area.x,
153                    y: suggestions_area.y + suggestions_area.height.saturating_sub(1),
154                    width: 1,
155                    height: 1,
156                }
157            } else {
158                Rect {
159                    x: suggestions_area.x.saturating_add(1),
160                    y: suggestions_area.y + suggestions_area.height.saturating_sub(2),
161                    width: 1,
162                    height: 1,
163                }
164            };
165            loading.render_in(frame, loading_area);
166        }
167    }
168
169    fn tick(&mut self) -> Result<Action> {
170        let mut state = self.state.write();
171        state.error.tick();
172        if let Some(loading) = &mut state.loading {
173            loading.tick();
174        }
175
176        Ok(Action::NoOp)
177    }
178
179    fn exit(&mut self) -> Result<Action> {
180        {
181            let mut token_guard = self.cancellation_token.lock().unwrap();
182            if let Some(token) = token_guard.take() {
183                token.cancel();
184            }
185        }
186        let mut state = self.state.write();
187        if let Some(VariableSuggestionItem::Existing { editing, .. }) = state.suggestions.selected_mut()
188            && editing.is_some()
189        {
190            tracing::debug!("Closing variable value edit mode: user request");
191            *editing = None;
192            Ok(Action::NoOp)
193        } else {
194            tracing::info!("User requested to exit");
195            Ok(Action::Quit(
196                ProcessOutput::success().fileout(state.template.to_string()),
197            ))
198        }
199    }
200
201    fn process_mouse_event(&mut self, mouse: MouseEvent) -> Result<Action> {
202        match mouse.kind {
203            MouseEventKind::ScrollDown => Ok(self.move_next()?),
204            MouseEventKind::ScrollUp => Ok(self.move_prev()?),
205            _ => Ok(Action::NoOp),
206        }
207    }
208
209    fn move_up(&mut self) -> Result<Action> {
210        let mut state = self.state.write();
211        match state.suggestions.selected() {
212            Some(VariableSuggestionItem::Existing { editing: Some(_), .. }) => (),
213            _ => state.suggestions.select_prev(),
214        }
215        Ok(Action::NoOp)
216    }
217
218    fn move_down(&mut self) -> Result<Action> {
219        let mut state = self.state.write();
220        match state.suggestions.selected() {
221            Some(VariableSuggestionItem::Existing { editing: Some(_), .. }) => (),
222            _ => state.suggestions.select_next(),
223        }
224        Ok(Action::NoOp)
225    }
226
227    fn move_left(&mut self, word: bool) -> Result<Action> {
228        let mut state = self.state.write();
229        match state.suggestions.selected_mut() {
230            Some(VariableSuggestionItem::New { textarea, .. }) => {
231                textarea.move_cursor_left(word);
232            }
233            Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
234                ta.move_cursor_left(word);
235            }
236            _ => (),
237        }
238        Ok(Action::NoOp)
239    }
240
241    fn move_right(&mut self, word: bool) -> Result<Action> {
242        let mut state = self.state.write();
243        match state.suggestions.selected_mut() {
244            Some(VariableSuggestionItem::New { textarea, .. }) => {
245                textarea.move_cursor_right(word);
246            }
247            Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
248                ta.move_cursor_right(word);
249            }
250            _ => (),
251        }
252        Ok(Action::NoOp)
253    }
254
255    fn move_prev(&mut self) -> Result<Action> {
256        self.move_up()
257    }
258
259    fn move_next(&mut self) -> Result<Action> {
260        self.move_down()
261    }
262
263    fn move_home(&mut self, absolute: bool) -> Result<Action> {
264        let mut state = self.state.write();
265        match state.suggestions.selected_mut() {
266            Some(VariableSuggestionItem::New { textarea, .. }) => {
267                textarea.move_home(absolute);
268            }
269            Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
270                ta.move_home(absolute);
271            }
272            _ => state.suggestions.select_first(),
273        }
274        Ok(Action::NoOp)
275    }
276
277    fn move_end(&mut self, absolute: bool) -> Result<Action> {
278        let mut state = self.state.write();
279        match state.suggestions.selected_mut() {
280            Some(VariableSuggestionItem::New { textarea, .. }) => {
281                textarea.move_end(absolute);
282            }
283            Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
284                ta.move_end(absolute);
285            }
286            _ => state.suggestions.select_last(),
287        }
288        Ok(Action::NoOp)
289    }
290
291    fn undo(&mut self) -> Result<Action> {
292        let mut state = self.state.write();
293        match state.suggestions.selected_mut() {
294            Some(VariableSuggestionItem::New {
295                textarea, is_secret, ..
296            }) => {
297                textarea.undo();
298                if !*is_secret {
299                    let query = textarea.lines_as_string();
300                    state.filter_suggestions(&query);
301                }
302            }
303            Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
304                ta.undo();
305            }
306            _ => {
307                if let Some(unset_value) = state.template.unset_last_variable() {
308                    state.last_unset_value = Some(unset_value);
309                    self.debounced_update_variable_context();
310                }
311            }
312        }
313        Ok(Action::NoOp)
314    }
315
316    fn redo(&mut self) -> Result<Action> {
317        let mut state = self.state.write();
318        match state.suggestions.selected_mut() {
319            Some(VariableSuggestionItem::New {
320                textarea, is_secret, ..
321            }) => {
322                textarea.redo();
323                if !*is_secret {
324                    let query = textarea.lines_as_string();
325                    state.filter_suggestions(&query);
326                }
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 {
348                textarea, is_secret, ..
349            }) => {
350                textarea.insert_str(text);
351                if !*is_secret {
352                    let query = textarea.lines_as_string();
353                    state.filter_suggestions(&query);
354                }
355            }
356            Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
357                ta.insert_str(text);
358            }
359            _ => (),
360        }
361        Ok(Action::NoOp)
362    }
363
364    fn insert_char(&mut self, c: char) -> Result<Action> {
365        let mut state = self.state.write();
366        let maybe_replacement = state
367            .template
368            .current_variable()
369            .and_then(|variable| variable.check_functions_char(c));
370        let insert_content = |ta: &mut CustomTextArea<'_>| {
371            if let Some(r) = &maybe_replacement {
372                ta.insert_str(r);
373            } else {
374                ta.insert_char(c);
375            }
376        };
377        match state.suggestions.selected_mut() {
378            Some(VariableSuggestionItem::New {
379                textarea, is_secret, ..
380            }) => {
381                insert_content(textarea);
382                if !*is_secret {
383                    let query = textarea.lines_as_string();
384                    state.filter_suggestions(&query);
385                }
386            }
387            Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
388                insert_content(ta);
389            }
390            _ => {
391                if let Some(VariableSuggestionItem::New { .. }) = state.suggestions.items().iter().next() {
392                    state.suggestions.select_first();
393                    if let Some(VariableSuggestionItem::New {
394                        textarea, is_secret, ..
395                    }) = state.suggestions.selected_mut()
396                    {
397                        insert_content(textarea);
398                        if !*is_secret {
399                            let query = textarea.lines_as_string();
400                            state.filter_suggestions(&query);
401                        }
402                    }
403                }
404            }
405        }
406        Ok(Action::NoOp)
407    }
408
409    fn delete(&mut self, backspace: bool, word: bool) -> Result<Action> {
410        let mut state = self.state.write();
411        match state.suggestions.selected_mut() {
412            Some(VariableSuggestionItem::New {
413                textarea, is_secret, ..
414            }) => {
415                textarea.delete(backspace, word);
416                if !*is_secret {
417                    let query = textarea.lines_as_string();
418                    state.filter_suggestions(&query);
419                }
420            }
421            Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
422                ta.delete(backspace, word);
423            }
424            _ => (),
425        }
426        Ok(Action::NoOp)
427    }
428
429    #[instrument(skip_all)]
430    async fn selection_delete(&mut self) -> Result<Action> {
431        let deleted_id = {
432            let mut state = self.state.write();
433            match state.suggestions.selected_mut() {
434                Some(VariableSuggestionItem::New { .. }) => return Ok(Action::NoOp),
435                Some(VariableSuggestionItem::Existing {
436                    value: VariableValue { id: Some(id), .. },
437                    editing,
438                    ..
439                }) => {
440                    if editing.is_none() {
441                        let id = *id;
442                        state.suggestions.delete_selected();
443                        id
444                    } else {
445                        return Ok(Action::NoOp);
446                    }
447                }
448                _ => {
449                    state.error.set_temp_message("This value is not yet stored");
450                    return Ok(Action::NoOp);
451                }
452            }
453        };
454
455        self.service
456            .delete_variable_value(deleted_id)
457            .await
458            .map_err(AppError::into_report)?;
459
460        self.state
461            .write()
462            .variable_suggestions
463            .retain(|s| !matches!(s, VariableSuggestionItem::Existing { value, .. } if value.id == Some(deleted_id)));
464
465        Ok(Action::NoOp)
466    }
467
468    #[instrument(skip_all)]
469    async fn selection_update(&mut self) -> Result<Action> {
470        let mut state = self.state.write();
471
472        match state.suggestions.selected_mut() {
473            Some(VariableSuggestionItem::New { .. }) => (),
474            Some(i @ VariableSuggestionItem::Existing { .. }) => {
475                if let VariableSuggestionItem::Existing { value, editing, .. } = i {
476                    if let Some(id) = value.id {
477                        if editing.is_none() {
478                            tracing::debug!("Entering edit mode for existing variable value: {id}");
479                            i.enter_edit_mode();
480                        }
481                    } else {
482                        state.error.set_temp_message("This value is not yet stored");
483                    }
484                }
485            }
486            _ => state.error.set_temp_message("This value is not yet stored"),
487        }
488        Ok(Action::NoOp)
489    }
490
491    async fn selection_confirm(&mut self) -> Result<Action> {
492        {
493            let mut token_guard = self.cancellation_token.lock().unwrap();
494            if let Some(token) = token_guard.take() {
495                token.cancel();
496            }
497        }
498
499        // Helper enum to hold the data extracted from the lock
500        enum NextAction {
501            NoOp,
502            ConfirmNewSecret(String),
503            ConfirmNewRegular(String),
504            ConfirmExistingEdition(VariableValue, String),
505            ConfirmExistingValue(VariableValue),
506            ConfirmLiteral(String, bool),
507        }
508
509        let next_action = {
510            let mut state = self.state.write();
511            match state.suggestions.selected_mut() {
512                None => NextAction::NoOp,
513                Some(VariableSuggestionItem::New {
514                    textarea,
515                    is_secret: true,
516                    ..
517                }) => NextAction::ConfirmNewSecret(textarea.lines_as_string()),
518                Some(VariableSuggestionItem::New {
519                    textarea,
520                    is_secret: false,
521                    ..
522                }) => NextAction::ConfirmNewRegular(textarea.lines_as_string()),
523                Some(VariableSuggestionItem::Existing { value, editing, .. }) => match editing.take() {
524                    Some(ta) => NextAction::ConfirmExistingEdition(value.clone(), ta.lines_as_string()),
525                    None => NextAction::ConfirmExistingValue(value.clone()),
526                },
527                Some(VariableSuggestionItem::Environment {
528                    content,
529                    is_value: false,
530                    ..
531                }) => NextAction::ConfirmLiteral(content.clone(), false),
532                Some(VariableSuggestionItem::Environment {
533                    content: value,
534                    is_value: true,
535                    ..
536                })
537                | Some(VariableSuggestionItem::Completion { value, .. })
538                | Some(VariableSuggestionItem::Derived { value, .. }) => {
539                    NextAction::ConfirmLiteral(value.clone(), true)
540                }
541            }
542        };
543
544        match next_action {
545            NextAction::NoOp => Ok(Action::NoOp),
546            NextAction::ConfirmNewSecret(value) => self.confirm_new_secret_value(value).await,
547            NextAction::ConfirmNewRegular(value) => self.confirm_new_regular_value(value).await,
548            NextAction::ConfirmExistingEdition(val, new_val) => self.confirm_existing_edition(val, new_val).await,
549            NextAction::ConfirmExistingValue(val) => self.confirm_existing_value(val, false).await,
550            NextAction::ConfirmLiteral(val, is_value) => self.confirm_literal_value(val, is_value).await,
551        }
552    }
553
554    async fn selection_execute(&mut self) -> Result<Action> {
555        self.selection_confirm().await
556    }
557}
558
559impl<'a> VariableReplacementComponentState<'a> {
560    /// Filters the suggestions widget based on the query
561    fn filter_suggestions(&mut self, query: &str) {
562        tracing::debug!("Filtering suggestions for: {query}");
563        // From the original variable suggestions, keep those matching the query only
564        let mut filtered_suggestions = self.variable_suggestions.clone();
565        filtered_suggestions.retain(|s| match s {
566            VariableSuggestionItem::New { .. } => false,
567            VariableSuggestionItem::Existing { value, .. } => value.value.contains(query),
568            VariableSuggestionItem::Environment { content: value, .. }
569            | VariableSuggestionItem::Completion { value, .. }
570            | VariableSuggestionItem::Derived { value, .. } => value.contains(query),
571        });
572
573        // Find and insert the new row, which contains the query
574        let new_row = self
575            .suggestions
576            .items()
577            .iter()
578            .find(|s| matches!(s, VariableSuggestionItem::New { .. }));
579        if let Some(new_row) = new_row.cloned() {
580            filtered_suggestions.insert(0, new_row);
581        }
582        // Retrieve the identifier for the selected item
583        let selected_id = self.suggestions.selected().map(|s| s.identifier());
584        // Update the items
585        self.suggestions.update_items(filtered_suggestions, false);
586        // Restore the same selected item
587        if let Some(selected_id) = selected_id {
588            self.suggestions.select_matching(|i| i.identifier() == selected_id);
589        }
590    }
591}
592
593impl VariableReplacementComponent {
594    /// Immediately starts a debounced task to update the variable context
595    fn debounced_update_variable_context(&self) {
596        let this = self.clone();
597        tokio::spawn(async move {
598            if let Err(err) = this.update_variable_context(false).await {
599                tracing::error!("Error updating variable context: {err:?}");
600            }
601        });
602    }
603
604    /// Updates the variable context and the suggestions widget, or returns an acton
605    async fn update_variable_context(&self, peek: bool) -> Result<Action> {
606        // Cancels previous completion task and issue a new one
607        let cancellation_token = {
608            let mut token_guard = self.cancellation_token.lock().unwrap();
609            if let Some(token) = token_guard.take() {
610                token.cancel();
611            }
612            let new_token = CancellationToken::new();
613            *token_guard = Some(new_token.clone());
614            new_token
615        };
616
617        // Retrieves the current variable and its context
618        let (flat_root_cmd, current_variable, context) = {
619            let state = self.state.read();
620            match state.template.current_variable().cloned() {
621                Some(variable) => (
622                    state.template.flat_root_cmd.clone(),
623                    variable,
624                    state.template.current_variable_context(),
625                ),
626                None => {
627                    if peek {
628                        tracing::info!("There are no variables to replace");
629                    } else {
630                        tracing::info!("There are no more variables");
631                    }
632                    return self.quit_action(peek, state.template.to_string());
633                }
634            }
635        };
636
637        // Search for the variable suggestions
638        let (initial_suggestions, completion_stream) = self
639            .service
640            .search_variable_suggestions(&flat_root_cmd, &current_variable, context)
641            .await
642            .map_err(AppError::into_report)?;
643
644        // Update the context
645        let mut state = self.state.write();
646        let suggestions = initial_suggestions
647            .into_iter()
648            .map(VariableSuggestionItem::from)
649            .collect::<Vec<_>>();
650        state.flat_variable_name = current_variable.flat_name.clone();
651        state.variable_suggestions = suggestions.clone();
652
653        // And the displayed items
654        state.suggestions.update_items(suggestions, false);
655
656        // Pre-select the first non-derived suggestion
657        if let Some(idx) = state.suggestions.items().iter().position(|s| {
658            !matches!(
659                s,
660                VariableSuggestionItem::New { .. } | VariableSuggestionItem::Derived { .. }
661            )
662        }) {
663            state.suggestions.select(idx);
664        }
665
666        // If there's some completions stream
667        if let Some(mut stream) = completion_stream {
668            let token = cancellation_token.clone();
669            let state_clone = self.state.clone();
670
671            // Show the loading spinner
672            state.loading = Some(LoadingSpinner::new(&self.theme));
673
674            // Spawn a background task to wait for them
675            tokio::spawn(async move {
676                while let Some((score_boost, result)) = tokio::select! {
677                    biased;
678                    _ = token.cancelled() => None,
679                    item = stream.next() => item,
680                } {
681                    match result {
682                        // If an error happens while resolving the completion, display the first line
683                        Err(err) => {
684                            let mut state = state_clone.write();
685                            if let Some(line) = err.lines().next() {
686                                state.error.set_temp_message(line.to_string());
687                            }
688                        }
689                        // Otherwise, merge suggestions
690                        Ok(completion_suggestions) => {
691                            let mut state = state_clone.write();
692
693                            // Retrieve the current set of suggestions
694                            let master_suggestions = &mut state.variable_suggestions;
695
696                            // Remove all `Derived` items that are about to be added as a `Completion`
697                            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 each new suggestion given by the completion
707                            for suggestion in completion_suggestions {
708                                // Check if there's already a suggestion for the same value
709                                let mut skip_completion = false;
710                                for item in master_suggestions.iter_mut() {
711                                    match item {
712                                        // `New` items doesn't affect
713                                        VariableSuggestionItem::New { .. } => (),
714                                        // `Derived` are already handled above
715                                        VariableSuggestionItem::Derived { .. } => (),
716                                        // If already an environment, just skip completion
717                                        VariableSuggestionItem::Environment { content, is_value, .. } => {
718                                            if *is_value && content == &suggestion {
719                                                skip_completion = true;
720                                                break;
721                                            }
722                                        }
723                                        // If already an existing, boost its score and skip completion
724                                        VariableSuggestionItem::Existing {
725                                            value,
726                                            score,
727                                            completion_merged,
728                                            ..
729                                        } => {
730                                            if value.value == suggestion {
731                                                if !*completion_merged {
732                                                    *score += score_boost;
733                                                    *completion_merged = true;
734                                                }
735                                                skip_completion = true;
736                                                break;
737                                            }
738                                        }
739                                        // If already a completion, keep the maximum score and skip this one
740                                        VariableSuggestionItem::Completion { value, score, .. } => {
741                                            if value == &suggestion {
742                                                *score += score_boost.max(*score);
743                                                skip_completion = true;
744                                                break;
745                                            }
746                                        }
747                                    }
748                                }
749                                if skip_completion {
750                                    continue;
751                                }
752
753                                // Add the new suggestion
754                                master_suggestions.push(VariableSuggestionItem::Completion {
755                                    sort_index: 3,
756                                    value: suggestion,
757                                    score: score_boost,
758                                });
759                            }
760
761                            // Re-sort suggestions
762                            master_suggestions.sort_by(|a, b| {
763                                a.sort_index()
764                                    .cmp(&b.sort_index())
765                                    .then_with(|| b.score().partial_cmp(&a.score()).unwrap_or(Ordering::Equal))
766                            });
767
768                            // After sorting, filter suggestions
769                            let query = state
770                                .suggestions
771                                .items()
772                                .iter()
773                                .find_map(|s| match s {
774                                    VariableSuggestionItem::New {
775                                        textarea,
776                                        is_secret: false,
777                                        ..
778                                    } => Some(textarea.lines_as_string()),
779                                    _ => None,
780                                })
781                                .unwrap_or_default();
782                            state.filter_suggestions(&query);
783                        }
784                    }
785                }
786                state_clone.write().loading = None;
787            });
788        }
789
790        Ok(Action::NoOp)
791    }
792
793    #[instrument(skip_all)]
794    async fn confirm_new_secret_value(&mut self, value: String) -> Result<Action> {
795        tracing::debug!("Secret variable value selected");
796        self.state.write().template.set_next_variable(value);
797        self.update_variable_context(false).await
798    }
799
800    #[instrument(skip_all)]
801    async fn confirm_new_regular_value(&mut self, value: String) -> Result<Action> {
802        if !value.trim().is_empty() {
803            let variable_value = {
804                let state = self.state.read();
805                state.template.new_variable_value_for(&state.flat_variable_name, &value)
806            };
807            match self.service.insert_variable_value(variable_value).await {
808                Ok(v) => {
809                    tracing::debug!("New variable value stored");
810                    self.confirm_existing_value(v, true).await
811                }
812                Err(AppError::UserFacing(err)) => {
813                    tracing::warn!("{err}");
814                    self.state.write().error.set_temp_message(err.to_string());
815                    Ok(Action::NoOp)
816                }
817                Err(AppError::Unexpected(report)) => Err(report),
818            }
819        } else {
820            tracing::debug!("New empty variable value selected");
821            self.state.write().template.set_next_variable(value);
822            self.update_variable_context(false).await
823        }
824    }
825
826    #[instrument(skip_all)]
827    async fn confirm_existing_edition(&mut self, mut value: VariableValue, new_value: String) -> Result<Action> {
828        value.value = new_value;
829        match self.service.update_variable_value(value).await {
830            Ok(v) => {
831                let mut state = self.state.write();
832                if let VariableSuggestionItem::Existing { value, .. } = state.suggestions.selected_mut().unwrap() {
833                    *value = v;
834                };
835                Ok(Action::NoOp)
836            }
837            Err(AppError::UserFacing(err)) => {
838                tracing::warn!("{err}");
839                self.state.write().error.set_temp_message(err.to_string());
840                Ok(Action::NoOp)
841            }
842            Err(AppError::Unexpected(report)) => Err(report),
843        }
844    }
845
846    #[instrument(skip_all)]
847    async fn confirm_existing_value(&mut self, mut value: VariableValue, new: bool) -> Result<Action> {
848        let value_id = match value.id {
849            Some(id) => id,
850            None => {
851                value = self
852                    .service
853                    .insert_variable_value(value)
854                    .await
855                    .map_err(AppError::into_report)?;
856                value.id.expect("just inserted")
857            }
858        };
859        let context = self.state.read().template.current_variable_context();
860        match self
861            .service
862            .increment_variable_value_usage(value_id, context)
863            .await
864            .map_err(AppError::into_report)
865        {
866            Ok(_) => {
867                if !new {
868                    tracing::debug!("Existing variable value selected");
869                }
870                self.state.write().template.set_next_variable(value.value);
871                self.update_variable_context(false).await
872            }
873            Err(report) => Err(report),
874        }
875    }
876
877    #[instrument(skip_all)]
878    async fn confirm_literal_value(&mut self, value: String, store: bool) -> Result<Action> {
879        if store && !value.trim().is_empty() {
880            let variable_value = {
881                let state = self.state.read();
882                state.template.new_variable_value_for(&state.flat_variable_name, &value)
883            };
884            match self.service.insert_variable_value(variable_value).await {
885                Ok(v) => {
886                    tracing::debug!("Literal variable value selected and stored");
887                    self.confirm_existing_value(v, true).await
888                }
889                Err(AppError::UserFacing(err)) => {
890                    tracing::debug!("Literal variable value selected but couldn't be stored: {err}");
891                    self.state.write().template.set_next_variable(value);
892                    self.update_variable_context(false).await
893                }
894                Err(AppError::Unexpected(report)) => Err(report),
895            }
896        } else {
897            tracing::debug!("Literal variable value selected");
898            self.state.write().template.set_next_variable(value);
899            self.update_variable_context(false).await
900        }
901    }
902
903    /// Returns an action to quit the component, with the current variable content
904    fn quit_action(&self, peek: bool, cmd: String) -> Result<Action> {
905        if self.execute_mode {
906            Ok(Action::Quit(ProcessOutput::execute(cmd)))
907        } else if self.replace_process && peek {
908            Ok(Action::Quit(
909                ProcessOutput::success()
910                    .stderr(format_msg!(self.theme, "There are no variables to replace"))
911                    .stdout(&cmd)
912                    .fileout(cmd),
913            ))
914        } else {
915            Ok(Action::Quit(ProcessOutput::success().stdout(&cmd).fileout(cmd)))
916        }
917    }
918}