debugger: Run debug scenarios from package.json (#32958)

Piotr Osiewicz , Remco Smits , and Anthony Eid created

Release Notes:

- New session modal for a debugger will now show tasks from package.json
as debuggable scenarios

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>

Change summary

crates/languages/src/json.rs                 | 96 +++++++++++++++++----
crates/languages/src/json/config.toml        |  2 
crates/languages/src/lib.rs                  |  5 
crates/project/src/debugger/locators/node.rs |  5 
4 files changed, 83 insertions(+), 25 deletions(-)

Detailed changes

crates/languages/src/json.rs 🔗

@@ -5,12 +5,14 @@ use async_trait::async_trait;
 use collections::HashMap;
 use dap::DapRegistry;
 use futures::StreamExt;
-use gpui::{App, AsyncApp};
+use gpui::{App, AsyncApp, Task};
 use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
-use language::{LanguageRegistry, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
+use language::{
+    ContextProvider, LanguageRegistry, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
+};
 use lsp::{LanguageServerBinary, LanguageServerName};
 use node_runtime::NodeRuntime;
-use project::{ContextProviderWithTasks, Fs, lsp_store::language_server_settings};
+use project::{Fs, lsp_store::language_server_settings};
 use serde_json::{Value, json};
 use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
 use smol::{
@@ -36,25 +38,77 @@ const SERVER_PATH: &str =
 const TSCONFIG_SCHEMA: &str = include_str!("json/schemas/tsconfig.json");
 const PACKAGE_JSON_SCHEMA: &str = include_str!("json/schemas/package.json");
 
-pub(super) fn json_task_context() -> ContextProviderWithTasks {
-    ContextProviderWithTasks::new(TaskTemplates(vec![
-        TaskTemplate {
-            label: "package script $ZED_CUSTOM_script".to_owned(),
-            command: "npm --prefix $ZED_DIRNAME run".to_owned(),
-            args: vec![VariableName::Custom("script".into()).template_value()],
-            tags: vec!["package-script".into()],
-            ..TaskTemplate::default()
-        },
-        TaskTemplate {
-            label: "composer script $ZED_CUSTOM_script".to_owned(),
-            command: "composer -d $ZED_DIRNAME".to_owned(),
-            args: vec![VariableName::Custom("script".into()).template_value()],
-            tags: vec!["composer-script".into()],
-            ..TaskTemplate::default()
-        },
-    ]))
-}
+pub(crate) struct JsonTaskProvider;
 
+impl ContextProvider for JsonTaskProvider {
+    fn associated_tasks(
+        &self,
+        _: Arc<dyn Fs>,
+        file: Option<Arc<dyn language::File>>,
+        cx: &App,
+    ) -> gpui::Task<Option<TaskTemplates>> {
+        let Some(file) = project::File::from_dyn(file.as_ref())
+            .filter(|file| file.path.file_name() == Some("package.json".as_ref()))
+            .cloned()
+        else {
+            return Task::ready(None);
+        };
+
+        cx.spawn(async move |cx| {
+            let contents = file
+                .worktree
+                .update(cx, |this, cx| this.load_file(&file.path, cx))
+                .ok()?
+                .await
+                .ok()?;
+
+            let as_json = serde_json_lenient::Value::from_str(&contents.text).ok()?;
+            let gutter_tasks = [
+                TaskTemplate {
+                    label: "package script $ZED_CUSTOM_script".to_owned(),
+                    command: "npm".to_owned(),
+                    args: vec![
+                        "--prefix".into(),
+                        "$ZED_DIRNAME".into(),
+                        "run".into(),
+                        VariableName::Custom("script".into()).template_value(),
+                    ],
+                    tags: vec!["package-script".into()],
+                    ..TaskTemplate::default()
+                },
+                TaskTemplate {
+                    label: "composer script $ZED_CUSTOM_script".to_owned(),
+                    command: "composer".to_owned(),
+                    args: vec![
+                        "-d".into(),
+                        "$ZED_DIRNAME".into(),
+                        VariableName::Custom("script".into()).template_value(),
+                    ],
+                    tags: vec!["composer-script".into()],
+                    ..TaskTemplate::default()
+                },
+            ];
+            let tasks = as_json
+                .get("scripts")?
+                .as_object()?
+                .keys()
+                .map(|key| TaskTemplate {
+                    label: format!("run {key}"),
+                    command: "npm".to_owned(),
+                    args: vec![
+                        "--prefix".into(),
+                        "$ZED_DIRNAME".into(),
+                        "run".into(),
+                        key.into(),
+                    ],
+                    ..TaskTemplate::default()
+                })
+                .chain(gutter_tasks)
+                .collect();
+            Some(TaskTemplates(tasks))
+        })
+    }
+}
 fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
     vec![server_path.into(), "--stdio".into()]
 }

crates/languages/src/json/config.toml 🔗

@@ -10,6 +10,6 @@ brackets = [
 ]
 tab_size = 2
 prettier_parser_name = "json"
-
+debuggers = ["JavaScript"]
 [overrides.string]
 completion_query_characters = [":", " "]

crates/languages/src/lib.rs 🔗

@@ -1,6 +1,5 @@
 use anyhow::Context as _;
 use gpui::{App, UpdateGlobal};
-use json::json_task_context;
 use node_runtime::NodeRuntime;
 use python::PyprojectTomlManifestProvider;
 use rust::CargoManifestProvider;
@@ -12,6 +11,8 @@ use util::{ResultExt, asset_str};
 
 pub use language::*;
 
+use crate::json::JsonTaskProvider;
+
 mod bash;
 mod c;
 mod css;
@@ -78,7 +79,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
     let eslint_adapter = Arc::new(typescript::EsLintLspAdapter::new(node.clone()));
     let go_context_provider = Arc::new(go::GoContextProvider);
     let go_lsp_adapter = Arc::new(go::GoLspAdapter);
-    let json_context_provider = Arc::new(json_task_context());
+    let json_context_provider = Arc::new(JsonTaskProvider);
     let json_lsp_adapter = Arc::new(json::JsonLspAdapter::new(node.clone(), languages.clone()));
     let node_version_lsp_adapter = Arc::new(json::NodeVersionAdapter);
     let py_lsp_adapter = Arc::new(python::PyLspAdapter::new());

crates/project/src/debugger/locators/node.rs 🔗

@@ -28,7 +28,10 @@ impl DapLocator for NodeLocator {
         if adapter.0.as_ref() != "JavaScript" {
             return None;
         }
-        if build_config.command != TYPESCRIPT_RUNNER_VARIABLE.template_value() {
+        if build_config.command != TYPESCRIPT_RUNNER_VARIABLE.template_value()
+            && build_config.command != "composer"
+            && build_config.command != "npm"
+        {
             return None;
         }