Display more specific tasks above in the modal (#10485)

This commit is contained in:
Kirill Bulatov 2024-04-12 20:19:11 +02:00 committed by GitHub
parent 49371b44cb
commit 28586060a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 262 additions and 148 deletions

View file

@ -5,7 +5,7 @@ pub mod static_source;
mod task_template;
mod vscode_format;
use collections::HashMap;
use collections::{HashMap, HashSet};
use gpui::ModelContext;
use serde::Serialize;
use std::any::Any;
@ -55,14 +55,28 @@ pub struct ResolvedTask {
/// so it's impossible to determine the id equality without more context in a generic case.
pub id: TaskId,
/// A template the task got resolved from.
pub original_task: TaskTemplate,
original_task: TaskTemplate,
/// Full, unshortened label of the task after all resolutions are made.
pub resolved_label: String,
/// Variables that were substituted during the task template resolution.
substituted_variables: HashSet<VariableName>,
/// Further actions that need to take place after the resolved task is spawned,
/// with all task variables resolved.
pub resolved: Option<SpawnInTerminal>,
}
impl ResolvedTask {
/// A task template before the resolution.
pub fn original_task(&self) -> &TaskTemplate {
&self.original_task
}
/// Variables that were substituted during the task template resolution.
pub fn substituted_variables(&self) -> &HashSet<VariableName> {
&self.substituted_variables
}
}
/// Variables, available for use in [`TaskContext`] when a Zed's [`TaskTemplate`] gets resolved into a [`ResolvedTask`].
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub enum VariableName {
@ -117,14 +131,6 @@ impl std::fmt::Display for VariableName {
pub struct TaskVariables(HashMap<VariableName, String>);
impl TaskVariables {
/// Converts the container into a map of environment variables and their values.
fn into_env_variables(self) -> HashMap<String, String> {
self.0
.into_iter()
.map(|(name, value)| (name.to_string(), value))
.collect()
}
/// Inserts another variable into the container, overwriting the existing one if it already exists — in this case, the old value is returned.
pub fn insert(&mut self, variable: VariableName, value: String) -> Option<String> {
self.0.insert(variable, value)

View file

@ -1,13 +1,15 @@
use std::path::PathBuf;
use anyhow::{bail, Context};
use collections::HashMap;
use collections::{HashMap, HashSet};
use schemars::{gen::SchemaSettings, JsonSchema};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use util::{truncate_and_remove_front, ResultExt};
use crate::{ResolvedTask, SpawnInTerminal, TaskContext, TaskId, ZED_VARIABLE_NAME_PREFIX};
use crate::{
ResolvedTask, SpawnInTerminal, TaskContext, TaskId, VariableName, ZED_VARIABLE_NAME_PREFIX,
};
/// A template definition of a Zed task to run.
/// May use the [`VariableName`] to get the corresponding substitutions into its fields.
@ -78,30 +80,64 @@ impl TaskTemplate {
///
/// Every [`ResolvedTask`] gets a [`TaskId`], based on the `id_base` (to avoid collision with various task sources),
/// and hashes of its template and [`TaskContext`], see [`ResolvedTask`] fields' documentation for more details.
pub fn resolve_task(&self, id_base: &str, cx: TaskContext) -> Option<ResolvedTask> {
pub fn resolve_task(&self, id_base: &str, cx: &TaskContext) -> Option<ResolvedTask> {
if self.label.trim().is_empty() || self.command.trim().is_empty() {
return None;
}
let TaskContext {
cwd,
task_variables,
} = cx;
let task_variables = task_variables.into_env_variables();
let mut variable_names = HashMap::default();
let mut substituted_variables = HashSet::default();
let task_variables = cx
.task_variables
.0
.iter()
.map(|(key, value)| {
let key_string = key.to_string();
if !variable_names.contains_key(&key_string) {
variable_names.insert(key_string.clone(), key.clone());
}
(key_string, value.as_str())
})
.collect::<HashMap<_, _>>();
let truncated_variables = truncate_variables(&task_variables);
let cwd = match self.cwd.as_deref() {
Some(cwd) => Some(substitute_all_template_variables_in_str(
cwd,
&task_variables,
)?),
Some(cwd) => {
let substitured_cwd = substitute_all_template_variables_in_str(
cwd,
&task_variables,
&variable_names,
&mut substituted_variables,
)?;
Some(substitured_cwd)
}
None => None,
}
.map(PathBuf::from)
.or(cwd);
let shortened_label =
substitute_all_template_variables_in_str(&self.label, &truncated_variables)?;
let full_label = substitute_all_template_variables_in_str(&self.label, &task_variables)?;
let command = substitute_all_template_variables_in_str(&self.command, &task_variables)?;
let args = substitute_all_template_variables_in_vec(self.args.clone(), &task_variables)?;
.or(cx.cwd.clone());
let shortened_label = substitute_all_template_variables_in_str(
&self.label,
&truncated_variables,
&variable_names,
&mut substituted_variables,
)?;
let full_label = substitute_all_template_variables_in_str(
&self.label,
&task_variables,
&variable_names,
&mut substituted_variables,
)?;
let command = substitute_all_template_variables_in_str(
&self.command,
&task_variables,
&variable_names,
&mut substituted_variables,
)?;
let args = substitute_all_template_variables_in_vec(
&self.args,
&task_variables,
&variable_names,
&mut substituted_variables,
)?;
let task_hash = to_hex_hash(&self)
.context("hashing task template")
@ -110,10 +146,16 @@ impl TaskTemplate {
.context("hashing task variables")
.log_err()?;
let id = TaskId(format!("{id_base}_{task_hash}_{variables_hash}"));
let mut env = substitute_all_template_variables_in_map(self.env.clone(), &task_variables)?;
env.extend(task_variables);
let mut env = substitute_all_template_variables_in_map(
&self.env,
&task_variables,
&variable_names,
&mut substituted_variables,
)?;
env.extend(task_variables.into_iter().map(|(k, v)| (k, v.to_owned())));
Some(ResolvedTask {
id: id.clone(),
substituted_variables,
original_task: self.clone(),
resolved_label: full_label.clone(),
resolved: Some(SpawnInTerminal {
@ -134,7 +176,7 @@ impl TaskTemplate {
const MAX_DISPLAY_VARIABLE_LENGTH: usize = 15;
fn truncate_variables(task_variables: &HashMap<String, String>) -> HashMap<String, String> {
fn truncate_variables(task_variables: &HashMap<String, &str>) -> HashMap<String, String> {
task_variables
.iter()
.map(|(key, value)| {
@ -153,25 +195,29 @@ fn to_hex_hash(object: impl Serialize) -> anyhow::Result<String> {
Ok(hex::encode(hasher.finalize()))
}
fn substitute_all_template_variables_in_str(
fn substitute_all_template_variables_in_str<A: AsRef<str>>(
template_str: &str,
task_variables: &HashMap<String, String>,
task_variables: &HashMap<String, A>,
variable_names: &HashMap<String, VariableName>,
substituted_variables: &mut HashSet<VariableName>,
) -> Option<String> {
let substituted_string = shellexpand::env_with_context(&template_str, |var| {
let substituted_string = shellexpand::env_with_context(template_str, |var| {
// Colons denote a default value in case the variable is not set. We want to preserve that default, as otherwise shellexpand will substitute it for us.
let colon_position = var.find(':').unwrap_or(var.len());
let (variable_name, default) = var.split_at(colon_position);
let append_previous_default = |ret: &mut String| {
if !default.is_empty() {
ret.push_str(default);
if let Some(name) = task_variables.get(variable_name) {
if let Some(substituted_variable) = variable_names.get(variable_name) {
substituted_variables.insert(substituted_variable.clone());
}
};
if let Some(mut name) = task_variables.get(variable_name).cloned() {
let mut name = name.as_ref().to_owned();
// Got a task variable hit
append_previous_default(&mut name);
if !default.is_empty() {
name.push_str(default);
}
return Ok(Some(name));
} else if variable_name.starts_with(ZED_VARIABLE_NAME_PREFIX) {
bail!("Unknown variable name: {}", variable_name);
bail!("Unknown variable name: {variable_name}");
}
// This is an unknown variable.
// We should not error out, as they may come from user environment (e.g. $PATH). That means that the variable substitution might not be perfect.
@ -187,24 +233,44 @@ fn substitute_all_template_variables_in_str(
}
fn substitute_all_template_variables_in_vec(
mut template_strs: Vec<String>,
task_variables: &HashMap<String, String>,
template_strs: &[String],
task_variables: &HashMap<String, &str>,
variable_names: &HashMap<String, VariableName>,
substituted_variables: &mut HashSet<VariableName>,
) -> Option<Vec<String>> {
for variable in template_strs.iter_mut() {
let new_value = substitute_all_template_variables_in_str(&variable, task_variables)?;
*variable = new_value;
let mut expanded = Vec::with_capacity(template_strs.len());
for variable in template_strs {
let new_value = substitute_all_template_variables_in_str(
variable,
task_variables,
variable_names,
substituted_variables,
)?;
expanded.push(new_value);
}
Some(template_strs)
Some(expanded)
}
fn substitute_all_template_variables_in_map(
keys_and_values: HashMap<String, String>,
task_variables: &HashMap<String, String>,
keys_and_values: &HashMap<String, String>,
task_variables: &HashMap<String, &str>,
variable_names: &HashMap<String, VariableName>,
substituted_variables: &mut HashSet<VariableName>,
) -> Option<HashMap<String, String>> {
let mut new_map: HashMap<String, String> = Default::default();
for (key, value) in keys_and_values {
let new_value = substitute_all_template_variables_in_str(&value, task_variables)?;
let new_key = substitute_all_template_variables_in_str(&key, task_variables)?;
let new_value = substitute_all_template_variables_in_str(
&value,
task_variables,
variable_names,
substituted_variables,
)?;
let new_key = substitute_all_template_variables_in_str(
&key,
task_variables,
variable_names,
substituted_variables,
)?;
new_map.insert(new_key, new_value);
}
Some(new_map)
@ -246,7 +312,7 @@ mod tests {
},
] {
assert_eq!(
task_with_blank_property.resolve_task(TEST_ID_BASE, TaskContext::default()),
task_with_blank_property.resolve_task(TEST_ID_BASE, &TaskContext::default()),
None,
"should not resolve task with blank label and/or command: {task_with_blank_property:?}"
);
@ -266,6 +332,7 @@ mod tests {
let resolved_task = task_template
.resolve_task(TEST_ID_BASE, task_cx)
.unwrap_or_else(|| panic!("failed to resolve task {task_without_cwd:?}"));
assert_substituted_variables(&resolved_task, Vec::new());
resolved_task
.resolved
.clone()
@ -274,30 +341,23 @@ mod tests {
})
};
let cx = TaskContext {
cwd: None,
task_variables: TaskVariables::default(),
};
assert_eq!(
resolved_task(
&task_without_cwd,
TaskContext {
cwd: None,
task_variables: TaskVariables::default(),
}
)
.cwd,
resolved_task(&task_without_cwd, &cx).cwd,
None,
"When neither task nor task context have cwd, it should be None"
);
let context_cwd = Path::new("a").join("b").join("c");
let cx = TaskContext {
cwd: Some(context_cwd.clone()),
task_variables: TaskVariables::default(),
};
assert_eq!(
resolved_task(
&task_without_cwd,
TaskContext {
cwd: Some(context_cwd.clone()),
task_variables: TaskVariables::default(),
}
)
.cwd
.as_deref(),
resolved_task(&task_without_cwd, &cx).cwd.as_deref(),
Some(context_cwd.as_path()),
"TaskContext's cwd should be taken on resolve if task's cwd is None"
);
@ -307,30 +367,22 @@ mod tests {
task_with_cwd.cwd = Some(task_cwd.display().to_string());
let task_with_cwd = task_with_cwd;
let cx = TaskContext {
cwd: None,
task_variables: TaskVariables::default(),
};
assert_eq!(
resolved_task(
&task_with_cwd,
TaskContext {
cwd: None,
task_variables: TaskVariables::default(),
}
)
.cwd
.as_deref(),
resolved_task(&task_with_cwd, &cx).cwd.as_deref(),
Some(task_cwd.as_path()),
"TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is None"
);
let cx = TaskContext {
cwd: Some(context_cwd.clone()),
task_variables: TaskVariables::default(),
};
assert_eq!(
resolved_task(
&task_with_cwd,
TaskContext {
cwd: Some(context_cwd.clone()),
task_variables: TaskVariables::default(),
}
)
.cwd
.as_deref(),
resolved_task(&task_with_cwd, &cx).cwd.as_deref(),
Some(task_cwd.as_path()),
"TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is not None"
);
@ -400,14 +452,14 @@ mod tests {
for i in 0..15 {
let resolved_task = task_with_all_variables.resolve_task(
TEST_ID_BASE,
TaskContext {
&TaskContext {
cwd: None,
task_variables: TaskVariables::from_iter(all_variables.clone()),
},
).unwrap_or_else(|| panic!("Should successfully resolve task {task_with_all_variables:?} with variables {all_variables:?}"));
match &first_resolved_id {
None => first_resolved_id = Some(resolved_task.id),
None => first_resolved_id = Some(resolved_task.id.clone()),
Some(first_id) => assert_eq!(
&resolved_task.id, first_id,
"Step {i}, for the same task template and context, there should be the same resolved task id"
@ -423,6 +475,10 @@ mod tests {
format!("test label for 1234 and {long_value}"),
"Resolved task label should be substituted with variables and those should not be shortened"
);
assert_substituted_variables(
&resolved_task,
all_variables.iter().map(|(name, _)| name.clone()).collect(),
);
let spawn_in_terminal = resolved_task
.resolved
@ -478,7 +534,7 @@ mod tests {
let removed_variable = not_all_variables.remove(i);
let resolved_task_attempt = task_with_all_variables.resolve_task(
TEST_ID_BASE,
TaskContext {
&TaskContext {
cwd: None,
task_variables: TaskVariables::from_iter(not_all_variables),
},
@ -495,11 +551,11 @@ mod tests {
args: vec!["$PATH".into()],
..Default::default()
};
let resolved = task
.resolve_task(TEST_ID_BASE, TaskContext::default())
.unwrap()
.resolved
let resolved_task = task
.resolve_task(TEST_ID_BASE, &TaskContext::default())
.unwrap();
assert_substituted_variables(&resolved_task, Vec::new());
let resolved = resolved_task.resolved.unwrap();
assert_eq!(resolved.label, task.label);
assert_eq!(resolved.command, task.command);
assert_eq!(resolved.args, task.args);
@ -514,7 +570,74 @@ mod tests {
..Default::default()
};
assert!(task
.resolve_task(TEST_ID_BASE, TaskContext::default())
.resolve_task(TEST_ID_BASE, &TaskContext::default())
.is_none());
}
#[test]
fn test_symbol_dependent_tasks() {
let task_with_all_properties = TaskTemplate {
label: "test_label".to_string(),
command: "test_command".to_string(),
args: vec!["test_arg".to_string()],
env: HashMap::from_iter([("test_env_key".to_string(), "test_env_var".to_string())]),
..TaskTemplate::default()
};
let cx = TaskContext {
cwd: None,
task_variables: TaskVariables::from_iter(Some((
VariableName::Symbol,
"test_symbol".to_string(),
))),
};
for (i, symbol_dependent_task) in [
TaskTemplate {
label: format!("test_label_{}", VariableName::Symbol.template_value()),
..task_with_all_properties.clone()
},
TaskTemplate {
command: format!("test_command_{}", VariableName::Symbol.template_value()),
..task_with_all_properties.clone()
},
TaskTemplate {
args: vec![format!(
"test_arg_{}",
VariableName::Symbol.template_value()
)],
..task_with_all_properties.clone()
},
TaskTemplate {
env: HashMap::from_iter([(
"test_env_key".to_string(),
format!("test_env_var_{}", VariableName::Symbol.template_value()),
)]),
..task_with_all_properties.clone()
},
]
.into_iter()
.enumerate()
{
let resolved = symbol_dependent_task
.resolve_task(TEST_ID_BASE, &cx)
.unwrap_or_else(|| panic!("Failed to resolve task {symbol_dependent_task:?}"));
assert_eq!(
resolved.substituted_variables,
HashSet::from_iter(Some(VariableName::Symbol)),
"(index {i}) Expected the task to depend on symbol task variable: {resolved:?}"
)
}
}
#[track_caller]
fn assert_substituted_variables(resolved_task: &ResolvedTask, mut expected: Vec<VariableName>) {
let mut resolved_variables = resolved_task
.substituted_variables
.iter()
.cloned()
.collect::<Vec<_>>();
resolved_variables.sort_by_key(|var| var.to_string());
expected.sort_by_key(|var| var.to_string());
assert_eq!(resolved_variables, expected)
}
}