tasks: Prefer worktree tasks to global tasks in tag selection (#11427)

Piotr Osiewicz created

Release Notes:

- Added test indicators in Rust files, backed by task system.

Change summary

crates/editor/src/editor.rs          | 47 +++++++++++++++++------------
crates/project/src/project_tests.rs  | 32 ++++++++++----------
crates/project/src/task_inventory.rs | 32 ++++++++++----------
crates/task/src/lib.rs               |  2 
4 files changed, 60 insertions(+), 53 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -401,7 +401,7 @@ impl Default for ScrollbarMarkerState {
 
 #[derive(Clone, Debug)]
 struct RunnableTasks {
-    templates: SmallVec<[(TaskSourceKind, TaskTemplate); 1]>,
+    templates: Vec<(TaskSourceKind, TaskTemplate)>,
     // We need the column at which the task context evaluation should take place.
     column: u32,
 }
@@ -7725,10 +7725,7 @@ impl Editor {
         &self,
         runnable: &mut Runnable,
         cx: &WindowContext<'_>,
-    ) -> (
-        SmallVec<[(TaskSourceKind, TaskTemplate); 1]>,
-        Option<WorktreeId>,
-    ) {
+    ) -> (Vec<(TaskSourceKind, TaskTemplate)>, Option<WorktreeId>) {
         let Some(project) = self.project.as_ref() else {
             return Default::default();
         };
@@ -7743,22 +7740,32 @@ impl Editor {
 
         let inventory = inventory.read(cx);
         let tags = mem::take(&mut runnable.tags);
-        (
-            SmallVec::from_iter(
-                tags.into_iter()
-                    .flat_map(|tag| {
-                        let tag = tag.0.clone();
-                        inventory
-                            .list_tasks(Some(runnable.language.clone()), worktree_id)
-                            .into_iter()
-                            .filter(move |(_, template)| {
-                                template.tags.iter().any(|source_tag| source_tag == &tag)
-                            })
+        let mut tags: Vec<_> = tags
+            .into_iter()
+            .flat_map(|tag| {
+                let tag = tag.0.clone();
+                inventory
+                    .list_tasks(Some(runnable.language.clone()), worktree_id)
+                    .into_iter()
+                    .filter(move |(_, template)| {
+                        template.tags.iter().any(|source_tag| source_tag == &tag)
                     })
-                    .sorted_by_key(|(kind, _)| kind.to_owned()),
-            ),
-            worktree_id,
-        )
+            })
+            .sorted_by_key(|(kind, _)| kind.to_owned())
+            .collect();
+        if let Some((leading_tag_source, _)) = tags.first() {
+            // Strongest source wins; if we have worktree tag binding, prefer that to
+            // global and language bindings;
+            // if we have a global binding, prefer that to language binding.
+            let first_mismatch = tags
+                .iter()
+                .position(|(tag_source, _)| tag_source != leading_tag_source);
+            if let Some(index) = first_mismatch {
+                tags.truncate(index);
+            }
+        }
+
+        (tags, worktree_id)
     }
 
     pub fn move_to_enclosing_bracket(

crates/project/src/project_tests.rs 🔗

@@ -192,6 +192,12 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
         assert_eq!(
             all_tasks,
             vec![
+                (
+                    global_task_source_kind.clone(),
+                    "cargo check".to_string(),
+                    vec!["check".to_string(), "--all".to_string()],
+                    HashMap::default(),
+                ),
                 (
                     TaskSourceKind::Worktree {
                         id: workree_id,
@@ -202,12 +208,6 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
                     vec!["check".to_string()],
                     HashMap::default(),
                 ),
-                (
-                    global_task_source_kind.clone(),
-                    "cargo check".to_string(),
-                    vec!["check".to_string(), "--all".to_string()],
-                    HashMap::default(),
-                ),
             ]
         );
     });
@@ -278,16 +278,6 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
         assert_eq!(
             all_tasks,
             vec![
-                (
-                    TaskSourceKind::Worktree {
-                        id: workree_id,
-                        abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"),
-                        id_base: "local_tasks_for_worktree",
-                    },
-                    "cargo check".to_string(),
-                    vec!["check".to_string()],
-                    HashMap::default(),
-                ),
                 (
                     TaskSourceKind::Worktree {
                         id: workree_id,
@@ -305,6 +295,16 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
                         "-Zunstable-options".to_string()
                     ))),
                 ),
+                (
+                    TaskSourceKind::Worktree {
+                        id: workree_id,
+                        abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"),
+                        id_base: "local_tasks_for_worktree",
+                    },
+                    "cargo check".to_string(),
+                    vec!["check".to_string()],
+                    HashMap::default(),
+                ),
             ]
         );
     });

crates/project/src/task_inventory.rs 🔗

@@ -6,7 +6,7 @@ use std::{
     sync::Arc,
 };
 
-use collections::{hash_map, HashMap, VecDeque};
+use collections::{btree_map, BTreeMap, VecDeque};
 use gpui::{AppContext, Context, Model, ModelContext};
 use itertools::{Either, Itertools};
 use language::Language;
@@ -32,17 +32,17 @@ struct SourceInInventory {
 pub enum TaskSourceKind {
     /// bash-like commands spawned by users, not associated with any path
     UserInput,
-    /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path
-    AbsPath {
-        id_base: &'static str,
-        abs_path: PathBuf,
-    },
     /// Tasks from the worktree's .zed/task.json
     Worktree {
         id: WorktreeId,
         abs_path: PathBuf,
         id_base: &'static str,
     },
+    /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path
+    AbsPath {
+        id_base: &'static str,
+        abs_path: PathBuf,
+    },
     /// Languages-specific tasks coming from extensions.
     Language { name: Arc<str> },
 }
@@ -197,7 +197,7 @@ impl Inventory {
                 }
             })
             .fold(
-                HashMap::default(),
+                BTreeMap::default(),
                 |mut tasks, (task_source_kind, resolved_task)| {
                     tasks.entry(&resolved_task.id).or_insert_with(|| {
                         (task_source_kind, resolved_task, post_inc(&mut lru_score))
@@ -238,18 +238,18 @@ impl Inventory {
             .into_iter()
             .map(|(_, (kind, task, lru_score))| (kind.clone(), task.clone(), lru_score));
 
-        let mut tasks_by_label = HashMap::default();
+        let mut tasks_by_label = BTreeMap::default();
         tasks_by_label = previously_spawned_tasks.into_iter().fold(
             tasks_by_label,
             |mut tasks_by_label, (source, task, lru_score)| {
                 match tasks_by_label.entry((source, task.resolved_label.clone())) {
-                    hash_map::Entry::Occupied(mut o) => {
+                    btree_map::Entry::Occupied(mut o) => {
                         let (_, previous_lru_score) = o.get();
                         if previous_lru_score >= &lru_score {
                             o.insert((task, lru_score));
                         }
                     }
-                    hash_map::Entry::Vacant(v) => {
+                    btree_map::Entry::Vacant(v) => {
                         v.insert((task, lru_score));
                     }
                 }
@@ -260,7 +260,7 @@ impl Inventory {
             tasks_by_label,
             |mut tasks_by_label, (source, task, lru_score)| {
                 match tasks_by_label.entry((source, task.resolved_label.clone())) {
-                    hash_map::Entry::Occupied(mut o) => {
+                    btree_map::Entry::Occupied(mut o) => {
                         let (previous_task, _) = o.get();
                         let new_template = task.original_task();
                         if new_template.ignore_previously_resolved
@@ -269,7 +269,7 @@ impl Inventory {
                             o.insert((task, lru_score));
                         }
                     }
-                    hash_map::Entry::Vacant(v) => {
+                    btree_map::Entry::Vacant(v) => {
                         v.insert((task, lru_score));
                     }
                 }
@@ -668,21 +668,21 @@ mod tests {
             (
                 TaskSourceKind::AbsPath {
                     id_base: "test source",
-                    abs_path: path_2.to_path_buf(),
+                    abs_path: path_1.to_path_buf(),
                 },
-                common_name.to_string(),
+                "static_source_1".to_string(),
             ),
             (
                 TaskSourceKind::AbsPath {
                     id_base: "test source",
                     abs_path: path_1.to_path_buf(),
                 },
-                "static_source_1".to_string(),
+                common_name.to_string(),
             ),
             (
                 TaskSourceKind::AbsPath {
                     id_base: "test source",
-                    abs_path: path_1.to_path_buf(),
+                    abs_path: path_2.to_path_buf(),
                 },
                 common_name.to_string(),
             ),

crates/task/src/lib.rs 🔗

@@ -16,7 +16,7 @@ pub use vscode_format::VsCodeTaskFile;
 
 /// Task identifier, unique within the application.
 /// Based on it, task reruns and terminal tabs are managed.
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
 pub struct TaskId(pub String);
 
 /// Contains all information needed by Zed to spawn a new terminal tab for the given task.