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}