Display more specific tasks above in the modal (#10485)
This commit is contained in:
parent
49371b44cb
commit
28586060a1
6 changed files with 262 additions and 148 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -12706,15 +12706,6 @@ dependencies = [
|
||||||
"wit-bindgen",
|
"wit-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zed_extension_api"
|
|
||||||
version = "0.0.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a5f4ae4e302a80591635ef9a236b35fde6fcc26cfd060e66fde4ba9f9fd394a1"
|
|
||||||
dependencies = [
|
|
||||||
"wit-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed_extension_api"
|
name = "zed_extension_api"
|
||||||
version = "0.0.6"
|
version = "0.0.6"
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
cmp,
|
cmp::{self, Reverse},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
@ -11,7 +11,7 @@ use collections::{HashMap, VecDeque};
|
||||||
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
|
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
|
||||||
use itertools::{Either, Itertools};
|
use itertools::{Either, Itertools};
|
||||||
use language::Language;
|
use language::Language;
|
||||||
use task::{ResolvedTask, TaskContext, TaskId, TaskSource, TaskTemplate};
|
use task::{ResolvedTask, TaskContext, TaskId, TaskSource, TaskTemplate, VariableName};
|
||||||
use util::{post_inc, NumericPrefixWithSuffix};
|
use util::{post_inc, NumericPrefixWithSuffix};
|
||||||
use worktree::WorktreeId;
|
use worktree::WorktreeId;
|
||||||
|
|
||||||
|
@ -198,7 +198,7 @@ impl Inventory {
|
||||||
&self,
|
&self,
|
||||||
language: Option<Arc<Language>>,
|
language: Option<Arc<Language>>,
|
||||||
worktree: Option<WorktreeId>,
|
worktree: Option<WorktreeId>,
|
||||||
task_context: TaskContext,
|
task_context: &TaskContext,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> (
|
) -> (
|
||||||
Vec<(TaskSourceKind, ResolvedTask)>,
|
Vec<(TaskSourceKind, ResolvedTask)>,
|
||||||
|
@ -242,7 +242,7 @@ impl Inventory {
|
||||||
.chain(language_tasks)
|
.chain(language_tasks)
|
||||||
.filter_map(|(kind, task)| {
|
.filter_map(|(kind, task)| {
|
||||||
let id_base = kind.to_id_base();
|
let id_base = kind.to_id_base();
|
||||||
Some((kind, task.resolve_task(&id_base, task_context.clone())?))
|
Some((kind, task.resolve_task(&id_base, task_context)?))
|
||||||
})
|
})
|
||||||
.map(|(kind, task)| {
|
.map(|(kind, task)| {
|
||||||
let lru_score = task_usage
|
let lru_score = task_usage
|
||||||
|
@ -299,22 +299,14 @@ fn task_lru_comparator(
|
||||||
(kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
|
(kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
|
||||||
) -> cmp::Ordering {
|
) -> cmp::Ordering {
|
||||||
lru_score_a
|
lru_score_a
|
||||||
|
// First, display recently used templates above all.
|
||||||
.cmp(&lru_score_b)
|
.cmp(&lru_score_b)
|
||||||
|
// Then, ensure more specific sources are displayed first.
|
||||||
.then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
|
.then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
|
||||||
.then(
|
// After that, display first more specific tasks, using more template variables.
|
||||||
kind_a
|
// Bonus points for tasks with symbol variables.
|
||||||
.worktree()
|
.then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
|
||||||
.is_none()
|
// Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
|
||||||
.cmp(&kind_b.worktree().is_none()),
|
|
||||||
)
|
|
||||||
.then(kind_a.worktree().cmp(&kind_b.worktree()))
|
|
||||||
.then(
|
|
||||||
kind_a
|
|
||||||
.abs_path()
|
|
||||||
.is_none()
|
|
||||||
.cmp(&kind_b.abs_path().is_none()),
|
|
||||||
)
|
|
||||||
.then(kind_a.abs_path().cmp(&kind_b.abs_path()))
|
|
||||||
.then({
|
.then({
|
||||||
NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
|
NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
|
||||||
.cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
|
.cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
|
||||||
|
@ -333,6 +325,15 @@ fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
|
||||||
|
let task_variables = task.substituted_variables();
|
||||||
|
Reverse(if task_variables.contains(&VariableName::Symbol) {
|
||||||
|
task_variables.len() + 1
|
||||||
|
} else {
|
||||||
|
task_variables.len()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_inventory {
|
mod test_inventory {
|
||||||
use gpui::{AppContext, Context as _, Model, ModelContext, TestAppContext};
|
use gpui::{AppContext, Context as _, Model, ModelContext, TestAppContext};
|
||||||
|
@ -421,12 +422,12 @@ mod test_inventory {
|
||||||
let (used, current) = inventory.used_and_current_resolved_tasks(
|
let (used, current) = inventory.used_and_current_resolved_tasks(
|
||||||
None,
|
None,
|
||||||
worktree,
|
worktree,
|
||||||
TaskContext::default(),
|
&TaskContext::default(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
used.into_iter()
|
used.into_iter()
|
||||||
.chain(current)
|
.chain(current)
|
||||||
.map(|(_, task)| task.original_task.label)
|
.map(|(_, task)| task.original_task().label.clone())
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -445,7 +446,7 @@ mod test_inventory {
|
||||||
let id_base = task_source_kind.to_id_base();
|
let id_base = task_source_kind.to_id_base();
|
||||||
inventory.task_scheduled(
|
inventory.task_scheduled(
|
||||||
task_source_kind.clone(),
|
task_source_kind.clone(),
|
||||||
task.resolve_task(&id_base, TaskContext::default())
|
task.resolve_task(&id_base, &TaskContext::default())
|
||||||
.unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
|
.unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -460,7 +461,7 @@ mod test_inventory {
|
||||||
let (used, current) = inventory.used_and_current_resolved_tasks(
|
let (used, current) = inventory.used_and_current_resolved_tasks(
|
||||||
None,
|
None,
|
||||||
worktree,
|
worktree,
|
||||||
TaskContext::default(),
|
&TaskContext::default(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
let mut all = used;
|
let mut all = used;
|
||||||
|
|
|
@ -5,7 +5,7 @@ pub mod static_source;
|
||||||
mod task_template;
|
mod task_template;
|
||||||
mod vscode_format;
|
mod vscode_format;
|
||||||
|
|
||||||
use collections::HashMap;
|
use collections::{HashMap, HashSet};
|
||||||
use gpui::ModelContext;
|
use gpui::ModelContext;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::any::Any;
|
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.
|
/// so it's impossible to determine the id equality without more context in a generic case.
|
||||||
pub id: TaskId,
|
pub id: TaskId,
|
||||||
/// A template the task got resolved from.
|
/// 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.
|
/// Full, unshortened label of the task after all resolutions are made.
|
||||||
pub resolved_label: String,
|
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,
|
/// Further actions that need to take place after the resolved task is spawned,
|
||||||
/// with all task variables resolved.
|
/// with all task variables resolved.
|
||||||
pub resolved: Option<SpawnInTerminal>,
|
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`].
|
/// Variables, available for use in [`TaskContext`] when a Zed's [`TaskTemplate`] gets resolved into a [`ResolvedTask`].
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
|
||||||
pub enum VariableName {
|
pub enum VariableName {
|
||||||
|
@ -117,14 +131,6 @@ impl std::fmt::Display for VariableName {
|
||||||
pub struct TaskVariables(HashMap<VariableName, String>);
|
pub struct TaskVariables(HashMap<VariableName, String>);
|
||||||
|
|
||||||
impl TaskVariables {
|
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.
|
/// 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> {
|
pub fn insert(&mut self, variable: VariableName, value: String) -> Option<String> {
|
||||||
self.0.insert(variable, value)
|
self.0.insert(variable, value)
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, Context};
|
||||||
use collections::HashMap;
|
use collections::{HashMap, HashSet};
|
||||||
use schemars::{gen::SchemaSettings, JsonSchema};
|
use schemars::{gen::SchemaSettings, JsonSchema};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use util::{truncate_and_remove_front, ResultExt};
|
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.
|
/// A template definition of a Zed task to run.
|
||||||
/// May use the [`VariableName`] to get the corresponding substitutions into its fields.
|
/// 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),
|
/// 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.
|
/// 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() {
|
if self.label.trim().is_empty() || self.command.trim().is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let TaskContext {
|
|
||||||
cwd,
|
let mut variable_names = HashMap::default();
|
||||||
task_variables,
|
let mut substituted_variables = HashSet::default();
|
||||||
} = cx;
|
let task_variables = cx
|
||||||
let task_variables = task_variables.into_env_variables();
|
.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 truncated_variables = truncate_variables(&task_variables);
|
||||||
let cwd = match self.cwd.as_deref() {
|
let cwd = match self.cwd.as_deref() {
|
||||||
Some(cwd) => Some(substitute_all_template_variables_in_str(
|
Some(cwd) => {
|
||||||
cwd,
|
let substitured_cwd = substitute_all_template_variables_in_str(
|
||||||
&task_variables,
|
cwd,
|
||||||
)?),
|
&task_variables,
|
||||||
|
&variable_names,
|
||||||
|
&mut substituted_variables,
|
||||||
|
)?;
|
||||||
|
Some(substitured_cwd)
|
||||||
|
}
|
||||||
None => None,
|
None => None,
|
||||||
}
|
}
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.or(cwd);
|
.or(cx.cwd.clone());
|
||||||
let shortened_label =
|
let shortened_label = substitute_all_template_variables_in_str(
|
||||||
substitute_all_template_variables_in_str(&self.label, &truncated_variables)?;
|
&self.label,
|
||||||
let full_label = substitute_all_template_variables_in_str(&self.label, &task_variables)?;
|
&truncated_variables,
|
||||||
let command = substitute_all_template_variables_in_str(&self.command, &task_variables)?;
|
&variable_names,
|
||||||
let args = substitute_all_template_variables_in_vec(self.args.clone(), &task_variables)?;
|
&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)
|
let task_hash = to_hex_hash(&self)
|
||||||
.context("hashing task template")
|
.context("hashing task template")
|
||||||
|
@ -110,10 +146,16 @@ impl TaskTemplate {
|
||||||
.context("hashing task variables")
|
.context("hashing task variables")
|
||||||
.log_err()?;
|
.log_err()?;
|
||||||
let id = TaskId(format!("{id_base}_{task_hash}_{variables_hash}"));
|
let id = TaskId(format!("{id_base}_{task_hash}_{variables_hash}"));
|
||||||
let mut env = substitute_all_template_variables_in_map(self.env.clone(), &task_variables)?;
|
let mut env = substitute_all_template_variables_in_map(
|
||||||
env.extend(task_variables);
|
&self.env,
|
||||||
|
&task_variables,
|
||||||
|
&variable_names,
|
||||||
|
&mut substituted_variables,
|
||||||
|
)?;
|
||||||
|
env.extend(task_variables.into_iter().map(|(k, v)| (k, v.to_owned())));
|
||||||
Some(ResolvedTask {
|
Some(ResolvedTask {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
|
substituted_variables,
|
||||||
original_task: self.clone(),
|
original_task: self.clone(),
|
||||||
resolved_label: full_label.clone(),
|
resolved_label: full_label.clone(),
|
||||||
resolved: Some(SpawnInTerminal {
|
resolved: Some(SpawnInTerminal {
|
||||||
|
@ -134,7 +176,7 @@ impl TaskTemplate {
|
||||||
|
|
||||||
const MAX_DISPLAY_VARIABLE_LENGTH: usize = 15;
|
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
|
task_variables
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(key, value)| {
|
.map(|(key, value)| {
|
||||||
|
@ -153,25 +195,29 @@ fn to_hex_hash(object: impl Serialize) -> anyhow::Result<String> {
|
||||||
Ok(hex::encode(hasher.finalize()))
|
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,
|
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> {
|
) -> 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.
|
// 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 colon_position = var.find(':').unwrap_or(var.len());
|
||||||
let (variable_name, default) = var.split_at(colon_position);
|
let (variable_name, default) = var.split_at(colon_position);
|
||||||
let append_previous_default = |ret: &mut String| {
|
if let Some(name) = task_variables.get(variable_name) {
|
||||||
if !default.is_empty() {
|
if let Some(substituted_variable) = variable_names.get(variable_name) {
|
||||||
ret.push_str(default);
|
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
|
// Got a task variable hit
|
||||||
append_previous_default(&mut name);
|
if !default.is_empty() {
|
||||||
|
name.push_str(default);
|
||||||
|
}
|
||||||
return Ok(Some(name));
|
return Ok(Some(name));
|
||||||
} else if variable_name.starts_with(ZED_VARIABLE_NAME_PREFIX) {
|
} 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.
|
// 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.
|
// 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(
|
fn substitute_all_template_variables_in_vec(
|
||||||
mut template_strs: Vec<String>,
|
template_strs: &[String],
|
||||||
task_variables: &HashMap<String, String>,
|
task_variables: &HashMap<String, &str>,
|
||||||
|
variable_names: &HashMap<String, VariableName>,
|
||||||
|
substituted_variables: &mut HashSet<VariableName>,
|
||||||
) -> Option<Vec<String>> {
|
) -> Option<Vec<String>> {
|
||||||
for variable in template_strs.iter_mut() {
|
let mut expanded = Vec::with_capacity(template_strs.len());
|
||||||
let new_value = substitute_all_template_variables_in_str(&variable, task_variables)?;
|
for variable in template_strs {
|
||||||
*variable = new_value;
|
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(
|
fn substitute_all_template_variables_in_map(
|
||||||
keys_and_values: HashMap<String, String>,
|
keys_and_values: &HashMap<String, String>,
|
||||||
task_variables: &HashMap<String, String>,
|
task_variables: &HashMap<String, &str>,
|
||||||
|
variable_names: &HashMap<String, VariableName>,
|
||||||
|
substituted_variables: &mut HashSet<VariableName>,
|
||||||
) -> Option<HashMap<String, String>> {
|
) -> Option<HashMap<String, String>> {
|
||||||
let mut new_map: HashMap<String, String> = Default::default();
|
let mut new_map: HashMap<String, String> = Default::default();
|
||||||
for (key, value) in keys_and_values {
|
for (key, value) in keys_and_values {
|
||||||
let new_value = substitute_all_template_variables_in_str(&value, task_variables)?;
|
let new_value = substitute_all_template_variables_in_str(
|
||||||
let new_key = substitute_all_template_variables_in_str(&key, task_variables)?;
|
&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);
|
new_map.insert(new_key, new_value);
|
||||||
}
|
}
|
||||||
Some(new_map)
|
Some(new_map)
|
||||||
|
@ -246,7 +312,7 @@ mod tests {
|
||||||
},
|
},
|
||||||
] {
|
] {
|
||||||
assert_eq!(
|
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,
|
None,
|
||||||
"should not resolve task with blank label and/or command: {task_with_blank_property:?}"
|
"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
|
let resolved_task = task_template
|
||||||
.resolve_task(TEST_ID_BASE, task_cx)
|
.resolve_task(TEST_ID_BASE, task_cx)
|
||||||
.unwrap_or_else(|| panic!("failed to resolve task {task_without_cwd:?}"));
|
.unwrap_or_else(|| panic!("failed to resolve task {task_without_cwd:?}"));
|
||||||
|
assert_substituted_variables(&resolved_task, Vec::new());
|
||||||
resolved_task
|
resolved_task
|
||||||
.resolved
|
.resolved
|
||||||
.clone()
|
.clone()
|
||||||
|
@ -274,30 +341,23 @@ mod tests {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let cx = TaskContext {
|
||||||
|
cwd: None,
|
||||||
|
task_variables: TaskVariables::default(),
|
||||||
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolved_task(
|
resolved_task(&task_without_cwd, &cx).cwd,
|
||||||
&task_without_cwd,
|
|
||||||
TaskContext {
|
|
||||||
cwd: None,
|
|
||||||
task_variables: TaskVariables::default(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.cwd,
|
|
||||||
None,
|
None,
|
||||||
"When neither task nor task context have cwd, it should be None"
|
"When neither task nor task context have cwd, it should be None"
|
||||||
);
|
);
|
||||||
|
|
||||||
let context_cwd = Path::new("a").join("b").join("c");
|
let context_cwd = Path::new("a").join("b").join("c");
|
||||||
|
let cx = TaskContext {
|
||||||
|
cwd: Some(context_cwd.clone()),
|
||||||
|
task_variables: TaskVariables::default(),
|
||||||
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolved_task(
|
resolved_task(&task_without_cwd, &cx).cwd.as_deref(),
|
||||||
&task_without_cwd,
|
|
||||||
TaskContext {
|
|
||||||
cwd: Some(context_cwd.clone()),
|
|
||||||
task_variables: TaskVariables::default(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.cwd
|
|
||||||
.as_deref(),
|
|
||||||
Some(context_cwd.as_path()),
|
Some(context_cwd.as_path()),
|
||||||
"TaskContext's cwd should be taken on resolve if task's cwd is None"
|
"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());
|
task_with_cwd.cwd = Some(task_cwd.display().to_string());
|
||||||
let task_with_cwd = task_with_cwd;
|
let task_with_cwd = task_with_cwd;
|
||||||
|
|
||||||
|
let cx = TaskContext {
|
||||||
|
cwd: None,
|
||||||
|
task_variables: TaskVariables::default(),
|
||||||
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolved_task(
|
resolved_task(&task_with_cwd, &cx).cwd.as_deref(),
|
||||||
&task_with_cwd,
|
|
||||||
TaskContext {
|
|
||||||
cwd: None,
|
|
||||||
task_variables: TaskVariables::default(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.cwd
|
|
||||||
.as_deref(),
|
|
||||||
Some(task_cwd.as_path()),
|
Some(task_cwd.as_path()),
|
||||||
"TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is None"
|
"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!(
|
assert_eq!(
|
||||||
resolved_task(
|
resolved_task(&task_with_cwd, &cx).cwd.as_deref(),
|
||||||
&task_with_cwd,
|
|
||||||
TaskContext {
|
|
||||||
cwd: Some(context_cwd.clone()),
|
|
||||||
task_variables: TaskVariables::default(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.cwd
|
|
||||||
.as_deref(),
|
|
||||||
Some(task_cwd.as_path()),
|
Some(task_cwd.as_path()),
|
||||||
"TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is not None"
|
"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 {
|
for i in 0..15 {
|
||||||
let resolved_task = task_with_all_variables.resolve_task(
|
let resolved_task = task_with_all_variables.resolve_task(
|
||||||
TEST_ID_BASE,
|
TEST_ID_BASE,
|
||||||
TaskContext {
|
&TaskContext {
|
||||||
cwd: None,
|
cwd: None,
|
||||||
task_variables: TaskVariables::from_iter(all_variables.clone()),
|
task_variables: TaskVariables::from_iter(all_variables.clone()),
|
||||||
},
|
},
|
||||||
).unwrap_or_else(|| panic!("Should successfully resolve task {task_with_all_variables:?} with variables {all_variables:?}"));
|
).unwrap_or_else(|| panic!("Should successfully resolve task {task_with_all_variables:?} with variables {all_variables:?}"));
|
||||||
|
|
||||||
match &first_resolved_id {
|
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!(
|
Some(first_id) => assert_eq!(
|
||||||
&resolved_task.id, first_id,
|
&resolved_task.id, first_id,
|
||||||
"Step {i}, for the same task template and context, there should be the same resolved task 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}"),
|
format!("test label for 1234 and {long_value}"),
|
||||||
"Resolved task label should be substituted with variables and those should not be shortened"
|
"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
|
let spawn_in_terminal = resolved_task
|
||||||
.resolved
|
.resolved
|
||||||
|
@ -478,7 +534,7 @@ mod tests {
|
||||||
let removed_variable = not_all_variables.remove(i);
|
let removed_variable = not_all_variables.remove(i);
|
||||||
let resolved_task_attempt = task_with_all_variables.resolve_task(
|
let resolved_task_attempt = task_with_all_variables.resolve_task(
|
||||||
TEST_ID_BASE,
|
TEST_ID_BASE,
|
||||||
TaskContext {
|
&TaskContext {
|
||||||
cwd: None,
|
cwd: None,
|
||||||
task_variables: TaskVariables::from_iter(not_all_variables),
|
task_variables: TaskVariables::from_iter(not_all_variables),
|
||||||
},
|
},
|
||||||
|
@ -495,11 +551,11 @@ mod tests {
|
||||||
args: vec!["$PATH".into()],
|
args: vec!["$PATH".into()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let resolved = task
|
let resolved_task = task
|
||||||
.resolve_task(TEST_ID_BASE, TaskContext::default())
|
.resolve_task(TEST_ID_BASE, &TaskContext::default())
|
||||||
.unwrap()
|
|
||||||
.resolved
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
assert_substituted_variables(&resolved_task, Vec::new());
|
||||||
|
let resolved = resolved_task.resolved.unwrap();
|
||||||
assert_eq!(resolved.label, task.label);
|
assert_eq!(resolved.label, task.label);
|
||||||
assert_eq!(resolved.command, task.command);
|
assert_eq!(resolved.command, task.command);
|
||||||
assert_eq!(resolved.args, task.args);
|
assert_eq!(resolved.args, task.args);
|
||||||
|
@ -514,7 +570,74 @@ mod tests {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
assert!(task
|
assert!(task
|
||||||
.resolve_task(TEST_ID_BASE, TaskContext::default())
|
.resolve_task(TEST_ID_BASE, &TaskContext::default())
|
||||||
.is_none());
|
.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,20 +29,19 @@ pub fn init(cx: &mut AppContext) {
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
if action.reevaluate_context {
|
if action.reevaluate_context {
|
||||||
let mut original_task = last_scheduled_task.original_task;
|
let mut original_task = last_scheduled_task.original_task().clone();
|
||||||
if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
|
if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
|
||||||
original_task.allow_concurrent_runs = allow_concurrent_runs;
|
original_task.allow_concurrent_runs = allow_concurrent_runs;
|
||||||
}
|
}
|
||||||
if let Some(use_new_terminal) = action.use_new_terminal {
|
if let Some(use_new_terminal) = action.use_new_terminal {
|
||||||
original_task.use_new_terminal = use_new_terminal;
|
original_task.use_new_terminal = use_new_terminal;
|
||||||
}
|
}
|
||||||
let cwd = task_cwd(workspace, cx).log_err().flatten();
|
let task_context = task_context(workspace, cx);
|
||||||
let task_context = task_context(workspace, cwd, cx);
|
|
||||||
schedule_task(
|
schedule_task(
|
||||||
workspace,
|
workspace,
|
||||||
task_source_kind,
|
task_source_kind,
|
||||||
&original_task,
|
&original_task,
|
||||||
task_context,
|
&task_context,
|
||||||
false,
|
false,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
@ -77,8 +76,7 @@ fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewC
|
||||||
None => {
|
None => {
|
||||||
let inventory = workspace.project().read(cx).task_inventory().clone();
|
let inventory = workspace.project().read(cx).task_inventory().clone();
|
||||||
let workspace_handle = workspace.weak_handle();
|
let workspace_handle = workspace.weak_handle();
|
||||||
let cwd = task_cwd(workspace, cx).log_err().flatten();
|
let task_context = task_context(workspace, cx);
|
||||||
let task_context = task_context(workspace, cwd, cx);
|
|
||||||
workspace.toggle_modal(cx, |cx| {
|
workspace.toggle_modal(cx, |cx| {
|
||||||
TasksModal::new(inventory, task_context, workspace_handle, cx)
|
TasksModal::new(inventory, task_context, workspace_handle, cx)
|
||||||
})
|
})
|
||||||
|
@ -98,13 +96,12 @@ fn spawn_task_with_name(name: String, cx: &mut ViewContext<Workspace>) {
|
||||||
});
|
});
|
||||||
let (task_source_kind, target_task) =
|
let (task_source_kind, target_task) =
|
||||||
tasks.into_iter().find(|(_, task)| task.label == name)?;
|
tasks.into_iter().find(|(_, task)| task.label == name)?;
|
||||||
let cwd = task_cwd(workspace, cx).log_err().flatten();
|
let task_context = task_context(workspace, cx);
|
||||||
let task_context = task_context(workspace, cwd, cx);
|
|
||||||
schedule_task(
|
schedule_task(
|
||||||
workspace,
|
workspace,
|
||||||
task_source_kind,
|
task_source_kind,
|
||||||
&target_task,
|
&target_task,
|
||||||
task_context,
|
&task_context,
|
||||||
false,
|
false,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -148,11 +145,8 @@ fn active_item_selection_properties(
|
||||||
(worktree_id, language)
|
(worktree_id, language)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn task_context(
|
fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {
|
||||||
workspace: &Workspace,
|
let cwd = task_cwd(workspace, cx).log_err().flatten();
|
||||||
cwd: Option<PathBuf>,
|
|
||||||
cx: &mut WindowContext<'_>,
|
|
||||||
) -> TaskContext {
|
|
||||||
let current_editor = workspace
|
let current_editor = workspace
|
||||||
.active_item(cx)
|
.active_item(cx)
|
||||||
.and_then(|item| item.act_as::<Editor>(cx));
|
.and_then(|item| item.act_as::<Editor>(cx));
|
||||||
|
@ -253,7 +247,7 @@ fn schedule_task(
|
||||||
workspace: &Workspace,
|
workspace: &Workspace,
|
||||||
task_source_kind: TaskSourceKind,
|
task_source_kind: TaskSourceKind,
|
||||||
task_to_resolve: &TaskTemplate,
|
task_to_resolve: &TaskTemplate,
|
||||||
task_cx: TaskContext,
|
task_cx: &TaskContext,
|
||||||
omit_history: bool,
|
omit_history: bool,
|
||||||
cx: &mut ViewContext<'_, Workspace>,
|
cx: &mut ViewContext<'_, Workspace>,
|
||||||
) {
|
) {
|
||||||
|
@ -338,7 +332,7 @@ mod tests {
|
||||||
use ui::VisualContext;
|
use ui::VisualContext;
|
||||||
use workspace::{AppState, Workspace};
|
use workspace::{AppState, Workspace};
|
||||||
|
|
||||||
use crate::{task_context, task_cwd};
|
use crate::task_context;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_default_language_context(cx: &mut TestAppContext) {
|
async fn test_default_language_context(cx: &mut TestAppContext) {
|
||||||
|
@ -433,7 +427,7 @@ mod tests {
|
||||||
this.add_item_to_center(Box::new(editor2.clone()), cx);
|
this.add_item_to_center(Box::new(editor2.clone()), cx);
|
||||||
assert_eq!(this.active_item(cx).unwrap().item_id(), editor2.entity_id());
|
assert_eq!(this.active_item(cx).unwrap().item_id(), editor2.entity_id());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
task_context(this, task_cwd(this, cx).unwrap(), cx),
|
task_context(this, cx),
|
||||||
TaskContext {
|
TaskContext {
|
||||||
cwd: Some("/dir".into()),
|
cwd: Some("/dir".into()),
|
||||||
task_variables: TaskVariables::from_iter([
|
task_variables: TaskVariables::from_iter([
|
||||||
|
@ -450,7 +444,7 @@ mod tests {
|
||||||
this.change_selections(None, cx, |selections| selections.select_ranges([14..18]))
|
this.change_selections(None, cx, |selections| selections.select_ranges([14..18]))
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
task_context(this, task_cwd(this, cx).unwrap(), cx),
|
task_context(this, cx),
|
||||||
TaskContext {
|
TaskContext {
|
||||||
cwd: Some("/dir".into()),
|
cwd: Some("/dir".into()),
|
||||||
task_variables: TaskVariables::from_iter([
|
task_variables: TaskVariables::from_iter([
|
||||||
|
@ -467,7 +461,7 @@ mod tests {
|
||||||
// Now, let's switch the active item to .ts file.
|
// Now, let's switch the active item to .ts file.
|
||||||
this.activate_item(&editor1, cx);
|
this.activate_item(&editor1, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
task_context(this, task_cwd(this, cx).unwrap(), cx),
|
task_context(this, cx),
|
||||||
TaskContext {
|
TaskContext {
|
||||||
cwd: Some("/dir".into()),
|
cwd: Some("/dir".into()),
|
||||||
task_variables: TaskVariables::from_iter([
|
task_variables: TaskVariables::from_iter([
|
||||||
|
|
|
@ -102,7 +102,7 @@ impl TasksModalDelegate {
|
||||||
};
|
};
|
||||||
Some((
|
Some((
|
||||||
source_kind,
|
source_kind,
|
||||||
new_oneshot.resolve_task(&id_base, self.task_context.clone())?,
|
new_oneshot.resolve_task(&id_base, &self.task_context)?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,7 +212,7 @@ impl PickerDelegate for TasksModalDelegate {
|
||||||
inventory.used_and_current_resolved_tasks(
|
inventory.used_and_current_resolved_tasks(
|
||||||
language,
|
language,
|
||||||
worktree,
|
worktree,
|
||||||
picker.delegate.task_context.clone(),
|
&picker.delegate.task_context,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -403,7 +403,6 @@ impl PickerDelegate for TasksModalDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO kb more tests on recent tasks from language templates
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use gpui::{TestAppContext, VisualTestContext};
|
use gpui::{TestAppContext, VisualTestContext};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue