Skip to content

Commit 4ad6185

Browse files
osiewiczvitallium
andauthored
Debugger prototype for Ruby (#96)
This is a prototype implementation of Ruby debugger implementation, based on existing impl in core Zed. Input is most welcome on what it looks like and all that jazz. This is based on unreleased version of zed-extension-api. --------- Co-authored-by: Vitaly Slobodin <vitaliy.slobodin@gmail.com>
1 parent 95ac4cd commit 4ad6185

File tree

6 files changed

+191
-1
lines changed

6 files changed

+191
-1
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ crate-type = ["cdylib"]
1111

1212
[dependencies]
1313
regex = "1.11.1"
14+
serde_json = "1.0"
15+
serde = {version = "1.0", features = ["derive"]}
1416
zed_extension_api = "0.6.0"

debug_adapter_schemas/rdbg.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"command": {
5+
"type": "string",
6+
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)"
7+
},
8+
"script": {
9+
"type": "string",
10+
"description": "Absolute path to a Ruby file."
11+
},
12+
"cwd": {
13+
"type": "string",
14+
"description": "Directory to execute the program in",
15+
"default": "${ZED_WORKTREE_ROOT}"
16+
},
17+
"args": {
18+
"type": "array",
19+
"description": "Command line arguments passed to the program",
20+
"items": {
21+
"type": "string"
22+
},
23+
"default": []
24+
},
25+
"env": {
26+
"type": "object",
27+
"description": "Additional environment variables to pass to the debugging (and debugged) process",
28+
"default": {}
29+
}
30+
}
31+
}

extension.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ schema_version = 1
66
authors = ["Vitaly Slobodin <vitaliy.slobodin@gmail.com>"]
77
repository = "https://github.com/zed-extensions/ruby"
88

9+
910
[language_servers.solargraph]
1011
name = "Solargraph"
1112
languages = ["Ruby"]
@@ -62,3 +63,5 @@ args = ["outdated", "--norc"]
6263
kind = "process:exec"
6364
command = "gem"
6465
args = ["update", "--norc", "*"]
66+
67+
[debug_adapters.RDBG]

languages/ruby/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ collapsed_placeholder = "# ..."
6161
tab_size = 2
6262
scope_opt_in_language_servers = ["tailwindcss-language-server"]
6363
word_characters = ["?", "!"]
64+
debuggers = ["RDBG"]
6465

6566
[overrides.string]
6667
completion_query_characters = ["-", "."]

src/ruby.rs

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@ mod command_executor;
33
mod gemset;
44
mod language_servers;
55

6+
use std::{collections::HashMap, path::Path};
7+
68
use language_servers::{LanguageServer, Rubocop, RubyLsp, Solargraph, Sorbet, Steep};
7-
use zed_extension_api::{self as zed};
9+
use serde::{Deserialize, Serialize};
10+
use zed_extension_api::{
11+
self as zed, resolve_tcp_template, Command, DebugAdapterBinary, DebugConfig, DebugRequest,
12+
DebugScenario, DebugTaskDefinition, StartDebuggingRequestArguments,
13+
StartDebuggingRequestArgumentsRequest, TcpArgumentsTemplate, Worktree,
14+
};
815

