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}