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        _cx: &mut AsyncApp,
123    ) -> Result<DebugAdapterBinary> {
124        let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
125        let mut rdbg_path = adapter_path.join("rdbg");
126        if !delegate.fs().is_file(&rdbg_path).await {
127            match delegate.which("rdbg".as_ref()).await {
128                Some(path) => rdbg_path = path,
129                None => {
130                    delegate.output_to_console(
131                        "rdbg not found on path, trying `gem install debug`".to_string(),
132                    );
133                    let output = new_smol_command("gem")
134                        .arg("install")
135                        .arg("--no-document")
136                        .arg("--bindir")
137                        .arg(adapter_path)
138                        .arg("debug")
139                        .output()
140                        .await?;
141                    anyhow::ensure!(
142                        output.status.success(),
143                        "Failed to install rdbg:\n{}",
144                        String::from_utf8_lossy(&output.stderr).to_string()
145                    );
146                }
147            }
148        }
149
150        let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
151        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
152        let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?;
153
154        let mut arguments = vec![
155            "--open".to_string(),
156            format!("--port={}", port),
157            format!("--host={}", host),
158        ];
159
160        if let Some(script) = &ruby_config.script {
161            arguments.push(script.clone());
162        } else if let Some(command) = &ruby_config.command {
163            arguments.push("--command".to_string());
164            arguments.push(command.clone());
165        } else if let Some(command_or_script) = &ruby_config.script_or_command {
166            if delegate
167                .which(OsStr::new(&command_or_script))
168                .await
169                .is_some()
170            {
171                arguments.push("--command".to_string());
172            }
173            arguments.push(command_or_script.clone());
174        } else {
175            bail!("Ruby debug config must have 'script' or 'command' args");
176        }
177
178        arguments.extend(ruby_config.args);
179
180        let mut configuration = definition.config.clone();
181        if let Some(configuration) = configuration.as_object_mut() {
182            configuration
183                .entry("cwd")
184                .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
185        }
186
187        Ok(DebugAdapterBinary {
188            command: Some(rdbg_path.to_string_lossy().to_string()),
189            arguments,
190            connection: Some(dap::adapters::TcpArguments {
191                host,
192                port,
193                timeout,
194            }),
195            cwd: Some(
196                ruby_config
197                    .cwd
198                    .unwrap_or(delegate.worktree_root_path().to_owned()),
199            ),
200            envs: ruby_config.env.into_iter().collect(),
201            request_args: StartDebuggingRequestArguments {
202                request: self.request_kind(&definition.config).await?,
203                configuration,
204            },
205        })
206    }
207}