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}