Cleanup corresponding task history on task file update (#31720)

Kirill Bulatov created

Closes https://github.com/zed-industries/zed/issues/31715

Release Notes:

- Fixed old task history not erased after task file update

Change summary

crates/project/src/task_inventory.rs | 101 ++++++++++++++++++++++++++++-
1 file changed, 96 insertions(+), 5 deletions(-)

Detailed changes

crates/project/src/task_inventory.rs 🔗

@@ -586,6 +586,13 @@ impl Inventory {
                     .global
                     .entry(path.to_owned())
                     .insert_entry(new_templates.collect());
+                self.last_scheduled_tasks.retain(|(kind, _)| {
+                    if let TaskSourceKind::AbsPath { abs_path, .. } = kind {
+                        abs_path != path
+                    } else {
+                        true
+                    }
+                });
             }
             TaskSettingsLocation::Worktree(location) => {
                 let new_templates = new_templates.collect::<Vec<_>>();
@@ -602,6 +609,18 @@ impl Inventory {
                         .or_default()
                         .insert(Arc::from(location.path), new_templates);
                 }
+                self.last_scheduled_tasks.retain(|(kind, _)| {
+                    if let TaskSourceKind::Worktree {
+                        directory_in_worktree,
+                        id,
+                        ..
+                    } = kind
+                    {
+                        *id != location.worktree_id || directory_in_worktree != location.path
+                    } else {
+                        true
+                    }
+                });
             }
         }
 
@@ -756,6 +775,27 @@ mod test_inventory {
         });
     }
 
+    pub(super) fn register_worktree_task_used(
+        inventory: &Entity<Inventory>,
+        worktree_id: WorktreeId,
+        task_name: &str,
+        cx: &mut TestAppContext,
+    ) {
+        inventory.update(cx, |inventory, cx| {
+            let (task_source_kind, task) = inventory
+                .list_tasks(None, None, Some(worktree_id), cx)
+                .into_iter()
+                .find(|(_, task)| task.label == task_name)
+                .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
+            let id_base = task_source_kind.to_id_base();
+            inventory.task_scheduled(
+                task_source_kind.clone(),
+                task.resolve_task(&id_base, &TaskContext::default())
+                    .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
+            );
+        });
+    }
+
     pub(super) async fn list_tasks(
         inventory: &Entity<Inventory>,
         worktree: Option<WorktreeId>,
@@ -997,6 +1037,53 @@ mod tests {
                 "2_task".to_string(),
                 "1_a_task".to_string(),
             ],
+            "Most recently used task should be at the top"
+        );
+
+        let worktree_id = WorktreeId::from_usize(0);
+        let local_worktree_location = SettingsLocation {
+            worktree_id,
+            path: Path::new("foo"),
+        };
+        inventory.update(cx, |inventory, _| {
+            inventory
+                .update_file_based_tasks(
+                    TaskSettingsLocation::Worktree(local_worktree_location),
+                    Some(&mock_tasks_from_names(["worktree_task_1"])),
+                )
+                .unwrap();
+        });
+        assert_eq!(
+            resolved_task_names(&inventory, None, cx),
+            vec![
+                "3_task".to_string(),
+                "1_task".to_string(),
+                "2_task".to_string(),
+                "1_a_task".to_string(),
+            ],
+            "Most recently used task should be at the top"
+        );
+        assert_eq!(
+            resolved_task_names(&inventory, Some(worktree_id), cx),
+            vec![
+                "3_task".to_string(),
+                "1_task".to_string(),
+                "2_task".to_string(),
+                "worktree_task_1".to_string(),
+                "1_a_task".to_string(),
+            ],
+        );
+        register_worktree_task_used(&inventory, worktree_id, "worktree_task_1", cx);
+        assert_eq!(
+            resolved_task_names(&inventory, Some(worktree_id), cx),
+            vec![
+                "worktree_task_1".to_string(),
+                "3_task".to_string(),
+                "1_task".to_string(),
+                "2_task".to_string(),
+                "1_a_task".to_string(),
+            ],
+            "Most recently used worktree task should be at the top"
         );
 
         inventory.update(cx, |inventory, _| {
@@ -1027,13 +1114,15 @@ mod tests {
         assert_eq!(
             resolved_task_names(&inventory, None, cx),
             vec![
-                "3_task".to_string(),
+                "worktree_task_1".to_string(),
+                "1_a_task".to_string(),
                 "1_task".to_string(),
                 "2_task".to_string(),
-                "1_a_task".to_string(),
+                "3_task".to_string(),
                 "10_hello".to_string(),
                 "11_hello".to_string(),
             ],
+            "After global tasks update, worktree task usage is not erased and it's the first still; global task is back to regular order as its file was updated"
         );
 
         register_task_used(&inventory, "11_hello", cx);
@@ -1045,10 +1134,11 @@ mod tests {
             resolved_task_names(&inventory, None, cx),
             vec![
                 "11_hello".to_string(),
-                "3_task".to_string(),
+                "worktree_task_1".to_string(),
+                "1_a_task".to_string(),
                 "1_task".to_string(),
                 "2_task".to_string(),
-                "1_a_task".to_string(),
+                "3_task".to_string(),
                 "10_hello".to_string(),
             ],
         );
@@ -1227,9 +1317,10 @@ mod tests {
         })
     }
 
-    fn mock_tasks_from_names<'a>(task_names: impl Iterator<Item = &'a str> + 'a) -> String {
+    fn mock_tasks_from_names<'a>(task_names: impl IntoIterator<Item = &'a str> + 'a) -> String {
         serde_json::to_string(&serde_json::Value::Array(
             task_names
+                .into_iter()
                 .map(|task_name| {
                     json!({
                         "label": task_name,