From 3dfbd9e57cd3e7f2e9718f0872de17d1b77cdbca Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 9 Jun 2025 16:11:24 -0600 Subject: [PATCH] Fix ruby debugger (#32407) Closes #ISSUE Release Notes: - debugger: Fix Ruby (was broken by #30833) --------- Co-authored-by: Anthony Eid Co-authored-by: Piotr Osiewicz Co-authored-by: Cole Miller --- Cargo.lock | 1 + crates/dap_adapters/Cargo.toml | 1 + crates/dap_adapters/src/ruby.rs | 268 +++++++++---------------- crates/task/src/lib.rs | 15 ++ crates/task/src/vscode_debug_format.rs | 5 +- 5 files changed, 116 insertions(+), 174 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3272bba810..99b432985d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4053,6 +4053,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "collections", "dap", "futures 0.3.31", "gpui", diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index f669d781cd..e2e922bd56 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/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 diff --git a/crates/dap_adapters/src/ruby.rs b/crates/dap_adapters/src/ruby.rs index 65a342ade9..7e2afa31ef 100644 --- a/crates/dap_adapters/src/ruby.rs +++ b/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, + script: Option, + command: Option, + #[serde(default)] + args: Vec, + #[serde(default)] + env: FxHashMap, + cwd: Option, +} + #[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 { + 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 { - 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")); + let config = RubyDebugConfig { + script_or_command: Some(launch.program), + script: None, + command: None, + args: launch.args, + env: launch.env, + cwd: launch.cwd.clone(), + }; - if !launch.args.is_empty() { - config.insert("args".to_string(), json!(launch.args)); - } + let config = serde_json::to_value(config)?; - 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 + 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::(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(), diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 63dfb4db04..fe84c1e06e 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -530,6 +530,21 @@ impl EnvVariableReplacer { fn new(variables: HashMap) -> 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| { diff --git a/crates/task/src/vscode_debug_format.rs b/crates/task/src/vscode_debug_format.rs index 0506d191a1..6ff617805c 100644 --- a/crates/task/src/vscode_debug_format.rs +++ b/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 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()