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