debug_format.rs

  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}