debugger: Fix issues with launch.json handling (#32563)

Cole Miller , Anthony Eid , Anthony , and Conrad Irwin created

After this PR we can run all the in-tree launch.json examples from [this
repo](https://github.com/microsoft/vscode-recipes).

Things done:

- Fill in default cwd at a lower level for all adapters
- Update launch.json parsing for DebugScenario changes
- Imitate how VS Code normalizes the `type` field for JS debug tasks
- Make version field optional
- Extend the variable replacer a bit

Release Notes:

- Debugger Beta: fixed issues preventing loading and running of debug
tasks from VS Code's launch.json.

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/dap_adapters/src/codelldb.rs    | 17 ++++---
crates/dap_adapters/src/gdb.rs         | 15 ++++--
crates/dap_adapters/src/go.rs          |  9 +++
crates/dap_adapters/src/javascript.rs  | 30 +++++++++++++
crates/dap_adapters/src/php.rs         |  8 +++
crates/dap_adapters/src/python.rs      |  8 +++
crates/dap_adapters/src/ruby.rs        |  9 +++
crates/task/src/vscode_debug_format.rs | 59 +++++++++++++++------------
8 files changed, 111 insertions(+), 44 deletions(-)

Detailed changes

crates/dap_adapters/src/codelldb.rs 🔗

@@ -21,18 +21,21 @@ impl CodeLldbDebugAdapter {
 
     fn request_args(
         &self,
+        delegate: &Arc<dyn DapDelegate>,
         task_definition: &DebugTaskDefinition,
     ) -> Result<dap::StartDebuggingRequestArguments> {
         // CodeLLDB uses `name` for a terminal label.
         let mut configuration = task_definition.config.clone();
 
-        configuration
+        let obj = configuration
             .as_object_mut()
-            .context("CodeLLDB is not a valid json object")?
-            .insert(
-                "name".into(),
-                Value::String(String::from(task_definition.label.as_ref())),
-            );
+            .context("CodeLLDB is not a valid json object")?;
+
+        obj.entry("name")
+            .or_insert(Value::String(String::from(task_definition.label.as_ref())));
+
+        obj.entry("cwd")
+            .or_insert(delegate.worktree_root_path().to_string_lossy().into());
 
         let request = self.request_kind(&configuration)?;
 
@@ -365,7 +368,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
                 "--settings".into(),
                 json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
             ],
-            request_args: self.request_args(&config)?,
+            request_args: self.request_args(delegate, &config)?,
             envs: HashMap::default(),
             connection: None,
         })

crates/dap_adapters/src/gdb.rs 🔗

@@ -177,10 +177,12 @@ impl DebugAdapter for GdbDebugAdapter {
 
         let gdb_path = user_setting_path.unwrap_or(gdb_path?);
 
-        let request_args = StartDebuggingRequestArguments {
-            request: self.request_kind(&config.config)?,
-            configuration: config.config.clone(),
-        };
+        let mut configuration = config.config.clone();
+        if let Some(configuration) = configuration.as_object_mut() {
+            configuration
+                .entry("cwd")
+                .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
+        }
 
         Ok(DebugAdapterBinary {
             command: Some(gdb_path),
@@ -188,7 +190,10 @@ impl DebugAdapter for GdbDebugAdapter {
             envs: HashMap::default(),
             cwd: Some(delegate.worktree_root_path().to_path_buf()),
             connection: None,
-            request_args,
+            request_args: StartDebuggingRequestArguments {
+                request: self.request_kind(&config.config)?,
+                configuration,
+            },
         })
     }
 }

crates/dap_adapters/src/go.rs 🔗

@@ -462,6 +462,13 @@ impl DebugAdapter for GoDebugAdapter {
             ]
         };
 
