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