debugger: Add support for go tests (#31772)

Alex created

In the https://github.com/zed-industries/zed/pull/31559 I did not
introduce ability to debug test invocations.
Adding it here. E.g:
![Kapture 2025-05-30 at 19 59
13](https://github.com/user-attachments/assets/1111d4a5-8b0a-42e6-aa98-2d797f61ffe3)

Release Notes:
- Added support for debugging single tests written in go

Change summary

crates/project/src/debugger/locators/go.rs | 144 ++++++++++++++++++++++++
1 file changed, 144 insertions(+)

Detailed changes

crates/project/src/debugger/locators/go.rs 🔗

@@ -30,6 +30,47 @@ impl DapLocator for GoLocator {
         let go_action = build_config.args.first()?;
 
         match go_action.as_str() {
+            "test" => {
+                let binary_path = if build_config.env.contains_key("OUT_DIR") {
+                    "${OUT_DIR}/__debug".to_string()
+                } else {
+                    "__debug".to_string()
+                };
+
+                let build_task = TaskTemplate {
+                    label: "go test debug".into(),
+                    command: "go".into(),
+                    args: vec![
+                        "test".into(),
+                        "-c".into(),
+                        "-gcflags \"all=-N -l\"".into(),
+                        "-o".into(),
+                        binary_path,
+                    ],
+                    env: build_config.env.clone(),
+                    cwd: build_config.cwd.clone(),
+                    use_new_terminal: false,
+                    allow_concurrent_runs: false,
+                    reveal: RevealStrategy::Always,
+                    reveal_target: RevealTarget::Dock,
+                    hide: task::HideStrategy::Never,
+                    shell: Shell::System,
+                    tags: vec![],
+                    show_summary: true,
+                    show_command: true,
+                };
+
+                Some(DebugScenario {
+                    label: resolved_label.to_string().into(),
+                    adapter: adapter.0,
+                    build: Some(BuildTaskDefinition::Template {
+                        task_template: build_task,
+                        locator_name: Some(self.name()),
+                    }),
+                    config: serde_json::Value::Null,
+                    tcp_connection: None,
+                })
+            }
             "run" => {
                 let program = build_config
                     .args
@@ -91,6 +132,23 @@ impl DapLocator for GoLocator {
         }
 
         match go_action.as_str() {
+            "test" => {
+                let program = if let Some(out_dir) = build_config.env.get("OUT_DIR") {
+                    format!("{}/__debug", out_dir)
+                } else {
+                    PathBuf::from(&cwd)
+                        .join("__debug")
+                        .to_string_lossy()
+                        .to_string()
+                };
+
+                Ok(DebugRequest::Launch(task::LaunchRequest {
+                    program,
+                    cwd: Some(PathBuf::from(&cwd)),
+                    args: vec!["-test.v".into(), "-test.run=${ZED_SYMBOL}".into()],
+                    env,
+                }))
+            }
             "build" => {
                 let package = build_config
                     .args
@@ -221,6 +279,92 @@ mod tests {
         assert!(scenario.is_none());
     }
 
+    #[test]
+    fn test_create_scenario_for_go_test() {
+        let locator = GoLocator;
+        let task = TaskTemplate {
+            label: "go test".into(),
+            command: "go".into(),
+            args: vec!["test".into(), ".".into()],
+            env: Default::default(),
+            cwd: Some("${ZED_WORKTREE_ROOT}".into()),
+            use_new_terminal: false,
+            allow_concurrent_runs: false,
+            reveal: RevealStrategy::Always,
+            reveal_target: RevealTarget::Dock,
+            hide: HideStrategy::Never,
+            shell: Shell::System,
+            tags: vec![],
+            show_summary: true,
+            show_command: true,
+        };
+
+        let scenario =
+            locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
+
+        assert!(scenario.is_some());
+        let scenario = scenario.unwrap();
+        assert_eq!(scenario.adapter, "Delve");
+        assert_eq!(scenario.label, "test label");
+        assert!(scenario.build.is_some());
+
+        if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
+            assert_eq!(task_template.command, "go");
+            assert!(task_template.args.contains(&"test".into()));
+            assert!(task_template.args.contains(&"-c".into()));
+            assert!(
+                task_template
+                    .args
+                    .contains(&"-gcflags \"all=-N -l\"".into())
+            );
+            assert!(task_template.args.contains(&"-o".into()));
+            assert!(task_template.args.contains(&"__debug".into()));
+        } else {
+            panic!("Expected BuildTaskDefinition::Template");
+        }
+
+        assert!(
+            scenario.config.is_null(),
+            "Initial config should be null to ensure it's invalid"
+        );
+    }
+
+    #[test]
+    fn test_create_scenario_for_go_test_with_out_dir() {
+        let locator = GoLocator;
+        let mut env = FxHashMap::default();
+        env.insert("OUT_DIR".to_string(), "/tmp/build".to_string());
+
+        let task = TaskTemplate {
+            label: "go test".into(),
+            command: "go".into(),
+            args: vec!["test".into(), ".".into()],
+            env,
+            cwd: Some("${ZED_WORKTREE_ROOT}".into()),
+            use_new_terminal: false,
+            allow_concurrent_runs: false,
+            reveal: RevealStrategy::Always,
+            reveal_target: RevealTarget::Dock,
+            hide: HideStrategy::Never,
+            shell: Shell::System,
+            tags: vec![],
+            show_summary: true,
+            show_command: true,
+        };
+
+        let scenario =
+            locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
+
+        assert!(scenario.is_some());
+        let scenario = scenario.unwrap();
+
+        if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
+            assert!(task_template.args.contains(&"${OUT_DIR}/__debug".into()));
+        } else {
+            panic!("Expected BuildTaskDefinition::Template");
+        }
+    }
+
     #[test]
     fn test_skip_unsupported_go_commands() {
         let locator = GoLocator;