Detailed changes
@@ -482,6 +482,8 @@
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
// also possible to spawn tasks by name:
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
+ // or by tag:
+ // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
}
},
{
@@ -608,6 +608,8 @@
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
// also possible to spawn tasks by name:
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
+ // or by tag:
+ // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
}
},
// Bindings from Sublime Text
@@ -43,6 +43,8 @@
// "args": ["--login"]
// }
// }
- "shell": "system"
+ "shell": "system",
+ // Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
+ "tags": []
}
]
@@ -2,6 +2,7 @@
#![deny(missing_docs)]
mod debug_format;
+mod serde_helpers;
pub mod static_source;
mod task_template;
mod vscode_format;
@@ -0,0 +1,64 @@
+use schemars::{
+ SchemaGenerator,
+ schema::{ArrayValidation, InstanceType, Schema, SchemaObject, SingleOrVec, StringValidation},
+};
+use serde::de::{self, Deserializer, Visitor};
+use std::fmt;
+
+/// Generates a JSON schema for a non-empty string array.
+pub fn non_empty_string_vec_json_schema(_: &mut SchemaGenerator) -> Schema {
+ Schema::Object(SchemaObject {
+ instance_type: Some(InstanceType::Array.into()),
+ array: Some(Box::new(ArrayValidation {
+ unique_items: Some(true),
+ items: Some(SingleOrVec::Single(Box::new(Schema::Object(
+ SchemaObject {
+ instance_type: Some(InstanceType::String.into()),
+ string: Some(Box::new(StringValidation {
+ min_length: Some(1), // Ensures string in the array is non-empty
+ ..Default::default()
+ })),
+ ..Default::default()
+ },
+ )))),
+ ..Default::default()
+ })),
+ format: Some("vec-of-non-empty-strings".to_string()), // Use a custom format keyword
+ ..Default::default()
+ })
+}
+
+/// Deserializes a non-empty string array.
+pub fn non_empty_string_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ struct NonEmptyStringVecVisitor;
+
+ impl<'de> Visitor<'de> for NonEmptyStringVecVisitor {
+ type Value = Vec<String>;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a list of non-empty strings")
+ }
+
+ fn visit_seq<V>(self, mut seq: V) -> Result<Vec<String>, V::Error>
+ where
+ V: de::SeqAccess<'de>,
+ {
+ let mut vec = Vec::new();
+ while let Some(value) = seq.next_element::<String>()? {
+ if value.is_empty() {
+ return Err(de::Error::invalid_value(
+ de::Unexpected::Str(&value),
+ &"a non-empty string",
+ ));
+ }
+ vec.push(value);
+ }
+ Ok(vec)
+ }
+ }
+
+ deserializer.deserialize_seq(NonEmptyStringVecVisitor)
+}
@@ -1,16 +1,16 @@
-use std::path::PathBuf;
-use util::serde::default_true;
-
use anyhow::{Context, bail};
use collections::{HashMap, HashSet};
use schemars::{JsonSchema, r#gen::SchemaSettings};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
+use std::path::PathBuf;
+use util::serde::default_true;
use util::{ResultExt, truncate_and_remove_front};
use crate::{
AttachConfig, ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TCPHost, TaskContext, TaskId,
VariableName, ZED_VARIABLE_NAME_PREFIX,
+ serde_helpers::{non_empty_string_vec, non_empty_string_vec_json_schema},
};
/// A template definition of a Zed task to run.
@@ -61,8 +61,10 @@ pub struct TaskTemplate {
/// If this task should start a debugger or not
#[serde(default, skip)]
pub task_type: TaskType,
- /// Represents the tags which this template attaches to. Adding this removes this task from other UI.
- #[serde(default)]
+ /// Represents the tags which this template attaches to.
+ /// Adding this removes this task from other UI and gives you ability to run it by tag.
+ #[serde(default, deserialize_with = "non_empty_string_vec")]
+ #[schemars(schema_with = "non_empty_string_vec_json_schema")]
pub tags: Vec<String>,
/// Which shell to use when spawning the task.
#[serde(default)]
@@ -15,10 +15,11 @@ use task::{
};
use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon,
- IconButton, IconButtonShape, IconName, IconSize, IntoElement, KeyBinding, LabelSize, ListItem,
- ListItemSpacing, RenderOnce, Toggleable, Tooltip, div, h_flex, v_flex,
+ IconButton, IconButtonShape, IconName, IconSize, IntoElement, KeyBinding, Label, LabelSize,
+ ListItem, ListItemSpacing, RenderOnce, Toggleable, Tooltip, div, h_flex, v_flex,
};
-use util::ResultExt;
+
+use util::{ResultExt, truncate_and_trailoff};
use workspace::{ModalView, Workspace, tasks::schedule_resolved_task};
pub use zed_actions::{Rerun, Spawn};
@@ -187,6 +188,8 @@ impl Focusable for TasksModal {
impl ModalView for TasksModal {}
+const MAX_TAGS_LINE_LEN: usize = 30;
+
impl PickerDelegate for TasksModalDelegate {
type ListItem = ListItem;
@@ -398,6 +401,18 @@ impl PickerDelegate for TasksModalDelegate {
tooltip_label_text.push_str(&resolved.command_label);
}
}
+ if template.tags.len() > 0 {
+ tooltip_label_text.push('\n');
+ tooltip_label_text.push_str(
+ template
+ .tags
+ .iter()
+ .map(|tag| format!("\n#{}", tag))
+ .collect::<Vec<_>>()
+ .join("")
+ .as_str(),
+ );
+ }
let tooltip_label = if tooltip_label_text.trim().is_empty() {
None
} else {
@@ -439,7 +454,22 @@ impl PickerDelegate for TasksModalDelegate {
ListItem::new(SharedString::from(format!("tasks-modal-{ix}")))
.inset(true)
.start_slot::<Icon>(icon)
- .end_slot::<AnyElement>(history_run_icon)
+ .end_slot::<AnyElement>(
+ h_flex()
+ .gap_1()
+ .child(Label::new(truncate_and_trailoff(
+ &template
+ .tags
+ .iter()
+ .map(|tag| format!("#{}", tag))
+ .collect::<Vec<_>>()
+ .join(" "),
+ MAX_TAGS_LINE_LEN,
+ )))
+ .flex_none()
+ .child(history_run_icon.unwrap())
+ .into_any_element(),
+ )
.spacing(ListItemSpacing::Sparse)
.when_some(tooltip_label, |list_item, item_label| {
list_item.tooltip(move |_, _| item_label.clone())
@@ -6,8 +6,10 @@ use editor::Editor;
use feature_flags::{Debugger, FeatureFlagViewExt};
use gpui::{App, AppContext as _, Context, Entity, Task, Window};
use modal::{TaskOverrides, TasksModal};
-use project::{Location, TaskContexts, Worktree};
-use task::{RevealTarget, TaskContext, TaskId, TaskModal, TaskVariables, VariableName};
+use project::{Location, TaskContexts, TaskSourceKind, Worktree};
+use task::{
+ RevealTarget, TaskContext, TaskId, TaskModal, TaskTemplate, TaskVariables, VariableName,
+};
use workspace::tasks::schedule_task;
use workspace::{Workspace, tasks::schedule_resolved_task};
@@ -117,7 +119,25 @@ fn spawn_task_or_modal(
let overrides = reveal_target.map(|reveal_target| TaskOverrides {
reveal_target: Some(reveal_target),
});
- spawn_task_with_name(task_name.clone(), overrides, window, cx).detach_and_log_err(cx)
+ let name = task_name.clone();
+ spawn_tasks_filtered(move |(_, task)| task.label.eq(&name), overrides, window, cx)
+ .detach_and_log_err(cx)
+ }
+ Spawn::ByTag {
+ task_tag,
+ reveal_target,
+ } => {
+ let overrides = reveal_target.map(|reveal_target| TaskOverrides {
+ reveal_target: Some(reveal_target),
+ });
+ let tag = task_tag.clone();
+ spawn_tasks_filtered(
+ move |(_, task)| task.tags.contains(&tag),
+ overrides,
+ window,
+ cx,
+ )
+ .detach_and_log_err(cx)
}
Spawn::ViaModal { reveal_target } => toggle_modal(
workspace,
@@ -169,18 +189,21 @@ pub fn toggle_modal(
}
}
-fn spawn_task_with_name(
- name: String,
+fn spawn_tasks_filtered<F>(
+ mut predicate: F,
overrides: Option<TaskOverrides>,
window: &mut Window,
cx: &mut Context<Workspace>,
-) -> Task<anyhow::Result<()>> {
+) -> Task<anyhow::Result<()>>
+where
+ F: FnMut((&TaskSourceKind, &TaskTemplate)) -> bool + 'static,
+{
cx.spawn_in(window, async move |workspace, cx| {
let task_contexts = workspace.update_in(cx, |workspace, window, cx| {
task_contexts(workspace, window, cx)
})?;
let task_contexts = task_contexts.await;
- let tasks = workspace.update(cx, |workspace, cx| {
+ let mut tasks = workspace.update(cx, |workspace, cx| {
let Some(task_inventory) = workspace
.project()
.read(cx)
@@ -208,24 +231,31 @@ fn spawn_task_with_name(
let did_spawn = workspace
.update(cx, |workspace, cx| {
- let (task_source_kind, mut target_task) =
- tasks.into_iter().find(|(_, task)| task.label == name)?;
- if let Some(overrides) = &overrides {
- if let Some(target_override) = overrides.reveal_target {
- target_task.reveal_target = target_override;
- }
- }
let default_context = TaskContext::default();
let active_context = task_contexts.active_context().unwrap_or(&default_context);
- schedule_task(
- workspace,
- task_source_kind,
- &target_task,
- active_context,
- false,
- cx,
- );
- Some(())
+
+ tasks.retain_mut(|(task_source_kind, target_task)| {
+ if predicate((task_source_kind, target_task)) {
+ if let Some(overrides) = &overrides {
+ if let Some(target_override) = overrides.reveal_target {
+ target_task.reveal_target = target_override;
+ }
+ }
+ schedule_task(
+ workspace,
+ task_source_kind.clone(),
+ target_task,
+ active_context,
+ false,
+ cx,
+ );
+ true
+ } else {
+ false
+ }
+ });
+
+ if tasks.is_empty() { None } else { Some(()) }
})?
.is_some();
if !did_spawn {
@@ -230,6 +230,12 @@ pub enum Spawn {
#[serde(default)]
reveal_target: Option<RevealTarget>,
},
+ /// Spawns a task by the name given.
+ ByTag {
+ task_tag: String,
+ #[serde(default)]
+ reveal_target: Option<RevealTarget>,
+ },
/// Spawns a task via modal's selection.
ViaModal {
/// Selected task's `reveal_target` property override.
@@ -46,6 +46,8 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to
"show_summary": true,
// Whether to show the command line in the output of the spawned task, defaults to `true`.
"show_output": true
+ // Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
+ "tags": []
}
]
```