1use anyhow::Result;
2use async_trait::async_trait;
3use dap::{
4 DebugRequest, StartDebuggingRequestArguments,
5 adapters::{
6 DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
7 },
8};
9use gpui::{AsyncApp, SharedString};
10use language::LanguageName;
11use serde_json::json;
12use std::path::PathBuf;
13use std::sync::Arc;
14use task::{DebugScenario, ZedDebugConfig};
15use util::command::new_smol_command;
16
17#[derive(Default)]
18pub(crate) struct RubyDebugAdapter;
19
20impl RubyDebugAdapter {
21 const ADAPTER_NAME: &'static str = "Ruby";
22}
23
24#[async_trait(?Send)]
25impl DebugAdapter for RubyDebugAdapter {
26 fn name(&self) -> DebugAdapterName {
27 DebugAdapterName(Self::ADAPTER_NAME.into())
28 }
29
30 fn adapter_language_name(&self) -> Option<LanguageName> {
31 Some(SharedString::new_static("Ruby").into())
32 }
33
34 async fn dap_schema(&self) -> serde_json::Value {
35 json!({
36 "oneOf": [
37 {
38 "allOf": [
39 {
40 "type": "object",
41 "required": ["request"],
42 "properties": {
43 "request": {
44 "type": "string",
45 "enum": ["launch"],
46 "description": "Request to launch a new process"
47 }
48 }
49 },
50 {
51 "type": "object",
52 "required": ["script"],
53 "properties": {
54 "command": {
55 "type": "string",
56 "description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
57 "default": "ruby"
58 },
59 "script": {
60 "type": "string",
61 "description": "Absolute path to a Ruby file."
62 },
63 "cwd": {
64 "type": "string",
65 "description": "Directory to execute the program in",
66 "default": "${ZED_WORKTREE_ROOT}"
67 },
68 "args": {
69 "type": "array",
70 "description": "Command line arguments passed to the program",
71 "items": {
72 "type": "string"
73 },
74 "default": []
75 },
76 "env": {
77 "type": "object",
78 "description": "Additional environment variables to pass to the debugging (and debugged) process",
79 "default": {}
80 },
81 "showProtocolLog": {
82 "type": "boolean",
83 "description": "Show a log of DAP requests, events, and responses",
84 "default": false
85 },
86 "useBundler": {
87 "type": "boolean",
88 "description": "Execute Ruby programs with `bundle exec` instead of directly",
89 "default": false
90 },
91 "bundlePath": {
92 "type": "string",
93 "description": "Location of the bundle executable"
94 },
95 "rdbgPath": {
96 "type": "string",
97 "description": "Location of the rdbg executable"
98 },
99 "askParameters": {
100 "type": "boolean",
101 "description": "Ask parameters at first."
102 },
103 "debugPort": {
104 "type": "string",
105 "description": "UNIX domain socket name or TPC/IP host:port"
106 },
107 "waitLaunchTime": {
108 "type": "number",
109 "description": "Wait time before connection in milliseconds"
110 },
111 "localfs": {
112 "type": "boolean",
113 "description": "true if the VSCode and debugger run on a same machine",
114 "default": false
115 },
116 "useTerminal": {
117 "type": "boolean",
118 "description": "Create a new terminal and then execute commands there",
119 "default": false
120 }
121 }
122 }
123 ]
124 },
125 {
126 "allOf": [
127 {
128 "type": "object",
129 "required": ["request"],
130 "properties": {
131 "request": {
132 "type": "string",
133 "enum": ["attach"],
134 "description": "Request to attach to an existing process"
135 }
136 }
137 },
138 {
139 "type": "object",
140 "properties": {
141 "rdbgPath": {
142 "type": "string",
143 "description": "Location of the rdbg executable"
144 },
145 "debugPort": {
146 "type": "string",
147 "description": "UNIX domain socket name or TPC/IP host:port"
148 },
149 "showProtocolLog": {
150 "type": "boolean",
151 "description": "Show a log of DAP requests, events, and responses",
152 "default": false
153 },
154 "localfs": {
155 "type": "boolean",
156 "description": "true if the VSCode and debugger run on a same machine",
157 "default": false
158 },
159 "localfsMap": {
160 "type": "string",
161 "description": "Specify pairs of remote root path and local root path like `/remote_dir:/local_dir`. You can specify multiple pairs like `/rem1:/loc1,/rem2:/loc2` by concatenating with `,`."
162 },
163 "env": {
164 "type": "object",
165 "description": "Additional environment variables to pass to the rdbg process",
166 "default": {}
167 }
168 }
169 }
170 ]
171 }
172 ]
173 })
174 }
175
176 fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
177 let mut config = serde_json::Map::new();
178
179 match &zed_scenario.request {
180 DebugRequest::Launch(launch) => {
181 config.insert("request".to_string(), json!("launch"));
182 config.insert("script".to_string(), json!(launch.program));
183 config.insert("command".to_string(), json!("ruby"));
184
185 if !launch.args.is_empty() {
186 config.insert("args".to_string(), json!(launch.args));
187 }
188
189 if !launch.env.is_empty() {
190 config.insert("env".to_string(), json!(launch.env));
191 }
192
193 if let Some(cwd) = &launch.cwd {
194 config.insert("cwd".to_string(), json!(cwd));
195 }
196
197 // Ruby stops on entry so there's no need to handle that case
198 }
199 DebugRequest::Attach(attach) => {
200 config.insert("request".to_string(), json!("attach"));
201
202 config.insert("processId".to_string(), json!(attach.process_id));
203 }
204 }
205
206 Ok(DebugScenario {
207 adapter: zed_scenario.adapter,
208 label: zed_scenario.label,
209 config: serde_json::Value::Object(config),
210 tcp_connection: None,
211 build: None,
212 })
213 }
214
215 async fn get_binary(
216 &self,
217 delegate: &Arc<dyn DapDelegate>,
218 definition: &DebugTaskDefinition,
219 _user_installed_path: Option<PathBuf>,
220 _cx: &mut AsyncApp,
221 ) -> Result<DebugAdapterBinary> {
222 let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
223 let mut rdbg_path = adapter_path.join("rdbg");
224 if !delegate.fs().is_file(&rdbg_path).await {
225 match delegate.which("rdbg".as_ref()).await {
226 Some(path) => rdbg_path = path,
227 None => {
228 delegate.output_to_console(
229 "rdbg not found on path, trying `gem install debug`".to_string(),
230 );
231 let output = new_smol_command("gem")
232 .arg("install")
233 .arg("--no-document")
234 .arg("--bindir")
235 .arg(adapter_path)
236 .arg("debug")
237 .output()
238 .await?;
239 anyhow::ensure!(
240 output.status.success(),
241 "Failed to install rdbg:\n{}",
242 String::from_utf8_lossy(&output.stderr).to_string()
243 );
244 }
245 }
246 }
247
248 let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
249 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
250
251 let arguments = vec![
252 "--open".to_string(),
253 format!("--port={}", port),
254 format!("--host={}", host),
255 ];
256
257 Ok(DebugAdapterBinary {
258 command: rdbg_path.to_string_lossy().to_string(),
259 arguments,
260 connection: Some(dap::adapters::TcpArguments {
261 host,
262 port,
263 timeout,
264 }),
265 cwd: None,
266 envs: std::collections::HashMap::default(),
267 request_args: StartDebuggingRequestArguments {
268 request: self.request_kind(&definition.config)?,
269 configuration: definition.config.clone(),
270 },
271 })
272 }
273}