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