diff --git a/Cargo.lock b/Cargo.lock index 939e4b4453abcd..69d13d5bd13d24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12781,6 +12781,7 @@ dependencies = [ "file_icons", "fuzzy", "gpui", + "itertools 0.14.0", "language", "menu", "picker", diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3ffb861171ad8a..29bfde89fac30d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,3 +1,4 @@ +mod graph; pub mod buffer_store; mod color_extractor; pub mod connection_manager; diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index ede820e3e91138..6741aea2c576a8 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -21,7 +21,7 @@ use text::{Point, ToPoint}; use util::{post_inc, NumericPrefixWithSuffix, ResultExt as _}; use worktree::WorktreeId; -use crate::worktree_store::WorktreeStore; +use crate::{graph::{Cycle, Graph}, worktree_store::WorktreeStore}; /// Inventory tracks available tasks for a given project. #[derive(Debug, Default)] @@ -57,6 +57,21 @@ pub enum TaskSourceKind { } impl TaskSourceKind { + pub fn friendly_path(&self) -> Cow<'_, str> { + match self { + Self::UserInput => format!("custom-task").into(), + Self::Language { name } => format!("{name}-language-tasks").into(), + Self::AbsPath { + abs_path, + .. + } => abs_path.to_string_lossy(), + Self::Worktree { + directory_in_worktree, + .. + } => format!("{}", directory_in_worktree.join("tasks.json").display()).into() + } + } + pub fn to_id_base(&self) -> String { match self { TaskSourceKind::UserInput => "oneshot".to_string(), @@ -106,6 +121,71 @@ impl Inventory { .collect() } + /// Verify that the current task dependency graph does not contain any cycles + pub fn check_task_dep_graph(&self) { + // 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() + }) + .unique_by(|(_, task)| task.label.clone()) + .collect_vec(); + + // map task labels to their dep graph node idx, source, and dependencies + let tasks = tasks + .iter() + .enumerate() + .map(|(idx, (source, task))| ( + task.label.as_str(), + ( + idx as u32, + source, + task.pre.iter().map(|s| s.as_str()).unique().collect_vec() + ) + )) + .collect::>(); + + // map node idxs to task labels for retreival if a cycle is found + let node_idx_map = tasks + .iter() + .map(|(label, (idx, _, _))| (*idx, *label)) + .collect::>(); + + let mut dep_graph = Graph::new(); + + for (_, (node_idx, _, pre)) in &tasks { + dep_graph.add_node(*node_idx); + + for pre_label in pre { + if let Some((pre_node_idx, _, _)) = tasks.get(pre_label) { + dep_graph.add_edge(*node_idx, *pre_node_idx); + } + } + } + + for node in node_idx_map.keys() { + if let Some(Cycle { src_node, dst_node }) = dep_graph.has_cycle(*node) { + let src_label = node_idx_map.get(&src_node).unwrap(); + let dst_label = node_idx_map.get(&dst_node).unwrap(); + + let src_info = tasks.get(src_label).unwrap(); + let dst_info = tasks.get(dst_label).unwrap(); + + // error reporting in the UI is WIP, this is here so I can verify with the logs at runtime + + let src_fmt = format!("(task source: {}, task label: {})", src_info.1.friendly_path(), src_label); + let dst_fmt = format!("(task source: {}, task label: {})", dst_info.1.friendly_path(), dst_label); + + log::error!("found cycle: source task: {src_fmt}, dependent task: {dst_fmt}"); + } + } + } + /// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContext`] given. /// Joins the new resolutions with the resolved tasks that were used (spawned) before, /// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first. @@ -324,6 +404,9 @@ impl Inventory { } None => parsed_templates.global = new_templates.collect(), } + + self.check_task_dep_graph(); + Ok(()) } } diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 2d13a7e18b0855..d1e10e490a4f4b 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -61,6 +61,12 @@ pub struct TaskTemplate { /// Represents the tags which this template attaches to. Adding this removes this task from other UI. #[serde(default)] pub tags: Vec, + /// A list of other tasks to be run before executing this task, referenced by label + #[serde(default)] + pub pre: Vec, + /// A list of other tasks to be run after executing this task, referenced by label + #[serde(default)] + pub post: Vec, /// Which shell to use when spawning the task. #[serde(default)] pub shell: Shell, diff --git a/crates/tasks_ui/Cargo.toml b/crates/tasks_ui/Cargo.toml index 528d23832935ae..192cc5bca53a0b 100644 --- a/crates/tasks_ui/Cargo.toml +++ b/crates/tasks_ui/Cargo.toml @@ -26,6 +26,7 @@ util.workspace = true workspace.workspace = true language.workspace = true zed_actions.workspace = true +itertools.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] }