1use anyhow::{Context as _, bail};
2use collections::HashMap;
3use dap::{
4 StartDebuggingRequestArguments,
5 adapters::{
6 DebugTaskDefinition, DownloadedFileType, TcpArguments, download_adapter_from_github,
7 latest_github_release,
8 },
9};
10use fs::Fs;
11use futures::StreamExt;
12use gpui::{AsyncApp, SharedString};
13use language::LanguageName;
14use log::warn;
15use serde_json::{Map, Value};
16use task::TcpArgumentsTemplate;
17use util;
18
19use std::{
20 env::consts,
21 ffi::OsStr,
22 path::{Path, PathBuf},
23 str::FromStr,
24 sync::OnceLock,
25};
26
27use crate::*;
28
29#[derive(Default, Debug)]
30pub(crate) struct GoDebugAdapter {
31 shim_path: OnceLock<PathBuf>,
32}
33
34impl GoDebugAdapter {
35 const ADAPTER_NAME: &'static str = "Delve";
36 async fn fetch_latest_adapter_version(
37 delegate: &Arc<dyn DapDelegate>,
38 ) -> Result<AdapterVersion> {
39 let release = latest_github_release(
40 "zed-industries/delve-shim-dap",
41 true,
42 false,
43 delegate.http_client(),
44 )
45 .await?;
46
47 let os = match consts::OS {
48 "macos" => "apple-darwin",
49 "linux" => "unknown-linux-gnu",
50 "windows" => "pc-windows-msvc",
51 other => bail!("Running on unsupported os: {other}"),
52 };
53 let suffix = if consts::OS == "windows" {
54 ".zip"
55 } else {
56 ".tar.gz"
57 };
58 let asset_name = format!("delve-shim-dap-{}-{os}{suffix}", consts::ARCH);
59 let asset = release
60 .assets
61 .iter()
62 .find(|asset| asset.name == asset_name)
63 .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
64
65 Ok(AdapterVersion {
66 tag_name: release.tag_name,
67 url: asset.browser_download_url.clone(),
68 })
69 }
70 async fn install_shim(&self, delegate: &Arc<dyn DapDelegate>) -> anyhow::Result<PathBuf> {
71 if let Some(path) = self.shim_path.get().cloned() {
72 return Ok(path);
73 }
74
75 let adapter_dir = paths::debug_adapters_dir().join("delve-shim-dap");
76
77 match Self::fetch_latest_adapter_version(delegate).await {
78 Ok(asset) => {
79 let ty = if consts::OS == "windows" {
80 DownloadedFileType::Zip
81 } else {
82 DownloadedFileType::GzipTar
83 };
84 download_adapter_from_github(
85 "delve-shim-dap".into(),
86 asset.clone(),
87 ty,
88 delegate.as_ref(),
89 )
90 .await?;
91
92 let path = adapter_dir
93 .join(format!("delve-shim-dap_{}", asset.tag_name))
94 .join(format!("delve-shim-dap{}", consts::EXE_SUFFIX));
95 self.shim_path.set(path.clone()).ok();
96
97 Ok(path)
98 }
99 Err(error) => {
100 let binary_name = format!("delve-shim-dap{}", consts::EXE_SUFFIX);
101 let mut cached = None;
102 if let Ok(mut entries) = delegate.fs().read_dir(&adapter_dir).await {
103 while let Some(entry) = entries.next().await {
104 if let Ok(version_dir) = entry {
105 let candidate = version_dir.join(&binary_name);
106 if delegate
107 .fs()
108 .metadata(&candidate)
109 .await
110 .is_ok_and(|m| m.is_some())
111 {
112 cached = Some(candidate);
113 break;
114 }
115 }
116 }
117 }
118
119 if let Some(path) = cached {
120 warn!("Failed to fetch latest delve-shim-dap, using cached version: {error:#}");
121 self.shim_path.set(path.clone()).ok();
122 Ok(path)
123 } else {
124 Err(error)
125 }
126 }
127 }
128 }
129}
130
131#[async_trait(?Send)]
132impl DebugAdapter for GoDebugAdapter {
133 fn name(&self) -> DebugAdapterName {
134 DebugAdapterName(Self::ADAPTER_NAME.into())
135 }
136
137 fn adapter_language_name(&self) -> Option<LanguageName> {
138 Some(SharedString::new_static("Go").into())
139 }
140
141 fn dap_schema(&self) -> serde_json::Value {
142 // Create common properties shared between launch and attach
143 let common_properties = json!({
144 "debugAdapter": {
145 "enum": ["legacy", "dlv-dap"],
146 "description": "Select which debug adapter to use with this configuration.",
147 "default": "dlv-dap"
148 },
149 "stopOnEntry": {
150 "type": "boolean",
151 "description": "Automatically stop program after launch or attach.",
152 "default": false
153 },
154 "showLog": {
155 "type": "boolean",
156 "description": "Show log output from the delve debugger. Maps to dlv's `--log` flag.",
157 "default": false
158 },
159 "cwd": {
160 "type": "string",
161 "description": "Workspace relative or absolute path to the working directory of the program being debugged.",
162 "default": "${ZED_WORKTREE_ROOT}"
163 },
164 "dlvFlags": {
165 "type": "array",
166 "description": "Extra flags for `dlv`. See `dlv help` for the full list of supported flags.",
167 "items": {
168 "type": "string"
169 },
170 "default": []
171 },
172 "port": {
173 "type": "number",
174 "description": "Debug server port. For remote configurations, this is where to connect.",
175 "default": 2345
176 },
177 "host": {
178 "type": "string",
179 "description": "Debug server host. For remote configurations, this is where to connect.",
180 "default": "127.0.0.1"
181 },
182 "substitutePath": {
183 "type": "array",
184 "items": {
185 "type": "object",
186 "properties": {
187 "from": {
188 "type": "string",
189 "description": "The absolute local path to be replaced."
190 },
191 "to": {
192 "type": "string",
193 "description": "The absolute remote path to replace with."
194 }
195 }
196 },
197 "description": "Mappings from local to remote paths for debugging.",
198 "default": []
199 },
200 "trace": {
201 "type": "string",
202 "enum": ["verbose", "trace", "log", "info", "warn", "error"],
203 "default": "error",
204 "description": "Debug logging level."
205 },
206 "backend": {
207 "type": "string",
208 "enum": ["default", "native", "lldb", "rr"],
209 "description": "Backend used by delve. Maps to `dlv`'s `--backend` flag."
210 },
211 "logOutput": {
212 "type": "string",
213 "enum": ["debugger", "gdbwire", "lldbout", "debuglineerr", "rpc", "dap"],
214 "description": "Components that should produce debug output.",
215 "default": "debugger"
216 },
217 "logDest": {
218 "type": "string",
219 "description": "Log destination for delve."
220 },
221 "stackTraceDepth": {
222 "type": "number",
223 "description": "Maximum depth of stack traces.",
224 "default": 50
225 },
226 "showGlobalVariables": {
227 "type": "boolean",
228 "default": false,
229 "description": "Show global package variables in variables pane."
230 },
231 "showRegisters": {
232 "type": "boolean",
233 "default": false,
234 "description": "Show register variables in variables pane."
235 },
236 "hideSystemGoroutines": {
237 "type": "boolean",
238 "default": false,
239 "description": "Hide system goroutines from call stack view."
240 },
241 "console": {
242 "default": "internalConsole",
243 "description": "Where to launch the debugger.",
244 "enum": ["internalConsole", "integratedTerminal"]
245 },
246 "asRoot": {
247 "default": false,
248 "description": "Debug with elevated permissions (on Unix).",
249 "type": "boolean"
250 }
251 });
252
253 // Create launch-specific properties
254 let launch_properties = json!({
255 "program": {
256 "type": "string",
257 "description": "Path to the program folder or file to debug.",
258 "default": "${ZED_WORKTREE_ROOT}"
259 },
260 "args": {
261 "type": ["array", "string"],
262 "description": "Command line arguments for the program.",
263 "items": {
264 "type": "string"
265 },
266 "default": []
267 },
268 "env": {
269 "type": "object",
270 "description": "Environment variables for the debugged program.",
271 "default": {}
272 },
273 "envFile": {
274 "type": ["string", "array"],
275 "items": {
276 "type": "string"
277 },
278 "description": "Path(s) to files with environment variables.",
279 "default": ""
280 },
281 "buildFlags": {
282 "type": ["string", "array"],
283 "items": {
284 "type": "string"
285 },
286 "description": "Flags for the Go compiler.",
287 "default": []
288 },
289 "output": {
290 "type": "string",
291 "description": "Output path for the binary.",
292 "default": "debug"
293 },
294 "mode": {
295 "enum": [ "debug", "test", "exec", "replay", "core"],
296 "description": "Debug mode for launch configuration.",
297 },
298 "traceDirPath": {
299 "type": "string",
300 "description": "Directory for record trace (for 'replay' mode).",
301 "default": ""
302 },
303 "coreFilePath": {
304 "type": "string",
305 "description": "Path to core dump file (for 'core' mode).",
306 "default": ""
307 }
308 });
309
310 // Create attach-specific properties
311 let attach_properties = json!({
312 "processId": {
313 "anyOf": [
314 {
315 "enum": ["${command:pickProcess}", "${command:pickGoProcess}"],
316 "description": "Use process picker to select a process."
317 },
318 {
319 "type": "string",
320 "description": "Process name to attach to."
321 },
322 {
323 "type": "number",
324 "description": "Process ID to attach to."
325 }
326 ],
327 "default": 0
328 },
329 "mode": {
330 "enum": ["local", "remote"],
331 "description": "Local or remote debugging.",
332 "default": "local"
333 },
334 "remotePath": {
335 "type": "string",
336 "description": "Path to source on remote machine.",
337 "markdownDeprecationMessage": "Use `substitutePath` instead.",
338 "default": ""
339 }
340 });
341
342 // Create the final schema
343 json!({
344 "oneOf": [
345 {
346 "allOf": [
347 {
348 "type": "object",
349 "required": ["request"],
350 "properties": {
351 "request": {
352 "type": "string",
353 "enum": ["launch"],
354 "description": "Request to launch a new process"
355 }
356 }
357 },
358 {
359 "type": "object",
360 "properties": common_properties
361 },
362 {
363 "type": "object",
364 "required": ["program", "mode"],
365 "properties": launch_properties
366 }
367 ]
368 },
369 {
370 "allOf": [
371 {
372 "type": "object",
373 "required": ["request"],
374 "properties": {
375 "request": {
376 "type": "string",
377 "enum": ["attach"],
378 "description": "Request to attach to an existing process"
379 }
380 }
381 },
382 {
383 "type": "object",
384 "properties": common_properties
385 },
386 {
387 "type": "object",
388 "required": ["mode"],
389 "properties": attach_properties
390 }
391 ]
392 }
393 ]
394 })
395 }
396
397 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
398 let mut args = match &zed_scenario.request {
399 dap::DebugRequest::Attach(attach_config) => {
400 json!({
401 "request": "attach",
402 "mode": "local",
403 "processId": attach_config.process_id,
404 })
405 }
406 dap::DebugRequest::Launch(launch_config) => {
407 let mode = if launch_config.program != "." {
408 "exec"
409 } else {
410 "debug"
411 };
412
413 json!({
414 "request": "launch",
415 "mode": mode,
416 "program": launch_config.program,
417 "cwd": launch_config.cwd,
418 "args": launch_config.args,
419 "env": launch_config.env_json()
420 })
421 }
422 };
423
424 let map = args.as_object_mut().unwrap();
425
426 if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
427 map.insert("stopOnEntry".into(), stop_on_entry.into());
428 }
429
430 Ok(DebugScenario {
431 adapter: zed_scenario.adapter,
432 label: zed_scenario.label,
433 build: None,
434 config: args,
435 tcp_connection: None,
436 })
437 }
438
439 async fn get_binary(
440 &self,
441 delegate: &Arc<dyn DapDelegate>,
442 task_definition: &DebugTaskDefinition,
443 user_installed_path: Option<PathBuf>,
444 user_args: Option<Vec<String>>,
445 user_env: Option<HashMap<String, String>>,
446 _cx: &mut AsyncApp,
447 ) -> Result<DebugAdapterBinary> {
448 let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
449 let dlv_path = adapter_path.join("dlv");
450
451 let delve_path = if let Some(path) = user_installed_path {
452 path.to_string_lossy().into_owned()
453 } else if let Some(path) = delegate.which(OsStr::new("dlv")).await {
454 path.to_string_lossy().into_owned()
455 } else if delegate.fs().is_file(&dlv_path).await {
456 dlv_path.to_string_lossy().into_owned()
457 } else {
458 let go = delegate
459 .which(OsStr::new("go"))
460 .await
461 .context("Go not found in path. Please install Go first, then Dlv will be installed automatically.")?;
462
463 let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
464
465 let install_output = util::command::new_command(&go)
466 .env("GO111MODULE", "on")
467 .env("GOBIN", &adapter_path)
468 .args(&["install", "github.com/go-delve/delve/cmd/dlv@latest"])
469 .output()
470 .await?;
471
472 if !install_output.status.success() {
473 bail!(
474 "failed to install dlv via `go install`. stdout: {:?}, stderr: {:?}\n Please try installing it manually using 'go install github.com/go-delve/delve/cmd/dlv@latest'",
475 String::from_utf8_lossy(&install_output.stdout),
476 String::from_utf8_lossy(&install_output.stderr)
477 );
478 }
479
480 adapter_path.join("dlv").to_string_lossy().into_owned()
481 };
482
483 let cwd = Some(
484 task_definition
485 .config
486 .get("cwd")
487 .and_then(|s| s.as_str())
488 .map(PathBuf::from)
489 .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()),
490 );
491
492 let arguments;
493 let command;
494 let connection;
495
496 let mut configuration = task_definition.config.clone();
497 let mut envs = user_env.unwrap_or_default();
498
499 if let Some(configuration) = configuration.as_object_mut() {
500 configuration
501 .entry("cwd")
502 .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
503
504 handle_envs(
505 configuration,
506 &mut envs,
507 cwd.as_deref(),
508 delegate.fs().clone(),
509 )
510 .await;
511 }
512
513 if let Some(connection_options) = &task_definition.tcp_connection {
514 command = None;
515 arguments = vec![];
516 let (host, port, timeout) =
517 crate::configure_tcp_connection(connection_options.clone()).await?;
518 connection = Some(TcpArguments {
519 host,
520 port,
521 timeout,
522 });
523 } else {
524 let minidelve_path = self.install_shim(delegate).await?;
525 let (host, port, _) =
526 crate::configure_tcp_connection(TcpArgumentsTemplate::default()).await?;
527 command = Some(minidelve_path.to_string_lossy().into_owned());
528 connection = None;
529 arguments = if let Some(mut args) = user_args {
530 args.insert(0, delve_path);
531 args
532 } else if cfg!(windows) {
533 vec![
534 delve_path,
535 "dap".into(),
536 "--listen".into(),
537 format!("{}:{}", host, port),
538 "--headless".into(),
539 ]
540 } else {
541 vec![
542 delve_path,
543 "dap".into(),
544 "--listen".into(),
545 format!("{}:{}", host, port),
546 ]
547 };
548 }
549 Ok(DebugAdapterBinary {
550 command,
551 arguments,
552 cwd,
553 envs,
554 connection,
555 request_args: StartDebuggingRequestArguments {
556 configuration,
557 request: self.request_kind(&task_definition.config).await?,
558 },
559 })
560 }
561}
562
563// delve doesn't do anything with the envFile setting, so we intercept it
564async fn handle_envs(
565 config: &mut Map<String, Value>,
566 envs: &mut HashMap<String, String>,
567 cwd: Option<&Path>,
568 fs: Arc<dyn Fs>,
569) -> Option<()> {
570 let env_files = match config.get("envFile")? {
571 Value::Array(arr) => arr.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
572 Value::String(s) => vec![Some(s.as_str())],
573 _ => return None,
574 };
575
576 let rebase_path = |path: PathBuf| {
577 if path.is_absolute() {
578 Some(path)
579 } else {
580 cwd.map(|p| p.join(path))
581 }
582 };
583
584 let mut env_vars = HashMap::default();
585 for path in env_files {
586 let Some(path) = path
587 .and_then(|s| PathBuf::from_str(s).ok())
588 .and_then(rebase_path)
589 else {
590 continue;
591 };
592
593 if let Ok(file) = fs.open_sync(&path).await {
594 let file_envs: HashMap<String, String> = dotenvy::from_read_iter(file)
595 .filter_map(Result::ok)
596 .collect();
597 envs.extend(file_envs.iter().map(|(k, v)| (k.clone(), v.clone())));
598 env_vars.extend(file_envs);
599 } else {
600 warn!("While starting Go debug session: failed to read env file {path:?}");
601 };
602 }
603
604 let mut env_obj: serde_json::Map<String, Value> = serde_json::Map::new();
605
606 for (k, v) in env_vars {
607 env_obj.insert(k, Value::String(v));
608 }
609
610 if let Some(existing_env) = config.get("env").and_then(|v| v.as_object()) {
611 for (k, v) in existing_env {
612 env_obj.insert(k.clone(), v.clone());
613 }
614 }
615
616 if !env_obj.is_empty() {
617 config.insert("env".to_string(), Value::Object(env_obj));
618 }
619
620 // remove envFile now that it's been handled
621 config.remove("envFile");
622 Some(())
623}