Rework task modal (#10341)
New list (used tasks are above the separator line, sorted by the usage recency), then all language tasks, then project-local and global tasks are listed. Note that there are two test tasks (for `test_name_1` and `test_name_2` functions) that are created from the same task template: <img width="563" alt="Screenshot 2024-04-10 at 01 00 46" src="https://github.com/zed-industries/zed/assets/2690773/7455a82f-2af2-47bf-99bd-d9c5a36e64ab"> Tasks are deduplicated by labels, with the used tasks left in case of the conflict with the new tasks from the template: <img width="555" alt="Screenshot 2024-04-10 at 01 01 06" src="https://github.com/zed-industries/zed/assets/2690773/8f5a249e-abec-46ef-a991-08c6d0348648"> Regular recent tasks can be now removed too: <img width="565" alt="Screenshot 2024-04-10 at 01 00 55" src="https://github.com/zed-industries/zed/assets/2690773/0976b8fe-b5d7-4d2a-953d-1d8b1f216192"> When the caret is in the place where no function symbol could be retrieved, no cargo tests for function are listed in tasks: <img width="556" alt="image" src="https://github.com/zed-industries/zed/assets/2690773/df30feba-fe27-4645-8be9-02afc70f02da"> Part of https://github.com/zed-industries/zed/issues/10132 Reworks the task code to simplify it and enable proper task labels. * removes `trait Task`, renames `Definition` into `TaskTemplate` and use that instead of `Arc<dyn Task>` everywhere * implement more generic `TaskId` generation that depends on the `TaskContext` and `TaskTemplate` * remove `TaskId` out of the template and only create it after "resolving" the template into the `ResolvedTask`: this way, task templates, task state (`TaskContext`) and task "result" (resolved state) are clearly separated and are not mixed * implement the logic for filtering out non-related language tasks and tasks that have non-resolved Zed task variables * rework Zed template-vs-resolved-task display in modal: now all reruns and recently used tasks are resolved tasks with "fixed" context (unless configured otherwise in the task json) that are always shown, and Zed can add on top tasks with different context that are derived from the same template as the used, resolved tasks * sort the tasks list better, showing more specific and least recently used tasks higher * shows a separator between used and unused tasks, allow removing the used tasks same as the oneshot ones * remote the Oneshot task source as redundant: all oneshot tasks are now stored in the inventory's history * when reusing the tasks as query in the modal, paste the expanded task label now, show trimmed resolved label in the modal * adjusts Rust and Elixir task labels to be more descriptive and closer to bash scripts Release Notes: - Improved task modal ordering, run and deletion capabilities
This commit is contained in:
parent
b0eda77d73
commit
d1ad96782c
21 changed files with 1103 additions and 671 deletions
|
@ -13,9 +13,11 @@ anyhow.workspace = true
|
|||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
hex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
sha2.workspace = true
|
||||
shellexpand.workspace = true
|
||||
subst = "0.3.0"
|
||||
util.workspace = true
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
//! Baseline interface of Tasks in Zed: all tasks in Zed are intended to use those for implementing their own logic.
|
||||
#![deny(missing_docs)]
|
||||
|
||||
pub mod oneshot_source;
|
||||
pub mod static_source;
|
||||
mod task_template;
|
||||
mod vscode_format;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::ModelContext;
|
||||
use static_source::RevealStrategy;
|
||||
use serde::Serialize;
|
||||
use std::any::Any;
|
||||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use task_template::{RevealStrategy, TaskTemplate, TaskTemplates};
|
||||
pub use vscode_format::VsCodeTaskFile;
|
||||
|
||||
/// Task identifier, unique within the application.
|
||||
|
@ -20,7 +21,7 @@ pub use vscode_format::VsCodeTaskFile;
|
|||
pub struct TaskId(pub String);
|
||||
|
||||
/// Contains all information needed by Zed to spawn a new terminal tab for the given task.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SpawnInTerminal {
|
||||
/// Id of the task to use when determining task tab affinity.
|
||||
pub id: TaskId,
|
||||
|
@ -42,8 +43,26 @@ pub struct SpawnInTerminal {
|
|||
pub reveal: RevealStrategy,
|
||||
}
|
||||
|
||||
/// Variables, available for use in [`TaskContext`] when a Zed's task gets turned into real command.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
/// A final form of the [`TaskTemplate`], that got resolved with a particualar [`TaskContext`] and now is ready to spawn the actual task.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResolvedTask {
|
||||
/// A way to distinguish tasks produced by the same template, but different contexts.
|
||||
/// NOTE: Resolved tasks may have the same labels, commands and do the same things,
|
||||
/// but still may have different ids if the context was different during the resolution.
|
||||
/// Since the template has `env` field, for a generic task that may be a bash command,
|
||||
/// 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,
|
||||
/// Full, unshortened label of the task after all resolutions are made.
|
||||
pub resolved_label: String,
|
||||
/// Further actions that need to take place after the resolved task is spawned,
|
||||
/// with all task variables resolved.
|
||||
pub resolved: Option<SpawnInTerminal>,
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// An absolute path of the currently opened file.
|
||||
File,
|
||||
|
@ -74,22 +93,25 @@ impl VariableName {
|
|||
}
|
||||
}
|
||||
|
||||
/// A prefix that all [`VariableName`] variants are prefixed with when used in environment variables and similar template contexts.
|
||||
pub const ZED_VARIABLE_NAME_PREFIX: &str = "ZED_";
|
||||
|
||||
impl std::fmt::Display for VariableName {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::File => write!(f, "ZED_FILE"),
|
||||
Self::WorktreeRoot => write!(f, "ZED_WORKTREE_ROOT"),
|
||||
Self::Symbol => write!(f, "ZED_SYMBOL"),
|
||||
Self::Row => write!(f, "ZED_ROW"),
|
||||
Self::Column => write!(f, "ZED_COLUMN"),
|
||||
Self::SelectedText => write!(f, "ZED_SELECTED_TEXT"),
|
||||
Self::Custom(s) => write!(f, "ZED_{s}"),
|
||||
Self::File => write!(f, "{ZED_VARIABLE_NAME_PREFIX}FILE"),
|
||||
Self::WorktreeRoot => write!(f, "{ZED_VARIABLE_NAME_PREFIX}WORKTREE_ROOT"),
|
||||
Self::Symbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SYMBOL"),
|
||||
Self::Row => write!(f, "{ZED_VARIABLE_NAME_PREFIX}ROW"),
|
||||
Self::Column => write!(f, "{ZED_VARIABLE_NAME_PREFIX}COLUMN"),
|
||||
Self::SelectedText => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SELECTED_TEXT"),
|
||||
Self::Custom(s) => write!(f, "{ZED_VARIABLE_NAME_PREFIX}CUSTOM_{s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Container for predefined environment variables that describe state of Zed at the time the task was spawned.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct TaskVariables(HashMap<VariableName, String>);
|
||||
|
||||
impl TaskVariables {
|
||||
|
@ -118,8 +140,9 @@ impl FromIterator<(VariableName, String)> for TaskVariables {
|
|||
}
|
||||
}
|
||||
|
||||
/// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function)
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
/// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function).
|
||||
/// Keeps all Zed-related state inside, used to produce a resolved task out of its template.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct TaskContext {
|
||||
/// A path to a directory in which the task should be executed.
|
||||
pub cwd: Option<PathBuf>,
|
||||
|
@ -127,20 +150,6 @@ pub struct TaskContext {
|
|||
pub task_variables: TaskVariables,
|
||||
}
|
||||
|
||||
/// Represents a short lived recipe of a task, whose main purpose
|
||||
/// is to get spawned.
|
||||
pub trait Task {
|
||||
/// Unique identifier of the task to spawn.
|
||||
fn id(&self) -> &TaskId;
|
||||
/// Human readable name of the task to display in the UI.
|
||||
fn name(&self) -> &str;
|
||||
/// Task's current working directory. If `None`, current project's root will be used.
|
||||
fn cwd(&self) -> Option<&str>;
|
||||
/// Sets up everything needed to spawn the task in the given directory (`cwd`).
|
||||
/// If a task is intended to be spawned in the terminal, it should return the corresponding struct filled with the data necessary.
|
||||
fn prepare_exec(&self, cx: TaskContext) -> Option<SpawnInTerminal>;
|
||||
}
|
||||
|
||||
/// [`Source`] produces tasks that can be scheduled.
|
||||
///
|
||||
/// Implementations of this trait could be e.g. [`StaticSource`] that parses tasks from a .json files and provides process templates to be spawned;
|
||||
|
@ -149,8 +158,5 @@ pub trait TaskSource: Any {
|
|||
/// A way to erase the type of the source, processing and storing them generically.
|
||||
fn as_any(&mut self) -> &mut dyn Any;
|
||||
/// Collects all tasks available for scheduling.
|
||||
fn tasks_to_schedule(
|
||||
&mut self,
|
||||
cx: &mut ModelContext<Box<dyn TaskSource>>,
|
||||
) -> Vec<Arc<dyn Task>>;
|
||||
fn tasks_to_schedule(&mut self, cx: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates;
|
||||
}
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
//! A source of tasks, based on ad-hoc user command prompt input.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
static_source::RevealStrategy, SpawnInTerminal, Task, TaskContext, TaskId, TaskSource,
|
||||
};
|
||||
use gpui::{AppContext, Context, Model};
|
||||
|
||||
/// A storage and source of tasks generated out of user command prompt inputs.
|
||||
pub struct OneshotSource {
|
||||
tasks: Vec<Arc<dyn Task>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct OneshotTask {
|
||||
id: TaskId,
|
||||
}
|
||||
|
||||
impl OneshotTask {
|
||||
fn new(prompt: String) -> Self {
|
||||
Self { id: TaskId(prompt) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Task for OneshotTask {
|
||||
fn id(&self) -> &TaskId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.id.0
|
||||
}
|
||||
|
||||
fn cwd(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn prepare_exec(&self, cx: TaskContext) -> Option<SpawnInTerminal> {
|
||||
if self.id().0.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let TaskContext {
|
||||
cwd,
|
||||
task_variables,
|
||||
} = cx;
|
||||
Some(SpawnInTerminal {
|
||||
id: self.id().clone(),
|
||||
label: self.name().to_owned(),
|
||||
command: self.id().0.clone(),
|
||||
args: vec![],
|
||||
cwd,
|
||||
env: task_variables.into_env_variables(),
|
||||
use_new_terminal: Default::default(),
|
||||
allow_concurrent_runs: Default::default(),
|
||||
reveal: RevealStrategy::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OneshotSource {
|
||||
/// Initializes the oneshot source, preparing to store user prompts.
|
||||
pub fn new(cx: &mut AppContext) -> Model<Box<dyn TaskSource>> {
|
||||
cx.new_model(|_| Box::new(Self { tasks: Vec::new() }) as Box<dyn TaskSource>)
|
||||
}
|
||||
|
||||
/// Spawns a certain task based on the user prompt.
|
||||
pub fn spawn(&mut self, prompt: String) -> Arc<dyn Task> {
|
||||
if let Some(task) = self.tasks.iter().find(|task| task.id().0 == prompt) {
|
||||
// If we already have an oneshot task with that command, let's just reuse it.
|
||||
task.clone()
|
||||
} else {
|
||||
let new_oneshot = Arc::new(OneshotTask::new(prompt));
|
||||
self.tasks.push(new_oneshot.clone());
|
||||
new_oneshot
|
||||
}
|
||||
}
|
||||
/// Removes a task with a given ID from this source.
|
||||
pub fn remove(&mut self, id: &TaskId) {
|
||||
let position = self.tasks.iter().position(|task| task.id() == id);
|
||||
if let Some(position) = position {
|
||||
self.tasks.remove(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskSource for OneshotSource {
|
||||
fn as_any(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn tasks_to_schedule(
|
||||
&mut self,
|
||||
_cx: &mut gpui::ModelContext<Box<dyn TaskSource>>,
|
||||
) -> Vec<Arc<dyn Task>> {
|
||||
self.tasks.clone()
|
||||
}
|
||||
}
|
|
@ -1,154 +1,20 @@
|
|||
//! A source of tasks, based on a static configuration, deserialized from the tasks config file, and related infrastructure for tracking changes to the file.
|
||||
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use collections::HashMap;
|
||||
use futures::StreamExt;
|
||||
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
|
||||
use schemars::{gen::SchemaSettings, JsonSchema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Deserialize;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{SpawnInTerminal, Task, TaskContext, TaskId, TaskSource};
|
||||
use crate::{TaskSource, TaskTemplates};
|
||||
use futures::channel::mpsc::UnboundedReceiver;
|
||||
|
||||
/// A single config file entry with the deserialized task definition.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct StaticTask {
|
||||
id: TaskId,
|
||||
definition: Definition,
|
||||
}
|
||||
|
||||
impl StaticTask {
|
||||
fn new(definition: Definition, (id_base, index_in_file): (&str, usize)) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
id: TaskId(format!(
|
||||
"static_{id_base}_{index_in_file}_{}",
|
||||
definition.label
|
||||
)),
|
||||
definition,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO: doc
|
||||
pub fn tasks_for(tasks: TaskDefinitions, id_base: &str) -> Vec<Arc<dyn Task>> {
|
||||
tasks
|
||||
.0
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, task)| StaticTask::new(task, (id_base, index)) as Arc<_>)
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl Task for StaticTask {
|
||||
fn prepare_exec(&self, cx: TaskContext) -> Option<SpawnInTerminal> {
|
||||
let TaskContext {
|
||||
cwd,
|
||||
task_variables,
|
||||
} = cx;
|
||||
let task_variables = task_variables.into_env_variables();
|
||||
let cwd = self
|
||||
.definition
|
||||
.cwd
|
||||
.clone()
|
||||
.and_then(|path| {
|
||||
subst::substitute(&path, &task_variables)
|
||||
.map(Into::into)
|
||||
.ok()
|
||||
})
|
||||
.or(cwd);
|
||||
let mut definition_env = self.definition.env.clone();
|
||||
definition_env.extend(task_variables);
|
||||
Some(SpawnInTerminal {
|
||||
id: self.id.clone(),
|
||||
cwd,
|
||||
use_new_terminal: self.definition.use_new_terminal,
|
||||
allow_concurrent_runs: self.definition.allow_concurrent_runs,
|
||||
label: self.definition.label.clone(),
|
||||
command: self.definition.command.clone(),
|
||||
args: self.definition.args.clone(),
|
||||
reveal: self.definition.reveal,
|
||||
env: definition_env,
|
||||
})
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.definition.label
|
||||
}
|
||||
|
||||
fn id(&self) -> &TaskId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn cwd(&self) -> Option<&str> {
|
||||
self.definition.cwd.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
/// The source of tasks defined in a tasks config file.
|
||||
pub struct StaticSource {
|
||||
tasks: Vec<Arc<StaticTask>>,
|
||||
_definitions: Model<TrackedFile<TaskDefinitions>>,
|
||||
tasks: TaskTemplates,
|
||||
_templates: Model<TrackedFile<TaskTemplates>>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
/// Static task definition from the tasks config file.
|
||||
#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct Definition {
|
||||
/// Human readable name of the task to display in the UI.
|
||||
pub label: String,
|
||||
/// Executable command to spawn.
|
||||
pub command: String,
|
||||
/// Arguments to the command.
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
/// Env overrides for the command, will be appended to the terminal's environment from the settings.
|
||||
#[serde(default)]
|
||||
pub env: HashMap<String, String>,
|
||||
/// Current working directory to spawn the command into, defaults to current project root.
|
||||
#[serde(default)]
|
||||
pub cwd: Option<String>,
|
||||
/// Whether to use a new terminal tab or reuse the existing one to spawn the process.
|
||||
#[serde(default)]
|
||||
pub use_new_terminal: bool,
|
||||
/// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish.
|
||||
#[serde(default)]
|
||||
pub allow_concurrent_runs: bool,
|
||||
/// What to do with the terminal pane and tab, after the command was started:
|
||||
/// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
|
||||
/// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
||||
#[serde(default)]
|
||||
pub reveal: RevealStrategy,
|
||||
}
|
||||
|
||||
/// What to do with the terminal pane and tab, after the command was started.
|
||||
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RevealStrategy {
|
||||
/// Always show the terminal pane, add and focus the corresponding task's tab in it.
|
||||
#[default]
|
||||
Always,
|
||||
/// Do not change terminal pane focus, but still add/reuse the task's tab there.
|
||||
Never,
|
||||
}
|
||||
|
||||
/// A group of Tasks defined in a JSON file.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TaskDefinitions(pub Vec<Definition>);
|
||||
|
||||
impl TaskDefinitions {
|
||||
/// Generates JSON schema of Tasks JSON definition format.
|
||||
pub fn generate_json_schema() -> serde_json_lenient::Value {
|
||||
let schema = SchemaSettings::draft07()
|
||||
.with(|settings| settings.option_add_null_type = false)
|
||||
.into_generator()
|
||||
.into_root_schema_for::<Self>();
|
||||
|
||||
serde_json_lenient::to_value(schema).unwrap()
|
||||
}
|
||||
}
|
||||
/// A Wrapper around deserializable T that keeps track of its contents
|
||||
/// via a provided channel. Once T value changes, the observers of [`TrackedFile`] are
|
||||
/// notified.
|
||||
|
@ -235,32 +101,22 @@ impl<T: PartialEq + 'static> TrackedFile<T> {
|
|||
impl StaticSource {
|
||||
/// Initializes the static source, reacting on tasks config changes.
|
||||
pub fn new(
|
||||
id_base: impl Into<Cow<'static, str>>,
|
||||
definitions: Model<TrackedFile<TaskDefinitions>>,
|
||||
templates: Model<TrackedFile<TaskTemplates>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Model<Box<dyn TaskSource>> {
|
||||
cx.new_model(|cx| {
|
||||
let id_base = id_base.into();
|
||||
let _subscription = cx.observe(
|
||||
&definitions,
|
||||
move |source: &mut Box<(dyn TaskSource + 'static)>, new_definitions, cx| {
|
||||
&templates,
|
||||
move |source: &mut Box<(dyn TaskSource + 'static)>, new_templates, cx| {
|
||||
if let Some(static_source) = source.as_any().downcast_mut::<Self>() {
|
||||
static_source.tasks = new_definitions
|
||||
.read(cx)
|
||||
.get()
|
||||
.0
|
||||
.clone()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, definition)| StaticTask::new(definition, (&id_base, i)))
|
||||
.collect();
|
||||
static_source.tasks = new_templates.read(cx).get().clone();
|
||||
cx.notify();
|
||||
}
|
||||
},
|
||||
);
|
||||
Box::new(Self {
|
||||
tasks: Vec::new(),
|
||||
_definitions: definitions,
|
||||
tasks: TaskTemplates::default(),
|
||||
_templates: templates,
|
||||
_subscription,
|
||||
})
|
||||
})
|
||||
|
@ -268,14 +124,8 @@ impl StaticSource {
|
|||
}
|
||||
|
||||
impl TaskSource for StaticSource {
|
||||
fn tasks_to_schedule(
|
||||
&mut self,
|
||||
_: &mut ModelContext<Box<dyn TaskSource>>,
|
||||
) -> Vec<Arc<dyn Task>> {
|
||||
self.tasks
|
||||
.iter()
|
||||
.map(|task| task.clone() as Arc<dyn Task>)
|
||||
.collect()
|
||||
fn tasks_to_schedule(&mut self, _: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates {
|
||||
self.tasks.clone()
|
||||
}
|
||||
|
||||
fn as_any(&mut self) -> &mut dyn std::any::Any {
|
||||
|
|
481
crates/task/src/task_template.rs
Normal file
481
crates/task/src/task_template.rs
Normal file
|
@ -0,0 +1,481 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context;
|
||||
use collections::HashMap;
|
||||
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};
|
||||
|
||||
/// A template definition of a Zed task to run.
|
||||
/// May use the [`VariableName`] to get the corresponding substitutions into its fields.
|
||||
///
|
||||
/// Template itself is not ready to spawn a task, it needs to be resolved with a [`TaskContext`] first, that
|
||||
/// contains all relevant Zed state in task variables.
|
||||
/// A single template may produce different tasks (or none) for different contexts.
|
||||
#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct TaskTemplate {
|
||||
/// Human readable name of the task to display in the UI.
|
||||
pub label: String,
|
||||
/// Executable command to spawn.
|
||||
pub command: String,
|
||||
/// Arguments to the command.
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
/// Env overrides for the command, will be appended to the terminal's environment from the settings.
|
||||
#[serde(default)]
|
||||
pub env: HashMap<String, String>,
|
||||
/// Current working directory to spawn the command into, defaults to current project root.
|
||||
#[serde(default)]
|
||||
pub cwd: Option<String>,
|
||||
/// Whether to use a new terminal tab or reuse the existing one to spawn the process.
|
||||
#[serde(default)]
|
||||
pub use_new_terminal: bool,
|
||||
/// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish.
|
||||
#[serde(default)]
|
||||
pub allow_concurrent_runs: bool,
|
||||
/// What to do with the terminal pane and tab, after the command was started:
|
||||
/// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
|
||||
/// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
||||
#[serde(default)]
|
||||
pub reveal: RevealStrategy,
|
||||
}
|
||||
|
||||
/// What to do with the terminal pane and tab, after the command was started.
|
||||
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RevealStrategy {
|
||||
/// Always show the terminal pane, add and focus the corresponding task's tab in it.
|
||||
#[default]
|
||||
Always,
|
||||
/// Do not change terminal pane focus, but still add/reuse the task's tab there.
|
||||
Never,
|
||||
}
|
||||
|
||||
/// A group of Tasks defined in a JSON file.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TaskTemplates(pub Vec<TaskTemplate>);
|
||||
|
||||
impl TaskTemplates {
|
||||
/// Generates JSON schema of Tasks JSON template format.
|
||||
pub fn generate_json_schema() -> serde_json_lenient::Value {
|
||||
let schema = SchemaSettings::draft07()
|
||||
.with(|settings| settings.option_add_null_type = false)
|
||||
.into_generator()
|
||||
.into_root_schema_for::<Self>();
|
||||
|
||||
serde_json_lenient::to_value(schema).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskTemplate {
|
||||
/// Replaces all `VariableName` task variables in the task template string fields.
|
||||
/// If any replacement fails or the new string substitutions still have [`ZED_VARIABLE_NAME_PREFIX`],
|
||||
/// `None` is returned.
|
||||
///
|
||||
/// 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> {
|
||||
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 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,
|
||||
)?),
|
||||
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)?;
|
||||
let task_hash = to_hex_hash(self)
|
||||
.context("hashing task template")
|
||||
.log_err()?;
|
||||
let variables_hash = to_hex_hash(&task_variables)
|
||||
.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);
|
||||
Some(ResolvedTask {
|
||||
id: id.clone(),
|
||||
original_task: self.clone(),
|
||||
resolved_label: full_label,
|
||||
resolved: Some(SpawnInTerminal {
|
||||
id,
|
||||
cwd,
|
||||
label: shortened_label,
|
||||
command,
|
||||
args,
|
||||
env,
|
||||
use_new_terminal: self.use_new_terminal,
|
||||
allow_concurrent_runs: self.allow_concurrent_runs,
|
||||
reveal: self.reveal,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_DISPLAY_VARIABLE_LENGTH: usize = 15;
|
||||
|
||||
fn truncate_variables(task_variables: &HashMap<String, String>) -> HashMap<String, String> {
|
||||
task_variables
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
(
|
||||
key.clone(),
|
||||
truncate_and_remove_front(value, MAX_DISPLAY_VARIABLE_LENGTH),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn to_hex_hash(object: impl Serialize) -> anyhow::Result<String> {
|
||||
let json = serde_json_lenient::to_string(&object).context("serializing the object")?;
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(json.as_bytes());
|
||||
Ok(hex::encode(hasher.finalize()))
|
||||
}
|
||||
|
||||
fn substitute_all_template_variables_in_str(
|
||||
template_str: &str,
|
||||
task_variables: &HashMap<String, String>,
|
||||
) -> Option<String> {
|
||||
let substituted_string = subst::substitute(&template_str, task_variables).ok()?;
|
||||
if substituted_string.contains(ZED_VARIABLE_NAME_PREFIX) {
|
||||
return None;
|
||||
}
|
||||
Some(substituted_string)
|
||||
}
|
||||
|
||||
fn substitute_all_template_variables_in_vec(
|
||||
mut template_strs: Vec<String>,
|
||||
task_variables: &HashMap<String, String>,
|
||||
) -> Option<Vec<String>> {
|
||||
for template_str in &mut template_strs {
|
||||
let substituted_string = subst::substitute(&template_str, task_variables).ok()?;
|
||||
if substituted_string.contains(ZED_VARIABLE_NAME_PREFIX) {
|
||||
return None;
|
||||
}
|
||||
*template_str = substituted_string
|
||||
}
|
||||
Some(template_strs)
|
||||
}
|
||||
|
||||
fn substitute_all_template_variables_in_map(
|
||||
keys_and_values: HashMap<String, String>,
|
||||
task_variables: &HashMap<String, String>,
|
||||
) -> Option<HashMap<String, String>> {
|
||||
keys_and_values
|
||||
.into_iter()
|
||||
.try_fold(HashMap::default(), |mut expanded_keys, (mut key, value)| {
|
||||
match task_variables.get(&key) {
|
||||
Some(variable_expansion) => key = variable_expansion.clone(),
|
||||
None => {
|
||||
if key.starts_with(ZED_VARIABLE_NAME_PREFIX) {
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
}
|
||||
expanded_keys.insert(
|
||||
key,
|
||||
subst::substitute(&value, task_variables)
|
||||
.map_err(|_| ())?
|
||||
.to_string(),
|
||||
);
|
||||
Ok(expanded_keys)
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{borrow::Cow, path::Path};
|
||||
|
||||
use crate::{TaskVariables, VariableName};
|
||||
|
||||
use super::*;
|
||||
|
||||
const TEST_ID_BASE: &str = "test_base";
|
||||
|
||||
#[test]
|
||||
fn test_resolving_templates_with_blank_command_and_label() {
|
||||
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()
|
||||
};
|
||||
|
||||
for task_with_blank_property in &[
|
||||
TaskTemplate {
|
||||
label: "".to_string(),
|
||||
..task_with_all_properties.clone()
|
||||
},
|
||||
TaskTemplate {
|
||||
command: "".to_string(),
|
||||
..task_with_all_properties.clone()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "".to_string(),
|
||||
command: "".to_string(),
|
||||
..task_with_all_properties.clone()
|
||||
},
|
||||
] {
|
||||
assert_eq!(
|
||||
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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_cwd_resolution() {
|
||||
let task_without_cwd = TaskTemplate {
|
||||
cwd: None,
|
||||
label: "test task".to_string(),
|
||||
command: "echo 4".to_string(),
|
||||
..TaskTemplate::default()
|
||||
};
|
||||
|
||||
let resolved_task = |task_template: &TaskTemplate, task_cx| {
|
||||
let resolved_task = task_template
|
||||
.resolve_task(TEST_ID_BASE, task_cx)
|
||||
.unwrap_or_else(|| panic!("failed to resolve task {task_without_cwd:?}"));
|
||||
resolved_task
|
||||
.resolved
|
||||
.clone()
|
||||
.unwrap_or_else(|| {
|
||||
panic!("failed to get resolve data for resolved task. Template: {task_without_cwd:?} Resolved: {resolved_task:?}")
|
||||
})
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
resolved_task(
|
||||
&task_without_cwd,
|
||||
TaskContext {
|
||||
cwd: None,
|
||||
task_variables: TaskVariables::default(),
|
||||
}
|
||||
)
|
||||
.cwd,
|
||||
None,
|
||||
"When neither task nor task context have cwd, it should be None"
|
||||
);
|
||||
|
||||
let context_cwd = Path::new("a").join("b").join("c");
|
||||
assert_eq!(
|
||||
resolved_task(
|
||||
&task_without_cwd,
|
||||
TaskContext {
|
||||
cwd: Some(context_cwd.clone()),
|
||||
task_variables: TaskVariables::default(),
|
||||
}
|
||||
)
|
||||
.cwd
|
||||
.as_deref(),
|
||||
Some(context_cwd.as_path()),
|
||||
"TaskContext's cwd should be taken on resolve if task's cwd is None"
|
||||
);
|
||||
|
||||
let task_cwd = Path::new("d").join("e").join("f");
|
||||
let mut task_with_cwd = task_without_cwd.clone();
|
||||
task_with_cwd.cwd = Some(task_cwd.display().to_string());
|
||||
let task_with_cwd = task_with_cwd;
|
||||
|
||||
assert_eq!(
|
||||
resolved_task(
|
||||
&task_with_cwd,
|
||||
TaskContext {
|
||||
cwd: None,
|
||||
task_variables: TaskVariables::default(),
|
||||
}
|
||||
)
|
||||
.cwd
|
||||
.as_deref(),
|
||||
Some(task_cwd.as_path()),
|
||||
"TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is None"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
resolved_task(
|
||||
&task_with_cwd,
|
||||
TaskContext {
|
||||
cwd: Some(context_cwd.clone()),
|
||||
task_variables: TaskVariables::default(),
|
||||
}
|
||||
)
|
||||
.cwd
|
||||
.as_deref(),
|
||||
Some(task_cwd.as_path()),
|
||||
"TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is not None"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_variables_resolution() {
|
||||
let custom_variable_1 = VariableName::Custom(Cow::Borrowed("custom_variable_1"));
|
||||
let custom_variable_2 = VariableName::Custom(Cow::Borrowed("custom_variable_2"));
|
||||
let long_value = "01".repeat(MAX_DISPLAY_VARIABLE_LENGTH * 2);
|
||||
let all_variables = [
|
||||
(VariableName::Row, "1234".to_string()),
|
||||
(VariableName::Column, "5678".to_string()),
|
||||
(VariableName::File, "test_file".to_string()),
|
||||
(VariableName::SelectedText, "test_selected_text".to_string()),
|
||||
(VariableName::Symbol, long_value.clone()),
|
||||
(VariableName::WorktreeRoot, "/test_root/".to_string()),
|
||||
(
|
||||
custom_variable_1.clone(),
|
||||
"test_custom_variable_1".to_string(),
|
||||
),
|
||||
(
|
||||
custom_variable_2.clone(),
|
||||
"test_custom_variable_2".to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
let task_with_all_variables = TaskTemplate {
|
||||
label: format!(
|
||||
"test label for {} and {}",
|
||||
VariableName::Row.template_value(),
|
||||
VariableName::Symbol.template_value(),
|
||||
),
|
||||
command: format!(
|
||||
"echo {} {}",
|
||||
VariableName::File.template_value(),
|
||||
VariableName::Symbol.template_value(),
|
||||
),
|
||||
args: vec![
|
||||
format!("arg1 {}", VariableName::SelectedText.template_value()),
|
||||
format!("arg2 {}", VariableName::Column.template_value()),
|
||||
format!("arg3 {}", VariableName::Symbol.template_value()),
|
||||
],
|
||||
env: HashMap::from_iter([
|
||||
("test_env_key".to_string(), "test_env_var".to_string()),
|
||||
(
|
||||
"env_key_1".to_string(),
|
||||
VariableName::WorktreeRoot.template_value(),
|
||||
),
|
||||
(
|
||||
"env_key_2".to_string(),
|
||||
format!(
|
||||
"env_var_2_{}_{}",
|
||||
custom_variable_1.template_value(),
|
||||
custom_variable_2.template_value()
|
||||
),
|
||||
),
|
||||
(
|
||||
"env_key_3".to_string(),
|
||||
format!("env_var_3_{}", VariableName::Symbol.template_value()),
|
||||
),
|
||||
]),
|
||||
..TaskTemplate::default()
|
||||
};
|
||||
|
||||
let mut first_resolved_id = None;
|
||||
for i in 0..15 {
|
||||
let resolved_task = task_with_all_variables.resolve_task(
|
||||
TEST_ID_BASE,
|
||||
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),
|
||||
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"
|
||||
),
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
resolved_task.original_task, task_with_all_variables,
|
||||
"Resolved task should store its template without changes"
|
||||
);
|
||||
assert_eq!(
|
||||
resolved_task.resolved_label,
|
||||
format!("test label for 1234 and {long_value}"),
|
||||
"Resolved task label should be substituted with variables and those should not be shortened"
|
||||
);
|
||||
|
||||
let spawn_in_terminal = resolved_task
|
||||
.resolved
|
||||
.as_ref()
|
||||
.expect("should have resolved a spawn in terminal task");
|
||||
assert_eq!(
|
||||
spawn_in_terminal.label,
|
||||
format!(
|
||||
"test label for 1234 and …{}",
|
||||
&long_value[..=MAX_DISPLAY_VARIABLE_LENGTH]
|
||||
),
|
||||
"Human-readable label should have long substitutions trimmed"
|
||||
);
|
||||
assert_eq!(
|
||||
spawn_in_terminal.command,
|
||||
format!("echo test_file {long_value}"),
|
||||
"Command should be substituted with variables and those should not be shortened"
|
||||
);
|
||||
assert_eq!(
|
||||
spawn_in_terminal.args,
|
||||
&[
|
||||
"arg1 test_selected_text",
|
||||
"arg2 5678",
|
||||
&format!("arg3 {long_value}")
|
||||
],
|
||||
"Args should be substituted with variables and those should not be shortened"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
spawn_in_terminal
|
||||
.env
|
||||
.get("test_env_key")
|
||||
.map(|s| s.as_str()),
|
||||
Some("test_env_var")
|
||||
);
|
||||
assert_eq!(
|
||||
spawn_in_terminal.env.get("env_key_1").map(|s| s.as_str()),
|
||||
Some("/test_root/")
|
||||
);
|
||||
assert_eq!(
|
||||
spawn_in_terminal.env.get("env_key_2").map(|s| s.as_str()),
|
||||
Some("env_var_2_test_custom_variable_1_test_custom_variable_2")
|
||||
);
|
||||
assert_eq!(
|
||||
spawn_in_terminal.env.get("env_key_3"),
|
||||
Some(&format!("env_var_3_{long_value}")),
|
||||
"Env vars should be substituted with variables and those should not be shortened"
|
||||
);
|
||||
}
|
||||
|
||||
for i in 0..all_variables.len() {
|
||||
let mut not_all_variables = all_variables.to_vec();
|
||||
let removed_variable = not_all_variables.remove(i);
|
||||
let resolved_task_attempt = task_with_all_variables.resolve_task(
|
||||
TEST_ID_BASE,
|
||||
TaskContext {
|
||||
cwd: None,
|
||||
task_variables: TaskVariables::from_iter(not_all_variables),
|
||||
},
|
||||
);
|
||||
assert_eq!(resolved_task_attempt, None, "If any of the Zed task variables is not substituted, the task should not be resolved, but got some resolution without the variable {removed_variable:?} (index {i})");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,10 +3,7 @@ use collections::HashMap;
|
|||
use serde::Deserialize;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{
|
||||
static_source::{Definition, TaskDefinitions},
|
||||
VariableName,
|
||||
};
|
||||
use crate::{TaskTemplate, TaskTemplates, VariableName};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -87,7 +84,7 @@ impl EnvVariableReplacer {
|
|||
}
|
||||
|
||||
impl VsCodeTaskDefinition {
|
||||
fn to_zed_format(self, replacer: &EnvVariableReplacer) -> anyhow::Result<Definition> {
|
||||
fn to_zed_format(self, replacer: &EnvVariableReplacer) -> anyhow::Result<TaskTemplate> {
|
||||
if self.other_attributes.contains_key("dependsOn") {
|
||||
bail!("Encountered unsupported `dependsOn` key during deserialization");
|
||||
}
|
||||
|
@ -107,7 +104,7 @@ impl VsCodeTaskDefinition {
|
|||
// Per VSC docs, only `command`, `args` and `options` support variable substitution.
|
||||
let command = replacer.replace(&command);
|
||||
let args = args.into_iter().map(|arg| replacer.replace(&arg)).collect();
|
||||
let mut ret = Definition {
|
||||
let mut ret = TaskTemplate {
|
||||
label: self.label,
|
||||
command,
|
||||
args,
|
||||
|
@ -127,7 +124,7 @@ pub struct VsCodeTaskFile {
|
|||
tasks: Vec<VsCodeTaskDefinition>,
|
||||
}
|
||||
|
||||
impl TryFrom<VsCodeTaskFile> for TaskDefinitions {
|
||||
impl TryFrom<VsCodeTaskFile> for TaskTemplates {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: VsCodeTaskFile) -> Result<Self, Self::Error> {
|
||||
|
@ -143,12 +140,12 @@ impl TryFrom<VsCodeTaskFile> for TaskDefinitions {
|
|||
VariableName::SelectedText.to_string(),
|
||||
),
|
||||
]));
|
||||
let definitions = value
|
||||
let templates = value
|
||||
.tasks
|
||||
.into_iter()
|
||||
.filter_map(|vscode_definition| vscode_definition.to_zed_format(&replacer).log_err())
|
||||
.collect();
|
||||
Ok(Self(definitions))
|
||||
Ok(Self(templates))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,9 +154,8 @@ mod tests {
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
static_source::{Definition, TaskDefinitions},
|
||||
vscode_format::{Command, VsCodeTaskDefinition},
|
||||
VsCodeTaskFile,
|
||||
TaskTemplate, TaskTemplates, VsCodeTaskFile,
|
||||
};
|
||||
|
||||
use super::EnvVariableReplacer;
|
||||
|
@ -257,13 +253,13 @@ mod tests {
|
|||
.for_each(|(lhs, rhs)| compare_without_other_attributes(lhs.clone(), rhs));
|
||||
|
||||
let expected = vec![
|
||||
Definition {
|
||||
TaskTemplate {
|
||||
label: "gulp: tests".to_string(),
|
||||
command: "npm".to_string(),
|
||||
args: vec!["run".to_string(), "build:tests:notypecheck".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Definition {
|
||||
TaskTemplate {
|
||||
label: "tsc: watch ./src".to_string(),
|
||||
command: "node".to_string(),
|
||||
args: vec![
|
||||
|
@ -274,13 +270,13 @@ mod tests {
|
|||
],
|
||||
..Default::default()
|
||||
},
|
||||
Definition {
|
||||
TaskTemplate {
|
||||
label: "npm: build:compiler".to_string(),
|
||||
command: "npm".to_string(),
|
||||
args: vec!["run".to_string(), "build:compiler".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Definition {
|
||||
TaskTemplate {
|
||||
label: "npm: build:tests".to_string(),
|
||||
command: "npm".to_string(),
|
||||
args: vec!["run".to_string(), "build:tests:notypecheck".to_string()],
|
||||
|
@ -288,7 +284,7 @@ mod tests {
|
|||
},
|
||||
];
|
||||
|
||||
let tasks: TaskDefinitions = vscode_definitions.try_into().unwrap();
|
||||
let tasks: TaskTemplates = vscode_definitions.try_into().unwrap();
|
||||
assert_eq!(tasks.0, expected);
|
||||
}
|
||||
|
||||
|
@ -360,36 +356,36 @@ mod tests {
|
|||
.zip(expected)
|
||||
.for_each(|(lhs, rhs)| compare_without_other_attributes(lhs.clone(), rhs));
|
||||
let expected = vec![
|
||||
Definition {
|
||||
TaskTemplate {
|
||||
label: "Build Extension in Background".to_string(),
|
||||
command: "npm".to_string(),
|
||||
args: vec!["run".to_string(), "watch".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Definition {
|
||||
TaskTemplate {
|
||||
label: "Build Extension".to_string(),
|
||||
command: "npm".to_string(),
|
||||
args: vec!["run".to_string(), "build".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Definition {
|
||||
TaskTemplate {
|
||||
label: "Build Server".to_string(),
|
||||
command: "cargo build --package rust-analyzer".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
Definition {
|
||||
TaskTemplate {
|
||||
label: "Build Server (Release)".to_string(),
|
||||
command: "cargo build --release --package rust-analyzer".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
Definition {
|
||||
TaskTemplate {
|
||||
label: "Pretest".to_string(),
|
||||
command: "npm".to_string(),
|
||||
args: vec!["run".to_string(), "pretest".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
let tasks: TaskDefinitions = vscode_definitions.try_into().unwrap();
|
||||
let tasks: TaskTemplates = vscode_definitions.try_into().unwrap();
|
||||
assert_eq!(tasks.0, expected);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue