Fix ruby debugger (#32407)

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

Closes #ISSUE

Release Notes:

- debugger: Fix Ruby (was broken by #30833)

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>

Change summary

Cargo.lock                             |   1 
crates/dap_adapters/Cargo.toml         |   1 
crates/dap_adapters/src/ruby.rs        | 272 ++++++++++-----------------
crates/task/src/lib.rs                 |  15 +
crates/task/src/vscode_debug_format.rs |   5 
5 files changed, 118 insertions(+), 176 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4053,6 +4053,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-trait",
+ "collections",
  "dap",
  "futures 0.3.31",
  "gpui",

crates/dap_adapters/Cargo.toml 🔗

@@ -23,6 +23,7 @@ doctest = false
 [dependencies]
 anyhow.workspace = true
 async-trait.workspace = true
+collections.workspace = true
 dap.workspace = true
 futures.workspace = true
 gpui.workspace = true

crates/dap_adapters/src/ruby.rs 🔗

@@ -1,16 +1,18 @@
-use anyhow::Result;
+use anyhow::{Result, bail};
 use async_trait::async_trait;
+use collections::FxHashMap;
 use dap::{
-    DebugRequest, StartDebuggingRequestArguments,
+    DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
     adapters::{
         DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
     },
 };
 use gpui::{AsyncApp, SharedString};
 use language::LanguageName;
+use serde::{Deserialize, Serialize};
 use serde_json::json;
 use std::path::PathBuf;
-use std::sync::Arc;
+use std::{ffi::OsStr, sync::Arc};
 use task::{DebugScenario, ZedDebugConfig};
 use util::command::new_smol_command;
 
@@ -21,6 +23,18 @@ impl RubyDebugAdapter {
     const ADAPTER_NAME: &'static str = "Ruby";
 }
 
+#[derive(Serialize, Deserialize)]
+struct RubyDebugConfig {
+    script_or_command: Option<String>,
+    script: Option<String>,
+    command: Option<String>,
+    #[serde(default)]
+    args: Vec<String>,
+    #[serde(default)]
+    env: FxHashMap<String, String>,
+    cwd: Option<PathBuf>,
+}
+
 #[async_trait(?Send)]
 impl DebugAdapter for RubyDebugAdapter {
     fn name(&self) -> DebugAdapterName {
@@ -31,185 +45,70 @@ impl DebugAdapter for RubyDebugAdapter {
         Some(SharedString::new_static("Ruby").into())
     }
 
+    fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> {
+        Ok(StartDebuggingRequestArgumentsRequest::Launch)
+    }
+
     async fn dap_schema(&self) -> serde_json::Value {
         json!({
-            "oneOf": [
-                {
-                    "allOf": [
-                        {
-                            "type": "object",
-                            "required": ["request"],
-                            "properties": {
-                                "request": {
-                                    "type": "string",
-                                    "enum": ["launch"],
-                                    "description": "Request to launch a new process"
-                                }
-                            }
-                        },
-                        {
-                            "type": "object",
-                            "required": ["script"],
-                            "properties": {
-                                "command": {
-                                    "type": "string",
-                                    "description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
-                                    "default": "ruby"
-                                },
-                                "script": {
-                                    "type": "string",
-                                    "description": "Absolute path to a Ruby file."
-                                },
-                                "cwd": {
-                                    "type": "string",
-                                    "description": "Directory to execute the program in",
-                                    "default": "${ZED_WORKTREE_ROOT}"
-                                },
-                                "args": {
-                                    "type": "array",
-                                    "description": "Command line arguments passed to the program",
-                                    "items": {
-                                        "type": "string"
-                                    },
-                                    "default": []
-                                },
-                                "env": {
-                                    "type": "object",
-                                    "description": "Additional environment variables to pass to the debugging (and debugged) process",
-                                    "default": {}
-                                },
-                                "showProtocolLog": {
-                                    "type": "boolean",
-                                    "description": "Show a log of DAP requests, events, and responses",
-                                    "default": false
-                                },
-                                "useBundler": {
-                                    "type": "boolean",
-                                    "description": "Execute Ruby programs with `bundle exec` instead of directly",
-                                    "default": false
-                                },
-                                "bundlePath": {
-                                    "type": "string",
-                                    "description": "Location of the bundle executable"
-                                },
-                                "rdbgPath": {
-                                    "type": "string",
-                                    "description": "Location of the rdbg executable"
-                                },
-                                "askParameters": {
-                                    "type": "boolean",
-                                    "description": "Ask parameters at first."
-                                },
-                                "debugPort": {
-                                    "type": "string",
-                                    "description": "UNIX domain socket name or TPC/IP host:port"
-                                },
-                                "waitLaunchTime": {
-                                    "type": "number",
-                                    "description": "Wait time before connection in milliseconds"
-                                },
-                                "localfs": {
-                                    "type": "boolean",
-                                    "description": "true if the VSCode and debugger run on a same machine",
-                                    "default": false
-                                },
-                                "useTerminal": {
-                                    "type": "boolean",
-                                    "description": "Create a new terminal and then execute commands there",
-                                    "default": false
-                                }
-                            }
-                        }
-                    ]
+            "type": "object",
+            "properties": {
+                "command": {
+                    "type": "string",
+                    "description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
                 },
-                {
-                    "allOf": [
-                        {
-                            "type": "object",
-                            "required": ["request"],
-                            "properties": {
-                                "request": {
-                                    "type": "string",
-                                    "enum": ["attach"],
-                                    "description": "Request to attach to an existing process"
-                                }
-                            }
-                        },
-                        {
-                            "type": "object",
-                            "properties": {
-                                "rdbgPath": {
-                                    "type": "string",
-                                    "description": "Location of the rdbg executable"
-                                },
-                                "debugPort": {
-                                    "type": "string",
-                                    "description": "UNIX domain socket name or TPC/IP host:port"
-                                },
-                                "showProtocolLog": {
-                                    "type": "boolean",
-                                    "description": "Show a log of DAP requests, events, and responses",
-                                    "default": false
-                                },
-                                "localfs": {
-                                    "type": "boolean",
-                                    "description": "true if the VSCode and debugger run on a same machine",
-                                    "default": false
-                                },
-                                "localfsMap": {
-                                    "type": "string",
-                                    "description": "Specify pairs of remote root path and local root path like `/remote_dir:/local_dir`. You can specify multiple pairs like `/rem1:/loc1,/rem2:/loc2` by concatenating with `,`."
-                                },
-                                "env": {
-                                    "type": "object",
-                                    "description": "Additional environment variables to pass to the rdbg process",
-                                    "default": {}
-                                }
-                            }
-                        }
-                    ]
-                }
-            ]
+                "script": {
+                    "type": "string",
+                    "description": "Absolute path to a Ruby file."
+                },
+                "cwd": {
+                    "type": "string",
+                    "description": "Directory to execute the program in",
+                    "default": "${ZED_WORKTREE_ROOT}"
+                },
+                "args": {
+                    "type": "array",
+                    "description": "Command line arguments passed to the program",
+                    "items": {
+                        "type": "string"
+                    },
+                    "default": []
+                },
+                "env": {
+                    "type": "object",
+                    "description": "Additional environment variables to pass to the debugging (and debugged) process",
+                    "default": {}
+                },
+            }
         })
     }
 
     fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
-        let mut config = serde_json::Map::new();
-
-        match &zed_scenario.request {
+        match zed_scenario.request {
             DebugRequest::Launch(launch) => {
-                config.insert("request".to_string(), json!("launch"));
-                config.insert("script".to_string(), json!(launch.program));
-                config.insert("command".to_string(), json!("ruby"));
-
-                if !launch.args.is_empty() {
-                    config.insert("args".to_string(), json!(launch.args));
-                }
-
-                if !launch.env.is_empty() {
-                    config.insert("env".to_string(), json!(launch.env));
-                }
-
-                if let Some(cwd) = &launch.cwd {
-                    config.insert("cwd".to_string(), json!(cwd));
-                }
-
-                // Ruby stops on entry so there's no need to handle that case
+                let config = RubyDebugConfig {
+                    script_or_command: Some(launch.program),
+                    script: None,
+                    command: None,
+                    args: launch.args,
+                    env: launch.env,
+                    cwd: launch.cwd.clone(),
+                };
+
+                let config = serde_json::to_value(config)?;
+
+                Ok(DebugScenario {
+                    adapter: zed_scenario.adapter,
+                    label: zed_scenario.label,
+                    config,
+                    tcp_connection: None,
+                    build: None,
+                })
             }
-            DebugRequest::Attach(attach) => {
-                config.insert("request".to_string(), json!("attach"));
-
-                config.insert("processId".to_string(), json!(attach.process_id));
+            DebugRequest::Attach(_) => {
+                anyhow::bail!("Attach requests are unsupported");
             }
         }
-
-        Ok(DebugScenario {
-            adapter: zed_scenario.adapter,
-            label: zed_scenario.label,
-            config: serde_json::Value::Object(config),
-            tcp_connection: None,
-            build: None,
-        })
     }
 
     async fn get_binary(
@@ -247,13 +146,34 @@ impl DebugAdapter for RubyDebugAdapter {
 
         let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
         let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
+        let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?;
 
-        let arguments = vec![
+        let mut arguments = vec![
             "--open".to_string(),
             format!("--port={}", port),
             format!("--host={}", host),
         ];
 
+        if let Some(script) = &ruby_config.script {
+            arguments.push(script.clone());
+        } else if let Some(command) = &ruby_config.command {
+            arguments.push("--command".to_string());
+            arguments.push(command.clone());
+        } else if let Some(command_or_script) = &ruby_config.script_or_command {
+            if delegate
+                .which(OsStr::new(&command_or_script))
+                .await
+                .is_some()
+            {
+                arguments.push("--command".to_string());
+            }
+            arguments.push(command_or_script.clone());
+        } else {
+            bail!("Ruby debug config must have 'script' or 'command' args");
+        }
+
+        arguments.extend(ruby_config.args);
+
         Ok(DebugAdapterBinary {
             command: rdbg_path.to_string_lossy().to_string(),
             arguments,
@@ -262,8 +182,12 @@ impl DebugAdapter for RubyDebugAdapter {
                 port,
                 timeout,
             }),
-            cwd: None,
-            envs: std::collections::HashMap::default(),
+            cwd: Some(
+                ruby_config
+                    .cwd
+                    .unwrap_or(delegate.worktree_root_path().to_owned()),
+            ),
+            envs: ruby_config.env.into_iter().collect(),
             request_args: StartDebuggingRequestArguments {
                 request: self.request_kind(&definition.config)?,
                 configuration: definition.config.clone(),

crates/task/src/lib.rs 🔗

@@ -530,6 +530,21 @@ impl EnvVariableReplacer {
     fn new(variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>) -> Self {
         Self { variables }
     }
+
+    fn replace_value(&self, input: serde_json::Value) -> serde_json::Value {
+        match input {
+            serde_json::Value::String(s) => serde_json::Value::String(self.replace(&s)),
+            serde_json::Value::Array(arr) => {
+                serde_json::Value::Array(arr.into_iter().map(|v| self.replace_value(v)).collect())
+            }
+            serde_json::Value::Object(obj) => serde_json::Value::Object(
+                obj.into_iter()
+                    .map(|(k, v)| (self.replace(&k), self.replace_value(v)))
+                    .collect(),
+            ),
+            _ => input,
+        }
+    }
     // Replaces occurrences of VsCode-specific environment variables with Zed equivalents.
     fn replace(&self, input: &str) -> String {
         shellexpand::env_with_context_no_errors(&input, |var: &str| {

crates/task/src/vscode_debug_format.rs 🔗

@@ -53,7 +53,7 @@ impl VsCodeDebugTaskDefinition {
                 host: None,
                 timeout: None,
             }),
-            config: self.other_attributes,
+            config: replacer.replace_value(self.other_attributes),
         };
         Ok(definition)
     }
@@ -75,7 +75,7 @@ impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
                 "workspaceFolder".to_owned(),
                 VariableName::WorktreeRoot.to_string(),
             ),
-            // TODO other interesting variables?
+            ("file".to_owned(), VariableName::Filename.to_string()), // TODO other interesting variables?
         ]));
         let templates = file
             .configurations
@@ -94,6 +94,7 @@ fn task_type_to_adapter_name(task_type: &str) -> SharedString {
         "php" => "PHP",
         "cppdbg" | "lldb" => "CodeLLDB",
         "debugpy" => "Debugpy",
+        "rdbg" => "Ruby",
         _ => task_type,
     }
     .to_owned()