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 async 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 Ok(DebugAdapterBinary {
178 command: Some(rdbg_path.to_string_lossy().to_string()),
179 arguments,
180 connection: Some(dap::adapters::TcpArguments {
181 host,
182 port,
183 timeout,
184 }),
185 cwd: Some(
186 ruby_config
187 .cwd
188 .unwrap_or(delegate.worktree_root_path().to_owned()),
189 ),
190 envs: ruby_config.env.into_iter().collect(),
191 request_args: StartDebuggingRequestArguments {
192 request: self.request_kind(&definition.config)?,
193 configuration: definition.config.clone(),
194 },
195 })
196 }
197}