task: Skip .vscode tasks when .zed/tasks.json exists (#51797)

moktamd and moktamd created

Fixes #51733

The previous fix in #32590 only filtered `.vscode` tasks at the modal
presentation layer, so they still appeared in other code paths and in
the used-tasks list. This moves the filtering into
`worktree_scenarios()` in the task inventory, so all consumers see the
correct behavior.

Changes:
- `InventoryFor::worktree_scenarios()` now skips `.vscode` entries when
`.zed` entries exist for the same worktree
- `used_and_current_resolved_tasks()` filters previously spawned
`.vscode` tasks from the used-tasks list
- Removes the now-redundant `.vscode` filter from the task picker modal

Release Notes:

- Fixed .vscode/tasks.json still being used when .zed/tasks.json is
present

Co-authored-by: moktamd <moktamd@users.noreply.github.com>

Change summary

crates/project/src/task_inventory.rs               | 33 ++++++++++
crates/project/tests/integration/task_inventory.rs | 48 ++++++++++++++++
crates/tasks_ui/src/modal.rs                       | 12 ----
3 files changed, 79 insertions(+), 14 deletions(-)

Detailed changes

crates/project/src/task_inventory.rs 🔗

@@ -84,10 +84,20 @@ impl<T: InventoryContents> InventoryFor<T> {
         &self,
         worktree: WorktreeId,
     ) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> {
-        self.worktree
-            .get(&worktree)
+        let worktree_dirs = self.worktree.get(&worktree);
+        let has_zed_dir = worktree_dirs
+            .map(|dirs| {
+                dirs.keys()
+                    .any(|dir| dir.file_name().is_some_and(|name| name == ".zed"))
+            })
+            .unwrap_or(false);
+
+        worktree_dirs
             .into_iter()
             .flatten()
+            .filter(move |(directory, _)| {
+                !(has_zed_dir && directory.file_name().is_some_and(|name| name == ".vscode"))
+            })
             .flat_map(|(directory, templates)| {
                 templates.iter().map(move |template| (directory, template))
             })
@@ -437,6 +447,17 @@ impl Inventory {
         });
         let file = location.and_then(|location| location.buffer.read(cx).file().cloned());
 
+        let worktrees_with_zed_tasks: HashSet<WorktreeId> = self
+            .templates_from_settings
+            .worktree
+            .iter()
+            .filter(|(_, dirs)| {
+                dirs.keys()
+                    .any(|dir| dir.file_name().is_some_and(|name| name == ".zed"))
+            })
+            .map(|(id, _)| *id)
+            .collect();
+
         let mut task_labels_to_ids = HashMap::<String, HashSet<TaskId>>::default();
         let mut lru_score = 0_u32;
         let previously_spawned_tasks = self
@@ -446,6 +467,14 @@ impl Inventory {
             .filter(|(task_kind, _)| {
                 if matches!(task_kind, TaskSourceKind::Language { .. }) {
                     Some(task_kind) == task_source_kind.as_ref()
+                } else if let TaskSourceKind::Worktree {
+                    id,
+                    directory_in_worktree: dir,
+                    ..
+                } = task_kind
+                {
+                    !(worktrees_with_zed_tasks.contains(id)
+                        && dir.file_name().is_some_and(|name| name == ".vscode"))
                 } else {
                     true
                 }

crates/project/tests/integration/task_inventory.rs 🔗

@@ -560,6 +560,54 @@ async fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_zed_tasks_take_precedence_over_vscode(cx: &mut TestAppContext) {
+    init_test(cx);
+    let inventory = cx.update(|cx| Inventory::new(cx));
+    let worktree_id = WorktreeId::from_usize(0);
+
+    inventory.update(cx, |inventory, _| {
+        inventory
+            .update_file_based_tasks(
+                TaskSettingsLocation::Worktree(SettingsLocation {
+                    worktree_id,
+                    path: rel_path(".vscode"),
+                }),
+                Some(&mock_tasks_from_names(["vscode_task"])),
+            )
+            .unwrap();
+    });
+    assert_eq!(
+        task_template_names(&inventory, Some(worktree_id), cx).await,
+        vec!["vscode_task"],
+        "With only .vscode tasks, they should appear"
+    );
+
+    inventory.update(cx, |inventory, _| {
+        inventory
+            .update_file_based_tasks(
+                TaskSettingsLocation::Worktree(SettingsLocation {
+                    worktree_id,
+                    path: rel_path(".zed"),
+                }),
+                Some(&mock_tasks_from_names(["zed_task"])),
+            )
+            .unwrap();
+    });
+    assert_eq!(
+        task_template_names(&inventory, Some(worktree_id), cx).await,
+        vec!["zed_task"],
+        "With both .zed and .vscode tasks, only .zed tasks should appear"
+    );
+
+    register_worktree_task_used(&inventory, worktree_id, "zed_task", cx).await;
+    let resolved = resolved_task_names(&inventory, Some(worktree_id), cx).await;
+    assert!(
+        !resolved.iter().any(|name| name == "vscode_task"),
+        "Previously used .vscode tasks should not appear when .zed tasks exist, got: {resolved:?}"
+    );
+}
+
 fn init_test(_cx: &mut TestAppContext) {
     zlog::init_test();
     TaskStore::init(None);

crates/tasks_ui/src/modal.rs 🔗

@@ -184,23 +184,11 @@ impl TasksModal {
         };
         let mut new_candidates = used_tasks;
         new_candidates.extend(lsp_tasks);
-        let hide_vscode = current_resolved_tasks.iter().any(|(kind, _)| match kind {
-            TaskSourceKind::Worktree {
-                id: _,
-                directory_in_worktree: dir,
-                id_base: _,
-            } => dir.file_name().is_some_and(|name| name == ".zed"),
-            _ => false,
-        });
         // todo(debugger): We're always adding lsp tasks here even if prefer_lsp is false
         // We should move the filter to new_candidates instead of on current
         // and add a test for this
         new_candidates.extend(current_resolved_tasks.into_iter().filter(|(task_kind, _)| {
             match task_kind {
-                TaskSourceKind::Worktree {
-                    directory_in_worktree: dir,
-                    ..
-                } => !(hide_vscode && dir.file_name().is_some_and(|name| name == ".vscode")),
                 TaskSourceKind::Language { .. } => add_current_language_tasks,
                 _ => true,
             }