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}