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