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 previous_values: Option<Vec<String>>,
94 context: BTreeMap<String, String>,
95 ) -> Result<(
96 Vec<(u8, VariableSuggestion, f64)>,
97 Option<impl Stream<Item = (f64, Result<Vec<String>, String>)> + use<>>,
98 )> {
99 tracing::info!(
100 "Searching for variable suggestions: [{flat_root_cmd}] {}",
101 variable.flat_name
102 );
103
104 let mut suggestions = Vec::new();
105 if variable.secret {
106 suggestions.push((0, VariableSuggestion::Secret, 0.0));
108 if let Some(values) = previous_values {
110 for (ix, value) in values.into_iter().enumerate() {
111 suggestions.push((1, VariableSuggestion::Previous(value), ix as f64));
112 }
113 }
114 let env_var_names = variable.env_var_names(true);
116 let env_var_len = env_var_names.len();
117 for (rev_ix, env_var_name) in env_var_names
118 .into_iter()
119 .enumerate()
120 .map(|(ix, item)| (env_var_len - 1 - ix, item))
121 {
122 if env::var(&env_var_name).is_ok() {
123 suggestions.push((
124 2,
125 VariableSuggestion::Environment {
126 env_var_name,
127 value: None,
128 },
129 rev_ix as f64,
130 ));
131 }
132 }
133 } else {
134 suggestions.push((0, VariableSuggestion::New, 0.0));
136
137 let mut existing_values = self
139 .storage
140 .find_variable_values(
141 flat_root_cmd,
142 &variable.flat_name,
143 variable.flat_names.clone(),
144 get_working_dir(),
145 &context,
146 &self.tuning.variables,
147 )
148 .await?;
149
150 if let Some(values) = previous_values {
152 for (ix, value) in values.into_iter().enumerate() {
153 if let Some(index) = existing_values.iter().position(|(s, _)| s.value == value) {
155 let (existing, _) = existing_values.remove(index);
157 suggestions.push((1, VariableSuggestion::Existing(existing), ix as f64));
158 } else {
159 suggestions.push((1, VariableSuggestion::Previous(value), ix as f64));
161 }
162 }
163 }
164
165 let env_var_names = variable.env_var_names(true);
167 let env_var_len = env_var_names.len();
168 for (rev_ix, env_var_name) in env_var_names
169 .into_iter()
170 .enumerate()
171 .map(|(ix, item)| (env_var_len - 1 - ix, item))
172 {
173 if let Ok(value) = env::var(&env_var_name)
174 && !value.trim().is_empty()
175 {
176 let value = variable.apply_functions_to(value);
177 if let Some(existing_index) = existing_values.iter().position(|(s, _)| s.value == value) {
179 let (existing, _) = existing_values.remove(existing_index);
181 suggestions.push((2, VariableSuggestion::Existing(existing), rev_ix as f64));
182 } else {
183 suggestions.push((
185 2,
186 VariableSuggestion::Environment {
187 env_var_name,
188 value: Some(value),
189 },
190 rev_ix as f64,
191 ));
192 };
193 }
194 }
195
196 suggestions.extend(
198 existing_values
199 .into_iter()
200 .map(|(s, score)| (3, VariableSuggestion::Existing(s), score)),
201 );
202
203 let options = variable
205 .options
206 .iter()
207 .filter(|o| {
208 !suggestions.iter().any(|(_, s, _)| match s {
209 VariableSuggestion::Environment { value: Some(value), .. } => value == *o,
210 VariableSuggestion::Existing(sv) => &sv.value == *o,
211 VariableSuggestion::Completion(value) => value == *o,
212 _ => false,
213 })
214 })
215 .map(|o| (4, VariableSuggestion::Derived(o.to_owned()), 0.0))
216 .collect::<Vec<_>>();
217 suggestions.extend(options);
218 }
219
220 let completions = self
222 .storage
223 .get_completions_for(flat_root_cmd, variable.flat_names.clone())
224 .await?;
225 let completion_stream = if !completions.is_empty() {
226 let completion_points = self.tuning.variables.completion.points as f64;
227 let stream = resolve_completions(completions, context.clone()).await;
228 Some(stream.map(move |(score_boost, result)| (completion_points + score_boost, result)))
229 } else {
230 None
231 };
232
233 Ok((suggestions, completion_stream))
234 }
235
236 #[instrument(skip_all)]
238 pub async fn insert_variable_value(&self, value: VariableValue) -> Result<VariableValue> {
239 tracing::info!(
240 "Inserting a variable value for '{}' '{}': {}",
241 value.flat_root_cmd,
242 value.flat_variable,
243 value.value
244 );
245 self.storage.insert_variable_value(value).await
246 }
247
248 #[instrument(skip_all)]
250 pub async fn update_variable_value(&self, value: VariableValue) -> Result<VariableValue> {
251 tracing::info!(
252 "Updating variable value '{}': {}",
253 value.id.unwrap_or_default(),
254 value.value
255 );
256 self.storage.update_variable_value(value).await
257 }
258
259 #[instrument(skip_all)]
261 pub async fn increment_variable_value_usage(
262 &self,
263 value_id: i32,
264 context: BTreeMap<String, String>,
265 ) -> Result<i32> {
266 tracing::info!("Increasing usage for variable value '{value_id}'");
267 self.storage
268 .increment_variable_value_usage(value_id, get_working_dir(), &context)
269 .await
270 }
271
272 #[instrument(skip_all)]
274 pub async fn delete_variable_value(&self, id: i32) -> Result<()> {
275 tracing::info!("Deleting variable value: {}", id);
276 self.storage.delete_variable_value(id).await
277 }
278}