diff --git a/Cargo.lock b/Cargo.lock index b1fc9a5216946b..46faa226e6cd4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12856,6 +12856,7 @@ dependencies = [ "futures 0.3.31", "gpui", "hex", + "itertools 0.14.0", "parking_lot", "schemars", "serde", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1212084b067c5e..3c8ddccf29e85b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4208,9 +4208,10 @@ impl Editor { match action { CodeActionsItem::Task(task_source_kind, resolved_task) => { workspace.update(cx, |workspace, cx| { - workspace::tasks::schedule_resolved_task( + workspace::tasks::schedule_resolved_tasks( workspace, task_source_kind, + vec![], resolved_task, false, cx, @@ -5087,9 +5088,35 @@ impl Editor { workspace .update(&mut cx, |workspace, cx| { - workspace::tasks::schedule_resolved_task( + let worktree = match task_source_kind { + TaskSourceKind::Worktree { id, .. } => Some(id), + _ => None + }; + + let pre_tasks = workspace + .project() + .read(cx) + .task_store() + .read(cx) + .task_inventory() + .map_or(vec![], |inventory| { + inventory + .read(cx) + .build_pre_task_list( + &resolved_task, + worktree, + &context + ) + .unwrap_or(vec![]) + .into_iter() + .map(|(_, task)| task) + .collect_vec() + }); + + workspace::tasks::schedule_resolved_tasks( workspace, task_source_kind, + pre_tasks, resolved_task, false, cx, diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index cc201012a3c7da..71e1a5465bc681 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -123,37 +123,42 @@ impl Inventory { } /// Topological sort of the dependency graph of `task` - pub fn build_pre_task_list(&self, task: &TaskTemplate) -> anyhow::Result> { - // collect all tasks from all available worktrees - let tasks = self - .templates_from_settings - .worktree - .iter() - .flat_map(|leaf| { - self.worktree_templates_from_settings(Some(*leaf.0)) - .chain(self.global_templates_from_settings()) - .collect_vec() + pub fn build_pre_task_list( + &self, + base_task: &ResolvedTask, + worktree: Option, + task_context: &TaskContext, + ) -> anyhow::Result> { + let tasks_in_scope = self + .worktree_templates_from_settings(worktree) + .chain(self.global_templates_from_settings()) + .filter_map(|(kind, task)| { + let base_id = kind.to_id_base(); + task + .resolve_task(&base_id, task_context) + .map(|task| (kind, task)) }) - .unique_by(|(_, task)| task.label.clone()) + .unique_by(|(_, task)| task.resolved_label.clone()) .collect_vec(); - if let None = tasks + if let None = tasks_in_scope .iter() - .find(|(_, TaskTemplate { label, .. })| task.label.as_str() == label.as_str()) + .find(|(_, task)| task.resolved_label == base_task.resolved_label) { - return Err(anyhow::anyhow!("couldn't find with label {} in available tasks", &task.label)); + return Err(anyhow::anyhow!("couldn't find with label {} in available tasks", base_task.resolved_label)); } // map task labels to their dep graph node idx, source, and dependencies - let nodes = tasks + let nodes = tasks_in_scope .iter() .enumerate() .map(|(idx, (source, task))| ( - task.label.as_str(), + task.resolved_label, ( idx as u32, source, - task.pre + task + .resolved_pre_labels .iter() .map(|s| s.as_str()) .unique() @@ -181,9 +186,8 @@ impl Inventory { } } - dep_graph - .subgraph(nodes.get(task.label.as_str()).unwrap().0) + .subgraph(nodes.get(base_task.resolved_label.as_str()).unwrap().0) .topo_sort() .map(|tasks| { tasks diff --git a/crates/task/Cargo.toml b/crates/task/Cargo.toml index 6bc7489d86eaff..e765af9d5b0263 100644 --- a/crates/task/Cargo.toml +++ b/crates/task/Cargo.toml @@ -22,6 +22,7 @@ sha2.workspace = true shellexpand.workspace = true util.workspace = true zed_actions.workspace = true +itertools.workspace = true [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 0470e1e3a2841e..ed2fe329b3317d 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -78,6 +78,8 @@ pub struct ResolvedTask { /// Further actions that need to take place after the resolved task is spawned, /// with all task variables resolved. pub resolved: Option, + /// Pretasks with their variables expanded + pub resolved_pre_labels: Vec } impl ResolvedTask { diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index d1e10e490a4f4b..8176adb47d2cc7 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -1,4 +1,5 @@ use std::path::PathBuf; +use itertools::Itertools as _; use util::serde::default_true; use anyhow::{bail, Context}; @@ -160,6 +161,7 @@ impl TaskTemplate { None => None, } .or(cx.cwd.clone()); + let human_readable_label = substitute_all_template_variables_in_str( &self.label, &truncated_variables, @@ -176,18 +178,32 @@ impl TaskTemplate { } string }); + let full_label = substitute_all_template_variables_in_str( &self.label, &task_variables, &variable_names, &mut substituted_variables, )?; + + let pre_labels = self + .pre + .iter() + .filter_map(|pre_label| substitute_all_template_variables_in_str( + pre_label, + &truncated_variables, + &variable_names, + &mut substituted_variables + )) + .collect_vec(); + let command = substitute_all_template_variables_in_str( &self.command, &task_variables, &variable_names, &mut substituted_variables, )?; + let args_with_substitutions = substitute_all_template_variables_in_vec( &self.args, &task_variables, @@ -228,6 +244,7 @@ impl TaskTemplate { substituted_variables, original_task: self.clone(), resolved_label: full_label.clone(), + resolved_pre_labels: pre_labels, resolved: Some(SpawnInTerminal { id, cwd, diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 21625cbe249831..8c93d221220c94 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -1,11 +1,14 @@ +use itertools::Itertools; use ::settings::Settings; use editor::{tasks::task_context, Editor}; use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext}; use modal::{TaskOverrides, TasksModal}; -use project::{Location, WorktreeId}; +use project::{Location, TaskSourceKind, WorktreeId}; use task::{RevealTarget, TaskId}; +use ui::VisualContext; +use util::ResultExt; use workspace::tasks::schedule_task; -use workspace::{tasks::schedule_resolved_task, Workspace}; +use workspace::{tasks::schedule_resolved_tasks, Workspace}; mod modal; mod settings; @@ -37,12 +40,15 @@ pub fn init(cx: &mut AppContext) { { if action.reevaluate_context { let mut original_task = last_scheduled_task.original_task().clone(); + if let Some(allow_concurrent_runs) = action.allow_concurrent_runs { original_task.allow_concurrent_runs = allow_concurrent_runs; } + if let Some(use_new_terminal) = action.use_new_terminal { original_task.use_new_terminal = use_new_terminal; } + let context_task = task_context(workspace, cx); cx.spawn(|workspace, mut cx| async move { let task_context = context_task.await; @@ -70,13 +76,50 @@ pub fn init(cx: &mut AppContext) { } } - schedule_resolved_task( - workspace, - task_source_kind, - last_scheduled_task, - false, - cx, - ); + let worktree = match task_source_kind { + TaskSourceKind::Worktree { id, .. } => Some(id), + _ => None + }; + + let task_context = task_context(workspace, cx); + + let _ = cx.spawn(|workspace, mut cx| async move { + let task_context = task_context.await; + let Some(workspace) = workspace.upgrade() else { return; }; + + cx.update_view(&workspace, |workspace, cx| { + let pre_tasks = workspace + .project() + .read(cx) + .task_store() + .read(cx) + .task_inventory() + .map_or(vec![], |inventory| { + inventory + .read(cx) + .build_pre_task_list( + &last_scheduled_task, + worktree, + &task_context + ) + .unwrap_or(vec![]) + .into_iter() + .map(|(_, task)| task) + .collect_vec() + }); + + schedule_resolved_tasks( + workspace, + task_source_kind, + pre_tasks, + last_scheduled_task, + false, + cx, + ); + }).log_err(); + + }); + } } else { toggle_modal(workspace, None, cx).detach(); diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 1dc63c4852a09e..c26b021a894735 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -3,10 +3,9 @@ use std::sync::Arc; use crate::active_item_selection_properties; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusableView, - InteractiveElement, Model, ParentElement, Render, SharedString, Styled, Subscription, Task, - View, ViewContext, VisualContext, WeakView, + rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusableView, InteractiveElement, Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView }; +use itertools::Itertools; use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate}; use project::{task_store::TaskStore, TaskSourceKind}; use task::{ResolvedTask, RevealTarget, TaskContext, TaskTemplate}; @@ -17,7 +16,7 @@ use ui::{ WindowContext, }; use util::ResultExt; -use workspace::{tasks::schedule_resolved_task, ModalView, Workspace}; +use workspace::{tasks::schedule_resolved_tasks, ModalView, Workspace}; pub use zed_actions::{Rerun, Spawn}; /// A modal used to spawn new tasks. @@ -296,7 +295,39 @@ impl PickerDelegate for TasksModalDelegate { self.workspace .update(cx, |workspace, cx| { - schedule_resolved_task(workspace, task_source_kind, task, omit_history_entry, cx); + let worktree = match task_source_kind { + TaskSourceKind::Worktree { id, .. } => Some(id), + _ => None + }; + + let pre_tasks = workspace + .project() + .read(cx) + .task_store() + .read(cx) + .task_inventory() + .map_or(vec![], |inventory| { + inventory + .read(cx) + .build_pre_task_list( + &task, + worktree, + &self.task_context + ) + .unwrap_or(vec![]) + .into_iter() + .map(|(_, task)| task) + .collect_vec() + }); + + schedule_resolved_tasks( + workspace, + task_source_kind, + pre_tasks, + task, + omit_history_entry, + cx + ); }) .ok(); cx.emit(DismissEvent); @@ -443,7 +474,14 @@ impl PickerDelegate for TasksModalDelegate { } self.workspace .update(cx, |workspace, cx| { - schedule_resolved_task(workspace, task_source_kind, task, omit_history_entry, cx); + schedule_resolved_tasks( + workspace, + task_source_kind, + vec![], + task, + omit_history_entry, + cx + ); }) .ok(); cx.emit(DismissEvent); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index cea23da3bc3e47..21aa7d7005c519 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -254,7 +254,7 @@ impl TerminalPanel { action: spawn_in_terminal, } = e { - terminal_panel.spawn_task(spawn_in_terminal, cx); + terminal_panel.spawn_task_queue(spawn_in_terminal, cx); }; }) .detach(); @@ -447,68 +447,71 @@ impl TerminalPanel { .detach_and_log_err(cx); } - fn spawn_task(&mut self, task: &SpawnInTerminal, cx: &mut ViewContext) { - let Ok(is_local) = self - .workspace - .update(cx, |workspace, cx| workspace.project().read(cx).is_local()) - else { - return; - }; + fn spawn_task_queue(&mut self, tasks: &[SpawnInTerminal], cx: &mut ViewContext) { + todo!("implement task synchronization"); + for task in tasks { + let Ok(is_local) = self + .workspace + .update(cx, |workspace, cx| workspace.project().read(cx).is_local()) + else { + return; + }; - let builder = ShellBuilder::new(is_local, &task.shell); - let command_label = builder.command_label(&task.command_label); - let (command, args) = builder.build(task.command.clone(), &task.args); + let builder = ShellBuilder::new(is_local, &task.shell); + let command_label = builder.command_label(&task.command_label); + let (command, args) = builder.build(task.command.clone(), &task.args); - let task = SpawnInTerminal { - command_label, - command, - args, - ..task.clone() - }; + let task = SpawnInTerminal { + command_label, + command, + args, + ..task.clone() + }; - if task.allow_concurrent_runs && task.use_new_terminal { - self.spawn_in_new_terminal(task, cx).detach_and_log_err(cx); - return; - } + if task.allow_concurrent_runs && task.use_new_terminal { + self.spawn_in_new_terminal(task, cx).detach_and_log_err(cx); + return; + } - let mut terminals_for_task = self.terminals_for_task(&task.full_label, cx); - let Some(existing) = terminals_for_task.pop() else { - self.spawn_in_new_terminal(task, cx).detach_and_log_err(cx); - return; - }; + let mut terminals_for_task = self.terminals_for_task(&task.full_label, cx); + let Some(existing) = terminals_for_task.pop() else { + self.spawn_in_new_terminal(task, cx).detach_and_log_err(cx); + return; + }; - let (existing_item_index, task_pane, existing_terminal) = existing; - if task.allow_concurrent_runs { - self.replace_terminal(task, task_pane, existing_item_index, existing_terminal, cx) - .detach(); - return; - } + let (existing_item_index, task_pane, existing_terminal) = existing; + if task.allow_concurrent_runs { + self.replace_terminal(task, task_pane, existing_item_index, existing_terminal, cx) + .detach(); + return; + } - self.deferred_tasks.insert( - task.id.clone(), - cx.spawn(|terminal_panel, mut cx| async move { - wait_for_terminals_tasks(terminals_for_task, &mut cx).await; - let task = terminal_panel.update(&mut cx, |terminal_panel, cx| { - if task.use_new_terminal { - terminal_panel - .spawn_in_new_terminal(task, cx) - .detach_and_log_err(cx); - None - } else { - Some(terminal_panel.replace_terminal( - task, - task_pane, - existing_item_index, - existing_terminal, - cx, - )) + self.deferred_tasks.insert( + task.id.clone(), + cx.spawn(|terminal_panel, mut cx| async move { + wait_for_terminals_tasks(terminals_for_task, &mut cx).await; + let task = terminal_panel.update(&mut cx, |terminal_panel, cx| { + if task.use_new_terminal { + terminal_panel + .spawn_in_new_terminal(task, cx) + .detach_and_log_err(cx); + None + } else { + Some(terminal_panel.replace_terminal( + task, + task_pane, + existing_item_index, + existing_terminal, + cx, + )) + } + }); + if let Ok(Some(task)) = task { + task.await; } - }); - if let Ok(Some(task)) = task { - task.await; - } - }), - ); + }), + ); + } } pub fn spawn_in_new_terminal( diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index abb4ec8bd8973d..658f6731c06fd6 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1258,7 +1258,7 @@ impl ShellExec { let cwd = project.first_project_directory(cx); let shell = project.terminal_settings(&cwd, cx).shell.clone(); cx.emit(workspace::Event::SpawnTask { - action: Box::new(SpawnInTerminal { + action: vec![SpawnInTerminal { id: TaskId("vim".to_string()), full_label: self.command.clone(), label: self.command.clone(), @@ -1275,7 +1275,7 @@ impl ShellExec { shell, show_summary: false, show_command: false, - }), + }], }); }); return; diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs index c01e2ae52be327..9d783c59a9c518 100644 --- a/crates/workspace/src/tasks.rs +++ b/crates/workspace/src/tasks.rs @@ -1,3 +1,4 @@ +use itertools::Itertools as _; use project::TaskSourceKind; use remote::ConnectionState; use task::{ResolvedTask, TaskContext, TaskTemplate}; @@ -29,9 +30,35 @@ pub fn schedule_task( if let Some(spawn_in_terminal) = task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx) { - schedule_resolved_task( + let worktree = match task_source_kind { + TaskSourceKind::Worktree { id, .. } => Some(id), + _ => None + }; + + let pre_tasks = workspace + .project() + .read(cx) + .task_store() + .read(cx) + .task_inventory() + .map_or(vec![], |inventory| { + inventory + .read(cx) + .build_pre_task_list( + &spawn_in_terminal, + worktree, + task_cx + ) + .unwrap_or(vec![]) + .into_iter() + .map(|(_, task)| task) + .collect_vec() + }); + + schedule_resolved_tasks( workspace, task_source_kind, + pre_tasks, spawn_in_terminal, omit_history, cx, @@ -39,9 +66,10 @@ pub fn schedule_task( } } -pub fn schedule_resolved_task( +pub fn schedule_resolved_tasks( workspace: &mut Workspace, task_source_kind: TaskSourceKind, + pre_tasks: Vec, mut resolved_task: ResolvedTask, omit_history: bool, cx: &mut ViewContext, @@ -60,8 +88,15 @@ pub fn schedule_resolved_task( }); } + let mut all_tasks = pre_tasks + .into_iter() + .filter_map(|mut task| task.resolved.take()) + .collect_vec(); + + all_tasks.push(spawn_in_terminal); + cx.emit(crate::Event::SpawnTask { - action: Box::new(spawn_in_terminal), + action: all_tasks, }); } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e4488650ec3b6d..af60f558076522 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -728,7 +728,7 @@ pub enum Event { ContactRequestedJoin(u64), WorkspaceCreated(WeakView), SpawnTask { - action: Box, + action: Vec, }, OpenBundledFile { text: Cow<'static, str>,