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 let adapter_names = adapter_schemas
356 .iter()
357 .map(|(adapter_name, _)| adapter_name.clone())
358 .collect::<Vec<_>>();
359 let adapter_conditions = adapter_schemas
360 .iter()
361 .map(|(adapter_name, schema)| {
362 add_new_subschema(
363 &mut generator,
364 &format!("{adapter_name}DebugSettings"),
365 serde_json::json!({
366 "if": {
367 "properties": {
368 "adapter": { "const": adapter_name }
369 }
370 },
371 "then": schema
372 }),
373 )
374 })
375 .collect::<Vec<_>>();
376
377 let build_task_schema = BuildTaskDefinition::json_schema(&mut generator).to_value();
378 let build_task_definition_ref = add_new_subschema(
379 &mut generator,
380 BuildTaskDefinition::schema_name().as_ref(),
381 build_task_schema,
382 );
383 let tcp_connection_schema = TcpArgumentsTemplate::json_schema(&mut generator).to_value();
384 let tcp_connection_definition_ref = add_new_subschema(
385 &mut generator,
386 TcpArgumentsTemplate::schema_name().as_ref(),
387 tcp_connection_schema,
388 );
389
390 let meta_schema = generator
391 .settings()
392 .meta_schema
393 .as_ref()
394 .expect("meta_schema should be present in schemars settings")
395 .to_string();
396
397 serde_json::json!({
398 "$schema": meta_schema,
399 "title": "Debug Configurations",
400 "description": "Configuration for debug scenarios",
401 "type": "array",
402 "items": {
403 "type": "object",
404 "required": ["adapter", "label"],
405 "unevaluatedProperties": false,
406 "properties": {
407 "adapter": {
408 "enum": adapter_names,
409 "description": "The name of the debug adapter"
410 },
411 "label": {
412 "type": "string",
413 "description": "The name of the debug configuration"
414 },
415 "build": build_task_definition_ref,
416 "tcp_connection": tcp_connection_definition_ref,
417 },
418 "allOf": adapter_conditions
419 },
420 "$defs": generator.take_definitions(true),
421 })
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use crate::DebugScenario;
428 use serde_json::json;
429
430 #[test]
431 fn test_just_build_args() {
432 let json = r#"{
433 "label": "Build & debug rust",
434 "adapter": "CodeLLDB",
435 "build": {
436 "command": "rust",
437 "args": ["build"]
438 }
439 }"#;
440
441 let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
442 assert!(deserialized.build.is_some());
443 match deserialized.build.as_ref().unwrap() {
444 crate::BuildTaskDefinition::Template { task_template, .. } => {
445 assert_eq!("debug-build", task_template.label);
446 assert_eq!("rust", task_template.command);
447 assert_eq!(vec!["build"], task_template.args);
448 }
449 _ => panic!("Expected Template variant"),
450 }
451 assert_eq!(json!({}), deserialized.config);
452 assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
453 assert_eq!("Build & debug rust", deserialized.label.as_ref());
454 }
455
456 #[test]
457 fn test_empty_scenario_has_none_request() {
458 let json = r#"{
459 "label": "Build & debug rust",
460 "build": "rust",
461 "adapter": "CodeLLDB"
462 }"#;
463
464 let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
465
466 assert_eq!(json!({}), deserialized.config);
467 assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
468 assert_eq!("Build & debug rust", deserialized.label.as_ref());
469 }
470
471 #[test]
472 fn test_launch_scenario_deserialization() {
473 let json = r#"{
474 "label": "Launch program",
475 "adapter": "CodeLLDB",
476 "request": "launch",
477 "program": "target/debug/myapp",
478 "args": ["--test"]
479 }"#;
480
481 let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
482
483 assert_eq!(
484 json!({ "request": "launch", "program": "target/debug/myapp", "args": ["--test"] }),
485 deserialized.config
486 );
487 assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
488 assert_eq!("Launch program", deserialized.label.as_ref());
489 }
490
491 #[test]
492 fn test_attach_scenario_deserialization() {
493 let json = r#"{
494 "label": "Attach to process",
495 "adapter": "CodeLLDB",
496 "process_id": 1234,
497 "request": "attach"
498 }"#;
499
500 let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
501
502 assert_eq!(
503 json!({ "request": "attach", "process_id": 1234 }),
504 deserialized.config
505 );
506 assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
507 assert_eq!("Attach to process", deserialized.label.as_ref());
508 }
509
510 #[test]
511 fn test_build_task_definition_without_label() {
512 use crate::BuildTaskDefinition;
513
514 let json = r#""my_build_task""#;
515 let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
516 match deserialized {
517 BuildTaskDefinition::ByName(name) => assert_eq!("my_build_task", name.as_ref()),
518 _ => panic!("Expected ByName variant"),
519 }
520
521 let json = r#"{
522 "command": "cargo",
523 "args": ["build", "--release"]
524 }"#;
525 let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
526 match deserialized {
527 BuildTaskDefinition::Template { task_template, .. } => {
528 assert_eq!("debug-build", task_template.label);
529 assert_eq!("cargo", task_template.command);
530 assert_eq!(vec!["build", "--release"], task_template.args);
531 }
532 _ => panic!("Expected Template variant"),
533 }
534
535 let json = r#"{
536 "label": "Build Release",
537 "command": "cargo",
538 "args": ["build", "--release"]
539 }"#;
540 let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
541 match deserialized {
542 BuildTaskDefinition::Template { task_template, .. } => {
543 assert_eq!("Build Release", task_template.label);
544 assert_eq!("cargo", task_template.command);
545 assert_eq!(vec!["build", "--release"], task_template.args);
546 }
547 _ => panic!("Expected Template variant"),
548 }
549 }
550}