use std::{
cmp::Ordering,
collections::HashSet,
sync::{Arc, Mutex},
time::Duration,
};
use async_trait::async_trait;
use color_eyre::Result;
use crossterm::event::{MouseEvent, MouseEventKind};
use futures_util::StreamExt;
use parking_lot::RwLock;
use ratatui::{
Frame,
layout::{Constraint, Layout, Rect},
};
use tokio_util::sync::CancellationToken;
use tracing::instrument;
use super::Component;
use crate::{
app::Action,
config::Theme,
errors::AppError,
format_msg,
model::{CommandTemplate, VariableValue},
process::ProcessOutput,
service::IntelliShellService,
widgets::{
CommandTemplateWidget, CustomList, CustomTextArea, ErrorPopup, LoadingSpinner, NewVersionBanner,
items::VariableSuggestionItem,
},
};
const INITIAL_COMPLETION_WAIT: Duration = Duration::from_millis(250);
#[derive(Clone)]
pub struct VariableReplacementComponent {
theme: Theme,
inline: bool,
service: IntelliShellService,
layout: Layout,
execute_mode: bool,
replace_process: bool,
global_cancellation_token: CancellationToken,
cancellation_token: Arc<Mutex<Option<CancellationToken>>>,
state: Arc<RwLock<VariableReplacementComponentState<'static>>>,
}
struct VariableReplacementComponentState<'a> {
template: CommandTemplateWidget,
current_variable_ctx: (String, bool),
variable_suggestions: Vec<VariableSuggestionItem<'static>>,
suggestions: CustomList<'a, VariableSuggestionItem<'a>>,
error: ErrorPopup<'a>,
loading: Option<LoadingSpinner<'a>>,
current_variable_index: usize,
variable_values: Vec<Option<String>>,
confirmed_variables: Vec<usize>,
redo_stack: Vec<(usize, String)>,
}
impl VariableReplacementComponent {
pub fn new(
service: IntelliShellService,
theme: Theme,
inline: bool,
execute_mode: bool,
replace_process: bool,
command: CommandTemplate,
cancellation_token: CancellationToken,
) -> Self {
let command = CommandTemplateWidget::new(&theme, inline, command);
let suggestions = CustomList::new(theme.clone(), inline, Vec::new());
let error = ErrorPopup::empty(&theme);
let layout = if inline {
Layout::vertical([Constraint::Length(1), Constraint::Min(3)])
} else {
Layout::vertical([Constraint::Length(3), Constraint::Min(5)]).margin(1)
};
let total_vars = command.count_variables();
let variable_values = vec![None; total_vars];
Self {
theme,
inline,
service,
layout,
execute_mode,
replace_process,
cancellation_token: Arc::new(Mutex::new(None)),
global_cancellation_token: cancellation_token,
state: Arc::new(RwLock::new(VariableReplacementComponentState {
template: command,
current_variable_ctx: (String::new(), true),
variable_suggestions: Vec::new(),
suggestions,
error,
loading: None,
current_variable_index: 0,
variable_values,
confirmed_variables: Vec::new(),
redo_stack: Vec::new(),
})),
}
}
}
#[async_trait]
impl Component for VariableReplacementComponent {
fn name(&self) -> &'static str {
"VariableReplacementComponent"
}
fn min_inline_height(&self) -> u16 {
1 + 5
}
#[instrument(skip_all)]
async fn init_and_peek(&mut self) -> Result<Action> {
self.update_variable_context(true).await
}
#[instrument(skip_all)]
fn render(&mut self, frame: &mut Frame, area: Rect) {
let [cmd_area, suggestions_area] = self.layout.areas(area);
let mut state = self.state.write();
let values = state.variable_values.clone();
state.template.set_variable_values(&values);
state.template.current_variable_index = state.current_variable_index;
frame.render_widget(&state.template, cmd_area);
frame.render_widget(&mut state.suggestions, suggestions_area);
if let Some(new_version) = self.service.poll_new_version() {
NewVersionBanner::new(&self.theme, new_version).render_in(frame, area);
}
state.error.render_in(frame, area);
if let Some(loading) = &state.loading {
let loading_area = if self.inline {
Rect {
x: suggestions_area.x,
y: suggestions_area.y + suggestions_area.height.saturating_sub(1),
width: 1,
height: 1,
}
} else {
Rect {
x: suggestions_area.x.saturating_add(1),
y: suggestions_area.y + suggestions_area.height.saturating_sub(2),
width: 1,
height: 1,
}
};
loading.render_in(frame, loading_area);
}
}
fn tick(&mut self) -> Result<Action> {
let mut state = self.state.write();
state.error.tick();
if let Some(loading) = &mut state.loading {
loading.tick();
}
Ok(Action::NoOp)
}
fn exit(&mut self) -> Result<Action> {
{
let mut token_guard = self.cancellation_token.lock().unwrap();
if let Some(token) = token_guard.take() {
token.cancel();
}
}
let mut state = self.state.write();
if let Some(VariableSuggestionItem::Existing { editing, .. }) = state.suggestions.selected_mut()
&& editing.is_some()
{
tracing::debug!("Closing variable value edit mode: user request");
*editing = None;
Ok(Action::NoOp)
} else {
tracing::info!("User requested to exit");
Ok(Action::Quit(
ProcessOutput::success().fileout(state.template.to_string()),
))
}
}
fn process_mouse_event(&mut self, mouse: MouseEvent) -> Result<Action> {
match mouse.kind {
MouseEventKind::ScrollDown => Ok(self.move_next()?),
MouseEventKind::ScrollUp => Ok(self.move_prev()?),
_ => Ok(Action::NoOp),
}
}
fn move_up(&mut self) -> Result<Action> {
let mut state = self.state.write();
match state.suggestions.selected() {
Some(VariableSuggestionItem::Existing { editing: Some(_), .. }) => (),
_ => state.suggestions.select_prev(),
}
Ok(Action::NoOp)
}
fn move_down(&mut self) -> Result<Action> {
let mut state = self.state.write();
match state.suggestions.selected() {
Some(VariableSuggestionItem::Existing { editing: Some(_), .. }) => (),
_ => state.suggestions.select_next(),
}
Ok(Action::NoOp)
}
fn move_left(&mut self, word: bool) -> Result<Action> {
let mut state = self.state.write();
match state.suggestions.selected_mut() {
Some(VariableSuggestionItem::New { textarea, .. }) => {
textarea.move_cursor_left(word);
}
Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
ta.move_cursor_left(word);
}
_ => (),
}
Ok(Action::NoOp)
}
fn move_right(&mut self, word: bool) -> Result<Action> {
let mut state = self.state.write();
if let Some(item) = state.suggestions.selected_mut()
&& let VariableSuggestionItem::New { textarea, .. }
| VariableSuggestionItem::Existing {
editing: Some(textarea),
..
} = item
{
textarea.move_cursor_right(word);
return Ok(Action::NoOp);
}
let text_to_copy = match state.suggestions.selected() {
None | Some(VariableSuggestionItem::New { .. }) => None,
Some(VariableSuggestionItem::Existing { value, .. }) => Some(value.value.clone()),
Some(
VariableSuggestionItem::Previous { value, .. }
| VariableSuggestionItem::Environment { content: value, .. }
| VariableSuggestionItem::Completion { value, .. }
| VariableSuggestionItem::Derived { value, .. },
) => Some(value.clone()),
};
if let Some(text) = text_to_copy
&& !text.is_empty()
&& let Some(VariableSuggestionItem::New { .. }) = state.suggestions.items().iter().next()
{
state.suggestions.select_first();
if let Some(VariableSuggestionItem::New { textarea, .. }) = state.suggestions.selected_mut() {
if !textarea.lines_as_string().is_empty() {
textarea.select_all();
textarea.cut();
}
textarea.insert_str(&text);
state.filter_suggestions(&text);
}
}
Ok(Action::NoOp)
}
fn move_prev(&mut self) -> Result<Action> {
self.move_up()
}
fn move_next(&mut self) -> Result<Action> {
self.move_down()
}
fn move_prev_variable(&mut self) -> Result<Action> {
let mut state = self.state.write();
if matches!(
state.suggestions.selected(),
Some(VariableSuggestionItem::Existing { editing: Some(_), .. })
) {
return Ok(Action::NoOp);
}
let total_vars = state.template.count_variables();
if total_vars <= 1 {
return Ok(Action::NoOp);
}
if state.current_variable_index == 0 {
state.current_variable_index = total_vars - 1; } else {
state.current_variable_index -= 1;
}
drop(state);
self.debounced_update_variable_context();
Ok(Action::NoOp)
}
fn move_next_variable(&mut self) -> Result<Action> {
let mut state = self.state.write();
if matches!(
state.suggestions.selected(),
Some(VariableSuggestionItem::Existing { editing: Some(_), .. })
) {
return Ok(Action::NoOp);
}
let total_vars = state.template.count_variables();
if total_vars <= 1 {
return Ok(Action::NoOp);
}
state.current_variable_index += 1;
if state.current_variable_index >= total_vars {
state.current_variable_index = 0; }
drop(state);
self.debounced_update_variable_context();
Ok(Action::NoOp)
}
fn move_home(&mut self, absolute: bool) -> Result<Action> {
let mut state = self.state.write();
match state.suggestions.selected_mut() {
Some(VariableSuggestionItem::New { textarea, .. }) => {
textarea.move_home(absolute);
}
Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
ta.move_home(absolute);
}
_ => state.suggestions.select_first(),
}
Ok(Action::NoOp)
}
fn move_end(&mut self, absolute: bool) -> Result<Action> {
let mut state = self.state.write();
match state.suggestions.selected_mut() {
Some(VariableSuggestionItem::New { textarea, .. }) => {
textarea.move_end(absolute);
}
Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
ta.move_end(absolute);
}
_ => state.suggestions.select_last(),
}
Ok(Action::NoOp)
}
fn undo(&mut self) -> Result<Action> {
let mut state = self.state.write();
match state.suggestions.selected_mut() {
Some(VariableSuggestionItem::New { textarea, .. }) => {
textarea.undo();
let query = textarea.lines_as_string();
state.filter_suggestions(&query);
}
Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
ta.undo();
}
_ => {
if let Some(last_index) = state.confirmed_variables.pop()
&& let Some(value) = state.variable_values[last_index].take()
{
state.redo_stack.push((last_index, value));
state.current_variable_index = last_index;
self.debounced_update_variable_context();
}
}
}
Ok(Action::NoOp)
}
fn redo(&mut self) -> Result<Action> {
let mut state = self.state.write();
match state.suggestions.selected_mut() {
Some(VariableSuggestionItem::New { textarea, .. }) => {
textarea.redo();
let query = textarea.lines_as_string();
state.filter_suggestions(&query);
}
Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
ta.redo();
}
_ => {
if let Some((index, value)) = state.redo_stack.pop() {
state.variable_values[index] = Some(value.clone());
state.confirmed_variables.push(index);
state.current_variable_index = index + 1;
self.debounced_update_variable_context();
}
}
}
Ok(Action::NoOp)
}
fn insert_text(&mut self, mut text: String) -> Result<Action> {
let mut state = self.state.write();
let current_index = state.current_variable_index;
if let Some(variable) = state.template.variable_at(current_index) {
text = variable.apply_functions_to(text);
}
match state.suggestions.selected_mut() {
Some(VariableSuggestionItem::New { textarea, .. }) => {
textarea.insert_str(text);
let query = textarea.lines_as_string();
state.filter_suggestions(&query);
}
Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
ta.insert_str(text);
}
_ => (),
}
Ok(Action::NoOp)
}
fn insert_char(&mut self, c: char) -> Result<Action> {
let mut state = self.state.write();
let current_index = state.current_variable_index;
let maybe_replacement = state
.template
.variable_at(current_index)
.and_then(|variable| variable.check_functions_char(c));
let insert_content = |ta: &mut CustomTextArea<'_>| {
if let Some(r) = &maybe_replacement {
ta.insert_str(r);
} else {
ta.insert_char(c);
}
};
match state.suggestions.selected_mut() {
Some(VariableSuggestionItem::New { textarea, .. }) => {
insert_content(textarea);
let query = textarea.lines_as_string();
state.filter_suggestions(&query);
}
Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
insert_content(ta);
}
_ => {
if let Some(VariableSuggestionItem::New { .. }) = state.suggestions.items().iter().next() {
state.suggestions.select_first();
if let Some(VariableSuggestionItem::New { textarea, .. }) = state.suggestions.selected_mut() {
insert_content(textarea);
let query = textarea.lines_as_string();
state.filter_suggestions(&query);
}
}
}
}
Ok(Action::NoOp)
}
fn delete(&mut self, backspace: bool, word: bool) -> Result<Action> {
let mut state = self.state.write();
match state.suggestions.selected_mut() {
Some(VariableSuggestionItem::New { textarea, .. }) => {
textarea.delete(backspace, word);
let query = textarea.lines_as_string();
state.filter_suggestions(&query);
}
Some(VariableSuggestionItem::Existing { editing: Some(ta), .. }) => {
ta.delete(backspace, word);
}
_ => (),
}
Ok(Action::NoOp)
}
#[instrument(skip_all)]
async fn selection_delete(&mut self) -> Result<Action> {
let deleted_id = {
let mut state = self.state.write();
match state.suggestions.selected_mut() {
Some(VariableSuggestionItem::New { .. }) => return Ok(Action::NoOp),
Some(VariableSuggestionItem::Existing {
value: VariableValue { id: Some(id), .. },
editing,
..
}) => {
if editing.is_none() {
let id = *id;
state.suggestions.delete_selected();
id
} else {
return Ok(Action::NoOp);
}
}
_ => {
state.error.set_temp_message("This value is not yet stored");
return Ok(Action::NoOp);
}
}
};
self.service
.delete_variable_value(deleted_id)
.await
.map_err(AppError::into_report)?;
self.state
.write()
.variable_suggestions
.retain(|s| !matches!(s, VariableSuggestionItem::Existing { value, .. } if value.id == Some(deleted_id)));
Ok(Action::NoOp)
}
#[instrument(skip_all)]
async fn selection_update(&mut self) -> Result<Action> {
let mut state = self.state.write();
match state.suggestions.selected_mut() {
Some(VariableSuggestionItem::New { .. }) => (),
Some(i @ VariableSuggestionItem::Existing { .. }) => {
if let VariableSuggestionItem::Existing { value, editing, .. } = i {
if let Some(id) = value.id {
if editing.is_none() {
tracing::debug!("Entering edit mode for existing variable value: {id}");
i.enter_edit_mode();
}
} else {
state.error.set_temp_message("This value is not yet stored");
}
}
}
_ => state.error.set_temp_message("This value is not yet stored"),
}
Ok(Action::NoOp)
}
async fn selection_confirm(&mut self) -> Result<Action> {
{
let mut token_guard = self.cancellation_token.lock().unwrap();
if let Some(token) = token_guard.take() {
token.cancel();
}
}
enum NextAction {
NoOp,
ConfirmNewSecret(String),
ConfirmNewRegular(String),
ConfirmExistingEdition(VariableValue, String),
ConfirmExistingValue(VariableValue),
ConfirmLiteral(String, bool),
}
let next_action = {
let mut state = self.state.write();
let (_, is_secret) = state.current_variable_ctx;
match state.suggestions.selected_mut() {
None => NextAction::NoOp,
Some(VariableSuggestionItem::New {
textarea,
is_secret: true,
..
}) => NextAction::ConfirmNewSecret(textarea.lines_as_string()),
Some(VariableSuggestionItem::New {
textarea,
is_secret: false,
..
}) => NextAction::ConfirmNewRegular(textarea.lines_as_string()),
Some(VariableSuggestionItem::Existing { value, editing, .. }) => match editing.take() {
Some(ta) => NextAction::ConfirmExistingEdition(value.clone(), ta.lines_as_string()),
None => NextAction::ConfirmExistingValue(value.clone()),
},
Some(VariableSuggestionItem::Environment {
content,
is_value: false,
..
}) => NextAction::ConfirmLiteral(content.clone(), false),
Some(VariableSuggestionItem::Environment {
content: value,
is_value: true,
..
})
| Some(VariableSuggestionItem::Previous { value, .. })
| Some(VariableSuggestionItem::Completion { value, .. })
| Some(VariableSuggestionItem::Derived { value, .. }) => {
NextAction::ConfirmLiteral(value.clone(), !is_secret)
}
}
};
match next_action {
NextAction::NoOp => Ok(Action::NoOp),
NextAction::ConfirmNewSecret(value) => self.confirm_new_secret_value(value).await,
NextAction::ConfirmNewRegular(value) => self.confirm_new_regular_value(value).await,
NextAction::ConfirmExistingEdition(val, new_val) => self.confirm_existing_edition(val, new_val).await,
NextAction::ConfirmExistingValue(val) => self.confirm_existing_value(val, false).await,
NextAction::ConfirmLiteral(val, is_value) => self.confirm_literal_value(val, is_value).await,
}
}
async fn selection_execute(&mut self) -> Result<Action> {
self.selection_confirm().await
}
}
impl<'a> VariableReplacementComponentState<'a> {
fn filter_suggestions(&mut self, query: &str) {
tracing::debug!("Filtering suggestions for: {query}");
let mut filtered_suggestions = self.variable_suggestions.clone();
filtered_suggestions.retain(|s| match s {
VariableSuggestionItem::New { .. } => false,
VariableSuggestionItem::Existing { value, .. } => value_matches_filter_query(&value.value, query),
VariableSuggestionItem::Previous { value, .. }
| VariableSuggestionItem::Environment { content: value, .. }
| VariableSuggestionItem::Completion { value, .. }
| VariableSuggestionItem::Derived { value, .. } => value_matches_filter_query(value, query),
});
let new_row = self
.suggestions
.items()
.iter()
.find(|s| matches!(s, VariableSuggestionItem::New { .. }));
if let Some(new_row) = new_row.cloned() {
filtered_suggestions.insert(0, new_row);
}
let selected_id = self.suggestions.selected().map(|s| s.identifier());
self.suggestions.update_items(filtered_suggestions, false);
if let Some(selected_id) = selected_id {
self.suggestions.select_matching(|i| i.identifier() == selected_id);
}
}
fn merge_completions(&mut self, score_boost: f64, completion_suggestions: Vec<String>) {
let master_suggestions = &mut self.variable_suggestions;
let completion_set = completion_suggestions.iter().collect::<HashSet<_>>();
master_suggestions.retain_mut(|item| {
!matches!(
item,
VariableSuggestionItem::Derived { value, .. }
if completion_set.contains(value)
)
});
for suggestion in completion_suggestions {
let mut skip_completion = false;
for item in master_suggestions.iter_mut() {
match item {
VariableSuggestionItem::New { .. } => (),
VariableSuggestionItem::Derived { .. } => (),
VariableSuggestionItem::Previous { value, .. } => {
if value == &suggestion {
skip_completion = true;
break;
}
}
VariableSuggestionItem::Environment { content, is_value, .. } => {
if *is_value && content == &suggestion {
skip_completion = true;
break;
}
}
VariableSuggestionItem::Existing {
value,
score,
completion_merged,
..
} => {
if value.value == suggestion {
if !*completion_merged {
*score += score_boost;
*completion_merged = true;
}
skip_completion = true;
break;
}
}
VariableSuggestionItem::Completion { value, score, .. } => {
if value == &suggestion {
*score = score.max(score_boost);
skip_completion = true;
break;
}
}
}
}
if skip_completion {
continue;
}
master_suggestions.push(VariableSuggestionItem::Completion {
sort_index: 3,
value: suggestion,
score: score_boost,
});
}
master_suggestions.sort_by(|a, b| {
a.sort_index()
.cmp(&b.sort_index())
.then_with(|| b.score().partial_cmp(&a.score()).unwrap_or(Ordering::Equal))
});
let query = self
.suggestions
.items()
.iter()
.find_map(|s| match s {
VariableSuggestionItem::New { textarea, .. } => Some(textarea.lines_as_string()),
_ => None,
})
.unwrap_or_default();
self.filter_suggestions(&query);
}
}
impl VariableReplacementComponent {
fn debounced_update_variable_context(&self) {
let this = self.clone();
tokio::spawn(async move {
if let Err(err) = this.update_variable_context(false).await {
tracing::error!("Error updating variable context: {err:?}");
}
});
}
fn move_to_next_variable_with_value(&self, value: String) {
let mut state = self.state.write();
let current_index = state.current_variable_index;
state.variable_values[current_index] = Some(value);
state.confirmed_variables.push(current_index);
state.redo_stack.clear();
state.current_variable_index += 1;
if state.current_variable_index >= state.variable_values.len() {
let has_pending = state.variable_values.iter().any(|v| v.is_none());
if has_pending {
state.current_variable_index = 0;
}
}
}
async fn update_variable_context(&self, peek: bool) -> Result<Action> {
{
let mut state = self.state.write();
let values = state.variable_values.clone();
state.template.set_variable_values(&values);
}
let cancellation_token = {
let mut token_guard = self.cancellation_token.lock().unwrap();
if let Some(token) = token_guard.take() {
token.cancel();
}
let new_token = CancellationToken::new();
*token_guard = Some(new_token.clone());
new_token
};
let (flat_root_cmd, previous_values, current_variable, context, current_stored_value) = {
let state = self.state.read();
let current_index = state.current_variable_index;
match state.template.variable_at(current_index).cloned() {
Some(variable) => (
state.template.flat_root_cmd.clone(),
state.template.previous_values_for(&variable.flat_name),
variable,
state.template.variable_context(),
state.variable_values.get(current_index).and_then(|v| v.clone()),
),
None => {
if peek {
tracing::info!("There are no variables to replace");
} else {
tracing::info!("There are no more variables");
}
return self.quit_action(peek, state.template.to_string());
}
}
};
let (initial_suggestions, completion_stream) = self
.service
.search_variable_suggestions(&flat_root_cmd, ¤t_variable, previous_values, context)
.await
.map_err(AppError::into_report)?;
{
let mut state = self.state.write();
let suggestions = initial_suggestions
.into_iter()
.map(VariableSuggestionItem::from)
.collect::<Vec<_>>();
state.current_variable_ctx = (current_variable.flat_name.clone(), current_variable.secret);
state.variable_suggestions = suggestions.clone();
state.suggestions.update_items(suggestions, false);
}
let remaining_stream = if let Some(mut stream) = completion_stream {
let sleep = tokio::time::sleep(INITIAL_COMPLETION_WAIT);
tokio::pin!(sleep);
let mut has_more_items = true;
loop {
tokio::select! {
biased;
_ = &mut sleep => {
tracing::debug!(
"There are pending completions after initial {}ms wait, spawning a background task",
INITIAL_COMPLETION_WAIT.as_millis()
);
break;
}
item = stream.next() => {
if let Some((score_boost, result)) = item {
match result {
Err(err) => {
if let Some(line) = err.lines().next() {
self.state.write().error.set_temp_message(line.to_string());
}
}
Ok(completion_suggestions) => {
self.state.write().merge_completions(score_boost, completion_suggestions);
}
}
} else {
tracing::debug!(
"All completions were resolved on the initial {}ms window",
INITIAL_COMPLETION_WAIT.as_millis()
);
has_more_items = false;
break;
}
}
}
}
if has_more_items { Some(stream) } else { None }
} else {
None
};
{
let mut state = self.state.write();
let mut selected = false;
if let Some(ref stored_value) = current_stored_value
&& let Some(idx) = state.suggestions.items().iter().position(|item| match item {
VariableSuggestionItem::Existing { value, .. } => &value.value == stored_value,
VariableSuggestionItem::Previous { value, .. } => value == stored_value,
VariableSuggestionItem::Environment { content, .. } => content == stored_value,
VariableSuggestionItem::Completion { value, .. } => value == stored_value,
VariableSuggestionItem::Derived { value, .. } => value == stored_value,
VariableSuggestionItem::New { .. } => false,
})
{
state.suggestions.select(idx);
selected = true;
}
if !selected
&& let Some(idx) = state.suggestions.items().iter().position(|s| {
!matches!(
s,
VariableSuggestionItem::New { .. } | VariableSuggestionItem::Derived { .. }
)
})
{
state.suggestions.select(idx);
}
}
if let Some(mut stream) = remaining_stream {
let token = cancellation_token.clone();
let global_token = self.global_cancellation_token.clone();
let state_clone = self.state.clone();
self.state.write().loading = Some(LoadingSpinner::new(&self.theme));
tokio::spawn(async move {
while let Some((score_boost, result)) = tokio::select! {
biased;
_ = token.cancelled() => None,
_ = global_token.cancelled() => None,
item = stream.next() => item,
} {
match result {
Err(err) => {
if let Some(line) = err.lines().next() {
state_clone.write().error.set_temp_message(line.to_string());
}
}
Ok(completion_suggestions) => {
state_clone
.write()
.merge_completions(score_boost, completion_suggestions);
}
}
}
state_clone.write().loading = None;
});
}
Ok(Action::NoOp)
}
#[instrument(skip_all)]
async fn confirm_new_secret_value(&mut self, value: String) -> Result<Action> {
tracing::debug!("Secret variable value selected");
self.move_to_next_variable_with_value(value);
self.update_variable_context(false).await
}
#[instrument(skip_all)]
async fn confirm_new_regular_value(&mut self, value: String) -> Result<Action> {
if !value.trim().is_empty() {
let variable_value = {
let state = self.state.read();
let (flat_variable_name, _) = &state.current_variable_ctx;
state.template.new_variable_value_for(flat_variable_name, &value)
};
match self.service.insert_variable_value(variable_value).await {
Ok(v) => {
tracing::debug!("New variable value stored");
self.confirm_existing_value(v, true).await
}
Err(AppError::UserFacing(err)) => {
tracing::warn!("{err}");
self.state.write().error.set_temp_message(err.to_string());
Ok(Action::NoOp)
}
Err(AppError::Unexpected(report)) => Err(report),
}
} else {
tracing::debug!("New empty variable value selected");
self.move_to_next_variable_with_value(value);
self.update_variable_context(false).await
}
}
#[instrument(skip_all)]
async fn confirm_existing_edition(&mut self, mut value: VariableValue, new_value: String) -> Result<Action> {
value.value = new_value;
match self.service.update_variable_value(value).await {
Ok(v) => {
let mut state = self.state.write();
if let VariableSuggestionItem::Existing { value, .. } = state.suggestions.selected_mut().unwrap() {
*value = v;
};
Ok(Action::NoOp)
}
Err(AppError::UserFacing(err)) => {
tracing::warn!("{err}");
self.state.write().error.set_temp_message(err.to_string());
Ok(Action::NoOp)
}
Err(AppError::Unexpected(report)) => Err(report),
}
}
#[instrument(skip_all)]
async fn confirm_existing_value(&mut self, mut value: VariableValue, new: bool) -> Result<Action> {
let value_id = match value.id {
Some(id) => id,
None => {
value = self
.service
.insert_variable_value(value)
.await
.map_err(AppError::into_report)?;
value.id.expect("just inserted")
}
};
let context = self.state.read().template.variable_context();
match self
.service
.increment_variable_value_usage(value_id, context)
.await
.map_err(AppError::into_report)
{
Ok(_) => {
if !new {
tracing::debug!("Existing variable value selected");
}
self.move_to_next_variable_with_value(value.value);
self.update_variable_context(false).await
}
Err(report) => Err(report),
}
}
#[instrument(skip_all)]
async fn confirm_literal_value(&mut self, value: String, store: bool) -> Result<Action> {
if store && !value.trim().is_empty() {
let variable_value = {
let state = self.state.read();
let (flat_variable_name, _) = &state.current_variable_ctx;
state.template.new_variable_value_for(flat_variable_name, &value)
};
match self.service.insert_variable_value(variable_value).await {
Ok(v) => {
tracing::debug!("Literal variable value selected and stored");
self.confirm_existing_value(v, true).await
}
Err(AppError::UserFacing(err)) => {
tracing::debug!("Literal variable value selected but couldn't be stored: {err}");
self.move_to_next_variable_with_value(value);
self.update_variable_context(false).await
}
Err(AppError::Unexpected(report)) => Err(report),
}
} else {
tracing::debug!("Literal variable value selected");
self.move_to_next_variable_with_value(value);
self.update_variable_context(false).await
}
}
fn quit_action(&self, peek: bool, cmd: String) -> Result<Action> {
if self.execute_mode {
Ok(Action::Quit(ProcessOutput::execute(cmd)))
} else if self.replace_process && peek {
Ok(Action::Quit(
ProcessOutput::success()
.stderr(format_msg!(self.theme, "There are no variables to replace"))
.stdout(&cmd)
.fileout(cmd),
))
} else {
Ok(Action::Quit(ProcessOutput::success().stdout(&cmd).fileout(cmd)))
}
}
}
fn value_matches_filter_query(value: &str, query: &str) -> bool {
let mut search_offset = 0;
query.split_whitespace().all(|word| {
if let Some(relative_pos) = value[search_offset..].find(word) {
search_offset += relative_pos + 1;
true
} else {
false
}
})
}