ruby.rs

  1use anyhow::{Result, bail};
  2use async_trait::async_trait;
  3use collections::FxHashMap;
  4use dap::{
  5    DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
  6    adapters::{
  7        DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
  8    },
  9};
 10use gpui::{AsyncApp, SharedString};
 11use language::LanguageName;
 12use serde::{Deserialize, Serialize};
 13use serde_json::json;
 14use std::path::PathBuf;
 15use std::{ffi::OsStr, sync::Arc};
 16use task::{DebugScenario, ZedDebugConfig};
 17use util::command::new_smol_command;
 18
 19#[derive(Default)]
 20pub(crate) struct RubyDebugAdapter;
 21
 22impl RubyDebugAdapter {
 23    const ADAPTER_NAME: &'static str = "Ruby";
 24}
 25
 26#[derive(Serialize, Deserialize)]
 27struct RubyDebugConfig {
 28    script_or_command: Option<String>,
 29    script: Option<String>,
 30    command: Option<String>,
 31    #[serde(default)]
 32    args: Vec<String>,
 33    #[serde(default)]
 34    env: FxHashMap<String, String>,
 35    cwd: Option<PathBuf>,
 36}
 37
 38#[async_trait(?Send)]
 39impl DebugAdapter for RubyDebugAdapter {
 40    fn name(&self) -> DebugAdapterName {
 41        DebugAdapterName(Self::ADAPTER_NAME.into())
 42    }
 43
 44    fn adapter_language_name(&self) -> Option<LanguageName> {
 45        Some(SharedString::new_static("Ruby").into())
 46    }
 47
 48    fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> {
 49        Ok(StartDebuggingRequestArgumentsRequest::Launch)
 50    }
 51
 52    fn dap_schema(&self) -> serde_json::Value {
 53        json!({
 54            "type": "object",
 55            "properties": {
 56                "command": {
 57                    "type": "string",
 58                    "description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
 59                },
 60                "script": {
 61                    "type": "string",
 62                    "description": "Absolute path to a Ruby file."
 63                },
 64                "cwd": {
 65                    "type": "string",
 66                    "description": "Directory to execute the program in",
 67                    "default": "${ZED_WORKTREE_ROOT}"
 68                },
 69                "args": {
 70                    "type": "array",
 71                    "description": "Command line arguments passed to the program",
 72                    "items": {
 73                        "type": "string"
 74                    },
 75                    "default": []
 76                },
 77                "env": {
 78                    "type": "object",
 79                    "description": "Additional environment variables to pass to the debugging (and debugged) process",
 80                    "default": {}
 81                },
 82            }
 83        })
 84    }
 85
 86    fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
 87        match zed_scenario.request {
 88            DebugRequest::Launch(launch) => {
 89                let config = RubyDebugConfig {
 90                    script_or_command: Some(launch.program),
 91                    script: None,
 92                    command: None,
 93                    args: launch.args,
 94                    env: launch.env,
 95                    cwd: launch.cwd.clone(),
 96                };
 97
 98                let config = serde_json::to_value(config)?;
 99
100                Ok(DebugScenario {
101                    adapter: zed_scenario.adapter,
102                    label: zed_scenario.label,
103                    config,
104                    tcp_connection: None,
105                    build: None,
106                })
107            }
108            DebugRequest::Attach(_) => {
109                anyhow::bail!("Attach requests are unsupported");
110            }
111        }
112    }
113
114    async fn get_binary(
115        &self,
116        delegate: &Arc<dyn DapDelegate>,
117        definition: &DebugTaskDefinition,
118        _user_installed_path: Option<PathBuf>,
119        _cx: &mut AsyncApp,
120    ) -> Result<DebugAdapterBinary> {
121        let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
122        let mut rdbg_path = adapter_path.join("rdbg");
123        if !delegate.fs().is_file(&rdbg_path).await {
124            match delegate.which("rdbg".as_ref()).await {
125                Some(path) => rdbg_path = path,
126                None => {
127                    delegate.output_to_console(
128                        "rdbg not found on path, trying `gem install debug`".to_string(),
129                    );
130                    let output = new_smol_command("gem")
131                        .arg("install")
132                        .arg("--no-document")
133                        .arg("--bindir")
134                        .arg(adapter_path)
135                        .arg("debug")
136                        .output()
137                        .await?;
138                    anyhow::ensure!(
139                        output.status.success(),
140                        "Failed to install rdbg:\n{}",
141                        String::from_utf8_lossy(&output.stderr).to_string()
142                    );
143                }
144            }
145        }
146
147        let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
148        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
149        let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?;
150
151        let mut arguments = vec![
152            "--open".to_string(),
153            format!("--port={}", port),
154            format!("--host={}", host),
155        ];
156
157        if let Some(script) = &ruby_config.script {
158            arguments.push(script.clone());
159        } else if let Some(command) = &ruby_config.command {
160            arguments.push("--command".to_string());
161            arguments.push(command.clone());
162        } else if let Some(command_or_script) = &ruby_config.script_or_command {
163            if delegate
164                .which(OsStr::new(&command_or_script))
165                .await
166                .is_some()
167            {
168                arguments.push("--command".to_string());
169            }
170            arguments.push(command_or_script.clone());
171        } else {
172            bail!("Ruby debug config must have 'script' or 'command' args");
173        }
174
175        arguments.extend(ruby_config.args);
176
177        let mut configuration = definition.config.clone();
178        if let Some(configuration) = configuration.as_object_mut() {
179            configuration
180                .entry("cwd")
181                .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
182        }
183
184        Ok(DebugAdapterBinary {
185            command: Some(rdbg_path.to_string_lossy().to_string()),
186            arguments,
187            connection: Some(dap::adapters::TcpArguments {
188                host,
189                port,
190                timeout,
191            }),
192            cwd: Some(
193                ruby_config
194                    .cwd
195                    .unwrap_or(delegate.worktree_root_path().to_owned()),
196            ),
197            envs: ruby_config.env.into_iter().collect(),
198            request_args: StartDebuggingRequestArguments {
199                request: self.request_kind(&definition.config)?,
200                configuration,
201            },
202        })
203    }
204}