debug_format.rs

  1use anyhow::Result;
  2use collections::FxHashMap;
  3use gpui::SharedString;
  4use schemars::{JsonSchema, r#gen::SchemaSettings};
  5use serde::{Deserialize, Serialize};
  6use std::path::PathBuf;
  7use std::{net::Ipv4Addr, path::Path};
  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, 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
 56impl<'de> Deserialize<'de> for AttachRequest {
 57    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
 58    where
 59        D: serde::Deserializer<'de>,
 60    {
 61        #[derive(Deserialize)]
 62        struct Helper {
 63            process_id: Option<u32>,
 64        }
 65
 66        let helper = Helper::deserialize(deserializer)?;
 67
 68        // Skip creating an AttachRequest if process_id is None
 69        if helper.process_id.is_none() {
 70            return Err(serde::de::Error::custom("process_id is required"));
 71        }
 72
 73        Ok(AttachRequest {
 74            process_id: helper.process_id,
 75        })
 76    }
 77}
 78
 79/// Represents the launch request information of the debug adapter
 80#[derive(Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, Clone, Debug)]
 81pub struct LaunchRequest {
 82    /// The program that you trying to debug
 83    pub program: String,
 84    /// The current working directory of your project
 85    #[serde(default)]
 86    pub cwd: Option<PathBuf>,
 87    /// Arguments to pass to a debuggee
 88    #[serde(default)]
 89    pub args: Vec<String>,
 90    #[serde(default)]
 91    pub env: FxHashMap<String, String>,
 92}
 93
 94/// Represents the type that will determine which request to call on the debug adapter
 95#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
 96#[serde(rename_all = "lowercase", untagged)]
 97pub enum DebugRequest {
 98    /// Call the `launch` request on the debug adapter
 99    Launch(LaunchRequest),
100    /// Call the `attach` request on the debug adapter
101    Attach(AttachRequest),
102}
103
104impl DebugRequest {
105    pub fn to_proto(&self) -> proto::DebugRequest {
106        match self {
107            DebugRequest::Launch(launch_request) => proto::DebugRequest {
108                request: Some(proto::debug_request::Request::DebugLaunchRequest(
109                    proto::DebugLaunchRequest {
110                        program: launch_request.program.clone(),
111                        cwd: launch_request
112                            .cwd
113                            .as_ref()
114                            .map(|cwd| cwd.to_string_lossy().into_owned()),
115                        args: launch_request.args.clone(),
116                        env: launch_request
117                            .env
118                            .iter()
119                            .map(|(k, v)| (k.clone(), v.clone()))
120                            .collect(),
121                    },
122                )),
123            },
124            DebugRequest::Attach(attach_request) => proto::DebugRequest {
125                request: Some(proto::debug_request::Request::DebugAttachRequest(
126                    proto::DebugAttachRequest {
127                        process_id: attach_request
128                            .process_id
129                            .expect("The process ID to be already filled out."),
130                    },
131                )),
132            },
133        }
134    }
135
136    pub fn from_proto(val: proto::DebugRequest) -> Result<DebugRequest> {
137        let request = val
138            .request
139            .ok_or_else(|| anyhow::anyhow!("Missing debug request"))?;
140        match request {
141            proto::debug_request::Request::DebugLaunchRequest(proto::DebugLaunchRequest {
142                program,
143                cwd,
144                args,
145                env,
146            }) => Ok(DebugRequest::Launch(LaunchRequest {
147                program,
148                cwd: cwd.map(From::from),
149                args,
150                env: env.into_iter().collect(),
151            })),
152
153            proto::debug_request::Request::DebugAttachRequest(proto::DebugAttachRequest {
154                process_id,
155            }) => Ok(DebugRequest::Attach(AttachRequest {
156                process_id: Some(process_id),
157            })),
158        }
159    }
160}
161
162impl From<LaunchRequest> for DebugRequest {
163    fn from(launch_config: LaunchRequest) -> Self {
164        DebugRequest::Launch(launch_config)
165    }
166}
167
168impl From<AttachRequest> for DebugRequest {
169    fn from(attach_config: AttachRequest) -> Self {
170        DebugRequest::Attach(attach_config)
171    }
172}
173
174/// This struct represent a user created debug task
175#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
176#[serde(rename_all = "snake_case")]
177pub struct DebugScenario {
178    pub adapter: SharedString,
179    /// Name of the debug task
180    pub label: SharedString,
181    /// A task to run prior to spawning the debuggee.
182    #[serde(default)]
183    pub build: Option<SharedString>,
184    #[serde(flatten)]
185    pub request: Option<DebugRequest>,
186    /// Additional initialization arguments to be sent on DAP initialization
187    #[serde(default)]
188    pub initialize_args: Option<serde_json::Value>,
189    /// Optional TCP connection information
190    ///
191    /// If provided, this will be used to connect to the debug adapter instead of
192    /// spawning a new process. This is useful for connecting to a debug adapter
193    /// that is already running or is started by another process.
194    #[serde(default)]
195    pub tcp_connection: Option<TcpArgumentsTemplate>,
196    /// Whether to tell the debug adapter to stop on entry
197    #[serde(default)]
198    pub stop_on_entry: Option<bool>,
199}
200
201impl DebugScenario {
202    pub fn cwd(&self) -> Option<&Path> {
203        if let Some(DebugRequest::Launch(config)) = &self.request {
204            config.cwd.as_ref().map(Path::new)
205        } else {
206            None
207        }
208    }
209}
210
211/// A group of Debug Tasks defined in a JSON file.
212#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
213#[serde(transparent)]
214pub struct DebugTaskFile(pub Vec<DebugScenario>);
215
216impl DebugTaskFile {
217    /// Generates JSON schema of Tasks JSON template format.
218    pub fn generate_json_schema() -> serde_json_lenient::Value {
219        let schema = SchemaSettings::draft07()
220            .with(|settings| settings.option_add_null_type = false)
221            .into_generator()
222            .into_root_schema_for::<Self>();
223
224        serde_json_lenient::to_value(schema).unwrap()
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use crate::{DebugRequest, DebugScenario, LaunchRequest};
231
232    #[test]
233    fn test_can_deserialize_non_attach_task() {
234        let deserialized: DebugRequest =
235            serde_json::from_str(r#"{"program": "cafebabe"}"#).unwrap();
236        assert_eq!(
237            deserialized,
238            DebugRequest::Launch(LaunchRequest {
239                program: "cafebabe".to_owned(),
240                ..Default::default()
241            })
242        );
243    }
244
245    #[test]
246    fn test_empty_scenario_has_none_request() {
247        let json = r#"{
248            "label": "Build & debug rust",
249            "build": "rust",
250            "adapter": "CodeLLDB"
251        }"#;
252
253        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
254        assert_eq!(deserialized.request, None);
255    }
256
257    #[test]
258    fn test_launch_scenario_deserialization() {
259        let json = r#"{
260            "label": "Launch program",
261            "adapter": "CodeLLDB",
262            "program": "target/debug/myapp",
263            "args": ["--test"]
264        }"#;
265
266        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
267        match deserialized.request {
268            Some(DebugRequest::Launch(launch)) => {
269                assert_eq!(launch.program, "target/debug/myapp");
270                assert_eq!(launch.args, vec!["--test"]);
271            }
272            _ => panic!("Expected Launch request"),
273        }
274    }
275
276    #[test]
277    fn test_attach_scenario_deserialization() {
278        let json = r#"{
279            "label": "Attach to process",
280            "adapter": "CodeLLDB",
281            "process_id": 1234
282        }"#;
283
284        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
285        match deserialized.request {
286            Some(DebugRequest::Attach(attach)) => {
287                assert_eq!(attach.process_id, Some(1234));
288            }
289            _ => panic!("Expected Attach request"),
290        }
291    }
292}