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    async fn request_kind(
 49        &self,
 50        _: &serde_json::Value,
 51    ) -> Result<StartDebuggingRequestArgumentsRequest> {
 52        Ok(StartDebuggingRequestArgumentsRequest::Launch)
 53    }
 54
 55    fn dap_schema(&self) -> serde_json::Value {
 56        json!({
 57            "type": "object",
 58            "properties": {
 59                "command": {
 60                    "type": "string",
 61                    "description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
 62                },
 63                "script": {
 64                    "type": "string",
 65                    "description": "Absolute path to a Ruby file."
 66                },
 67                "cwd": {
 68                    "type": "string",
 69                    "description": "Directory to execute the program in",
 70                    "default": "${ZED_WORKTREE_ROOT}"
 71                },
 72                "args": {
 73                    "type": "array",
 74                    "description": "Command line arguments passed to the program",
 75                    "items": {
 76                        "type": "string"
 77                    },
 78                    "default": []
 79                },
 80                "env": {
 81                    "type": "object",
 82                    "description": "Additional environment variables to pass to the debugging (and debugged) process",
 83                    "default": {}
 84                },
 85            }
 86        })
 87    }
 88
 89    async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
 90        match zed_scenario.request {
 91            DebugRequest::Launch(launch) => {
 92                let config = RubyDebugConfig {
 93                    script_or_command: Some(launch.program),
 94                    script: None,
 95                    command: None,
 96                    args: launch.args,
 97                    env: launch.env,
 98                    cwd: launch.cwd.clone(),
 99                };
100
101                let config = serde_json::to_value(config)?;
102
103                Ok(DebugScenario {
104                    adapter: zed_scenario.adapter,
105                    label: zed_scenario.label,
106                    config,
107                    tcp_connection: None,
108                    build: None,
109                })
110            }
111            DebugRequest::Attach(_) => {
112                anyhow::bail!("Attach requests are unsupported");
113            }
114        }
115    }
116
117    async fn get_binary(
118        &self,
119        delegate: &Arc<dyn DapDelegate>,
120        definition: &DebugTaskDefinition,
121        _user_installed_path: Option<PathBuf>,
122        _user_args: Option<Vec<String>>,
123        _cx: &mut AsyncApp,
124    ) -> Result<DebugAdapterBinary> {
125        let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
126        let mut rdbg_path = adapter_path.join("rdbg");
127        if !delegate.fs().is_file(&rdbg_path).await {
128            match delegate.which("rdbg".as_ref()).await {
129                Some(path) => rdbg_path = path,
130                None => {
131                    delegate.output_to_console(
132                        "rdbg not found on path, trying `gem install debug`".to_string(),
133                    );
134                    let output = new_smol_command("gem")
135                        .arg("install")
136                        .arg("--no-document")
137                        .arg("--bindir")
138                        .arg(adapter_path)
139                        .arg("debug")
140                        .output()
141                        .await?;
142                    anyhow::ensure!(
143                        output.status.success(),
144                        "Failed to install rdbg:\n{}",
145                        String::from_utf8_lossy(&output.stderr).to_string()
146                    );
147                }
148            }
149        }
150
151        let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
152        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
153        let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?;
154
155        let mut arguments = vec![
156            "--open".to_string(),
157            format!("--port={}", port),
158            format!("--host={}", host),
159        ];
160
161        if let Some(script) = &ruby_config.script {
162            arguments.push(script.clone());
163        } else if let Some(command) = &ruby_config.command {
164            arguments.push("--command".to_string());
165            arguments.push(command.clone());
166        } else if let Some(command_or_script) = &ruby_config.script_or_command {
167            if delegate
168                .which(OsStr::new(&command_or_script))
169                .await
170                .is_some()
171            {
172                arguments.push("--command".to_string());
173            }
174            arguments.push(command_or_script.clone());
175        } else {
176            bail!("Ruby debug config must have 'script' or 'command' args");
177        }
178
179        arguments.extend(ruby_config.args);
180
181        let mut configuration = definition.config.clone();
182        if let Some(configuration) = configuration.as_object_mut() {
183            configuration
184                .entry("cwd")
185                .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
186        }
187
188        Ok(DebugAdapterBinary {
189            command: Some(rdbg_path.to_string_lossy().to_string()),
190            arguments,
191            connection: Some(dap::adapters::TcpArguments {
192                host,
193                port,
194                timeout,
195            }),
196            cwd: Some(
197                ruby_config
198                    .cwd
199                    .unwrap_or(delegate.worktree_root_path().to_owned()),
200            ),
201            envs: ruby_config.env.into_iter().collect(),
202            request_args: StartDebuggingRequestArguments {
203                request: self.request_kind(&definition.config).await?,
204                configuration,
205            },
206        })
207    }
208}