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