1use anyhow::{Context as _, Result};
2use collections::FxHashMap;
3use gpui::SharedString;
4use log as _;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use std::{borrow::Cow, net::Ipv4Addr};
9use util::schemars::add_new_subschema;
10
11use crate::TaskTemplate;
12
13// FIXME
14//"tcp_connection": {
15// "type": "object",
16// "description": "Optional TCP connection information for connecting to an already running debug adapter",
17// "properties": {
18// "port": {
19// "type": "integer",
20// "description": "The port that the debug adapter is listening on (default: auto-find open port)"
21// },
22// "host": {
23// "type": "string",
24// "pattern": "^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$",
25// "description": "The host that the debug adapter is listening to (default: 127.0.0.1)"
26// },
27// "timeout": {
28// "type": "integer",
29// "description": "The max amount of time in milliseconds to connect to a tcp DAP before returning an error (default: 2000ms)"
30// }
31// }
32//}
33/// Represents the host information of the debug adapter
34#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
35pub struct TcpArgumentsTemplate {
36 /// The port that the debug adapter is listening on
37 ///
38 /// Default: We will try to find an open port
39 pub port: Option<u16>,
40 /// The host that the debug adapter is listening too
41 ///
42 /// Default: 127.0.0.1
43 pub host: Option<Ipv4Addr>,
44 /// The max amount of time in milliseconds to connect to a tcp DAP before returning an error
45 ///
46 /// Default: 2000ms
47 pub timeout: Option<u64>,
48}
49
50impl TcpArgumentsTemplate {
51 /// Get the host or fallback to the default host
52 pub fn host(&self) -> Ipv4Addr {
53 self.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1))
54 }
55
56 pub fn from_proto(proto: proto::TcpHost) -> Result<Self> {
57 Ok(Self {
58 port: proto.port.map(|p| p.try_into()).transpose()?,
59 host: proto.host.map(|h| h.parse()).transpose()?,
60 timeout: proto.timeout,
61 })
62 }
63
64 pub fn to_proto(&self) -> proto::TcpHost {
65 proto::TcpHost {
66 port: self.port.map(|p| p.into()),
67 host: self.host.map(|h| h.to_string()),
68 timeout: self.timeout,
69 }
70 }
71}
72
73/// Represents the attach request information of the debug adapter
74#[derive(Default, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
75pub struct AttachRequest {
76 /// The processId to attach to, if left empty we will show a process picker
77 pub process_id: Option<u32>,
78}
79
80impl<'de> Deserialize<'de> for AttachRequest {
81 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
82 where
83 D: serde::Deserializer<'de>,
84 {
85 #[derive(Deserialize)]
86 struct Helper {
87 process_id: Option<u32>,
88 }
89
90 let helper = Helper::deserialize(deserializer)?;
91
92 // Skip creating an AttachRequest if process_id is None
93 if helper.process_id.is_none() {
94 return Err(serde::de::Error::custom("process_id is required"));
95 }
96
97 Ok(AttachRequest {
98 process_id: helper.process_id,
99 })
100 }
101}
102
103/// Represents the launch request information of the debug adapter
104#[derive(Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, Clone, Debug)]
105pub struct LaunchRequest {
106 /// The program that you trying to debug
107 pub program: String,
108 /// The current working directory of your project
109 #[serde(default)]
110 pub cwd: Option<PathBuf>,
111 /// Arguments to pass to a debuggee
112 #[serde(default)]
113 pub args: Vec<String>,
114 #[serde(default)]
115 pub env: FxHashMap<String, String>,
116}
117
118impl LaunchRequest {
119 pub fn env_json(&self) -> serde_json::Value {
120 serde_json::Value::Object(
121 self.env
122 .iter()
123 .map(|(k, v)| (k.clone(), v.to_owned().into()))
124 .collect::<serde_json::Map<String, serde_json::Value>>(),
125 )
126 }
127}
128
129/// Represents the type that will determine which request to call on the debug adapter
130#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
131#[serde(rename_all = "lowercase", tag = "request")]
132pub enum DebugRequest {
133 /// Call the `launch` request on the debug adapter
134 Launch(LaunchRequest),
135 /// Call the `attach` request on the debug adapter
136 Attach(AttachRequest),
137}
138
139impl DebugRequest {
140 pub fn to_proto(&self) -> proto::DebugRequest {
141 match self {
142 DebugRequest::Launch(launch_request) => proto::DebugRequest {
143 request: Some(proto::debug_request::Request::DebugLaunchRequest(
144 proto::DebugLaunchRequest {
145 program: launch_request.program.clone(),
146 cwd: launch_request
147 .cwd
148 .as_ref()
149 .map(|cwd| cwd.to_string_lossy().into_owned()),
150 args: launch_request.args.clone(),
151 env: launch_request
152 .env
153 .iter()
154 .map(|(k, v)| (k.clone(), v.clone()))
155 .collect(),
156 },
157 )),
158 },
159 DebugRequest::Attach(attach_request) => proto::DebugRequest {
160 request: Some(proto::debug_request::Request::DebugAttachRequest(
161 proto::DebugAttachRequest {
162 process_id: attach_request
163 .process_id
164 .expect("The process ID to be already filled out."),
165 },
166 )),
167 },
168 }
169 }
170
171 pub fn from_proto(val: proto::DebugRequest) -> Result<DebugRequest> {
172 let request = val.request.context("Missing debug request")?;
173 match request {
174 proto::debug_request::Request::DebugLaunchRequest(proto::DebugLaunchRequest {
175 program,
176 cwd,
177 args,
178 env,
179 }) => Ok(DebugRequest::Launch(LaunchRequest {
180 program,
181 cwd: cwd.map(From::from),
182 args,
183 env: env.into_iter().collect(),
184 })),
185
186 proto::debug_request::Request::DebugAttachRequest(proto::DebugAttachRequest {
187 process_id,
188 }) => Ok(DebugRequest::Attach(AttachRequest {
189 process_id: Some(process_id),
190 })),
191 }
192 }
193}
194
195impl From<LaunchRequest> for DebugRequest {
196 fn from(launch_config: LaunchRequest) -> Self {
197 DebugRequest::Launch(launch_config)
198 }
199}
200
201impl From<AttachRequest> for DebugRequest {
202 fn from(attach_config: AttachRequest) -> Self {
203 DebugRequest::Attach(attach_config)
204 }
205}
206
207#[derive(Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
208#[serde(untagged)]
209pub enum BuildTaskDefinition {
210 ByName(SharedString),
211 Template {
212 #[serde(flatten)]
213 task_template: TaskTemplate,
214 #[serde(skip)]
215 locator_name: Option<SharedString>,
216 },
217}
218
219impl<'de> Deserialize<'de> for BuildTaskDefinition {
220 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
221 where
222 D: serde::Deserializer<'de>,
223 {
224 #[derive(Deserialize)]
225 struct TemplateHelper {
226 #[serde(default)]
227 label: Option<String>,
228 #[serde(flatten)]
229 rest: serde_json::Value,
230 }
231
232 let value = serde_json::Value::deserialize(deserializer)?;
233
234 if let Ok(name) = serde_json::from_value::<SharedString>(value.clone()) {
235 return Ok(BuildTaskDefinition::ByName(name));
236 }
237
238 let helper: TemplateHelper =
239 serde_json::from_value(value).map_err(serde::de::Error::custom)?;
240
241 let mut template_value = helper.rest;
242 if let serde_json::Value::Object(ref mut map) = template_value {
243 map.insert(
244 "label".to_string(),
245 serde_json::to_value(helper.label.unwrap_or_else(|| "debug-build".to_owned()))
246 .map_err(serde::de::Error::custom)?,
247 );
248 }
249
250 let task_template: TaskTemplate =
251 serde_json::from_value(template_value).map_err(serde::de::Error::custom)?;
252
253 Ok(BuildTaskDefinition::Template {
254 task_template,
255 locator_name: None,
256 })
257 }
258}
259
260#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
261pub enum Request {
262 Launch,
263 Attach,
264}
265
266/// This struct represent a user created debug task from the new process modal
267#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
268#[serde(rename_all = "snake_case")]
269pub struct ZedDebugConfig {
270 /// Name of the debug task
271 pub label: SharedString,
272 /// The debug adapter to use
273 pub adapter: SharedString,
274 #[serde(flatten)]
275 pub request: DebugRequest,
276 /// Whether to tell the debug adapter to stop on entry
277 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub stop_on_entry: Option<bool>,
279}
280
281/// This struct represent a user created debug task
282#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
283#[serde(rename_all = "snake_case")]
284pub struct DebugScenario {
285 pub adapter: SharedString,
286 /// Name of the debug task
287 pub label: SharedString,
288 /// A task to run prior to spawning the debuggee.
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub build: Option<BuildTaskDefinition>,
291 /// The main arguments to be sent to the debug adapter
292 #[serde(default, flatten)]
293 pub config: serde_json::Value,
294 /// Optional TCP connection information
295 ///
296 /// If provided, this will be used to connect to the debug adapter instead of
297 /// spawning a new process. This is useful for connecting to a debug adapter
298 /// that is already running or is started by another process.
299 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub tcp_connection: Option<TcpArgumentsTemplate>,
301}
302
303/// A group of Debug Tasks defined in a JSON file.
304#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
305#[serde(transparent)]
306pub struct DebugTaskFile(pub Vec<DebugScenario>);
307
308impl DebugTaskFile {
309 pub fn generate_json_schema(
310 adapter_schemas: Vec<(SharedString, Cow<'static, serde_json::Value>)>,
311 ) -> serde_json::Value {
312 let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator();
313
314 // FIXME what is this doing
315 // if let Some(template_object) = build_task_schema
316 // .get_mut("anyOf")
317 // .and_then(|array| array.as_array_mut())
318 // .and_then(|array| array.get_mut(1))
319 // {
320 // if let Some(properties) = template_object
321 // .get_mut("properties")
322 // .and_then(|value| value.as_object_mut())
323 // {
324 // if properties.remove("label").is_none() {
325 // debug_panic!(
326 // "Generated TaskTemplate json schema did not have expected 'label' field. \
327 // Schema of 2nd alternative is: {template_object:?}"
328 // );
329 // }
330 // }
331
332 // if let Some(arr) = template_object
333 // .get_mut("required")
334 // .and_then(|array| array.as_array_mut())
335 // {
336 // arr.retain(|v| v.as_str() != Some("label"));
337 // }
338 // } else {
339 // debug_panic!(
340 // "Generated TaskTemplate json schema did not match expectations. \
341 // Schema is: {build_task_schema:?}"
342 // );
343 // }
344
345 let adapter_names = adapter_schemas
346 .iter()
347 .map(|(adapter_name, _)| adapter_name.clone())
348 .collect::<Vec<_>>();
349 let adapter_conditions = adapter_schemas
350 .iter()
351 .map(|(adapter_name, schema)| {
352 add_new_subschema(
353 &mut generator,
354 &format!("{adapter_name}DebugSettings"),
355 serde_json::json!({
356 "if": {
357 "properties": {
358 "adapter": { "const": adapter_name }
359 }
360 },
361 "then": schema
362 }),
363 )
364 })
365 .collect::<Vec<_>>();
366
367 let build_task_schema = BuildTaskDefinition::json_schema(&mut generator).to_value();
368 let build_task_definition_ref = add_new_subschema(
369 &mut generator,
370 BuildTaskDefinition::schema_name().as_ref(),
371 build_task_schema,
372 );
373 let tcp_connection_schema = TcpArgumentsTemplate::json_schema(&mut generator).to_value();
374 let tcp_connection_definition_ref = add_new_subschema(
375 &mut generator,
376 TcpArgumentsTemplate::schema_name().as_ref(),
377 tcp_connection_schema,
378 );
379
380 let meta_schema = generator
381 .settings()
382 .meta_schema
383 .as_ref()
384 .expect("meta_schema should be present in schemars settings")
385 .to_string();
386
387 serde_json::json!({
388 "$schema": meta_schema,
389 "title": "Debug Configurations",
390 "description": "Configuration for debug scenarios",
391 "type": "array",
392 "items": {
393 "type": "object",
394 "required": ["adapter", "label"],
395 "unevaluatedProperties": false,
396 "properties": {
397 "adapter": {
398 "enum": adapter_names,
399 "description": "The name of the debug adapter"
400 },
401 "label": {
402 "type": "string",
403 "description": "The name of the debug configuration"
404 },
405 "build": build_task_definition_ref,
406 "tcp_connection": tcp_connection_definition_ref,
407 },
408 "allOf": adapter_conditions
409 },
410 "$defs": generator.take_definitions(true),
411 })
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use crate::DebugScenario;
418 use serde_json::json;
419
420 #[test]
421 fn test_just_build_args() {
422 let json = r#"{
423 "label": "Build & debug rust",
424 "adapter": "CodeLLDB",
425 "build": {
426 "command": "rust",
427 "args": ["build"]
428 }
429 }"#;
430
431 let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
432 assert!(deserialized.build.is_some());
433 match deserialized.build.as_ref().unwrap() {
434 crate::BuildTaskDefinition::Template { task_template, .. } => {
435 assert_eq!("debug-build", task_template.label);
436 assert_eq!("rust", task_template.command);
437 assert_eq!(vec!["build"], task_template.args);
438 }
439 _ => panic!("Expected Template variant"),
440 }
441 assert_eq!(json!({}), deserialized.config);
442 assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
443 assert_eq!("Build & debug rust", deserialized.label.as_ref());
444 }
445
446 #[test]
447 fn test_empty_scenario_has_none_request() {
448 let json = r#"{
449 "label": "Build & debug rust",
450 "build": "rust",
451 "adapter": "CodeLLDB"
452 }"#;
453
454 let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
455
456 assert_eq!(json!({}), deserialized.config);
457 assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
458 assert_eq!("Build & debug rust", deserialized.label.as_ref());
459 }
460
461 #[test]
462 fn test_launch_scenario_deserialization() {
463 let json = r#"{
464 "label": "Launch program",
465 "adapter": "CodeLLDB",
466 "request": "launch",
467 "program": "target/debug/myapp",
468 "args": ["--test"]
469 }"#;
470
471 let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
472
473 assert_eq!(
474 json!({ "request": "launch", "program": "target/debug/myapp", "args": ["--test"] }),
475 deserialized.config
476 );
477 assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
478 assert_eq!("Launch program", deserialized.label.as_ref());
479 }
480
481 #[test]
482 fn test_attach_scenario_deserialization() {
483 let json = r#"{
484 "label": "Attach to process",
485 "adapter": "CodeLLDB",
486 "process_id": 1234,
487 "request": "attach"
488 }"#;
489
490 let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
491
492 assert_eq!(
493 json!({ "request": "attach", "process_id": 1234 }),
494 deserialized.config
495 );
496 assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
497 assert_eq!("Attach to process", deserialized.label.as_ref());
498 }
499
500 #[test]
501 fn test_build_task_definition_without_label() {
502 use crate::BuildTaskDefinition;
503
504 let json = r#""my_build_task""#;
505 let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
506 match deserialized {
507 BuildTaskDefinition::ByName(name) => assert_eq!("my_build_task", name.as_ref()),
508 _ => panic!("Expected ByName variant"),
509 }
510
511 let json = r#"{
512 "command": "cargo",
513 "args": ["build", "--release"]
514 }"#;
515 let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
516 match deserialized {
517 BuildTaskDefinition::Template { task_template, .. } => {
518 assert_eq!("debug-build", task_template.label);
519 assert_eq!("cargo", task_template.command);
520 assert_eq!(vec!["build", "--release"], task_template.args);
521 }
522 _ => panic!("Expected Template variant"),
523 }
524
525 let json = r#"{
526 "label": "Build Release",
527 "command": "cargo",
528 "args": ["build", "--release"]
529 }"#;
530 let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
531 match deserialized {
532 BuildTaskDefinition::Template { task_template, .. } => {
533 assert_eq!("Build Release", task_template.label);
534 assert_eq!("cargo", task_template.command);
535 assert_eq!(vec!["build", "--release"], task_template.args);
536 }
537 _ => panic!("Expected Template variant"),
538 }
539 }
540}