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 let Some((left, right)) = arg.split_once("/")
121                            && left.starts_with("\\^")
122                            && left.ends_with("\\$")
123                            && right.starts_with("\\^")
124                            && right.ends_with("\\$")
125                        {
126                            let mut left = left[1..left.len() - 2].to_string();
127                            left.push('$');
128
129                            let mut right = right[1..right.len() - 2].to_string();
130                            right.push('$');
131
132                            args.push(format!("{left}/{right}"));
133                        } else if arg.starts_with("\\^") && arg.ends_with("\\$") {
134                            let mut arg = arg[1..arg.len() - 2].to_string();
135                            arg.push('$');
136                            args.push(arg);
137                        } else {
138                            args.push(arg.clone());
139                        }
140                        next_arg_is_test = false;
141                    } else if next_arg_is_build {
142                        build_flags.push(arg.clone());
143                        next_arg_is_build = false;
144                    } else if arg.starts_with('-') {
145                        let flag = arg.trim_start_matches('-');
146                        if flag == "args" {
147                            all_args_are_test = true;
148                        } else if let Some(has_arg) = is_debug_flag(flag) {
149                            if flag == "v" || flag == "test.v" {
150                                seen_v = true;
151                            }
152                            if flag.starts_with("test.") {
153                                args.push(arg.clone());
154                            } else {
155                                args.push(format!("-test.{flag}"))
156                            }
157                            next_arg_is_test = has_arg;
158                        } else if let Some(has_arg) = is_build_flag(flag) {
159                            build_flags.push(arg.clone());
160                            next_arg_is_build = has_arg;
161                        }
162                    } else if !seen_pkg {
163                        program = arg.clone();
164                        seen_pkg = true;
165                    } else {
166                        args.push(arg.clone());
167                    }
168                }
169                if !seen_v {
170                    args.push("-test.v".to_string());
171                }
172
173                let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest {
174                    request: "launch".to_string(),
175                    mode: "test".to_string(),
176                    program,
177                    args,
178                    build_flags,
179                    cwd: build_config.cwd.clone(),
180                    env: build_config.env.clone(),
181                })
182                .unwrap();
183
184                Some(DebugScenario {
185                    label: resolved_label.to_string().into(),
186                    adapter: adapter.0.clone(),
187                    build: None,
188                    config,
189                    tcp_connection: None,
190                })
191            }
192            "run" => {
193                let mut next_arg_is_build = false;
194                let mut seen_pkg = false;
195
196                let mut program = ".".to_string();
197                let mut args = Vec::default();
198                let mut build_flags = Vec::default();
199
200                for arg in build_config.args.iter().skip(1) {
201                    if seen_pkg {
202                        args.push(arg.clone())
203                    } else if next_arg_is_build {
204                        build_flags.push(arg.clone());
205                        next_arg_is_build = false;
206                    } else if arg.starts_with("-") {
207                        if let Some(has_arg) = is_build_flag(arg.trim_start_matches("-")) {
208                            next_arg_is_build = has_arg;
209                        }
210                        build_flags.push(arg.clone())
211                    } else {
212                        program = arg.to_string();
213                        seen_pkg = true;
214                    }
215                }
216
217                let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest {
218                    cwd: build_config.cwd.clone(),
219                    env: build_config.env.clone(),
220                    request: "launch".to_string(),
221                    mode: "debug".to_string(),
222                    program,
223                    args,
224                    build_flags,
225                })
226                .unwrap();
227
228                Some(DebugScenario {
229                    label: resolved_label.to_string().into(),
230                    adapter: adapter.0.clone(),
231                    build: None,
232                    config,
233                    tcp_connection: None,
234                })
235            }
236            _ => None,
237        }
238    }
239
240    async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
241        unreachable!()
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use gpui::TestAppContext;
249    use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskTemplate};
250
251    #[gpui::test]
252    async fn test_create_scenario_for_go_build(_: &mut TestAppContext) {
253        let locator = GoLocator;
254        let task = TaskTemplate {
255            label: "go build".into(),
256            command: "go".into(),
257            args: vec!["build".into(), ".".into()],
258            env: Default::default(),
259            cwd: Some("${ZED_WORKTREE_ROOT}".into()),
260            use_new_terminal: false,
261            allow_concurrent_runs: false,
262            reveal: RevealStrategy::Always,
263            reveal_target: RevealTarget::Dock,
264            hide: HideStrategy::Never,
265            shell: Shell::System,
266            tags: vec![],
267            languages: vec![],
268            show_summary: true,
269            show_command: true,
270        };
271
272        let scenario = locator
273            .create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
274            .await;
275
276        assert!(scenario.is_none());
277    }
278
279    #[gpui::test]
280    async fn test_skip_non_go_commands_with_non_delve_adapter(_: &mut TestAppContext) {
281        let locator = GoLocator;
282        let task = TaskTemplate {
283            label: "cargo build".into(),
284            command: "cargo".into(),
285            args: vec!["build".into()],
286            env: Default::default(),
287            cwd: Some("${ZED_WORKTREE_ROOT}".into()),
288            use_new_terminal: false,
289            allow_concurrent_runs: false,
290            reveal: RevealStrategy::Always,
291            reveal_target: RevealTarget::Dock,
292            hide: HideStrategy::Never,
293            shell: Shell::System,
294            tags: vec![],
295            languages: vec![],
296            show_summary: true,
297            show_command: true,
298        };
299
300        let scenario = locator
301            .create_scenario(
302                &task,
303                "test label",
304                &DebugAdapterName("SomeOtherAdapter".into()),
305            )
306            .await;
307        assert!(scenario.is_none());
308
309        let scenario = locator
310            .create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
311            .await;
312        assert!(scenario.is_none());
313    }
314    #[gpui::test]
315    async fn test_go_locator_run(_: &mut TestAppContext) {
316        let locator = GoLocator;
317        let delve = DebugAdapterName("Delve".into());
318
319        let task = TaskTemplate {
320            label: "go run with flags".into(),
321            command: "go".into(),
322            args: vec![
323                "run".to_string(),
324                "-race".to_string(),
325                "-ldflags".to_string(),
326                "-X main.version=1.0".to_string(),
327                "./cmd/myapp".to_string(),
328                "--config".to_string(),
329                "production.yaml".to_string(),
330                "--verbose".to_string(),
331            ],
332            env: {
333                let mut env = HashMap::default();
334                env.insert("GO_ENV".to_string(), "production".to_string());
335                env
336            },
337            cwd: Some("/project/root".into()),
338            ..Default::default()
339        };
340
341        let scenario = locator
342            .create_scenario(&task, "test run label", &delve)
343            .await
344            .unwrap();
345
346        let config: DelveLaunchRequest = serde_json::from_value(scenario.config).unwrap();
347
348        assert_eq!(
349            config,
350            DelveLaunchRequest {
351                request: "launch".to_string(),
352                mode: "debug".to_string(),
353                program: "./cmd/myapp".to_string(),
354                build_flags: vec![
355                    "-race".to_string(),
356                    "-ldflags".to_string(),
357                    "-X main.version=1.0".to_string()
358                ],
359                args: vec![
360                    "--config".to_string(),
361                    "production.yaml".to_string(),
362                    "--verbose".to_string(),
363                ],
364                env: {
365                    let mut env = HashMap::default();
366                    env.insert("GO_ENV".to_string(), "production".to_string());
367                    env
368                },
369                cwd: Some("/project/root".to_string()),
370            }
371        );
372    }
373
374    #[gpui::test]
375    async fn test_go_locator_test(_: &mut TestAppContext) {
376        let locator = GoLocator;
377        let delve = DebugAdapterName("Delve".into());
378
379        // Test with tags and run flag
380        let task_with_tags = TaskTemplate {
381            label: "test".into(),
382            command: "go".into(),
383            args: vec![
384                "test".to_string(),
385                "-tags".to_string(),
386                "integration,unit".to_string(),
387                "-run".to_string(),
388                "Foo".to_string(),
389                ".".to_string(),
390            ],
391            ..Default::default()
392        };
393        let result = locator
394            .create_scenario(&task_with_tags, "", &delve)
395            .await
396            .unwrap();
397
398        let config: DelveLaunchRequest = serde_json::from_value(result.config).unwrap();
399
400        assert_eq!(
401            config,
402            DelveLaunchRequest {
403                request: "launch".to_string(),
404                mode: "test".to_string(),
405                program: ".".to_string(),
406                build_flags: vec!["-tags".to_string(), "integration,unit".to_string(),],
407                args: vec![
408                    "-test.run".to_string(),
409                    "Foo".to_string(),
410                    "-test.v".to_string()
411                ],
412                env: HashMap::default(),
413                cwd: None,
414            }
415        );
416    }
417
418    #[gpui::test]
419    async fn test_skip_unsupported_go_commands(_: &mut TestAppContext) {
420        let locator = GoLocator;
421        let task = TaskTemplate {
422            label: "go clean".into(),
423            command: "go".into(),
424            args: vec!["clean".into()],
425            env: Default::default(),
426            cwd: Some("${ZED_WORKTREE_ROOT}".into()),
427            use_new_terminal: false,
428            allow_concurrent_runs: false,
429            reveal: RevealStrategy::Always,
430            reveal_target: RevealTarget::Dock,
431            hide: HideStrategy::Never,
432            shell: Shell::System,
433            tags: vec![],
434            languages: vec![],
435            show_summary: true,
436            show_command: true,
437        };
438
439        let scenario = locator
440            .create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
441            .await;
442        assert!(scenario.is_none());
443    }
444}