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
  9use crate::TaskTemplate;
 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
 96/// Represents the type that will determine which request to call on the debug adapter
 97#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
 98#[serde(rename_all = "lowercase", untagged)]
 99pub enum DebugRequest {
100    /// Call the `launch` request on the debug adapter
101    Launch(LaunchRequest),
102    /// Call the `attach` request on the debug adapter
103    Attach(AttachRequest),
104}
105
106impl DebugRequest {
107    pub fn to_proto(&self) -> proto::DebugRequest {
108        match self {
109            DebugRequest::Launch(launch_request) => proto::DebugRequest {
110                request: Some(proto::debug_request::Request::DebugLaunchRequest(
111                    proto::DebugLaunchRequest {
112                        program: launch_request.program.clone(),
113                        cwd: launch_request
114                            .cwd
115                            .as_ref()
116                            .map(|cwd| cwd.to_string_lossy().into_owned()),
117                        args: launch_request.args.clone(),
118                        env: launch_request
119                            .env
120                            .iter()
121                            .map(|(k, v)| (k.clone(), v.clone()))
122                            .collect(),
123                    },
124                )),
125            },
126            DebugRequest::Attach(attach_request) => proto::DebugRequest {
127                request: Some(proto::debug_request::Request::DebugAttachRequest(
128                    proto::DebugAttachRequest {
129                        process_id: attach_request
130                            .process_id
131                            .expect("The process ID to be already filled out."),
132                    },
133                )),
134            },
135        }
136    }
137
138    pub fn from_proto(val: proto::DebugRequest) -> Result<DebugRequest> {
139        let request = val
140            .request
141            .ok_or_else(|| anyhow::anyhow!("Missing debug request"))?;
142        match request {
143            proto::debug_request::Request::DebugLaunchRequest(proto::DebugLaunchRequest {
144                program,
145                cwd,
146                args,
147                env,
148            }) => Ok(DebugRequest::Launch(LaunchRequest {
149                program,
150                cwd: cwd.map(From::from),
151                args,
152                env: env.into_iter().collect(),
153            })),
154
155            proto::debug_request::Request::DebugAttachRequest(proto::DebugAttachRequest {
156                process_id,
157            }) => Ok(DebugRequest::Attach(AttachRequest {
158                process_id: Some(process_id),
159            })),
160        }
161    }
162}
163
164impl From<LaunchRequest> for DebugRequest {
165    fn from(launch_config: LaunchRequest) -> Self {
166        DebugRequest::Launch(launch_config)
167    }
168}
169
170impl From<AttachRequest> for DebugRequest {
171    fn from(attach_config: AttachRequest) -> Self {
172        DebugRequest::Attach(attach_config)
173    }
174}
175
176#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
177#[serde(untagged)]
178#[allow(clippy::large_enum_variant)]
179pub enum BuildTaskDefinition {
180    ByName(SharedString),
181    Template {
182        #[serde(flatten)]
183        task_template: TaskTemplate,
184        #[serde(skip)]
185        locator_name: Option<SharedString>,
186    },
187}
188/// This struct represent a user created debug task
189#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
190#[serde(rename_all = "snake_case")]
191pub struct DebugScenario {
192    pub adapter: SharedString,
193    /// Name of the debug task
194    pub label: SharedString,
195    /// A task to run prior to spawning the debuggee.
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub build: Option<BuildTaskDefinition>,
198    #[serde(flatten)]
199    pub request: Option<DebugRequest>,
200    /// Additional initialization arguments to be sent on DAP initialization
201    #[serde(default, skip_serializing_if = "Option::is_none")]
202    pub initialize_args: Option<serde_json::Value>,
203    /// Optional TCP connection information
204    ///
205    /// If provided, this will be used to connect to the debug adapter instead of
206    /// spawning a new process. This is useful for connecting to a debug adapter
207    /// that is already running or is started by another process.
208    #[serde(default, skip_serializing_if = "Option::is_none")]
209    pub tcp_connection: Option<TcpArgumentsTemplate>,
210    /// Whether to tell the debug adapter to stop on entry
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub stop_on_entry: Option<bool>,
213}
214
215impl DebugScenario {
216    pub fn cwd(&self) -> Option<&Path> {
217        if let Some(DebugRequest::Launch(config)) = &self.request {
218            config.cwd.as_ref().map(Path::new)
219        } else {
220            None
221        }
222    }
223}
224
225/// A group of Debug Tasks defined in a JSON file.
226#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
227#[serde(transparent)]
228pub struct DebugTaskFile(pub Vec<DebugScenario>);
229
230impl DebugTaskFile {
231    /// Generates JSON schema of Tasks JSON template format.
232    pub fn generate_json_schema() -> serde_json_lenient::Value {
233        let schema = SchemaSettings::draft07()
234            .with(|settings| settings.option_add_null_type = false)
235            .into_generator()
236            .into_root_schema_for::<Self>();
237
238        serde_json_lenient::to_value(schema).unwrap()
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use crate::{DebugRequest, DebugScenario, LaunchRequest};
245
246    #[test]
247    fn test_can_deserialize_non_attach_task() {
248        let deserialized: DebugRequest =
249            serde_json::from_str(r#"{"program": "cafebabe"}"#).unwrap();
250        assert_eq!(
251            deserialized,
252            DebugRequest::Launch(LaunchRequest {
253                program: "cafebabe".to_owned(),
254                ..Default::default()
255            })
256        );
257    }
258
259    #[test]
260    fn test_empty_scenario_has_none_request() {
261        let json = r#"{
262            "label": "Build & debug rust",
263            "build": "rust",
264            "adapter": "CodeLLDB"
265        }"#;
266
267        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
268        assert_eq!(deserialized.request, None);
269    }
270
271    #[test]
272    fn test_launch_scenario_deserialization() {
273        let json = r#"{
274            "label": "Launch program",
275            "adapter": "CodeLLDB",
276            "program": "target/debug/myapp",
277            "args": ["--test"]
278        }"#;
279
280        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
281        match deserialized.request {
282            Some(DebugRequest::Launch(launch)) => {
283                assert_eq!(launch.program, "target/debug/myapp");
284                assert_eq!(launch.args, vec!["--test"]);
285            }
286            _ => panic!("Expected Launch request"),
287        }
288    }
289
290    #[test]
291    fn test_attach_scenario_deserialization() {
292        let json = r#"{
293            "label": "Attach to process",
294            "adapter": "CodeLLDB",
295            "process_id": 1234
296        }"#;
297
298        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
299        match deserialized.request {
300            Some(DebugRequest::Attach(attach)) => {
301                assert_eq!(attach.process_id, Some(1234));
302            }
303            _ => panic!("Expected Attach request"),
304        }
305    }
306}