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}