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