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};
 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}