Add a JS/TS debug locator (#31769)

Kirill Bulatov and Piotr Osiewicz created

With this, a semi-working debug session is possible from the JS/TS
gutter tasks:


https://github.com/user-attachments/assets/8db6ed29-b44a-4314-ae8b-a8213291bffc

For now, available in debug builds only as a base to improve on later on
the DAP front.

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com>

Change summary

crates/dap/src/adapters.rs                   |  1 
crates/dap/src/transport.rs                  |  2 
crates/dap_adapters/src/javascript.rs        |  4 
crates/languages/src/typescript.rs           | 25 +++++--
crates/project/src/debugger/dap_store.rs     |  3 
crates/project/src/debugger/locators.rs      |  1 
crates/project/src/debugger/locators/node.rs | 68 ++++++++++++++++++++++
crates/project/src/debugger/session.rs       |  4 
8 files changed, 95 insertions(+), 13 deletions(-)

Detailed changes

crates/dap/src/adapters.rs 🔗

@@ -298,6 +298,7 @@ pub async fn download_adapter_from_github(
         response.status().to_string()
     );
 
+    delegate.output_to_console("Download complete".to_owned());
     match file_type {
         DownloadedFileType::GzipTar => {
             let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));

crates/dap/src/transport.rs 🔗

@@ -434,7 +434,7 @@ impl TransportDelegate {
                 .with_context(|| "reading a message from server")?
                 == 0
             {
-                anyhow::bail!("debugger reader stream closed");
+                anyhow::bail!("debugger reader stream closed, last string output: '{buffer}'");
             };
 
             if buffer == "\r\n" {

crates/dap_adapters/src/javascript.rs 🔗

@@ -26,7 +26,7 @@ impl JsDebugAdapter {
         delegate: &Arc<dyn DapDelegate>,
     ) -> Result<AdapterVersion> {
         let release = latest_github_release(
-            &format!("{}/{}", "microsoft", Self::ADAPTER_NPM_NAME),
+            &format!("microsoft/{}", Self::ADAPTER_NPM_NAME),
             true,
             false,
             delegate.http_client(),
@@ -449,6 +449,8 @@ impl DebugAdapter for JsDebugAdapter {
                     delegate.as_ref(),
                 )
                 .await?;
+            } else {
+                delegate.output_to_console(format!("{} debug adapter is up to date", self.name()));
             }
         }
 

crates/languages/src/typescript.rs 🔗

@@ -168,8 +168,9 @@ impl ContextProvider for TypeScriptContextProvider {
             command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
             args: vec![
                 TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
-                VariableName::File.template_value(),
+                VariableName::RelativeFile.template_value(),
             ],
+            cwd: Some(VariableName::WorktreeRoot.template_value()),
             ..TaskTemplate::default()
         });
         task_templates.0.push(TaskTemplate {
@@ -183,13 +184,14 @@ impl ContextProvider for TypeScriptContextProvider {
                 TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
                 "--testNamePattern".to_owned(),
                 format!("\"{}\"", VariableName::Symbol.template_value()),
-                VariableName::File.template_value(),
+                VariableName::RelativeFile.template_value(),
             ],
             tags: vec![
                 "ts-test".to_owned(),
                 "js-test".to_owned(),
                 "tsx-test".to_owned(),
             ],
+            cwd: Some(VariableName::WorktreeRoot.template_value()),
             ..TaskTemplate::default()
         });
 
@@ -203,8 +205,9 @@ impl ContextProvider for TypeScriptContextProvider {
             args: vec![
                 TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
                 "run".to_owned(),
-                VariableName::File.template_value(),
+                VariableName::RelativeFile.template_value(),
             ],
+            cwd: Some(VariableName::WorktreeRoot.template_value()),
             ..TaskTemplate::default()
         });
         task_templates.0.push(TaskTemplate {
@@ -219,13 +222,14 @@ impl ContextProvider for TypeScriptContextProvider {
                 "run".to_owned(),
                 "--testNamePattern".to_owned(),
                 format!("\"{}\"", VariableName::Symbol.template_value()),
-                VariableName::File.template_value(),
+                VariableName::RelativeFile.template_value(),
             ],
             tags: vec![
                 "ts-test".to_owned(),
                 "js-test".to_owned(),
                 "tsx-test".to_owned(),
             ],
+            cwd: Some(VariableName::WorktreeRoot.template_value()),
             ..TaskTemplate::default()
         });
 
@@ -238,8 +242,9 @@ impl ContextProvider for TypeScriptContextProvider {
             command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
             args: vec![
                 TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),
-                VariableName::File.template_value(),
+                VariableName::RelativeFile.template_value(),
             ],
+            cwd: Some(VariableName::WorktreeRoot.template_value()),
             ..TaskTemplate::default()
         });
         task_templates.0.push(TaskTemplate {
@@ -253,13 +258,14 @@ impl ContextProvider for TypeScriptContextProvider {
                 TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),
                 "--grep".to_owned(),
                 format!("\"{}\"", VariableName::Symbol.template_value()),
-                VariableName::File.template_value(),
+                VariableName::RelativeFile.template_value(),
             ],
             tags: vec![
                 "ts-test".to_owned(),
                 "js-test".to_owned(),
                 "tsx-test".to_owned(),
             ],
+            cwd: Some(VariableName::WorktreeRoot.template_value()),
             ..TaskTemplate::default()
         });
 
