debug_format.rs

  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}