916
#[derive(Default)]
1017
struct RubyExtension {
@@ -15,6 +22,18 @@ struct RubyExtension {
1522
steep: Option<Steep>,
1623
}
1724

25+
#[derive(Serialize, Deserialize)]
26+
struct RubyDebugConfig {
27+
script_or_command: Option<String>,
28+
script: Option<String>,
29+
command: Option<String>,
30+
#[serde(default)]
31+
args: Vec<String>,
32+
#[serde(default)]
33+
env: HashMap<String, String>,
34+
cwd: Option<String>,
35+
}
36+
1837
impl zed::Extension for RubyExtension {
1938
fn new() -> Self {
2039
Self::default()
@@ -87,6 +106,138 @@ impl zed::Extension for RubyExtension {
87106
_ => None,
88107
}
89108
}
109+
110+
fn get_dap_binary(
111+
&mut self,
112+
adapter_name: String,
113+
config: DebugTaskDefinition,
114+
_: Option<String>,
115+
worktree: &Worktree,
116+
) -> Result<DebugAdapterBinary, String> {
117+
let mut rdbg_path = Path::new(&adapter_name)
118+
.join("rdbg")
119+
.to_string_lossy()
120+
.into_owned();
121+
122+
if worktree.which(&rdbg_path).is_none() {
123+
match worktree.which("rdbg".as_ref()) {
124+
Some(path) => rdbg_path = path,
125+
None => {
126+
let output = Command::new("gem")
127+
.arg("install")
128+
.arg("--no-document")
129+
.arg("--bindir")
130+
.arg(&adapter_name)
131+
.arg("debug")
132+
.output()?;
133+
if output.status.is_some_and(|status| status == 0) {
134+
return Err(format!(
135+
"Failed to install rdbg:\n{}",
136+
String::from_utf8_lossy(&output.stderr).into_owned()
137+
));
138+
}
139+
}
140+
}
141+
}
142+
143+
let tcp_connection = config.tcp_connection.unwrap_or(TcpArgumentsTemplate {
144+
port: None,
145+
host: None,
146+
timeout: None,
147+
});
148+
let connection = resolve_tcp_template(tcp_connection)?;
149+
let mut configuration: serde_json::Value = serde_json::from_str(&config.config)
150+
.map_err(|e| format!("`config` is not a valid JSON: {e}"))?;
151+
152+
let ruby_config: RubyDebugConfig = serde_json::from_value(configuration.clone())
153+
.map_err(|e| format!("`config` is not a valid rdbg config: {e}"))?;
154+
let mut arguments = vec![
155+
"--open".to_string(),
156+
format!("--port={}", connection.port),
157+
format!("--host={}", connection.host),
158+
];
159+
if let Some(script) = &ruby_config.script {
160+
arguments.push(script.clone());
161+
} else if let Some(command) = &ruby_config.command {
162+
arguments.push("--command".to_string());
163+
arguments.push(command.clone());
164+
} else if let Some(command_or_script) = &ruby_config.script_or_command {
165+
if worktree.which(command_or_script).is_some() {
166+
arguments.push("--command".to_string());
167+
}
168+
arguments.push(command_or_script.clone());
169+
} else {
170+
return Err("Ruby debug config must have 'script' or 'command' args".into());
171+
}
172+
if let Some(configuration) = configuration.as_object_mut() {
173+
configuration
174+
.entry("cwd")
175+
.or_insert_with(|| worktree.root_path().into());
176+
}
177+
arguments.extend(ruby_config.args);
178+
179+
Ok(DebugAdapterBinary {
180+
command: Some(rdbg_path.to_string()),
181+
arguments,
182+
connection: Some(connection),
183+
cwd: ruby_config.cwd,
184+
envs: ruby_config.env.into_iter().collect(),
185+
request_args: StartDebuggingRequestArguments {
186+
configuration: configuration.to_string(),
187+
request: StartDebuggingRequestArgumentsRequest::Launch,
188+
},
189+
})
190+
}
191+
192+
fn dap_request_kind(
193+
&mut self,
194+
_: String,
195+
value: serde_json::Value,
196+
) -> zed_extension_api::Result<StartDebuggingRequestArgumentsRequest, String> {
197+
value
198+
.get("request")
199+
.and_then(|request| {
200+
request.as_str().and_then(|s| match s {
201+
"launch" => Some(StartDebuggingRequestArgumentsRequest::Launch),
202+
"attach" => Some(StartDebuggingRequestArgumentsRequest::Attach),
203+
_ => None,
204+
})
205+
})
206+
.ok_or_else(|| {
207+
"Invalid request, expected `request` to be either `launch` or `attach`".into()
208+
})
209+
}
210+
211+
fn dap_config_to_scenario(
212+
&mut self,
213+
zed_scenario: DebugConfig,
214+
) -> Result<DebugScenario, String> {
215+
match zed_scenario.request {
216+
DebugRequest::Launch(launch) => {
217+
let config = RubyDebugConfig {
218+
script_or_command: Some(launch.program),
219+
script: None,
220+
command: None,
221+
args: launch.args,
222+
env: launch.envs.into_iter().collect(),
223+
cwd: launch.cwd.clone(),
224+
};
225+
226+
let config = serde_json::to_value(config)
227+
.map_err(|e| e.to_string())?
228+
.to_string();
229+
230+
Ok(DebugScenario {
231+
adapter: zed_scenario.adapter,
232+
label: zed_scenario.label,
233+
config,
234+
tcp_connection: None,
235+
build: None,
236+
})
237+
}
238+
DebugRequest::Attach(_) => Err("Attach requests are unsupported".into()),
239+
}
240+
}
90241
}
91242

92243
zed_extension_api::register_extension!(RubyExtension);

0 commit comments

Comments
 (0)