1use anyhow::Result;
2use async_trait::async_trait;
3use collections::HashMap;
4use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
5use gpui::{BackgroundExecutor, 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(
241 &self,
242 _build_config: SpawnInTerminal,
243 _executor: BackgroundExecutor,
244 ) -> Result<DebugRequest> {
245 unreachable!()
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use gpui::TestAppContext;
253 use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskTemplate};
254
255 #[gpui::test]
256 async fn test_create_scenario_for_go_build(_: &mut TestAppContext) {
257 let locator = GoLocator;
258 let task = TaskTemplate {
259 label: "go build".into(),
260 command: "go".into(),
261 args: vec!["build".into(), ".".into()],
262 env: Default::default(),
263 cwd: Some("${ZED_WORKTREE_ROOT}".into()),
264 use_new_terminal: false,
265 allow_concurrent_runs: false,
266 reveal: RevealStrategy::Always,
267 reveal_target: RevealTarget::Dock,
268 hide: HideStrategy::Never,
269 shell: Shell::System,
270 tags: vec![],
271 show_summary: true,
272 show_command: true,
273 };
274
275 let scenario = locator
276 .create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
277 .await;
278
279 assert!(scenario.is_none());
280 }
281
282 #[gpui::test]
283 async fn test_skip_non_go_commands_with_non_delve_adapter(_: &mut TestAppContext) {
284 let locator = GoLocator;
285 let task = TaskTemplate {
286 label: "cargo build".into(),
287 command: "cargo".into(),
288 args: vec!["build".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 = locator
303 .create_scenario(
304 &task,
305 "test label",
306 &DebugAdapterName("SomeOtherAdapter".into()),
307 )
308 .await;
309 assert!(scenario.is_none());
310
311 let scenario = locator
312 .create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
313 .await;
314 assert!(scenario.is_none());
315 }
316 #[gpui::test]
317 async fn test_go_locator_run(_: &mut TestAppContext) {
318 let locator = GoLocator;
319 let delve = DebugAdapterName("Delve".into());
320
321 let task = TaskTemplate {
322 label: "go run with flags".into(),
323 command: "go".into(),
324 args: vec![
325 "run".to_string(),
326 "-race".to_string(),
327 "-ldflags".to_string(),
328 "-X main.version=1.0".to_string(),
329 "./cmd/myapp".to_string(),
330 "--config".to_string(),
331 "production.yaml".to_string(),
332 "--verbose".to_string(),
333 ],
334 env: {
335 let mut env = HashMap::default();
336 env.insert("GO_ENV".to_string(), "production".to_string());
337 env
338 },
339 cwd: Some("/project/root".into()),
340 ..Default::default()
341 };
342
343 let scenario = locator
344 .create_scenario(&task, "test run label", &delve)
345 .await
346 .unwrap();
347
348 let config: DelveLaunchRequest = serde_json::from_value(scenario.config).unwrap();
349
350 assert_eq!(
351 config,
352 DelveLaunchRequest {
353 request: "launch".to_string(),
354 mode: "debug".to_string(),
355 program: "./cmd/myapp".to_string(),
356 build_flags: vec![
357 "-race".to_string(),
358 "-ldflags".to_string(),
359 "-X main.version=1.0".to_string()
360 ],
361 args: vec![
362 "--config".to_string(),
363 "production.yaml".to_string(),
364 "--verbose".to_string(),
365 ],
366 env: {
367 let mut env = HashMap::default();
368 env.insert("GO_ENV".to_string(), "production".to_string());
369 env
370 },
371 cwd: Some("/project/root".to_string()),
372 }
373 );
374 }
375
376 #[gpui::test]
377 async fn test_go_locator_test(_: &mut TestAppContext) {
378 let locator = GoLocator;
379 let delve = DebugAdapterName("Delve".into());
380
381 // Test with tags and run flag
382 let task_with_tags = TaskTemplate {
383 label: "test".into(),
384 command: "go".into(),
385 args: vec![
386 "test".to_string(),
387 "-tags".to_string(),
388 "integration,unit".to_string(),
389 "-run".to_string(),
390 "Foo".to_string(),
391 ".".to_string(),
392 ],
393 ..Default::default()
394 };
395 let result = locator
396 .create_scenario(&task_with_tags, "", &delve)
397 .await
398 .unwrap();
399
400 let config: DelveLaunchRequest = serde_json::from_value(result.config).unwrap();
401
402 assert_eq!(
403 config,
404 DelveLaunchRequest {
405 request: "launch".to_string(),
406 mode: "test".to_string(),
407 program: ".".to_string(),
408 build_flags: vec!["-tags".to_string(), "integration,unit".to_string(),],
409 args: vec![
410 "-test.run".to_string(),
411 "Foo".to_string(),
412 "-test.v".to_string()
413 ],
414 env: HashMap::default(),
415 cwd: None,
416 }
417 );
418 }
419
420 #[gpui::test]
421 async fn test_skip_unsupported_go_commands(_: &mut TestAppContext) {
422 let locator = GoLocator;
423 let task = TaskTemplate {
424 label: "go clean".into(),
425 command: "go".into(),
426 args: vec!["clean".into()],
427 env: Default::default(),
428 cwd: Some("${ZED_WORKTREE_ROOT}".into()),
429 use_new_terminal: false,
430 allow_concurrent_runs: false,
431 reveal: RevealStrategy::Always,
432 reveal_target: RevealTarget::Dock,
433 hide: HideStrategy::Never,
434 shell: Shell::System,
435 tags: vec![],
436 show_summary: true,
437 show_command: true,
438 };
439
440 let scenario = locator
441 .create_scenario(&task, "test label", &DebugAdapterName("Delve".into()))
442 .await;
443 assert!(scenario.is_none());
444 }
445}