debug_format.rs

  1use anyhow::{Context as _, Result};
  2use collections::FxHashMap;
  3use gpui::SharedString;
  4use schemars::JsonSchema;
  5use serde::{Deserialize, Serialize};
  6use std::net::Ipv4Addr;
  7use std::path::PathBuf;
  8
  9use crate::{TaskTemplate, adapter_schema::AdapterSchemas};
 10
 11/// Represents the host information of the debug adapter
 12#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
 13pub struct TcpArgumentsTemplate {
 14    /// The port that the debug adapter is listening on
 15    ///
 16    /// Default: We will try to find an open port
 17    pub port: Option<u16>,
 18    /// The host that the debug adapter is listening too
 19    ///
 20    /// Default: 127.0.0.1
 21    pub host: Option<Ipv4Addr>,
 22    /// The max amount of time in milliseconds to connect to a tcp DAP before returning an error
 23    ///
 24    /// 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
185#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
186#[serde(untagged)]
187pub enum BuildTaskDefinition {
188    ByName(SharedString),
189    Template {
190        #[serde(flatten)]
191        task_template: TaskTemplate,
192        #[serde(skip)]
193        locator_name: Option<SharedString>,
194    },
195}
196
197#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
198pub enum Request {
199    Launch,
200    Attach,
201}
202
203/// This struct represent a user created debug task from the new session modal
204#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
205#[serde(rename_all = "snake_case")]
206pub struct ZedDebugConfig {
207    /// Name of the debug task
208    pub label: SharedString,
209    /// The debug adapter to use
210    pub adapter: SharedString,
211    #[serde(flatten)]
212    pub request: DebugRequest,
213    /// Whether to tell the debug adapter to stop on entry
214    #[serde(default, skip_serializing_if = "Option::is_none")]
215    pub stop_on_entry: Option<bool>,
216}
217
218/// This struct represent a user created debug task
219#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
220#[serde(rename_all = "snake_case")]
221pub struct DebugScenario {
222    pub adapter: SharedString,
223    /// Name of the debug task
224    pub label: SharedString,
225    /// A task to run prior to spawning the debuggee.
226    #[serde(default, skip_serializing_if = "Option::is_none")]
227    pub build: Option<BuildTaskDefinition>,
228    /// The main arguments to be sent to the debug adapter
229    #[serde(default, flatten)]
230    pub config: serde_json::Value,
231    /// Optional TCP connection information
232    ///
233    /// If provided, this will be used to connect to the debug adapter instead of
234    /// spawning a new process. This is useful for connecting to a debug adapter
235    /// that is already running or is started by another process.
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub tcp_connection: Option<TcpArgumentsTemplate>,
238}
239
240/// A group of Debug Tasks defined in a JSON file.
241#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
242#[serde(transparent)]
243pub struct DebugTaskFile(pub Vec<DebugScenario>);
244
245impl DebugTaskFile {
246    /// Generates JSON schema of Tasks JSON template format.
247    pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json_lenient::Value {
248        schemas.generate_json_schema().unwrap_or_default()
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use crate::DebugScenario;
255    use serde_json::json;
256
257    #[test]
258    fn test_empty_scenario_has_none_request() {
259        let json = r#"{
260            "label": "Build & debug rust",
261            "build": "rust",
262            "adapter": "CodeLLDB"
263        }"#;
264
265        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
266
267        assert_eq!(json!({}), deserialized.config);
268        assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
269        assert_eq!("Build & debug rust", deserialized.label.as_ref());
270    }
271
272    #[test]
273    fn test_launch_scenario_deserialization() {
274        let json = r#"{
275            "label": "Launch program",
276            "adapter": "CodeLLDB",
277            "request": "launch",
278            "program": "target/debug/myapp",
279            "args": ["--test"]
280        }"#;
281
282        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
283
284        assert_eq!(
285            json!({ "request": "launch", "program": "target/debug/myapp", "args": ["--test"] }),
286            deserialized.config
287        );
288        assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
289        assert_eq!("Launch program", deserialized.label.as_ref());
290    }
291
292    #[test]
293    fn test_attach_scenario_deserialization() {
294        let json = r#"{
295            "label": "Attach to process",
296            "adapter": "CodeLLDB",
297            "process_id": 1234,
298            "request": "attach"
299        }"#;
300
301        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
302
303        assert_eq!(
304            json!({ "request": "attach", "process_id": 1234 }),
305            deserialized.config
306        );
307        assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
308        assert_eq!("Attach to process", deserialized.label.as_ref());
309    }
310}