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