+        let mut configuration = task_definition.config.clone();
+        if let Some(configuration) = configuration.as_object_mut() {
+            configuration
+                .entry("cwd")
+                .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
+        }
+
         Ok(DebugAdapterBinary {
             command: Some(minidelve_path.to_string_lossy().into_owned()),
             arguments,
@@ -469,7 +476,7 @@ impl DebugAdapter for GoDebugAdapter {
             envs: HashMap::default(),
             connection: None,
             request_args: StartDebuggingRequestArguments {
-                configuration: task_definition.config.clone(),
+                configuration,
                 request: self.request_kind(&task_definition.config)?,
             },
         })

crates/dap_adapters/src/javascript.rs 🔗

@@ -2,6 +2,7 @@ use adapters::latest_github_release;
 use anyhow::Context as _;
 use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
 use gpui::AsyncApp;
+use serde_json::Value;
 use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
 use task::DebugRequest;
 use util::ResultExt;
@@ -68,6 +69,15 @@ impl JsDebugAdapter {
         let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
         let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 
+        let mut configuration = task_definition.config.clone();
+        if let Some(configuration) = configuration.as_object_mut() {
+            configuration
+                .entry("cwd")
+                .or_insert(delegate.worktree_root_path().to_string_lossy().into());
+
+            configuration.entry("type").and_modify(normalize_task_type);
+        }
+
         Ok(DebugAdapterBinary {
             command: Some(
                 delegate
@@ -93,7 +103,7 @@ impl JsDebugAdapter {
                 timeout,
             }),
             request_args: StartDebuggingRequestArguments {
-                configuration: task_definition.config.clone(),
+                configuration,
                 request: self.request_kind(&task_definition.config)?,
             },
         })
@@ -173,7 +183,7 @@ impl DebugAdapter for JsDebugAdapter {
                             "properties": {
                                 "type": {
                                     "type": "string",
-                                    "enum": ["pwa-node", "node", "chrome", "pwa-chrome", "edge", "pwa-edge"],
+                                    "enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge"],
                                     "description": "The type of debug session",
                                     "default": "pwa-node"
                                 },
@@ -439,3 +449,19 @@ impl DebugAdapter for JsDebugAdapter {
         Some(label.to_owned())
     }
 }
+
+fn normalize_task_type(task_type: &mut Value) {
+    let Some(task_type_str) = task_type.as_str() else {
+        return;
+    };
+
+    let new_name = match task_type_str {
+        "node" | "pwa-node" => "pwa-node",
+        "chrome" | "pwa-chrome" => "pwa-chrome",
+        "edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
+        _ => task_type_str,
+    }
+    .to_owned();
+
+    *task_type = Value::String(new_name);
+}

crates/dap_adapters/src/php.rs 🔗

@@ -71,6 +71,12 @@ impl PhpDebugAdapter {
         let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
         let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 
+        let mut configuration = task_definition.config.clone();
+        if let Some(obj) = configuration.as_object_mut() {
+            obj.entry("cwd")
+                .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
+        }
+
         Ok(DebugAdapterBinary {
             command: Some(
                 delegate
@@ -95,7 +101,7 @@ impl PhpDebugAdapter {
             cwd: Some(delegate.worktree_root_path().to_path_buf()),
             envs: HashMap::default(),
             request_args: StartDebuggingRequestArguments {
-                configuration: task_definition.config.clone(),
+                configuration,
                 request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?,
             },
         })

crates/dap_adapters/src/python.rs 🔗

@@ -83,6 +83,7 @@ impl PythonDebugAdapter {
 
     fn request_args(
         &self,
+        delegate: &Arc<dyn DapDelegate>,
         task_definition: &DebugTaskDefinition,
     ) -> Result<StartDebuggingRequestArguments> {
         let request = self.request_kind(&task_definition.config)?;
@@ -95,6 +96,11 @@ impl PythonDebugAdapter {
             }
         }
 
+        if let Some(obj) = configuration.as_object_mut() {
+            obj.entry("cwd")
+                .or_insert(delegate.worktree_root_path().to_string_lossy().into());
+        }
+
         Ok(StartDebuggingRequestArguments {
             configuration,
             request,
@@ -196,7 +202,7 @@ impl PythonDebugAdapter {
             }),
             cwd: Some(delegate.worktree_root_path().to_path_buf()),
             envs: HashMap::default(),
-            request_args: self.request_args(config)?,
+            request_args: self.request_args(delegate, config)?,
         })
     }
 }

crates/dap_adapters/src/ruby.rs 🔗

@@ -174,6 +174,13 @@ impl DebugAdapter for RubyDebugAdapter {
 
         arguments.extend(ruby_config.args);
 
+        let mut configuration = definition.config.clone();
+        if let Some(configuration) = configuration.as_object_mut() {
+            configuration
+                .entry("cwd")
+                .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
+        }
+
         Ok(DebugAdapterBinary {
             command: Some(rdbg_path.to_string_lossy().to_string()),
             arguments,
@@ -190,7 +197,7 @@ impl DebugAdapter for RubyDebugAdapter {
             envs: ruby_config.env.into_iter().collect(),
             request_args: StartDebuggingRequestArguments {
                 request: self.request_kind(&definition.config)?,
-                configuration: definition.config.clone(),
+                configuration,
             },
         })
     }

crates/task/src/vscode_debug_format.rs 🔗

@@ -1,5 +1,4 @@
 use collections::HashMap;
-use gpui::SharedString;
 use serde::Deserialize;
 use util::ResultExt as _;
 
@@ -20,40 +19,32 @@ enum Request {
 struct VsCodeDebugTaskDefinition {
     r#type: String,
     name: String,
-    request: Request,
-
-    #[serde(default)]
-    program: Option<String>,
-    #[serde(default)]
-    args: Vec<String>,
-    #[serde(default)]
-    env: HashMap<String, Option<String>>,
-    // TODO envFile?
-    #[serde(default)]
-    cwd: Option<String>,
     #[serde(default)]
     port: Option<u16>,
-    #[serde(default)]
-    stop_on_entry: Option<bool>,
     #[serde(flatten)]
     other_attributes: serde_json::Value,
 }
 
 impl VsCodeDebugTaskDefinition {
     fn try_to_zed(self, replacer: &EnvVariableReplacer) -> anyhow::Result<DebugScenario> {
-        let label = replacer.replace(&self.name).into();
-        // TODO based on grep.app results it seems that vscode supports whitespace-splitting this field (ugh)
+        let label = replacer.replace(&self.name);
+        let mut config = replacer.replace_value(self.other_attributes);
+        let adapter = task_type_to_adapter_name(&self.r#type);
+        if let Some(config) = config.as_object_mut() {
+            if adapter == "JavaScript" {
+                config.insert("type".to_owned(), self.r#type.clone().into());
+            }
+        }
         let definition = DebugScenario {
-            label,
+            label: label.into(),
             build: None,
-            adapter: task_type_to_adapter_name(&self.r#type),
-            // TODO host?
+            adapter: adapter.into(),
             tcp_connection: self.port.map(|port| TcpArgumentsTemplate {
                 port: Some(port),
                 host: None,
                 timeout: None,
             }),
-            config: replacer.replace_value(self.other_attributes),
+            config,
         };
         Ok(definition)
     }
@@ -62,7 +53,8 @@ impl VsCodeDebugTaskDefinition {
 #[derive(Clone, Debug, Deserialize, PartialEq)]
 #[serde(rename_all = "camelCase")]
 pub struct VsCodeDebugTaskFile {
-    version: String,
+    #[serde(default)]
+    version: Option<String>,
     configurations: Vec<VsCodeDebugTaskDefinition>,
 }
 
@@ -75,7 +67,11 @@ impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
                 "workspaceFolder".to_owned(),
                 VariableName::WorktreeRoot.to_string(),
             ),
-            ("file".to_owned(), VariableName::Filename.to_string()), // TODO other interesting variables?
+            (
+                "relativeFile".to_owned(),
+                VariableName::RelativeFile.to_string(),
+            ),
+            ("file".to_owned(), VariableName::File.to_string()),
         ]));
         let templates = file
             .configurations
@@ -86,10 +82,10 @@ impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
     }
 }
 
-// todo(debugger) figure out how to make JsDebugAdapter::ADAPTER_NAME et al available here
-fn task_type_to_adapter_name(task_type: &str) -> SharedString {
+fn task_type_to_adapter_name(task_type: &str) -> String {
     match task_type {
-        "node" => "JavaScript",
+        "pwa-node" | "node" | "chrome" | "pwa-chrome" | "edge" | "pwa-edge" | "msedge"
+        | "pwa-msedge" => "JavaScript",
         "go" => "Delve",
         "php" => "PHP",
         "cppdbg" | "lldb" => "CodeLLDB",
@@ -98,7 +94,6 @@ fn task_type_to_adapter_name(task_type: &str) -> SharedString {
         _ => task_type,
     }
     .to_owned()
-    .into()
 }
 
 #[cfg(test)]
@@ -141,7 +136,19 @@ mod tests {
                 label: "Debug my JS app".into(),
                 adapter: "JavaScript".into(),
                 config: json!({
+                    "request": "launch",
+                    "program": "${ZED_WORKTREE_ROOT}/xyz.js",
                     "showDevDebugOutput": false,
+                    "stopOnEntry": true,
+                    "args": [
+                        "--foo",
+                        "${ZED_WORKTREE_ROOT}/thing",
+                    ],
+                    "cwd": "${ZED_WORKTREE_ROOT}/${FOO}/sub",
+                    "env": {
+                        "X": "Y",
+                    },
+                    "type": "node",
                 }),
                 tcp_connection: Some(TcpArgumentsTemplate {
                     port: Some(17),