Disambiguate package.json tasks by parent directory as needed (#33798)

Cole Miller created

Closes #33701, cc @afgomez 

Release Notes:

- Added the parent directory to the label as needed to disambiguate
tasks from package.json

Change summary

crates/languages/src/json.rs       |  6 ++-
crates/languages/src/typescript.rs | 57 ++++++++++++++++++++++++++++++-
2 files changed, 59 insertions(+), 4 deletions(-)

Detailed changes

crates/languages/src/json.rs 🔗

@@ -8,7 +8,8 @@ use futures::StreamExt;
 use gpui::{App, AsyncApp, Task};
 use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
 use language::{
-    ContextProvider, LanguageRegistry, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
+    ContextProvider, LanguageRegistry, LanguageToolchainStore, LocalFile as _, LspAdapter,
+    LspAdapterDelegate,
 };
 use lsp::{LanguageServerBinary, LanguageServerName};
 use node_runtime::NodeRuntime;
@@ -65,13 +66,14 @@ impl ContextProvider for JsonTaskProvider {
                 .ok()?
                 .await
                 .ok()?;
+            let path = cx.update(|cx| file.abs_path(cx)).ok()?.as_path().into();
 
             let task_templates = if is_package_json {
                 let package_json = serde_json_lenient::from_str::<
                     HashMap<String, serde_json_lenient::Value>,
                 >(&contents.text)
                 .ok()?;
-                let package_json = PackageJsonData::new(file.path.clone(), package_json);
+                let package_json = PackageJsonData::new(path, package_json);
                 let command = package_json.package_manager.unwrap_or("npm").to_owned();
                 package_json
                     .scripts

crates/languages/src/typescript.rs 🔗

@@ -221,15 +221,30 @@ impl PackageJsonData {
             });
         }
 
+        let script_name_counts: HashMap<_, usize> =
+            self.scripts
+                .iter()
+                .fold(HashMap::default(), |mut acc, (_, script)| {
+                    *acc.entry(script).or_default() += 1;
+                    acc
+                });
         for (path, script) in &self.scripts {
+            let label = if script_name_counts.get(script).copied().unwrap_or_default() > 1
+                && let Some(parent) = path.parent().and_then(|parent| parent.file_name())
+            {
+                let parent = parent.to_string_lossy();
+                format!("{parent}/package.json > {script}")
+            } else {
+                format!("package.json > {script}")
+            };
             task_templates.0.push(TaskTemplate {
-                label: format!("package.json > {script}",),
+                label,
                 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
                 args: vec!["run".to_owned(), script.to_owned()],
                 tags: vec!["package-script".into()],
                 cwd: Some(
                     path.parent()
-                        .unwrap_or(Path::new(""))
+                        .unwrap_or(Path::new("/"))
                         .to_string_lossy()
                         .to_string(),
                 ),
@@ -1014,6 +1029,7 @@ mod tests {
     use language::language_settings;
     use project::{FakeFs, Project};
     use serde_json::json;
+    use task::TaskTemplates;
     use unindent::Unindent;
     use util::path;
 
@@ -1135,5 +1151,42 @@ mod tests {
                 package_manager: None,
             }
         );
+
+        let mut task_templates = TaskTemplates::default();
+        package_json_data.fill_task_templates(&mut task_templates);
+        let task_templates = task_templates
+            .0
+            .into_iter()
+            .map(|template| (template.label, template.cwd))
+            .collect::<Vec<_>>();
+        pretty_assertions::assert_eq!(
+            task_templates,
+            [
+                (
+                    "vitest file test".into(),
+                    Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()),
+                ),
+                (
+                    "vitest test $ZED_SYMBOL".into(),
+                    Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()),
+                ),
+                (
+                    "mocha file test".into(),
+                    Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()),
+                ),
+                (
+                    "mocha test $ZED_SYMBOL".into(),
+                    Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()),
+                ),
+                (
+                    "root/package.json > test".into(),
+                    Some(path!("/root").into())
+                ),
+                (
+                    "sub/package.json > test".into(),
+                    Some(path!("/root/sub").into())
+                ),
+            ]
+        );
     }
 }