debug_format.rs

  1use anyhow::Result;
  2use schemars::{JsonSchema, r#gen::SchemaSettings};
  3use serde::{Deserialize, Serialize};
  4use std::path::PathBuf;
  5use std::{net::Ipv4Addr, path::Path};
  6
  7use crate::{TaskTemplate, TaskType, task_template::DebugArgs};
  8
  9/// Represents the host information of the debug adapter
 10#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
 11pub struct TcpArgumentsTemplate {
 12    /// The port that the debug adapter is listening on
 13    ///
 14    /// Default: We will try to find an open port
 15    pub port: Option<u16>,
 16    /// The host that the debug adapter is listening too
 17    ///
 18    /// Default: 127.0.0.1
 19    pub host: Option<Ipv4Addr>,
 20    /// The max amount of time in milliseconds to connect to a tcp DAP before returning an error
 21    ///
 22    /// Default: 2000ms
 23    pub timeout: Option<u64>,
 24}
 25
 26impl TcpArgumentsTemplate {
 27    /// Get the host or fallback to the default host
 28    pub fn host(&self) -> Ipv4Addr {
 29        self.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1))
 30    }
 31
 32    pub fn from_proto(proto: proto::TcpHost) -> Result<Self> {
 33        Ok(Self {
 34            port: proto.port.map(|p| p.try_into()).transpose()?,
 35            host: proto.host.map(|h| h.parse()).transpose()?,
 36            timeout: proto.timeout,
 37        })
 38    }
 39
 40    pub fn to_proto(&self) -> proto::TcpHost {
 41        proto::TcpHost {
 42            port: self.port.map(|p| p.into()),
 43            host: self.host.map(|h| h.to_string()),
 44            timeout: self.timeout,
 45        }
 46    }
 47}
 48
 49/// Represents the attach request information of the debug adapter
 50#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
 51pub struct AttachRequest {
 52    /// The processId to attach to, if left empty we will show a process picker
 53    pub process_id: Option<u32>,
 54}
 55
 56/// Represents the launch request information of the debug adapter
 57#[derive(Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, Clone, Debug)]
 58pub struct LaunchRequest {
 59    /// The program that you trying to debug
 60    pub program: String,
 61    /// The current working directory of your project
 62    pub cwd: Option<PathBuf>,
 63    /// Arguments to pass to a debuggee
 64    #[serde(default)]
 65    pub args: Vec<String>,
 66}
 67
 68/// Represents the type that will determine which request to call on the debug adapter
 69#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
 70#[serde(rename_all = "lowercase", untagged)]
 71pub enum DebugRequest {
 72    /// Call the `launch` request on the debug adapter
 73    Launch(LaunchRequest),
 74    /// Call the `attach` request on the debug adapter
 75    Attach(AttachRequest),
 76}
 77
 78impl From<LaunchRequest> for DebugRequest {
 79    fn from(launch_config: LaunchRequest) -> Self {
 80        DebugRequest::Launch(launch_config)
 81    }
 82}
 83
 84impl From<AttachRequest> for DebugRequest {
 85    fn from(attach_config: AttachRequest) -> Self {
 86        DebugRequest::Attach(attach_config)
 87    }
 88}
 89
 90impl TryFrom<TaskTemplate> for DebugTaskTemplate {
 91    type Error = ();
 92
 93    fn try_from(value: TaskTemplate) -> Result<Self, Self::Error> {
 94        let TaskType::Debug(debug_args) = value.task_type else {
 95            return Err(());
 96        };
 97
 98        let request = match debug_args.request {
 99            crate::DebugArgsRequest::Launch => DebugRequest::Launch(LaunchRequest {
100                program: value.command,
101                cwd: value.cwd.map(PathBuf::from),
102                args: value.args,
103            }),
104            crate::DebugArgsRequest::Attach(attach_config) => DebugRequest::Attach(attach_config),
105        };
106
107        Ok(DebugTaskTemplate {
108            locator: debug_args.locator,
109            definition: DebugTaskDefinition {
110                adapter: debug_args.adapter,
111                request,
112                label: value.label,
113                initialize_args: debug_args.initialize_args,
114                tcp_connection: debug_args.tcp_connection,
115                stop_on_entry: debug_args.stop_on_entry,
116            },
117        })
118    }
119}
120
121impl DebugTaskTemplate {
122    /// Translate from debug definition to a task template
123    pub fn to_zed_format(self) -> TaskTemplate {
124        let (command, cwd, request) = match self.definition.request {
125            DebugRequest::Launch(launch_config) => (
126                launch_config.program,
127                launch_config
128                    .cwd
129                    .map(|cwd| cwd.to_string_lossy().to_string()),
130                crate::task_template::DebugArgsRequest::Launch,
131            ),
132            DebugRequest::Attach(attach_config) => (
133                "".to_owned(),
134                None,
135                crate::task_template::DebugArgsRequest::Attach(attach_config),
136            ),
137        };
138
139        let task_type = TaskType::Debug(DebugArgs {
140            adapter: self.definition.adapter,
141            request,
142            initialize_args: self.definition.initialize_args,
143            locator: self.locator,
144            tcp_connection: self.definition.tcp_connection,
145            stop_on_entry: self.definition.stop_on_entry,
146        });
147
148        let label = self.definition.label.clone();
149
150        TaskTemplate {
151            label,
152            command,
153            args: vec![],
154            task_type,
155            cwd,
156            ..Default::default()
157        }
158    }
159}
160
161#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
162#[serde(rename_all = "snake_case")]
163pub struct DebugTaskTemplate {
164    pub locator: Option<String>,
165    #[serde(flatten)]
166    pub definition: DebugTaskDefinition,
167}
168
169/// This struct represent a user created debug task
170#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
171#[serde(rename_all = "snake_case")]
172pub struct DebugTaskDefinition {
173    /// The adapter to run
174    pub adapter: String,
175    /// The type of request that should be called on the debug adapter
176    #[serde(flatten)]
177    pub request: DebugRequest,
178    /// Name of the debug task
179    pub label: String,
180    /// Additional initialization arguments to be sent on DAP initialization
181    pub initialize_args: Option<serde_json::Value>,
182    /// Optional TCP connection information
183    ///
184    /// If provided, this will be used to connect to the debug adapter instead of
185    /// spawning a new process. This is useful for connecting to a debug adapter
186    /// that is already running or is started by another process.
187    pub tcp_connection: Option<TcpArgumentsTemplate>,
188    /// Whether to tell the debug adapter to stop on entry
189    pub stop_on_entry: Option<bool>,
190}
191
192impl DebugTaskDefinition {
193    pub fn cwd(&self) -> Option<&Path> {
194        if let DebugRequest::Launch(config) = &self.request {
195            config.cwd.as_deref()
196        } else {
197            None
198        }
199    }
200    pub fn to_proto(&self) -> proto::DebugTaskDefinition {
201        proto::DebugTaskDefinition {
202            adapter: self.adapter.clone(),
203            request: Some(match &self.request {
204                DebugRequest::Launch(config) => {
205                    proto::debug_task_definition::Request::DebugLaunchRequest(
206                        proto::DebugLaunchRequest {
207                            program: config.program.clone(),
208                            cwd: config.cwd.as_ref().map(|c| c.to_string_lossy().to_string()),
209                            args: config.args.clone(),
210                        },
211                    )
212                }
213                DebugRequest::Attach(attach_request) => {
214                    proto::debug_task_definition::Request::DebugAttachRequest(
215                        proto::DebugAttachRequest {
216                            process_id: attach_request.process_id.unwrap_or_default(),
217                        },
218                    )
219                }
220            }),
221            label: self.label.clone(),
222            initialize_args: self.initialize_args.as_ref().map(|v| v.to_string()),
223            tcp_connection: self.tcp_connection.as_ref().map(|t| t.to_proto()),
224            stop_on_entry: self.stop_on_entry,
225        }
226    }
227
228    pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> {
229        let request = proto
230            .request
231            .ok_or_else(|| anyhow::anyhow!("request is required"))?;
232        Ok(Self {
233            label: proto.label,
234            initialize_args: proto.initialize_args.map(|v| v.into()),
235            tcp_connection: proto
236                .tcp_connection
237                .map(TcpArgumentsTemplate::from_proto)
238                .transpose()?,
239            stop_on_entry: proto.stop_on_entry,
240            adapter: proto.adapter.clone(),
241            request: match request {
242                proto::debug_task_definition::Request::DebugAttachRequest(config) => {
243                    DebugRequest::Attach(AttachRequest {
244                        process_id: Some(config.process_id),
245                    })
246                }
247
248                proto::debug_task_definition::Request::DebugLaunchRequest(config) => {
249                    DebugRequest::Launch(LaunchRequest {
250                        program: config.program,
251                        cwd: config.cwd.map(|cwd| cwd.into()),
252                        args: config.args,
253                    })
254                }
255            },
256        })
257    }
258}
259
260/// A group of Debug Tasks defined in a JSON file.
261#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
262#[serde(transparent)]
263pub struct DebugTaskFile(pub Vec<DebugTaskTemplate>);
264
265impl DebugTaskFile {
266    /// Generates JSON schema of Tasks JSON template format.
267    pub fn generate_json_schema() -> serde_json_lenient::Value {
268        let schema = SchemaSettings::draft07()
269            .with(|settings| settings.option_add_null_type = false)
270            .into_generator()
271            .into_root_schema_for::<Self>();
272
273        serde_json_lenient::to_value(schema).unwrap()
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use crate::{DebugRequest, LaunchRequest};
280
281    #[test]
282    fn test_can_deserialize_non_attach_task() {
283        let deserialized: DebugRequest =
284            serde_json::from_str(r#"{"program": "cafebabe"}"#).unwrap();
285        assert_eq!(
286            deserialized,
287            DebugRequest::Launch(LaunchRequest {
288                program: "cafebabe".to_owned(),
289                ..Default::default()
290            })
291        );
292    }
293}