Add rdbg for Ruby (#30126)

Conrad Irwin created

Release Notes:

- N/A

Change summary

crates/dap/src/transport.rs                       |  16 ++
crates/dap_adapters/src/dap_adapters.rs           |   3 
crates/dap_adapters/src/ruby.rs                   | 102 +++++++++++++++++
crates/debugger_ui/src/session/running/console.rs |   2 
crates/project/src/debugger/session.rs            |   5 
5 files changed, 121 insertions(+), 7 deletions(-)

Detailed changes

crates/dap/src/transport.rs 🔗

@@ -580,21 +580,31 @@ impl TcpTransport {
                 .unwrap_or(2000u64)
         });
 
-        let (rx, tx) = select! {
+        let (mut process, (rx, tx)) = select! {
             _ = cx.background_executor().timer(Duration::from_millis(timeout)).fuse() => {
                 return Err(anyhow!(format!("Connection to TCP DAP timeout {}:{}", host, port)))
             },
             result = cx.spawn(async move |cx| {
                 loop {
                     match TcpStream::connect(address).await {
-                        Ok(stream) => return stream.split(),
+                        Ok(stream) => return Ok((process, stream.split())),
                         Err(_) => {
+                            if let Ok(Some(_)) = process.try_status() {
+                                let output = process.output().await?;
+                                let output = if output.stderr.is_empty() {
+                                    String::from_utf8_lossy(&output.stdout).to_string()
+                                } else {
+                                    String::from_utf8_lossy(&output.stderr).to_string()
+                                };
+                                return Err(anyhow!("{}\nerror: process exited before debugger attached.", output));
+                            }
                             cx.background_executor().timer(Duration::from_millis(100)).await;
                         }
                     }
                 }
-            }).fuse() => result
+            }).fuse() => result?
         };
+
         log::info!(
             "Debug adapter has connected to TCP server {}:{}",
             host,

crates/dap_adapters/src/dap_adapters.rs 🔗

@@ -4,6 +4,7 @@ mod go;
 mod javascript;
 mod php;
 mod python;
+mod ruby;
 
 use std::{net::Ipv4Addr, sync::Arc};
 
@@ -24,6 +25,7 @@ use gpui::{App, BorrowAppContext};
 use javascript::JsDebugAdapter;
 use php::PhpDebugAdapter;
 use python::PythonDebugAdapter;
+use ruby::RubyDebugAdapter;
 use serde_json::{Value, json};
 use task::TcpArgumentsTemplate;
 
@@ -33,6 +35,7 @@ pub fn init(cx: &mut App) {
         registry.add_adapter(Arc::from(PythonDebugAdapter::default()));
         registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
         registry.add_adapter(Arc::from(JsDebugAdapter::default()));
+        registry.add_adapter(Arc::from(RubyDebugAdapter));
         registry.add_adapter(Arc::from(GoDebugAdapter));
         registry.add_adapter(Arc::from(GdbDebugAdapter));
 

crates/dap_adapters/src/ruby.rs 🔗

@@ -0,0 +1,102 @@
+use anyhow::{Result, anyhow};
+use async_trait::async_trait;
+use dap::{
+    DebugRequest, StartDebuggingRequestArguments,
+    adapters::{
+        self, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
+    },
+};
+use gpui::AsyncApp;
+use std::path::PathBuf;
+use util::command::new_smol_command;
+
+use crate::ToDap;
+
+#[derive(Default)]
+pub(crate) struct RubyDebugAdapter;
+
+impl RubyDebugAdapter {
+    const ADAPTER_NAME: &'static str = "Ruby";
+}
+
+#[async_trait(?Send)]
+impl DebugAdapter for RubyDebugAdapter {
+    fn name(&self) -> DebugAdapterName {
+        DebugAdapterName(Self::ADAPTER_NAME.into())
+    }
+
+    async fn get_binary(
+        &self,
+        delegate: &dyn DapDelegate,
+        definition: &DebugTaskDefinition,
+        _user_installed_path: Option<PathBuf>,
+        _cx: &mut AsyncApp,
+    ) -> Result<DebugAdapterBinary> {
+        let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
+        let mut rdbg_path = adapter_path.join("rdbg");
+        if !delegate.fs().is_file(&rdbg_path).await {
+            match delegate.which("rdbg".as_ref()) {
+                Some(path) => rdbg_path = path,
+                None => {
+                    delegate.output_to_console(
+                        "rdbg not found on path, trying `gem install debug`".to_string(),
+                    );
+                    let output = new_smol_command("gem")
+                        .arg("install")
+                        .arg("--no-document")
+                        .arg("--bindir")
+                        .arg(adapter_path)
+                        .arg("debug")
+                        .output()
+                        .await?;
+                    if !output.status.success() {
+                        return Err(anyhow!(
+                            "Failed to install rdbg:\n{}",
+                            String::from_utf8_lossy(&output.stderr).to_string()
+                        ));
+                    }
+                }
+            }
+        }
+
+        let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
+        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
+
+        let DebugRequest::Launch(mut launch) = definition.request.clone() else {
+            anyhow::bail!("rdbg does not yet support attaching");
+        };
+
+        let mut arguments = vec![
+            "--open".to_string(),
+            format!("--port={}", port),
+            format!("--host={}", host),
+        ];
+        if launch.args.is_empty() {
+            let program = launch.program.clone();
+            let mut split = program.split(" ");
+            launch.program = split.next().unwrap().to_string();
+            launch.args = split.map(|s| s.to_string()).collect();
+        }
+        if delegate.which(launch.program.as_ref()).is_some() {
+            arguments.push("--command".to_string())
+        }
+        arguments.push(launch.program);
+        arguments.extend(launch.args);
+
+        Ok(DebugAdapterBinary {
+            command: rdbg_path.to_string_lossy().to_string(),
+            arguments,
+            connection: Some(adapters::TcpArguments {
+                host,
+                port,
+                timeout,
+            }),
+            cwd: launch.cwd,
+            envs: launch.env.into_iter().collect(),
+            request_args: StartDebuggingRequestArguments {
+                configuration: serde_json::Value::Object(Default::default()),
+                request: definition.request.to_dap(),
+            },
+        })
+    }
+}

crates/debugger_ui/src/session/running/console.rs 🔗

@@ -152,7 +152,7 @@ impl Console {
             session
                 .evaluate(
                     expression,
-                    Some(dap::EvaluateArgumentsContext::Variables),
+                    Some(dap::EvaluateArgumentsContext::Repl),
                     self.stack_frame_list.read(cx).selected_stack_frame_id(),
                     None,
                     cx,

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

@@ -10,7 +10,7 @@ use super::dap_command::{
     TerminateThreadsCommand, ThreadsCommand, VariablesCommand,
 };
 use super::dap_store::DapStore;
-use anyhow::{Context as _, Result, anyhow};
+use anyhow::{Result, anyhow};
 use collections::{HashMap, HashSet, IndexMap, IndexSet};
 use dap::adapters::{DebugAdapterBinary, DebugAdapterName};
 use dap::messages::Response;
@@ -169,8 +169,7 @@ impl LocalMode {
                     .await?
             } else {
                 DebugAdapterClient::start(session_id, binary.clone(), message_handler, cx.clone())
-                    .await
-                    .with_context(|| format!("Failed to start {:?}", &binary.command))?
+                    .await?
             },
         );