1use std::{
2 collections::{BTreeMap, HashMap},
3 env,
4};
5
6use futures_util::{Stream, StreamExt};
7use heck::ToShoutySnakeCase;
8use tracing::instrument;
9
10use super::IntelliShellService;
11use crate::{
12 errors::Result,
13 model::{CommandTemplate, TemplatePart, Variable, VariableSuggestion, VariableValue},
14 utils::{format_env_var, get_working_dir, resolve_completions},
15};
16
17impl IntelliShellService {
18 #[instrument(skip_all)]
22 pub fn replace_command_variables(
23 &self,
24 command: String,
25 values: Vec<(String, Option<String>)>,
26 use_env: bool,
27 ) -> Result<String, Vec<String>> {
28 let values: HashMap<String, Option<String>> =
30 values.into_iter().map(|(n, v)| (n.to_shouty_snake_case(), v)).collect();
31
32 let mut output = String::new();
33 let mut missing = Vec::new();
34
35 let template = CommandTemplate::parse(command, true);
37 for part in template.parts {
39 match part {
40 TemplatePart::VariableValue(_, _) => unreachable!(),
42 TemplatePart::Text(t) => output.push_str(&t),
44 TemplatePart::Variable(v) => {
46 let env_var_names = v.env_var_names(false);
47 let variable_value = env_var_names.iter().find_map(|env_var_name| values.get(env_var_name));
48 match (variable_value, use_env) {
49 (Some(Some(value)), _) => {
51 output.push_str(&v.apply_functions_to(value));
53 }
54 (Some(None), _) | (None, true) => {
56 let variable_value_env = env_var_names
58 .iter()
59 .find_map(|env_var_name| env::var(env_var_name).ok().map(|v| (env_var_name, v)));
60 match (variable_value_env, v.secret) {
61 (None, _) => missing.push(v.display),
63 (Some((_, env_value)), false) => {
65 output.push_str(&v.apply_functions_to(env_value));
66 }
67 (Some((env_var_name, _)), true) => {
69 output.push_str(&format_env_var(env_var_name));
71 }
72 }
73 }
74 _ => {
76 missing.push(v.display);
77 }
78 }
79 }
80 }
81 }
82
83 if !missing.is_empty() { Err(missing) } else { Ok(output) }
84 }
85
86 #[instrument(skip_all)]
88 #[allow(clippy::type_complexity)]
89 pub async fn search_variable_suggestions(
90 &self,
91 flat_root_cmd: &str,
92 variable: &Variable,
93 context: BTreeMap<String, String>,
94 ) -> Result<(
95 Vec<(u8, VariableSuggestion, f64)>,
96 Option<impl Stream<Item = (f64, Result<Vec<String>, String>)> + use<>>,
97 )> {
98 tracing::info!(
99 "Searching for variable suggestions: [{flat_root_cmd}] {}",
100 variable.flat_name
101 );
102
103 let mut suggestions = Vec::new();
104 let mut completion_stream = None;
105
106 if variable.secret {
107 suggestions.push((0, VariableSuggestion::Secret, 0.0));
109 let env_var_names = variable.env_var_names(true);
111 let env_var_len = env_var_names.len();
112 for (rev_ix, env_var_name) in env_var_names
113 .into_iter()
114 .enumerate()
115 .map(|(ix, item)| (env_var_len - 1 - ix, item))
116 {
117 if env::var(&env_var_name).is_ok() {
118 suggestions.push((
119 2,
120 VariableSuggestion::Environment {
121 env_var_name,
122 value: None,
123 },
124 rev_ix as f64,
125 ));
126 }
127 }
128 } else {
129 suggestions.push((0, VariableSuggestion::New, 0.0));
131
132 let mut existing_values = self
134 .storage
135 .find_variable_values(
136 flat_root_cmd,
137 &variable.flat_name,
138 variable.flat_names.clone(),
139 get_working_dir(),
140 &context,
141 &self.tuning.variables,
142 )
143 .await?;
144
145 let previous_value = context.get(&variable.flat_name).cloned();
147 if let Some(previous_value) = previous_value
148 && let Some(index) = existing_values.iter().position(|(s, _)| s.value == previous_value)
149 {
150 let (existing, _) = existing_values.remove(index);
152 suggestions.push((1, VariableSuggestion::Existing(existing), 0.0));
153 }
154
155 let env_var_names = variable.env_var_names(true);
157 let env_var_len = env_var_names.len();
158 for (rev_ix, env_var_name) in env_var_names
159 .into_iter()
160 .enumerate()
161 .map(|(ix, item)| (env_var_len - 1 - ix, item))
162 {
163 if let Ok(value) = env::var(&env_var_name)
164 && !value.trim().is_empty()
165 {
166 let value = variable.apply_functions_to(value);
167 if let Some(existing_index) = existing_values.iter().position(|(s, _)| s.value == value) {
169 let (existing, _) = existing_values.remove(existing_index);
171 suggestions.push((2, VariableSuggestion::Existing(existing), rev_ix as f64));
172 } else {
173 suggestions.push((
175 2,
176 VariableSuggestion::Environment {
177 env_var_name,
178 value: Some(value),
179 },
180 rev_ix as f64,
181 ));
182 };
183 }
184 }
185
186 suggestions.extend(
188 existing_values
189 .into_iter()
190 .map(|(s, score)| (3, VariableSuggestion::Existing(s), score)),
191 );
192
193 let options = variable
195 .options
196 .iter()
197 .filter(|o| {
198 !suggestions.iter().any(|(_, s, _)| match s {
199 VariableSuggestion::Environment { value: Some(value), .. } => value == *o,
200 VariableSuggestion::Existing(sv) => &sv.value == *o,
201 VariableSuggestion::Completion(value) => value == *o,
202 _ => false,
203 })
204 })
205 .map(|o| (4, VariableSuggestion::Derived(o.to_owned()), 0.0))
206 .collect::<Vec<_>>();
207 suggestions.extend(options);
208
209 let completions = self
211 .storage
212 .get_completions_for(flat_root_cmd, variable.flat_names.clone())
213 .await?;
214 if !completions.is_empty() {
215 let completion_points = self.tuning.variables.completion.points as f64;
216 let stream = resolve_completions(completions, context.clone()).await;
217 completion_stream =
218 Some(stream.map(move |(score_boost, result)| (completion_points + score_boost, result)));
219 }
220 }
221
222 Ok((suggestions, completion_stream))
223 }
224
225 #[instrument(skip_all)]
227 pub async fn insert_variable_value(&self, value: VariableValue) -> Result<VariableValue> {
228 tracing::info!(
229 "Inserting a variable value for '{}' '{}': {}",
230 value.flat_root_cmd,
231 value.flat_variable,
232 value.value
233 );
234 self.storage.insert_variable_value(value).await
235 }
236
237 #[instrument(skip_all)]
239 pub async fn update_variable_value(&self, value: VariableValue) -> Result<VariableValue> {
240 tracing::info!(
241 "Updating variable value '{}': {}",
242 value.id.unwrap_or_default(),
243 value.value
244 );
245 self.storage.update_variable_value(value).await
246 }
247
248 #[instrument(skip_all)]
250 pub async fn increment_variable_value_usage(
251 &self,
252 value_id: i32,
253 context: BTreeMap<String, String>,
254 ) -> Result<i32> {
255 tracing::info!("Increasing usage for variable value '{value_id}'");
256 self.storage
257 .increment_variable_value_usage(value_id, get_working_dir(), &context)
258 .await
259 }
260
261 #[instrument(skip_all)]
263 pub async fn delete_variable_value(&self, id: i32) -> Result<()> {
264 tracing::info!("Deleting variable value: {}", id);
265 self.storage.delete_variable_value(id).await
266 }
267}