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::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}