tasks: Add spawn option by tag (#25650)
Closes #19497 Fixed conflicts from https://github.com/zed-industries/zed/pull/19498 Added tags to tasks selector Release Notes: - Added ability to spawn tasks by tag with key bindings - Added tags to tasks selector https://github.com/user-attachments/assets/0eefea21-ec4e-407c-9d4f-2a0a4a0f74df --------- Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
This commit is contained in:
parent
80441f675b
commit
2f5a4f7e80
10 changed files with 174 additions and 33 deletions
|
@ -482,6 +482,8 @@
|
||||||
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
|
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
|
||||||
// also possible to spawn tasks by name:
|
// also possible to spawn tasks by name:
|
||||||
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
|
// "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" }]
|
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
|
||||||
// also possible to spawn tasks by name:
|
// also possible to spawn tasks by name:
|
||||||
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
|
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
|
||||||
|
// or by tag:
|
||||||
|
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Bindings from Sublime Text
|
// Bindings from Sublime Text
|
||||||
|
|
|
@ -43,6 +43,8 @@
|
||||||
// "args": ["--login"]
|
// "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)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
mod debug_format;
|
mod debug_format;
|
||||||
|
mod serde_helpers;
|
||||||
pub mod static_source;
|
pub mod static_source;
|
||||||
mod task_template;
|
mod task_template;
|
||||||
mod vscode_format;
|
mod vscode_format;
|
||||||
|
|
64
crates/task/src/serde_helpers.rs
Normal file
64
crates/task/src/serde_helpers.rs
Normal file
|
@ -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 anyhow::{Context, bail};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use schemars::{JsonSchema, r#gen::SchemaSettings};
|
use schemars::{JsonSchema, r#gen::SchemaSettings};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use util::serde::default_true;
|
||||||
use util::{ResultExt, truncate_and_remove_front};
|
use util::{ResultExt, truncate_and_remove_front};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AttachConfig, ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TCPHost, TaskContext, TaskId,
|
AttachConfig, ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TCPHost, TaskContext, TaskId,
|
||||||
VariableName, ZED_VARIABLE_NAME_PREFIX,
|
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.
|
/// 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
|
/// If this task should start a debugger or not
|
||||||
#[serde(default, skip)]
|
#[serde(default, skip)]
|
||||||
pub task_type: TaskType,
|
pub task_type: TaskType,
|
||||||
/// Represents the tags which this template attaches to. Adding this removes this task from other UI.
|
/// Represents the tags which this template attaches to.
|
||||||
#[serde(default)]
|
/// 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>,
|
pub tags: Vec<String>,
|
||||||
/// Which shell to use when spawning the task.
|
/// Which shell to use when spawning the task.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
|
@ -15,10 +15,11 @@ use task::{
|
||||||
};
|
};
|
||||||
use ui::{
|
use ui::{
|
||||||
ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon,
|
ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon,
|
||||||
IconButton, IconButtonShape, IconName, IconSize, IntoElement, KeyBinding, LabelSize, ListItem,
|
IconButton, IconButtonShape, IconName, IconSize, IntoElement, KeyBinding, Label, LabelSize,
|
||||||
ListItemSpacing, RenderOnce, Toggleable, Tooltip, div, h_flex, v_flex,
|
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};
|
use workspace::{ModalView, Workspace, tasks::schedule_resolved_task};
|
||||||
pub use zed_actions::{Rerun, Spawn};
|
pub use zed_actions::{Rerun, Spawn};
|
||||||
|
|
||||||
|
@ -187,6 +188,8 @@ impl Focusable for TasksModal {
|
||||||
|
|
||||||
impl ModalView for TasksModal {}
|
impl ModalView for TasksModal {}
|
||||||
|
|
||||||
|
const MAX_TAGS_LINE_LEN: usize = 30;
|
||||||
|
|
||||||
impl PickerDelegate for TasksModalDelegate {
|
impl PickerDelegate for TasksModalDelegate {
|
||||||
type ListItem = ListItem;
|
type ListItem = ListItem;
|
||||||
|
|
||||||
|
@ -398,6 +401,18 @@ impl PickerDelegate for TasksModalDelegate {
|
||||||
tooltip_label_text.push_str(&resolved.command_label);
|
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() {
|
let tooltip_label = if tooltip_label_text.trim().is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
@ -439,7 +454,22 @@ impl PickerDelegate for TasksModalDelegate {
|
||||||
ListItem::new(SharedString::from(format!("tasks-modal-{ix}")))
|
ListItem::new(SharedString::from(format!("tasks-modal-{ix}")))
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.start_slot::<Icon>(icon)
|
.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)
|
.spacing(ListItemSpacing::Sparse)
|
||||||
.when_some(tooltip_label, |list_item, item_label| {
|
.when_some(tooltip_label, |list_item, item_label| {
|
||||||
list_item.tooltip(move |_, _| item_label.clone())
|
list_item.tooltip(move |_, _| item_label.clone())
|
||||||
|
|
|
@ -6,8 +6,10 @@ use editor::Editor;
|
||||||
use feature_flags::{Debugger, FeatureFlagViewExt};
|
use feature_flags::{Debugger, FeatureFlagViewExt};
|
||||||
use gpui::{App, AppContext as _, Context, Entity, Task, Window};
|
use gpui::{App, AppContext as _, Context, Entity, Task, Window};
|
||||||
use modal::{TaskOverrides, TasksModal};
|
use modal::{TaskOverrides, TasksModal};
|
||||||
use project::{Location, TaskContexts, Worktree};
|
use project::{Location, TaskContexts, TaskSourceKind, Worktree};
|
||||||
use task::{RevealTarget, TaskContext, TaskId, TaskModal, TaskVariables, VariableName};
|
use task::{
|
||||||
|
RevealTarget, TaskContext, TaskId, TaskModal, TaskTemplate, TaskVariables, VariableName,
|
||||||
|
};
|
||||||
use workspace::tasks::schedule_task;
|
use workspace::tasks::schedule_task;
|
||||||
use workspace::{Workspace, tasks::schedule_resolved_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 {
|
let overrides = reveal_target.map(|reveal_target| TaskOverrides {
|
||||||
reveal_target: Some(reveal_target),
|
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(
|
Spawn::ViaModal { reveal_target } => toggle_modal(
|
||||||
workspace,
|
workspace,
|
||||||
|
@ -169,18 +189,21 @@ pub fn toggle_modal(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_task_with_name(
|
fn spawn_tasks_filtered<F>(
|
||||||
name: String,
|
mut predicate: F,
|
||||||
overrides: Option<TaskOverrides>,
|
overrides: Option<TaskOverrides>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Workspace>,
|
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| {
|
cx.spawn_in(window, async move |workspace, cx| {
|
||||||
let task_contexts = workspace.update_in(cx, |workspace, window, cx| {
|
let task_contexts = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
task_contexts(workspace, window, cx)
|
task_contexts(workspace, window, cx)
|
||||||
})?;
|
})?;
|
||||||
let task_contexts = task_contexts.await;
|
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
|
let Some(task_inventory) = workspace
|
||||||
.project()
|
.project()
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
@ -208,24 +231,31 @@ fn spawn_task_with_name(
|
||||||
|
|
||||||
let did_spawn = workspace
|
let did_spawn = workspace
|
||||||
.update(cx, |workspace, cx| {
|
.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 default_context = TaskContext::default();
|
||||||
let active_context = task_contexts.active_context().unwrap_or(&default_context);
|
let active_context = task_contexts.active_context().unwrap_or(&default_context);
|
||||||
schedule_task(
|
|
||||||
workspace,
|
tasks.retain_mut(|(task_source_kind, target_task)| {
|
||||||
task_source_kind,
|
if predicate((task_source_kind, target_task)) {
|
||||||
&target_task,
|
if let Some(overrides) = &overrides {
|
||||||
active_context,
|
if let Some(target_override) = overrides.reveal_target {
|
||||||
false,
|
target_task.reveal_target = target_override;
|
||||||
cx,
|
}
|
||||||
);
|
}
|
||||||
Some(())
|
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();
|
.is_some();
|
||||||
if !did_spawn {
|
if !did_spawn {
|
||||||
|
|
|
@ -230,6 +230,12 @@ pub enum Spawn {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
reveal_target: Option<RevealTarget>,
|
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.
|
/// Spawns a task via modal's selection.
|
||||||
ViaModal {
|
ViaModal {
|
||||||
/// Selected task's `reveal_target` property override.
|
/// 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,
|
"show_summary": true,
|
||||||
// Whether to show the command line in the output of the spawned task, defaults to `true`.
|
// Whether to show the command line in the output of the spawned task, defaults to `true`.
|
||||||
"show_output": true
|
"show_output": true
|
||||||
|
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
||||||
|
"tags": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue