go.rs

  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}