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