go.rs

  1use anyhow::Result;
  2use async_trait::async_trait;
  3use collections::HashMap;
  4use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
  5use gpui::SharedString;
  6use serde::{Deserialize, Serialize};
  7use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
  8
  9pub(crate) struct GoLocator;
 10
 11#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
 12#[serde(rename_all = "camelCase")]
 13struct DelveLaunchRequest {
 14    request: String,
 15    mode: String,
 16    program: String,
 17    #[serde(skip_serializing_if = "Option::is_none")]
 18    cwd: Option<String>,
 19    args: Vec<String>,
 20    build_flags: Vec<String>,
 21    env: HashMap<String, String>,
 22}
 23
 24fn is_debug_flag(arg: &str) -> Option<bool> {
 25    let mut part = if let Some(suffix) = arg.strip_prefix("test.") {
 26        suffix
 27    } else {
 28        arg
 29    };
 30    let mut might_have_arg = true;
 31    if let Some(idx) = part.find('=') {
 32        might_have_arg = false;
 33        part = &part[..idx];
 34    }
 35    match part {
 36        "benchmem" | "failfast" | "fullpath" | "fuzzworker" | "json" | "short" | "v"
 37        | "paniconexit0" => Some(false),
 38        "bench"
 39        | "benchtime"
 40        | "blockprofile"
 41        | "blockprofilerate"
 42        | "count"
 43        | "coverprofile"
 44        | "cpu"
 45        | "cpuprofile"
 46        | "fuzz"
 47        | "fuzzcachedir"
 48        | "fuzzminimizetime"
 49        | "fuzztime"
 50        | "gocoverdir"
 51        | "list"
 52        | "memprofile"
 53        | "memprofilerate"
 54        | "mutexprofile"
 55        | "mutexprofilefraction"
 56        | "outputdir"
 57        | "parallel"
 58        | "run"
 59        | "shuffle"
 60        | "skip"
 61        | "testlogfile"
 62        | "timeout"
 63        | "trace" => Some(might_have_arg),
 64        _ if arg.starts_with("test.") => Some(false),
 65        _ => None,
 66    }
 67}
 68
 69fn is_build_flag(mut arg: &str) -> Option<bool> {
 70    let mut might_have_arg = true;
 71    if let Some(idx) = arg.find('=') {
 72        might_have_arg = false;
 73        arg = &arg[..idx];
 74    }
 75    match arg {
 76        "a" | "n" | "race" | "msan" | "asan" | "cover" | "work" | "x" | "v" | "buildvcs"
 77        | "json" | "linkshared" | "modcacherw" | "trimpath" => Some(false),
 78
 79        "p" | "covermode" | "coverpkg" | "asmflags" | "buildmode" | "compiler" | "gccgoflags"
 80        | "gcflags" | "installsuffix" | "ldflags" | "mod" | "modfile" | "overlay" | "pgo"
 81        | "pkgdir" | "tags" | "toolexec" => Some(might_have_arg),
 82        _ => None,
 83    }
 84}
 85
 86#[async_trait]
 87impl DapLocator for GoLocator {
 88    fn name(&self) -> SharedString {
 89        SharedString::new_static("go-debug-locator")
 90    }
 91
 92    async fn create_scenario(
 93        &self,
 94        build_config: &TaskTemplate,
 95        resolved_label: &str,
 96        adapter: &DebugAdapterName,
 97    ) -> Option<DebugScenario> {
 98        if build_config.command != "go" {
 99            return None;
100        }
101        let go_action = build_config.args.first()?;
102
103        match go_action.as_str() {
104            "test" => {
105                let mut program = ".".to_string();
106                let mut args = Vec::default();
107                let mut build_flags = Vec::default();
108
109                let mut all_args_are_test = false;
110                let mut next_arg_is_test = false;
111                let mut next_arg_is_build = false;
112                let mut seen_pkg = false;
113                let mut seen_v = false;
114
115                for arg in build_config.args.iter().skip(1) {
116                    if all_args_are_test || next_arg_is_test {
117                        // HACK: tasks assume that they are run in a shell context,
118                        // so the -run regex has escaped specials. Delve correctly
119                        // handles escaping, so we undo that here.
120                        if arg.starts_with("\\^") && arg.ends_with("\\$") {
121                            let mut arg = arg[1..arg.len() - 2].to_string();
122                            arg.push('$');
123                            args.push(arg);
124                        } else {
125                            args.push(arg.clone());
126                        }
127                        next_arg_is_test = false;
128                    } else if next_arg_is_build {
129                        build_flags.push(arg.clone());
130                        next_arg_is_build = false;
131                    } else if arg.starts_with('-') {
132                        let flag = arg.trim_start_matches('-');
133                        if flag == "args" {
134                            all_args_are_test = true;
135                        } else if let Some(has_arg) = is_debug_flag(flag) {
136                            if flag == "v" || flag == "test.v" {
137                                seen_v = true;
138                            }
139                            if flag.starts_with("test.") {
140                                args.push(arg.clone());
141                            } else {
142                                args.push(format!("-test.{flag}"))
143                            }
144                            next_arg_is_test = has_arg;
145                        } else if let Some(has_arg) = is_build_flag(flag) {
146                            build_flags.push(arg.clone());
147                            next_arg_is_build = has_arg;
148                        }
149                    } else if !seen_pkg {
150                        program = arg.clone();
151                        seen_pkg = true;
152                    } else {
153                        args.push(arg.clone());
154                    }
155                }
156                if !seen_v {
157                    args.push("-test.v".to_string());
158                }
159
160                let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest {
161                    request: "launch".to_string(),
162                    mode: "test".to_string(),
163                    program,
164                    args: args,
165                    build_flags,
166                    cwd: build_config.cwd.clone(),
167                    env: build_config.env.clone(),
168                })
169                .unwrap();
170
171                Some(DebugScenario {
172                    label: resolved_label.to_string().into(),
173                    adapter: adapter.0.clone(),
174                    build: None,
175                    config: config,
176                    tcp_connection: None,
177                })
178            }
179            "run" => {
180                let mut next_arg_is_build = false;
181                let mut seen_pkg = false;
182
183                let mut program = ".".to_string();
184                let mut args = Vec::default();
185                let mut build_flags = Vec::default();
186
187                for arg in build_config.args.iter().skip(1) {
188                    if seen_pkg {
189                        args.push(arg.clone())
190                    } else if next_arg_is_build {
191                        build_flags.push(arg.clone());
192                        next_arg_is_build = false;
193                    } else if arg.starts_with("-") {
194                        if let Some(has_arg) = is_build_flag(arg.trim_start_matches("-")) {
195                            next_arg_is_build = has_arg;
196                        }
197                        build_flags.push(arg.clone())
198                    } else {
199                        program = arg.to_string();
200                        seen_pkg = true;
201                    }
202                }
203
204                let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest {
205                    cwd: build_config.cwd.clone(),
206                    env: build_config.env.clone(),
207                    request: "launch".to_string(),
208                    mode: "debug".to_string(),
209                    program,
210                    args: args,
211                    build_flags,
212                })
213                .unwrap();
214
215                Some(DebugScenario {
216                    label: resolved_label.to_string().into(),
217                    adapter: adapter.0.clone(),
218                    build: None,
219                    config,
220                    tcp_connection: None,
221                })
222            }
223            _ => None,
224        }
225    }
226
227    async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
228        unreachable!()
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use gpui::TestAppContext;
236    use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskTemplate};
237
238    #[gpui::test]
239    async fn test_create_scenario_for_go_build(_: &mut TestAppContext) {
240        let locator = GoLocator;
241        let task = TaskTemplate {
242            label: "go build".into(),
243            command: "go".into(),
244            args: vec!["build".into(), ".".into()],
245            env: Default::default(),
246            cwd: Some("${ZED_WORKTREE_ROOT}".into()),
247            use_new_terminal: false,
248            allow_concurrent_runs: false,
249            reveal: RevealStrategy::Always,
250            reveal_target: RevealTarget::Dock,
251            hide: HideStrategy::Never,
252            shell: Shell::System,
253            tags: vec![],
254            show_summary: true,
255            show_command: true,
256        };
257
258        let scenario = locator
259            .create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
260            .await;
261
262        assert!(scenario.is_none());
263    }
264
265    #[gpui::test]
266    async fn test_skip_non_go_commands_with_non_delve_adapter(_: &mut TestAppContext) {
267        let locator = GoLocator;
268        let task = TaskTemplate {
269            label: "cargo build".into(),
270            command: "cargo".into(),
271            args: vec!["build".into()],
272            env: Default::default(),
273            cwd: Some("${ZED_WORKTREE_ROOT}".into()),
274            use_new_terminal: false,
275            allow_concurrent_runs: false,
276            reveal: RevealStrategy::Always,
277            reveal_target: RevealTarget::Dock,
278            hide: HideStrategy::Never,
279            shell: Shell::System,
280            tags: vec![],
281            show_summary: true,
282            show_command: true,
283        };
284
285        let scenario = locator
286            .create_scenario(
287                &task,
288                "test label",
289                &DebugAdapterName("SomeOtherAdapter".into()),
290            )
291            .await;
292        assert!(scenario.is_none());
293
294        let scenario = locator
295            .create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
296            .await;
297        assert!(scenario.is_none());
298    }
299    #[gpui::test]
300    async fn test_go_locator_run(_: &mut TestAppContext) {
301        let locator = GoLocator;
302        let delve = DebugAdapterName("Delve".into());
303
304        let task = TaskTemplate {
305            label: "go run with flags".into(),
306            command: "go".into(),
307            args: vec![
308                "run".to_string(),
309                "-race".to_string(),
310                "-ldflags".to_string(),
311                "-X main.version=1.0".to_string(),
312                "./cmd/myapp".to_string(),
313                "--config".to_string(),
314                "production.yaml".to_string(),
315                "--verbose".to_string(),
316            ],
317            env: {
318                let mut env = HashMap::default();
319                env.insert("GO_ENV".to_string(), "production".to_string());
320                env
321            },
322            cwd: Some("/project/root".into()),
323            ..Default::default()
324        };
325
326        let scenario = locator
327            .create_scenario(&task, "test run label", &delve)
328            .await
329            .unwrap();
330
331        let config: DelveLaunchRequest = serde_json::from_value(scenario.config).unwrap();
332
333        assert_eq!(
334            config,
335            DelveLaunchRequest {
336                request: "launch".to_string(),
337                mode: "debug".to_string(),
338                program: "./cmd/myapp".to_string(),
339                build_flags: vec![
340                    "-race".to_string(),
341                    "-ldflags".to_string(),
342                    "-X main.version=1.0".to_string()
343                ],
344                args: vec![
345                    "--config".to_string(),
346                    "production.yaml".to_string(),
347                    "--verbose".to_string(),
348                ],
349                env: {
350                    let mut env = HashMap::default();
351                    env.insert("GO_ENV".to_string(), "production".to_string());
352                    env
353                },
354                cwd: Some("/project/root".to_string()),
355            }
356        );
357    }
358
359    #[gpui::test]
360    async fn test_go_locator_test(_: &mut TestAppContext) {
361        let locator = GoLocator;
362        let delve = DebugAdapterName("Delve".into());
363
364        // Test with tags and run flag
365        let task_with_tags = TaskTemplate {
366            label: "test".into(),
367            command: "go".into(),
368            args: vec![
369                "test".to_string(),
370                "-tags".to_string(),
371                "integration,unit".to_string(),
372                "-run".to_string(),
373                "Foo".to_string(),
374                ".".to_string(),
375            ],
376            ..Default::default()
377        };
378        let result = locator
379            .create_scenario(&task_with_tags, "", &delve)
380            .await
381            .unwrap();
382
383        let config: DelveLaunchRequest = serde_json::from_value(result.config).unwrap();
384
385        assert_eq!(
386            config,
387            DelveLaunchRequest {
388                request: "launch".to_string(),
389                mode: "test".to_string(),
390                program: ".".to_string(),
391                build_flags: vec!["-tags".to_string(), "integration,unit".to_string(),],
392                args: vec![
393                    "-test.run".to_string(),
394                    "Foo".to_string(),
395                    "-test.v".to_string()
396                ],
397                env: HashMap::default(),
398                cwd: None,
399            }
400        );
401    }
402
403    #[gpui::test]
404    async fn test_skip_unsupported_go_commands(_: &mut TestAppContext) {
405        let locator = GoLocator;
406        let task = TaskTemplate {
407            label: "go clean".into(),
408            command: "go".into(),
409            args: vec!["clean".into()],
410            env: Default::default(),
411            cwd: Some("${ZED_WORKTREE_ROOT}".into()),
412            use_new_terminal: false,
413            allow_concurrent_runs: false,
414            reveal: RevealStrategy::Always,
415            reveal_target: RevealTarget::Dock,
416            hide: HideStrategy::Never,
417            shell: Shell::System,
418            tags: vec![],
419            show_summary: true,
420            show_command: true,
421        };
422
423        let scenario = locator
424            .create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
425            .await;
426        assert!(scenario.is_none());
427    }
428}