Skip to content

Commit

Permalink
modify task schema to accept pre-/post-task tasks and implement task …
Browse files Browse the repository at this point in the history
…dep cycle detection
  • Loading branch information
Carter Canedy committed Jan 16, 2025
1 parent 1b1c2e5 commit d14355e
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 1 deletion.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/project/src/project.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod graph;
pub mod buffer_store;
mod color_extractor;
pub mod connection_manager;
Expand Down
85 changes: 84 additions & 1 deletion crates/project/src/task_inventory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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::<HashMap<_, _>>();

// 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::<HashMap<_, _>>();

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.
Expand Down Expand Up @@ -324,6 +404,9 @@ impl Inventory {
}
None => parsed_templates.global = new_templates.collect(),
}

self.check_task_dep_graph();

Ok(())
}
}
Expand Down
6 changes: 6 additions & 0 deletions crates/task/src/task_template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// A list of other tasks to be run before executing this task, referenced by label
#[serde(default)]
pub pre: Vec<String>,
/// A list of other tasks to be run after executing this task, referenced by label
#[serde(default)]
pub post: Vec<String>,
/// Which shell to use when spawning the task.
#[serde(default)]
pub shell: Shell,
Expand Down
1 change: 1 addition & 0 deletions crates/tasks_ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down

0 comments on commit d14355e

Please sign in to comment.