@@ -272,8 +278,9 @@ impl ContextProvider for TypeScriptContextProvider {
             command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
             args: vec![
                 TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),
-                VariableName::File.template_value(),
+                VariableName::RelativeFile.template_value(),
             ],
+            cwd: Some(VariableName::WorktreeRoot.template_value()),
             ..TaskTemplate::default()
         });
         task_templates.0.push(TaskTemplate {
@@ -286,13 +293,14 @@ impl ContextProvider for TypeScriptContextProvider {
             args: vec![
                 TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),
                 format!("--filter={}", VariableName::Symbol.template_value()),
-                VariableName::File.template_value(),
+                VariableName::RelativeFile.template_value(),
             ],
             tags: vec![
                 "ts-test".to_owned(),
                 "js-test".to_owned(),
                 "tsx-test".to_owned(),
             ],
+            cwd: Some(VariableName::WorktreeRoot.template_value()),
             ..TaskTemplate::default()
         });
 
@@ -313,6 +321,7 @@ impl ContextProvider for TypeScriptContextProvider {
                     package_json_script.template_value(),
                 ],
                 tags: vec!["package-script".into()],
+                cwd: Some(VariableName::WorktreeRoot.template_value()),
                 ..TaskTemplate::default()
             });
         }

crates/project/src/debugger/dap_store.rs 🔗

@@ -103,8 +103,9 @@ impl DapStore {
         ADD_LOCATORS.call_once(|| {
             let registry = DapRegistry::global(cx);
             registry.add_locator(Arc::new(locators::cargo::CargoLocator {}));
-            registry.add_locator(Arc::new(locators::python::PythonLocator));
             registry.add_locator(Arc::new(locators::go::GoLocator {}));
+            registry.add_locator(Arc::new(locators::node::NodeLocator));
+            registry.add_locator(Arc::new(locators::python::PythonLocator));
         });
         client.add_entity_request_handler(Self::handle_run_debug_locator);
         client.add_entity_request_handler(Self::handle_get_debug_adapter_binary);

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

@@ -0,0 +1,68 @@
+use std::{borrow::Cow, path::Path};
+
+use anyhow::{Result, bail};
+use async_trait::async_trait;
+use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
+use gpui::SharedString;
+
+use task::{DebugScenario, SpawnInTerminal, TaskTemplate, VariableName};
+
+pub(crate) struct NodeLocator;
+
+const TYPESCRIPT_RUNNER_VARIABLE: VariableName =
+    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));
+
+#[async_trait]
+impl DapLocator for NodeLocator {
+    fn name(&self) -> SharedString {
+        SharedString::new_static("Node")
+    }
+
+    /// Determines whether this locator can generate debug target for given task.
+    fn create_scenario(
+        &self,
+        build_config: &TaskTemplate,
+        resolved_label: &str,
+        adapter: DebugAdapterName,
+    ) -> Option<DebugScenario> {
+        // TODO(debugger) fix issues with `await` breakpoint step
+        if cfg!(not(debug_assertions)) {
+            return None;
+        }
+
+        if adapter.as_ref() != "JavaScript" {
+            return None;
+        }
+        if build_config.command != TYPESCRIPT_RUNNER_VARIABLE.template_value() {
+            return None;
+        }
+        let test_library = build_config.args.first()?;
+        let program_path = Path::new("$ZED_WORKTREE_ROOT")
+            .join("node_modules")
+            .join(".bin")
+            .join(test_library);
+        let args = build_config.args[1..].to_vec();
+
+        let config = serde_json::json!({
+            "request": "launch",
+            "type": "pwa-node",
+            "program": program_path,
+            "args": args,
+            "cwd": build_config.cwd.clone(),
+            "runtimeArgs": ["--inspect-brk"],
+            "console": "integratedTerminal",
+        });
+
+        Some(DebugScenario {
+            adapter: adapter.0,
+            label: resolved_label.to_string().into(),
+            build: None,
+            config,
+            tcp_connection: None,
+        })
+    }
+
+    async fn run(&self, _: SpawnInTerminal) -> Result<DebugRequest> {
+        bail!("Python locator should not require DapLocator::run to be ran");
+    }
+}

crates/project/src/debugger/session.rs 🔗

@@ -1421,7 +1421,7 @@ impl Session {
             ));
             return cx.spawn(async move |this, cx| {
                 this.update(cx, |this, cx| process_result(this, error, cx))
-                    .log_err()
+                    .ok()
                     .flatten()
             });
         }
@@ -1430,7 +1430,7 @@ impl Session {
         cx.spawn(async move |this, cx| {
             let result = request.await;
             this.update(cx, |this, cx| process_result(this, result, cx))
-                .log_err()
+                .ok()
                 .flatten()
         })
     }