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