debugger: Ensure both debug and regular global tasks are correctly merged (#27184)

Kirill Bulatov created

Follow-up of https://github.com/zed-industries/zed/pull/13433
Closes https://github.com/zed-industries/zed/issues/27124
Closes https://github.com/zed-industries/zed/issues/27066

After this change, both old global task source, `tasks.json` and new,
`debug.json` started to call for the same task update method:


https://github.com/zed-industries/zed/blob/14920ab910c6d0208d23ce6b6e2ed644e6f20f2e/crates/project/src/task_inventory.rs#L414

erasing previous declarations.

The PR puts this data under different paths instead and adjusts the code
around it.

Release Notes:

- Fixed custom tasks not shown

Change summary

crates/project/src/project_settings.rs |  4 
crates/project/src/project_tests.rs    |  5 +
crates/project/src/task_inventory.rs   | 58 +++++++++++++++------------
crates/project/src/task_store.rs       | 25 +++++++++--
4 files changed, 57 insertions(+), 35 deletions(-)

Detailed changes

crates/project/src/project_settings.rs 🔗

@@ -24,7 +24,7 @@ use util::ResultExt;
 use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
 
 use crate::{
-    task_store::TaskStore,
+    task_store::{TaskSettingsLocation, TaskStore},
     worktree_store::{WorktreeStore, WorktreeStoreEvent},
 };
 
@@ -642,7 +642,7 @@ impl SettingsObserver {
                 LocalSettingsKind::Tasks(task_kind) => task_store.update(cx, |task_store, cx| {
                     task_store
                         .update_user_tasks(
-                            Some(SettingsLocation {
+                            TaskSettingsLocation::Worktree(SettingsLocation {
                                 worktree_id,
                                 path: directory.as_ref(),
                             }),

crates/project/src/project_tests.rs 🔗

@@ -1,6 +1,6 @@
 #![allow(clippy::format_collect)]
 
-use crate::{task_inventory::TaskContexts, Event, *};
+use crate::{task_inventory::TaskContexts, task_store::TaskSettingsLocation, Event, *};
 use buffer_diff::{
     assert_hunks, BufferDiffEvent, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind,
 };
@@ -19,6 +19,7 @@ use lsp::{
     NumberOrString, TextDocumentEdit, WillRenameFiles,
 };
 use parking_lot::Mutex;
+use paths::tasks_file;
 use pretty_assertions::{assert_eq, assert_matches};
 use serde_json::json;
 #[cfg(not(windows))]
@@ -327,7 +328,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
             inventory.task_scheduled(topmost_local_task_source_kind.clone(), resolved_task);
             inventory
                 .update_file_based_tasks(
-                    None,
+                    TaskSettingsLocation::Global(tasks_file()),
                     Some(
                         &json!([{
                             "label": "cargo check unstable",

crates/project/src/task_inventory.rs 🔗

@@ -13,7 +13,7 @@ use collections::{HashMap, HashSet, VecDeque};
 use gpui::{App, AppContext as _, Entity, SharedString, Task};
 use itertools::Itertools;
 use language::{ContextProvider, File, Language, LanguageToolchainStore, Location};
-use settings::{parse_json_with_comments, SettingsLocation, TaskKind};
+use settings::{parse_json_with_comments, TaskKind};
 use task::{
     DebugTaskDefinition, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates,
     TaskVariables, VariableName,
@@ -22,7 +22,7 @@ use text::{Point, ToPoint};
 use util::{paths::PathExt as _, post_inc, NumericPrefixWithSuffix, ResultExt as _};
 use worktree::WorktreeId;
 
-use crate::worktree_store::WorktreeStore;
+use crate::{task_store::TaskSettingsLocation, worktree_store::WorktreeStore};
 
 /// Inventory tracks available tasks for a given project.
 #[derive(Debug, Default)]
@@ -33,7 +33,7 @@ pub struct Inventory {
 
 #[derive(Debug, Default)]
 struct ParsedTemplates {
-    global: Vec<TaskTemplate>,
+    global: HashMap<PathBuf, Vec<TaskTemplate>>,
     worktree: HashMap<WorktreeId, HashMap<(Arc<Path>, TaskKind), Vec<TaskTemplate>>>,
 }
 
@@ -324,22 +324,20 @@ impl Inventory {
     ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
         self.templates_from_settings
             .global
-            .clone()
-            .into_iter()
-            .map(|template| {
-                (
-                    TaskSourceKind::AbsPath {
-                        id_base: match template.task_type {
-                            task::TaskType::Script => Cow::Borrowed("global tasks.json"),
-                            task::TaskType::Debug(_) => Cow::Borrowed("global debug.json"),
-                        },
-                        abs_path: match template.task_type {
-                            task::TaskType::Script => paths::tasks_file().clone(),
-                            task::TaskType::Debug(_) => paths::debug_tasks_file().clone(),
+            .iter()
+            .flat_map(|(file_path, templates)| {
+                templates.into_iter().map(|template| {
+                    (
+                        TaskSourceKind::AbsPath {
+                            id_base: match template.task_type {
+                                task::TaskType::Script => Cow::Borrowed("global tasks.json"),
+                                task::TaskType::Debug(_) => Cow::Borrowed("global debug.json"),
+                            },
+                            abs_path: file_path.clone(),
                         },
-                    },
-                    template,
-                )
+                        template.clone(),
+                    )
+                })
             })
     }
 
@@ -377,7 +375,7 @@ impl Inventory {
     /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
     pub(crate) fn update_file_based_tasks(
         &mut self,
-        location: Option<SettingsLocation<'_>>,
+        location: TaskSettingsLocation<'_>,
         raw_tasks_json: Option<&str>,
         task_kind: TaskKind,
     ) -> anyhow::Result<()> {
@@ -395,7 +393,13 @@ impl Inventory {
 
         let parsed_templates = &mut self.templates_from_settings;
         match location {
-            Some(location) => {
+            TaskSettingsLocation::Global(path) => {
+                parsed_templates
+                    .global
+                    .entry(path.to_owned())
+                    .insert_entry(new_templates.collect());
+            }
+            TaskSettingsLocation::Worktree(location) => {
                 let new_templates = new_templates.collect::<Vec<_>>();
                 if new_templates.is_empty() {
                     if let Some(worktree_tasks) =
@@ -411,8 +415,8 @@ impl Inventory {
                         .insert((Arc::from(location.path), task_kind), new_templates);
                 }
             }
-            None => parsed_templates.global = new_templates.collect(),
         }
+
         Ok(())
     }
 }
@@ -651,8 +655,10 @@ impl ContextProvider for ContextProviderWithTasks {
 #[cfg(test)]
 mod tests {
     use gpui::TestAppContext;
+    use paths::tasks_file;
     use pretty_assertions::assert_eq;
     use serde_json::json;
+    use settings::SettingsLocation;
 
     use crate::task_store::TaskStore;
 
@@ -684,7 +690,7 @@ mod tests {
         inventory.update(cx, |inventory, _| {
             inventory
                 .update_file_based_tasks(
-                    None,
+                    TaskSettingsLocation::Global(tasks_file()),
                     Some(&mock_tasks_from_names(
                         expected_initial_state.iter().map(|name| name.as_str()),
                     )),
@@ -738,7 +744,7 @@ mod tests {
         inventory.update(cx, |inventory, _| {
             inventory
                 .update_file_based_tasks(
-                    None,
+                    TaskSettingsLocation::Global(tasks_file()),
                     Some(&mock_tasks_from_names(
                         ["10_hello", "11_hello"]
                             .into_iter()
@@ -863,7 +869,7 @@ mod tests {
         inventory.update(cx, |inventory, _| {
             inventory
                 .update_file_based_tasks(
-                    None,
+                    TaskSettingsLocation::Global(tasks_file()),
                     Some(&mock_tasks_from_names(
                         worktree_independent_tasks
                             .iter()
@@ -874,7 +880,7 @@ mod tests {
                 .unwrap();
             inventory
                 .update_file_based_tasks(
-                    Some(SettingsLocation {
+                    TaskSettingsLocation::Worktree(SettingsLocation {
                         worktree_id: worktree_1,
                         path: Path::new(".zed"),
                     }),
@@ -886,7 +892,7 @@ mod tests {
                 .unwrap();
             inventory
                 .update_file_based_tasks(
-                    Some(SettingsLocation {
+                    TaskSettingsLocation::Worktree(SettingsLocation {
                         worktree_id: worktree_2,
                         path: Path::new(".zed"),
                     }),

crates/project/src/task_store.rs 🔗

@@ -1,4 +1,7 @@
-use std::{path::PathBuf, sync::Arc};
+use std::{
+    path::{Path, PathBuf},
+    sync::Arc,
+};
 
 use anyhow::Context as _;
 use collections::HashMap;
@@ -48,6 +51,12 @@ enum StoreMode {
 
 impl EventEmitter<crate::Event> for TaskStore {}
 
+#[derive(Debug)]
+pub enum TaskSettingsLocation<'a> {
+    Global(&'a Path),
+    Worktree(SettingsLocation<'a>),
+}
+
 impl TaskStore {
     pub fn init(client: Option<&AnyProtoClient>) {
         if let Some(client) = client {
@@ -286,7 +295,7 @@ impl TaskStore {
 
     pub(super) fn update_user_tasks(
         &self,
-        location: Option<SettingsLocation<'_>>,
+        location: TaskSettingsLocation<'_>,
         raw_tasks_json: Option<&str>,
         task_type: TaskKind,
         cx: &mut Context<'_, Self>,
@@ -310,13 +319,19 @@ impl TaskStore {
         file_path: PathBuf,
         cx: &mut Context<'_, Self>,
     ) -> Task<()> {
-        let mut user_tasks_file_rx = watch_config_file(&cx.background_executor(), fs, file_path);
+        let mut user_tasks_file_rx =
+            watch_config_file(&cx.background_executor(), fs, file_path.clone());
         let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
         cx.spawn(async move |task_store, cx| {
             if let Some(user_tasks_content) = user_tasks_content {
                 let Ok(_) = task_store.update(cx, |task_store, cx| {
                     task_store
-                        .update_user_tasks(None, Some(&user_tasks_content), task_kind, cx)
+                        .update_user_tasks(
+                            TaskSettingsLocation::Global(&file_path),
+                            Some(&user_tasks_content),
+                            task_kind,
+                            cx,
+                        )
                         .log_err();
                 }) else {
                     return;
@@ -325,7 +340,7 @@ impl TaskStore {
             while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
                 let Ok(()) = task_store.update(cx, |task_store, cx| {
                     let result = task_store.update_user_tasks(
-                        None,
+                        TaskSettingsLocation::Global(&file_path),
                         Some(&user_tasks_content),
                         task_kind,
                         cx,