1use anyhow::Result;
2use async_trait::async_trait;
3use collections::FxHashMap;
4use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
5use gpui::SharedString;
6use std::path::PathBuf;
7use task::{
8 BuildTaskDefinition, DebugScenario, RevealStrategy, RevealTarget, Shell, SpawnInTerminal,
9 TaskTemplate,
10};
11use uuid::Uuid;
12
13pub(crate) struct GoLocator;
14
15#[async_trait]
16impl DapLocator for GoLocator {
17 fn name(&self) -> SharedString {
18 SharedString::new_static("go-debug-locator")
19 }
20
21 fn create_scenario(
22 &self,
23 build_config: &TaskTemplate,
24 resolved_label: &str,
25 adapter: DebugAdapterName,
26 ) -> Option<DebugScenario> {
27 if build_config.command != "go" {
28 return None;
29 }
30
31 let go_action = build_config.args.first()?;
32
33 match go_action.as_str() {
34 "test" => {
35 let binary_path = format!("__debug_{}", Uuid::new_v4().simple());
36
37 let build_task = TaskTemplate {
38 label: "go test debug".into(),
39 command: "go".into(),
40 args: vec![
41 "test".into(),
42 "-c".into(),
43 "-gcflags \"all=-N -l\"".into(),
44 "-o".into(),
45 binary_path,
46 ],
47 env: build_config.env.clone(),
48 cwd: build_config.cwd.clone(),
49 use_new_terminal: false,
50 allow_concurrent_runs: false,
51 reveal: RevealStrategy::Always,
52 reveal_target: RevealTarget::Dock,
53 hide: task::HideStrategy::Never,
54 shell: Shell::System,
55 tags: vec![],
56 show_summary: true,
57 show_command: true,
58 };
59
60 Some(DebugScenario {
61 label: resolved_label.to_string().into(),
62 adapter: adapter.0,
63 build: Some(BuildTaskDefinition::Template {
64 task_template: build_task,
65 locator_name: Some(self.name()),
66 }),
67 config: serde_json::Value::Null,
68 tcp_connection: None,
69 })
70 }
71 "run" => {
72 let program = build_config
73 .args
74 .get(1)
75 .cloned()
76 .unwrap_or_else(|| ".".to_string());
77
78 let build_task = TaskTemplate {
79 label: "go build debug".into(),
80 command: "go".into(),
81 args: vec![
82 "build".into(),
83 "-gcflags \"all=-N -l\"".into(),
84 program.clone(),
85 ],
86 env: build_config.env.clone(),
87 cwd: build_config.cwd.clone(),
88 use_new_terminal: false,
89 allow_concurrent_runs: false,
90 reveal: RevealStrategy::Always,
91 reveal_target: RevealTarget::Dock,
92 hide: task::HideStrategy::Never,
93 shell: Shell::System,
94 tags: vec![],
95 show_summary: true,
96 show_command: true,
97 };
98
99 Some(DebugScenario {
100 label: resolved_label.to_string().into(),
101 adapter: adapter.0,
102 build: Some(BuildTaskDefinition::Template {
103 task_template: build_task,
104 locator_name: Some(self.name()),
105 }),
106 config: serde_json::Value::Null,
107 tcp_connection: None,
108 })
109 }
110 _ => None,
111 }
112 }
113
114 async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
115 if build_config.args.is_empty() {
116 return Err(anyhow::anyhow!("Invalid Go command"));
117 }
118
119 let go_action = &build_config.args[0];
120 let cwd = build_config
121 .cwd
122 .as_ref()
123 .map(|p| p.to_string_lossy().to_string())
124 .unwrap_or_else(|| ".".to_string());
125
126 let mut env = FxHashMap::default();
127 for (key, value) in &build_config.env {
128 env.insert(key.clone(), value.clone());
129 }
130
131 match go_action.as_str() {
132 "test" => {
133 let binary_arg = build_config
134 .args
135 .get(4)
136 .ok_or_else(|| anyhow::anyhow!("can't locate debug binary"))?;
137
138 let program = PathBuf::from(&cwd)
139 .join(binary_arg)
140 .to_string_lossy()
141 .into_owned();
142
143 Ok(DebugRequest::Launch(task::LaunchRequest {
144 program,
145 cwd: Some(PathBuf::from(&cwd)),
146 args: vec!["-test.v".into(), "-test.run=${ZED_SYMBOL}".into()],
147 env,
148 }))
149 }
150 "build" => {
151 let package = build_config
152 .args
153 .get(2)
154 .cloned()
155 .unwrap_or_else(|| ".".to_string());
156
157 Ok(DebugRequest::Launch(task::LaunchRequest {
158 program: package,
159 cwd: Some(PathBuf::from(&cwd)),
160 args: vec![],
161 env,
162 }))
163 }
164 _ => Err(anyhow::anyhow!("Unsupported Go command: {}", go_action)),
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskId, TaskTemplate};
173
174 #[test]
175 fn test_create_scenario_for_go_run() {
176 let locator = GoLocator;
177 let task = TaskTemplate {
178 label: "go run main.go".into(),
179 command: "go".into(),
180 args: vec!["run".into(), "main.go".into()],
181 env: Default::default(),
182 cwd: Some("${ZED_WORKTREE_ROOT}".into()),
183 use_new_terminal: false,
184 allow_concurrent_runs: false,
185 reveal: RevealStrategy::Always,
186 reveal_target: RevealTarget::Dock,
187 hide: HideStrategy::Never,
188 shell: Shell::System,
189 tags: vec![],
190 show_summary: true,
191 show_command: true,
192 };
193
194 let scenario =
195 locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
196
197 assert!(scenario.is_some());
198 let scenario = scenario.unwrap();
199 assert_eq!(scenario.adapter, "Delve");
200 assert_eq!(scenario.label, "test label");
201 assert!(scenario.build.is_some());
202
203 if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
204 assert_eq!(task_template.command, "go");
205 assert!(task_template.args.contains(&"build".into()));
206 assert!(
207 task_template
208 .args
209 .contains(&"-gcflags \"all=-N -l\"".into())
210 );
211 assert!(task_template.args.contains(&"main.go".into()));
212 } else {
213 panic!("Expected BuildTaskDefinition::Template");
214 }
215
216 assert!(
217 scenario.config.is_null(),
218 "Initial config should be null to ensure it's invalid"
219 );
220 }
221
222 #[test]
223 fn test_create_scenario_for_go_build() {
224 let locator = GoLocator;
225 let task = TaskTemplate {
226 label: "go build".into(),
227 command: "go".into(),
228 args: vec!["build".into(), ".".into()],
229 env: Default::default(),
230 cwd: Some("${ZED_WORKTREE_ROOT}".into()),
231 use_new_terminal: false,
232 allow_concurrent_runs: false,
233 reveal: RevealStrategy::Always,
234 reveal_target: RevealTarget::Dock,
235 hide: HideStrategy::Never,
236 shell: Shell::System,
237 tags: vec![],
238 show_summary: true,
239 show_command: true,
240 };
241
242 let scenario =
243 locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
244
245 assert!(scenario.is_none());
246 }
247
248 #[test]
249 fn test_skip_non_go_commands_with_non_delve_adapter() {
250 let locator = GoLocator;
251 let task = TaskTemplate {
252 label: "cargo build".into(),
253 command: "cargo".into(),
254 args: vec!["build".into()],
255 env: Default::default(),
256 cwd: Some("${ZED_WORKTREE_ROOT}".into()),
257 use_new_terminal: false,
258 allow_concurrent_runs: false,
259 reveal: RevealStrategy::Always,
260 reveal_target: RevealTarget::Dock,
261 hide: HideStrategy::Never,
262 shell: Shell::System,
263 tags: vec![],
264 show_summary: true,
265 show_command: true,
266 };
267
268 let scenario = locator.create_scenario(
269 &task,
270 "test label",
271 DebugAdapterName("SomeOtherAdapter".into()),
272 );
273 assert!(scenario.is_none());
274
275 let scenario =
276 locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
277 assert!(scenario.is_none());
278 }
279
280 #[test]
281 fn test_create_scenario_for_go_test() {
282 let locator = GoLocator;
283 let task = TaskTemplate {
284 label: "go test".into(),
285 command: "go".into(),
286 args: vec!["test".into(), ".".into()],
287 env: Default::default(),
288 cwd: Some("${ZED_WORKTREE_ROOT}".into()),
289 use_new_terminal: false,
290 allow_concurrent_runs: false,
291 reveal: RevealStrategy::Always,
292 reveal_target: RevealTarget::Dock,
293 hide: HideStrategy::Never,
294 shell: Shell::System,
295 tags: vec![],
296 show_summary: true,
297 show_command: true,
298 };
299
300 let scenario =
301 locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
302
303 assert!(scenario.is_some());
304 let scenario = scenario.unwrap();
305 assert_eq!(scenario.adapter, "Delve");
306 assert_eq!(scenario.label, "test label");
307 assert!(scenario.build.is_some());
308
309 if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
310 assert_eq!(task_template.command, "go");
311 assert!(task_template.args.contains(&"test".into()));
312 assert!(task_template.args.contains(&"-c".into()));
313 assert!(
314 task_template
315 .args
316 .contains(&"-gcflags \"all=-N -l\"".into())
317 );
318 assert!(task_template.args.contains(&"-o".into()));
319 assert!(
320 task_template
321 .args
322 .iter()
323 .any(|arg| arg.starts_with("__debug_"))
324 );
325 } else {
326 panic!("Expected BuildTaskDefinition::Template");
327 }
328
329 assert!(
330 scenario.config.is_null(),
331 "Initial config should be null to ensure it's invalid"
332 );
333 }
334
335 #[test]
336 fn test_create_scenario_for_go_test_with_cwd_binary() {
337 let locator = GoLocator;
338
339 let task = TaskTemplate {
340 label: "go test".into(),
341 command: "go".into(),
342 args: vec!["test".into(), ".".into()],
343 env: Default::default(),
344 cwd: Some("${ZED_WORKTREE_ROOT}".into()),
345 use_new_terminal: false,
346 allow_concurrent_runs: false,
347 reveal: RevealStrategy::Always,
348 reveal_target: RevealTarget::Dock,
349 hide: HideStrategy::Never,
350 shell: Shell::System,
351 tags: vec![],
352 show_summary: true,
353 show_command: true,
354 };
355
356 let scenario =
357 locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
358
359 assert!(scenario.is_some());
360 let scenario = scenario.unwrap();
361
362 if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
363 assert!(
364 task_template
365 .args
366 .iter()
367 .any(|arg| arg.starts_with("__debug_"))
368 );
369 } else {
370 panic!("Expected BuildTaskDefinition::Template");
371 }
372 }
373
374 #[test]
375 fn test_skip_unsupported_go_commands() {
376 let locator = GoLocator;
377 let task = TaskTemplate {
378 label: "go clean".into(),
379 command: "go".into(),
380 args: vec!["clean".into()],
381 env: Default::default(),
382 cwd: Some("${ZED_WORKTREE_ROOT}".into()),
383 use_new_terminal: false,
384 allow_concurrent_runs: false,
385 reveal: RevealStrategy::Always,
386 reveal_target: RevealTarget::Dock,
387 hide: HideStrategy::Never,
388 shell: Shell::System,
389 tags: vec![],
390 show_summary: true,
391 show_command: true,
392 };
393
394 let scenario =
395 locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
396 assert!(scenario.is_none());
397 }
398
399 #[test]
400 fn test_run_go_test_missing_binary_path() {
401 let locator = GoLocator;
402 let build_config = SpawnInTerminal {
403 id: TaskId("test_task".to_string()),
404 full_label: "go test".to_string(),
405 label: "go test".to_string(),
406 command: "go".into(),
407 args: vec![
408 "test".into(),
409 "-c".into(),
410 "-gcflags \"all=-N -l\"".into(),
411 "-o".into(),
412 ], // Missing the binary path (arg 4)
413 command_label: "go test -c -gcflags \"all=-N -l\" -o".to_string(),
414 env: Default::default(),
415 cwd: Some(PathBuf::from("/test/path")),
416 use_new_terminal: false,
417 allow_concurrent_runs: false,
418 reveal: RevealStrategy::Always,
419 reveal_target: RevealTarget::Dock,
420 hide: HideStrategy::Never,
421 shell: Shell::System,
422 show_summary: true,
423 show_command: true,
424 show_rerun: true,
425 };
426
427 let result = futures::executor::block_on(locator.run(build_config));
428 assert!(result.is_err());
429 assert!(
430 result
431 .unwrap_err()
432 .to_string()
433 .contains("can't locate debug binary")
434 );
435 }
436}