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