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}