Skip to repository content535 lines · 19.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:35:55.338Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
debug_format.rs
1use anyhow::{Context as _, Result};
2use collections::FxHashMap;
3use gpui::SharedString;
4use log as _;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use std::net::IpAddr;
8use std::path::PathBuf;
9use util::{debug_panic, schemars::add_new_subschema};
10
11use crate::{TaskTemplate, adapter_schema::AdapterSchemas};
12
13/// Represents the host information of the debug adapter
14#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
15pub struct TcpArgumentsTemplate {
16 /// The port that the debug adapter is listening on
17 ///
18 /// Default: We will try to find an open port
19 pub port: Option<u16>,
20 /// The host that the debug adapter is listening too
21 ///
22 /// Default: 127.0.0.1
23 pub host: Option<IpAddr>,
24 /// The max amount of time in milliseconds to connect to a tcp DAP before returning an error
25 ///
26 /// Default: 2000ms
27 pub timeout: Option<u64>,
28}
29
30impl TcpArgumentsTemplate {
31 /// Get the host or fallback to the default host
32 pub fn host(&self) -> IpAddr {
33 self.host
34 .unwrap_or(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST))
35 }
36
37 pub fn from_proto(proto: proto::TcpHost) -> Result<Self> {
38 Ok(Self {
39 port: proto.port.map(|p| p.try_into()).transpose()?,
40 host: proto.host.map(|h| h.parse()).transpose()?,
41 timeout: proto.timeout,
42 })
43 }
44
45 pub fn to_proto(&self) -> proto::TcpHost {
46 proto::TcpHost {
47 port: self.port.map(|p| p.into()),
48 host: self.host.map(|h| h.to_string()),
49 timeout: self.timeout,
50 }
51 }
52}
53
54/// Represents the attach request information of the debug adapter
55#[derive(Default, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
56pub struct AttachRequest {
57 /// The processId to attach to, if left empty we will show a process picker
58 pub process_id: Option<u32>,
59}
60
61impl<'de> Deserialize<'de> for AttachRequest {
62 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63 where
64 D: serde::Deserializer<'de>,
65 {
66 #[derive(Deserialize)]
67 struct Helper {
68 process_id: Option<u32>,
69 }
70
71 let helper = Helper::deserialize(deserializer)?;
72
73 // Skip creating an AttachRequest if process_id is None
74 if helper.process_id.is_none() {
75 return Err(serde::de::Error::custom("process_id is required"));
76 }
77
78 Ok(AttachRequest {
79 process_id: helper.process_id,
80 })
81 }
82}
83
84/// Represents the launch request information of the debug adapter
85#[derive(Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, Clone, Debug)]
86pub struct LaunchRequest {
87 /// The program that you trying to debug
88 pub program: String,
89 /// The current working directory of your project
90 #[serde(default)]
91 pub cwd: Option<PathBuf>,
92 /// Arguments to pass to a debuggee
93 #[serde(default)]
94 pub args: Vec<String>,
95 #[serde(default)]
96 pub env: FxHashMap<String, String>,
97}
98
99impl LaunchRequest {
100 pub fn env_json(&self) -> serde_json::Value {
101 serde_json::Value::Object(
102 self.env
103 .iter()
104 .map(|(k, v)| (k.clone(), v.to_owned().into()))
105 .collect::<serde_json::Map<String, serde_json::Value>>(),
106 )
107 }
108}
109
110/// Represents the type that will determine which request to call on the debug adapter
111#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
112#[serde(rename_all = "lowercase", tag = "request")]
113pub enum DebugRequest {
114 /// Call the `launch` request on the debug adapter
115 Launch(LaunchRequest),
116 /// Call the `attach` request on the debug adapter
117 Attach(AttachRequest),
118}
119
120impl DebugRequest {
121 pub fn to_proto(&self) -> proto::DebugRequest {
122 match self {
123 DebugRequest::Launch(launch_request) => proto::DebugRequest {
124 request: Some(proto::debug_request::Request::DebugLaunchRequest(
125 proto::DebugLaunchRequest {
126 program: launch_request.program.clone(),
127 cwd: launch_request
128 .cwd
129 .as_ref()
130 .map(|cwd| cwd.to_string_lossy().into_owned()),
131 args: launch_request.args.clone(),
132 env: launch_request
133 .env
134 .iter()
135 .map(|(k, v)| (k.clone(), v.clone()))
136 .collect(),
137 },
138 )),
139 },
140 DebugRequest::Attach(attach_request) => proto::DebugRequest {
141 request: Some(proto::debug_request::Request::DebugAttachRequest(
142 proto::DebugAttachRequest {
143 process_id: attach_request
144 .process_id
145 .expect("The process ID to be already filled out."),
146 },
147 )),
148 },
149 }
150 }
151
152 pub fn from_proto(val: proto::DebugRequest) -> Result<DebugRequest> {
153 let request = val.request.context("Missing debug request")?;
154 match request {
155 proto::debug_request::Request::DebugLaunchRequest(proto::DebugLaunchRequest {
156 program,
157 cwd,
158 args,
159 env,
160 }) => Ok(DebugRequest::Launch(LaunchRequest {
161 program,
162 cwd: cwd.map(From::from),
163 args,
164 env: env.into_iter().collect(),
165 })),
166
167 proto::debug_request::Request::DebugAttachRequest(proto::DebugAttachRequest {
168 process_id,
169 }) => Ok(DebugRequest::Attach(AttachRequest {
170 process_id: Some(process_id),
171 })),
172 }
173 }
174}
175
176impl From<LaunchRequest> for DebugRequest {
177 fn from(launch_config: LaunchRequest) -> Self {
178 DebugRequest::Launch(launch_config)
179 }
180}
181
182impl From<AttachRequest> for DebugRequest {
183 fn from(attach_config: AttachRequest) -> Self {
184 DebugRequest::Attach(attach_config)
185 }
186}
187
188#[derive(Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
189#[serde(untagged)]
190pub enum BuildTaskDefinition {
191 ByName(SharedString),
192 Template {
193 #[serde(flatten)]
194 task_template: TaskTemplate,
195 #[serde(skip)]
196 locator_name: Option<SharedString>,
197 },
198}
199
200impl<'de> Deserialize<'de> for BuildTaskDefinition {
201 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
202 where
203 D: serde::Deserializer<'de>,
204 {
205 #[derive(Deserialize)]
206 struct TemplateHelper {
207 #[serde(default)]
208 label: Option<String>,
209 #[serde(flatten)]
210 rest: serde_json::Value,
211 }
212
213 let value = serde_json::Value::deserialize(deserializer)?;
214
215 if let Ok(name) = serde_json::from_value::<SharedString>(value.clone()) {
216 return Ok(BuildTaskDefinition::ByName(name));
217 }
218
219 let helper: TemplateHelper =
220 serde_json::from_value(value).map_err(serde::de::Error::custom)?;
221
222 let mut template_value = helper.rest;
223 if let serde_json::Value::Object(ref mut map) = template_value {
224 map.insert(
225 "label".to_string(),
226 serde_json::to_value(helper.label.unwrap_or_else(|| "debug-build".to_owned()))
227 .map_err(serde::de::Error::custom)?,
228 );
229 }
230
231 let task_template: TaskTemplate =
232 serde_json::from_value(template_value).map_err(serde::de::Error::custom)?;
233
234 Ok(BuildTaskDefinition::Template {
235 task_template,
236 locator_name: None,
237 })
238 }
239}
240
241#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
242pub enum Request {
243 Launch,
244 Attach,
245}
246
247/// This struct represent a user created debug task from the new process modal
248#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
249#[serde(rename_all = "snake_case")]
250pub struct ZedDebugConfig {
251 /// Name of the debug task
252 pub label: SharedString,
253 /// The debug adapter to use
254 pub adapter: SharedString,
255 #[serde(flatten)]
256 pub request: DebugRequest,
257 /// Whether to tell the debug adapter to stop on entry
258 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub stop_on_entry: Option<bool>,
260}
261
262/// This struct represent a user created debug task
263#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
264#[serde(rename_all = "snake_case")]
265pub struct DebugScenario {
266 pub adapter: SharedString,
267 /// Name of the debug task
268 pub label: SharedString,
269 /// A task to run prior to spawning the debuggee.
270 #[serde(default, skip_serializing_if = "Option::is_none")]
271 pub build: Option<BuildTaskDefinition>,
272 /// The main arguments to be sent to the debug adapter
273 #[serde(default, flatten)]
274 pub config: serde_json::Value,
275 /// Optional TCP connection information
276 ///
277 /// If provided, this will be used to connect to the debug adapter instead of
278 /// spawning a new process. This is useful for connecting to a debug adapter
279 /// that is already running or is started by another process.
280 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub tcp_connection: Option<TcpArgumentsTemplate>,
282}
283
284/// A group of Debug Tasks defined in a JSON file.
285#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
286#[serde(transparent)]
287pub struct DebugTaskFile(pub Vec<DebugScenario>);
288
289impl DebugTaskFile {
290 pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json::Value {
291 let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator();
292
293 let mut build_task_value = BuildTaskDefinition::json_schema(&mut generator).to_value();
294
295 if let Some(template_object) = build_task_value
296 .get_mut("anyOf")
297 .and_then(|array| array.as_array_mut())
298 .and_then(|array| array.get_mut(1))
299 {
300 if let Some(properties) = template_object
301 .get_mut("properties")
302 .and_then(|value| value.as_object_mut())
303 && properties.remove("label").is_none()
304 {
305 debug_panic!(
306 "Generated TaskTemplate json schema did not have expected 'label' field. \
307 Schema of 2nd alternative is: {template_object:?}"
308 );
309 }
310
311 if let Some(arr) = template_object
312 .get_mut("required")
313 .and_then(|array| array.as_array_mut())
314 {
315 arr.retain(|v| v.as_str() != Some("label"));
316 }
317 } else {
318 debug_panic!(
319 "Generated TaskTemplate json schema did not match expectations. \
320 Schema is: {build_task_value:?}"
321 );
322 }
323
324 let adapter_conditions = schemas
325 .0
326 .iter()
327 .map(|adapter_schema| {
328 let adapter_name = adapter_schema.adapter.to_string();
329 add_new_subschema(
330 &mut generator,
331 &format!("{adapter_name}DebugSettings"),
332 serde_json::json!({
333 "if": {
334 "properties": {
335 "adapter": { "const": adapter_name }
336 }
337 },
338 "then": adapter_schema.schema
339 }),
340 )
341 })
342 .collect::<Vec<_>>();
343
344 let build_task_definition_ref = add_new_subschema(
345 &mut generator,
346 BuildTaskDefinition::schema_name().as_ref(),
347 build_task_value,
348 );
349
350 let meta_schema = generator
351 .settings()
352 .meta_schema
353 .as_ref()
354 .expect("meta_schema should be present in schemars settings")
355 .to_string();
356
357 serde_json::json!({
358 "$schema": meta_schema,
359 "title": "Debug Configurations",
360 "description": "Configuration for debug scenarios",
361 "allowTrailingCommas": true,
362 "type": "array",
363 "items": {
364 "type": "object",
365 "required": ["adapter", "label"],
366 // TODO: Uncommenting this will cause json-language-server to provide warnings for
367 // unrecognized properties. It should be enabled if/when there's an adapter JSON
368 // schema that's comprehensive. In order to not get warnings for the other schemas,
369 // `additionalProperties` or `unevaluatedProperties` (to handle "allOf" etc style
370 // schema combinations) could be set to `true` for that schema.
371 //
372 // "unevaluatedProperties": false,
373 "properties": {
374 "adapter": {
375 "type": "string",
376 "description": "The name of the debug adapter"
377 },
378 "label": {
379 "type": "string",
380 "description": "The name of the debug configuration"
381 },
382 "build": build_task_definition_ref,
383 "tcp_connection": {
384 "type": "object",
385 "description": "Optional TCP connection information for connecting to an already running debug adapter",
386 "properties": {
387 "port": {
388 "type": "integer",
389 "description": "The port that the debug adapter is listening on (default: auto-find open port)"
390 },
391 "host": {
392 "type": "string",
393 "description": "The host that the debug adapter is listening to, as an IPv4 or IPv6 address (default: 127.0.0.1)"
394 },
395 "timeout": {
396 "type": "integer",
397 "description": "The max amount of time in milliseconds to connect to a tcp DAP before returning an error (default: 2000ms)"
398 }
399 }
400 }
401 },
402 "allOf": adapter_conditions
403 },
404 "$defs": generator.take_definitions(true),
405 })
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use crate::DebugScenario;
412 use serde_json::json;
413
414 #[test]
415 fn test_just_build_args() {
416 let json = r#"{
417 "label": "Build & debug rust",
418 "adapter": "CodeLLDB",
419 "build": {
420 "command": "rust",
421 "args": ["build"]
422 }
423 }"#;
424
425 let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
426 assert!(deserialized.build.is_some());
427 match deserialized.build.as_ref().unwrap() {
428 crate::BuildTaskDefinition::Template { task_template, .. } => {
429 assert_eq!("debug-build", task_template.label);
430 assert_eq!("rust", task_template.command);
431 assert_eq!(vec!["build"], task_template.args);
432 }
433 _ => panic!("Expected Template variant"),
434 }
435 assert_eq!(json!({}), deserialized.config);
436 assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
437 assert_eq!("Build & debug rust", deserialized.label.as_ref());
438 }
439
440 #[test]
441 fn test_empty_scenario_has_none_request() {
442 let json = r#"{
443 "label": "Build & debug rust",
444 "build": "rust",
445 "adapter": "CodeLLDB"
446 }"#;
447
448 let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
449
450 assert_eq!(json!({}), deserialized.config);
451 assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
452 assert_eq!("Build & debug rust", deserialized.label.as_ref());
453 }
454
455 #[test]
456 fn test_launch_scenario_deserialization() {
457 let json = r#"{
458 "label": "Launch program",
459 "adapter": "CodeLLDB",
460 "request": "launch",
461 "program": "target/debug/myapp",
462 "args": ["--test"]
463 }"#;
464
465 let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
466
467 assert_eq!(
468 json!({ "request": "launch", "program": "target/debug/myapp", "args": ["--test"] }),
469 deserialized.config
470 );
471 assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
472 assert_eq!("Launch program", deserialized.label.as_ref());
473 }
474
475 #[test]
476 fn test_attach_scenario_deserialization() {
477 let json = r#"{
478 "label": "Attach to process",
479 "adapter": "CodeLLDB",
480 "process_id": 1234,
481 "request": "attach"
482 }"#;
483
484 let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
485
486 assert_eq!(
487 json!({ "request": "attach", "process_id": 1234 }),
488 deserialized.config
489 );
490 assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
491 assert_eq!("Attach to process", deserialized.label.as_ref());
492 }
493
494 #[test]
495 fn test_build_task_definition_without_label() {
496 use crate::BuildTaskDefinition;
497
498 let json = r#""my_build_task""#;
499 let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
500 match deserialized {
501 BuildTaskDefinition::ByName(name) => assert_eq!("my_build_task", name.as_ref()),
502 _ => panic!("Expected ByName variant"),
503 }
504
505 let json = r#"{
506 "command": "cargo",
507 "args": ["build", "--release"]
508 }"#;
509 let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
510 match deserialized {
511 BuildTaskDefinition::Template { task_template, .. } => {
512 assert_eq!("debug-build", task_template.label);
513 assert_eq!("cargo", task_template.command);
514 assert_eq!(vec!["build", "--release"], task_template.args);
515 }
516 _ => panic!("Expected Template variant"),
517 }
518
519 let json = r#"{
520 "label": "Build Release",
521 "command": "cargo",
522 "args": ["build", "--release"]
523 }"#;
524 let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
525 match deserialized {
526 BuildTaskDefinition::Template { task_template, .. } => {
527 assert_eq!("Build Release", task_template.label);
528 assert_eq!("cargo", task_template.command);
529 assert_eq!(vec!["build", "--release"], task_template.args);
530 }
531 _ => panic!("Expected Template variant"),
532 }
533 }
534}
535