From d80ec30e8e75b11adbb59c9d3c79dc0cf575d1c4 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 20 May 2025 14:38:32 +0200 Subject: [PATCH 1/6] Debugger prototype for Ruby This is a prototype implementation of Ruby debugger implementation, based on existing impl in core Zed. Input is most welcome on what it looks like and all that jazz. --- extension.toml | 2 ++ src/ruby.rs | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/extension.toml b/extension.toml index 61b92f5..ad05bc3 100644 --- a/extension.toml +++ b/extension.toml @@ -5,6 +5,8 @@ version = "0.9.0" schema_version = 1 authors = ["Vitaly Slobodin "] repository = "https://github.com/zed-extensions/ruby" +debug_adapters = ["Ruby"] + [language_servers.solargraph] name = "Solargraph" diff --git a/src/ruby.rs b/src/ruby.rs index dbc9bb3..c531e15 100644 --- a/src/ruby.rs +++ b/src/ruby.rs @@ -6,6 +6,18 @@ mod language_servers; use language_servers::{LanguageServer, Rubocop, RubyLsp, Solargraph, Sorbet, Steep}; use zed_extension_api::{self as zed}; +use std::net::{Ipv4Addr, SocketAddrV4, TcpListener}; +use std::path::Path; + +use zed::lsp::{Completion, Symbol}; +use zed::settings::LspSettings; +use zed::{serde_json, CodeLabel, LanguageServerId}; +use zed::{DebugAdapterBinary, DebugRequest, DebugTaskDefinition}; +use zed_extension_api::{ + self as zed, resolve_tcp_template, Command, Result, StartDebuggingRequestArguments, + StartDebuggingRequestArgumentsRequest, TcpArgumentsTemplate, Worktree, +}; + #[derive(Default)] struct RubyExtension { solargraph: Option, @@ -87,6 +99,79 @@ impl zed::Extension for RubyExtension { _ => None, } } + fn get_dap_binary( + &mut self, + adapter_name: String, + config: DebugTaskDefinition, + _: Option, + worktree: &Worktree, + ) -> Result { + let mut rdbg_path = Path::new(&adapter_name) + .join("rdbg") + .to_string_lossy() + .into_owned(); + + if worktree.which(&rdbg_path).is_none() { + match worktree.which("rdbg".as_ref()) { + Some(path) => rdbg_path = path.into(), + None => { + let output = Command::new("gem") + .arg("install") + .arg("--no-document") + .arg("--bindir") + .arg(&adapter_name) + .arg("debug") + .output()?; + if !output.status.is_none_or(|status| status != 0) { + return Err(format!( + "Failed to install rdbg:\n{}", + String::from_utf8_lossy(&output.stderr).to_string() + )); + } + } + } + } + + let tcp_connection = + config + .tcp_connection + .clone() + .unwrap_or_else(|| TcpArgumentsTemplate { + port: None, + host: None, + timeout: None, + }); + let connection = resolve_tcp_template(tcp_connection)?; + let DebugRequest::Launch(launch) = config.request.clone() else { + return Err("rdbg does not yet support attaching".to_string()); + }; + + let mut arguments = vec![ + "--open".to_string(), + format!("--port={}", connection.port), + format!("--host={}", connection.host), + ]; + if worktree.which(launch.program.as_ref()).is_some() { + arguments.push("--command".to_string()) + } + arguments.push(launch.program); + arguments.extend(launch.args); + let request = match config.request { + DebugRequest::Launch(_) => StartDebuggingRequestArgumentsRequest::Launch, + DebugRequest::Attach(_) => StartDebuggingRequestArgumentsRequest::Attach, + }; + Ok(DebugAdapterBinary { + command: rdbg_path.to_string(), + arguments, + connection: Some(connection), + cwd: launch.cwd, + envs: launch.envs.into_iter().collect(), + request_args: StartDebuggingRequestArguments { + configuration: serde_json::Value::Object(Default::default()).to_string(), + request, + }, + }) + } } zed_extension_api::register_extension!(RubyExtension); From 7a332a96894cd35fdf535bc290a06b1cd2bfab84 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:09:47 +0200 Subject: [PATCH 2/6] Fixups --- src/ruby.rs | 56 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/src/ruby.rs b/src/ruby.rs index c531e15..94f5138 100644 --- a/src/ruby.rs +++ b/src/ruby.rs @@ -3,6 +3,8 @@ mod command_executor; mod gemset; mod language_servers; +use std::{collections::HashMap, path::Path}; + use language_servers::{LanguageServer, Rubocop, RubyLsp, Solargraph, Sorbet, Steep}; use zed_extension_api::{self as zed}; @@ -14,8 +16,9 @@ use zed::settings::LspSettings; use zed::{serde_json, CodeLabel, LanguageServerId}; use zed::{DebugAdapterBinary, DebugRequest, DebugTaskDefinition}; use zed_extension_api::{ - self as zed, resolve_tcp_template, Command, Result, StartDebuggingRequestArguments, - StartDebuggingRequestArgumentsRequest, TcpArgumentsTemplate, Worktree, + self as zed, resolve_tcp_template, Command, DebugAdapterBinary, DebugTaskDefinition, + StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, TcpArgumentsTemplate, + Worktree, }; #[derive(Default)] @@ -27,6 +30,18 @@ struct RubyExtension { steep: Option, } +#[derive(Serialize, Deserialize)] +struct RubyDebugConfig { + script_or_command: Option, + script: Option, + command: Option, + #[serde(default)] + args: Vec, + #[serde(default)] + env: HashMap, + cwd: Option, +} + impl zed::Extension for RubyExtension { fn new() -> Self { Self::default() @@ -151,24 +166,35 @@ impl zed::Extension for RubyExtension { format!("--port={}", connection.port), format!("--host={}", connection.host), ]; - if worktree.which(launch.program.as_ref()).is_some() { - arguments.push("--command".to_string()) + 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 worktree.which(&command_or_script).is_some() { + arguments.push("--command".to_string()); + } + arguments.push(command_or_script.clone()); + } else { + return Err("Ruby debug config must have 'script' or 'command' args".into()); } - arguments.push(launch.program); - arguments.extend(launch.args); - let request = match config.request { - DebugRequest::Launch(_) => StartDebuggingRequestArgumentsRequest::Launch, - DebugRequest::Attach(_) => StartDebuggingRequestArgumentsRequest::Attach, - }; + if let Some(configuration) = configuration.as_object_mut() { + configuration + .entry("cwd") + .or_insert_with(|| worktree.root_path().into()); + } + arguments.extend(ruby_config.args); + Ok(DebugAdapterBinary { - command: rdbg_path.to_string(), + command: Some(rdbg_path.to_string()), arguments, connection: Some(connection), - cwd: launch.cwd, - envs: launch.envs.into_iter().collect(), + cwd: ruby_config.cwd, + envs: ruby_config.env.into_iter().collect(), request_args: StartDebuggingRequestArguments { - configuration: serde_json::Value::Object(Default::default()).to_string(), - request, + configuration: configuration.to_string(), + request: StartDebuggingRequestArgumentsRequest::Launch, }, }) } From 0027422a15a6c3698631d07d987d9729204938c7 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 16 Jun 2025 19:03:53 +0200 Subject: [PATCH 3/6] Add other methods --- src/ruby.rs | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/ruby.rs b/src/ruby.rs index 94f5138..afe3c58 100644 --- a/src/ruby.rs +++ b/src/ruby.rs @@ -16,9 +16,9 @@ use zed::settings::LspSettings; use zed::{serde_json, CodeLabel, LanguageServerId}; use zed::{DebugAdapterBinary, DebugRequest, DebugTaskDefinition}; use zed_extension_api::{ - self as zed, resolve_tcp_template, Command, DebugAdapterBinary, DebugTaskDefinition, - StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, TcpArgumentsTemplate, - Worktree, + self as zed, resolve_tcp_template, Command, DebugAdapterBinary, DebugConfig, DebugRequest, + DebugScenario, DebugTaskDefinition, StartDebuggingRequestArguments, + StartDebuggingRequestArgumentsRequest, TcpArgumentsTemplate, Worktree, }; #[derive(Default)] @@ -198,6 +198,43 @@ impl zed::Extension for RubyExtension { }, }) } + fn dap_request_kind( + &mut self, + _: String, + _: serde_json::Value, + ) -> zed_extension_api::Result { + Ok(StartDebuggingRequestArgumentsRequest::Launch) + } + fn dap_config_to_scenario( + &mut self, + zed_scenario: DebugConfig, + ) -> Result { + match zed_scenario.request { + DebugRequest::Launch(launch) => { + let config = RubyDebugConfig { + script_or_command: Some(launch.program), + script: None, + command: None, + args: launch.args, + env: launch.envs.into_iter().collect(), + cwd: launch.cwd.clone(), + }; + + let config = serde_json::to_value(config) + .map_err(|e| e.to_string())? + .to_string(); + + Ok(DebugScenario { + adapter: zed_scenario.adapter, + label: zed_scenario.label, + config, + tcp_connection: None, + build: None, + }) + } + DebugRequest::Attach(_) => Err("Attach requests are unsupported".into()), + } + } } zed_extension_api::register_extension!(RubyExtension); From 873409d3d4cff01f5c33f28d8f0583aefeaf7142 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 17 Jun 2025 00:50:47 +0200 Subject: [PATCH 4/6] Touchups --- debug_adapter_schemas/rdbg.json | 31 +++++++++++++++++++++++++++++++ extension.toml | 3 ++- languages/ruby/config.toml | 1 + src/ruby.rs | 15 +++++++++++++-- 4 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 debug_adapter_schemas/rdbg.json diff --git a/debug_adapter_schemas/rdbg.json b/debug_adapter_schemas/rdbg.json new file mode 100644 index 0000000..9338842 --- /dev/null +++ b/debug_adapter_schemas/rdbg.json @@ -0,0 +1,31 @@ +{ + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)" + }, + "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": {} + } + } +} diff --git a/extension.toml b/extension.toml index ad05bc3..a8a01f9 100644 --- a/extension.toml +++ b/extension.toml @@ -5,7 +5,6 @@ version = "0.9.0" schema_version = 1 authors = ["Vitaly Slobodin "] repository = "https://github.com/zed-extensions/ruby" -debug_adapters = ["Ruby"] [language_servers.solargraph] @@ -64,3 +63,5 @@ args = ["outdated", "--norc"] kind = "process:exec" command = "gem" args = ["update", "--norc", "*"] + +[debug_adapters.RDBG] diff --git a/languages/ruby/config.toml b/languages/ruby/config.toml index 8c084e1..26692a8 100644 --- a/languages/ruby/config.toml +++ b/languages/ruby/config.toml @@ -61,6 +61,7 @@ collapsed_placeholder = "# ..." tab_size = 2 scope_opt_in_language_servers = ["tailwindcss-language-server"] word_characters = ["?", "!"] +debuggers = ["RDBG"] [overrides.string] completion_query_characters = ["-", "."] diff --git a/src/ruby.rs b/src/ruby.rs index afe3c58..51b472f 100644 --- a/src/ruby.rs +++ b/src/ruby.rs @@ -201,9 +201,20 @@ impl zed::Extension for RubyExtension { fn dap_request_kind( &mut self, _: String, - _: serde_json::Value, + value: serde_json::Value, ) -> zed_extension_api::Result { - Ok(StartDebuggingRequestArgumentsRequest::Launch) + value + .get("request") + .and_then(|request| { + request.as_str().and_then(|s| match s { + "launch" => Some(StartDebuggingRequestArgumentsRequest::Launch), + "attach" => Some(StartDebuggingRequestArgumentsRequest::Attach), + _ => None, + }) + }) + .ok_or_else(|| { + "Invalid request, expected `request` to be either `launch` or `attach`".into() + }) } fn dap_config_to_scenario( &mut self, From 906adf1e2334ed99037d01daa937570717d7902a Mon Sep 17 00:00:00 2001 From: Vitaly Slobodin Date: Sat, 21 Jun 2025 10:13:30 +0200 Subject: [PATCH 5/6] Resolve merge conflicts --- Cargo.lock | 2 ++ Cargo.toml | 2 ++ src/ruby.rs | 22 +++++++++------------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c52cd7d..7a10a72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -827,6 +827,8 @@ name = "zed_ruby" version = "0.9.0" dependencies = [ "regex", + "serde", + "serde_json", "zed_extension_api", ] diff --git a/Cargo.toml b/Cargo.toml index beba5c1..2924700 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,6 @@ crate-type = ["cdylib"] [dependencies] regex = "1.11.1" +serde_json = "1.0" +serde = {version = "1.0", features = ["derive"]} zed_extension_api = "0.6.0" diff --git a/src/ruby.rs b/src/ruby.rs index 51b472f..09dae9c 100644 --- a/src/ruby.rs +++ b/src/ruby.rs @@ -6,15 +6,7 @@ mod language_servers; use std::{collections::HashMap, path::Path}; use language_servers::{LanguageServer, Rubocop, RubyLsp, Solargraph, Sorbet, Steep}; -use zed_extension_api::{self as zed}; - -use std::net::{Ipv4Addr, SocketAddrV4, TcpListener}; -use std::path::Path; - -use zed::lsp::{Completion, Symbol}; -use zed::settings::LspSettings; -use zed::{serde_json, CodeLabel, LanguageServerId}; -use zed::{DebugAdapterBinary, DebugRequest, DebugTaskDefinition}; +use serde::{Deserialize, Serialize}; use zed_extension_api::{ self as zed, resolve_tcp_template, Command, DebugAdapterBinary, DebugConfig, DebugRequest, DebugScenario, DebugTaskDefinition, StartDebuggingRequestArguments, @@ -114,6 +106,7 @@ impl zed::Extension for RubyExtension { _ => None, } } + fn get_dap_binary( &mut self, adapter_name: String, @@ -140,7 +133,7 @@ impl zed::Extension for RubyExtension { if !output.status.is_none_or(|status| status != 0) { return Err(format!( "Failed to install rdbg:\n{}", - String::from_utf8_lossy(&output.stderr).to_string() + String::from_utf8_lossy(&output.stderr).into_owned() )); } } @@ -157,10 +150,11 @@ impl zed::Extension for RubyExtension { timeout: None, }); let connection = resolve_tcp_template(tcp_connection)?; - let DebugRequest::Launch(launch) = config.request.clone() else { - return Err("rdbg does not yet support attaching".to_string()); - }; + let mut configuration: serde_json::Value = serde_json::from_str(&config.config) + .map_err(|e| format!("`config` is not a valid JSON: {e}"))?; + let ruby_config: RubyDebugConfig = serde_json::from_value(configuration.clone()) + .map_err(|e| format!("`config` is not a valid rdbg config: {e}"))?; let mut arguments = vec![ "--open".to_string(), format!("--port={}", connection.port), @@ -198,6 +192,7 @@ impl zed::Extension for RubyExtension { }, }) } + fn dap_request_kind( &mut self, _: String, @@ -216,6 +211,7 @@ impl zed::Extension for RubyExtension { "Invalid request, expected `request` to be either `launch` or `attach`".into() }) } + fn dap_config_to_scenario( &mut self, zed_scenario: DebugConfig, From 64b92cc9a5e82f3b05dfe125e66bb7510fb2e080 Mon Sep 17 00:00:00 2001 From: Vitaly Slobodin Date: Sat, 21 Jun 2025 10:17:38 +0200 Subject: [PATCH 6/6] Make `clippy` happy --- src/ruby.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/ruby.rs b/src/ruby.rs index 09dae9c..5c685c9 100644 --- a/src/ruby.rs +++ b/src/ruby.rs @@ -121,7 +121,7 @@ impl zed::Extension for RubyExtension { if worktree.which(&rdbg_path).is_none() { match worktree.which("rdbg".as_ref()) { - Some(path) => rdbg_path = path.into(), + Some(path) => rdbg_path = path, None => { let output = Command::new("gem") .arg("install") @@ -130,7 +130,7 @@ impl zed::Extension for RubyExtension { .arg(&adapter_name) .arg("debug") .output()?; - if !output.status.is_none_or(|status| status != 0) { + if output.status.is_some_and(|status| status == 0) { return Err(format!( "Failed to install rdbg:\n{}", String::from_utf8_lossy(&output.stderr).into_owned() @@ -140,15 +140,11 @@ impl zed::Extension for RubyExtension { } } - let tcp_connection = - config - .tcp_connection - .clone() - .unwrap_or_else(|| TcpArgumentsTemplate { - port: None, - host: None, - timeout: None, - }); + let tcp_connection = config.tcp_connection.unwrap_or(TcpArgumentsTemplate { + port: None, + host: None, + timeout: None, + }); let connection = resolve_tcp_template(tcp_connection)?; let mut configuration: serde_json::Value = serde_json::from_str(&config.config) .map_err(|e| format!("`config` is not a valid JSON: {e}"))?; @@ -166,7 +162,7 @@ impl zed::Extension for RubyExtension { arguments.push("--command".to_string()); arguments.push(command.clone()); } else if let Some(command_or_script) = &ruby_config.script_or_command { - if worktree.which(&command_or_script).is_some() { + if worktree.which(command_or_script).is_some() { arguments.push("--command".to_string()); } arguments.push(command_or_script.clone());