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 user_env: Option<HashMap<String, String>>,
413 _cx: &mut AsyncApp,
414 ) -> Result<DebugAdapterBinary> {
415 let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
416 let dlv_path = adapter_path.join("dlv");
417
418 let delve_path = if let Some(path) = user_installed_path {
419 path.to_string_lossy().into_owned()
420 } else if let Some(path) = delegate.which(OsStr::new("dlv")).await {
421 path.to_string_lossy().into_owned()
422 } else if delegate.fs().is_file(&dlv_path).await {
423 dlv_path.to_string_lossy().into_owned()
424 } else {
425 let go = delegate
426 .which(OsStr::new("go"))
427 .await
428 .context("Go not found in path. Please install Go first, then Dlv will be installed automatically.")?;
429
430 let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
431
432 let install_output = util::command::new_smol_command(&go)
433 .env("GO111MODULE", "on")
434 .env("GOBIN", &adapter_path)
435 .args(&["install", "github.com/go-delve/delve/cmd/dlv@latest"])
436 .output()
437 .await?;
438
439 if !install_output.status.success() {
440 bail!(
441 "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'",
442 String::from_utf8_lossy(&install_output.stdout),
443 String::from_utf8_lossy(&install_output.stderr)
444 );
445 }
446
447 adapter_path.join("dlv").to_string_lossy().into_owned()
448 };
449
450 let cwd = Some(
451 task_definition
452 .config
453 .get("cwd")
454 .and_then(|s| s.as_str())
455 .map(PathBuf::from)
456 .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()),
457 );
458
459 let arguments;
460 let command;
461 let connection;
462
463 let mut configuration = task_definition.config.clone();
464 let mut envs = user_env.unwrap_or_default();
465
466 if let Some(configuration) = configuration.as_object_mut() {
467 configuration
468 .entry("cwd")
469 .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
470
471 handle_envs(
472 configuration,
473 &mut envs,
474 cwd.as_deref(),
475 delegate.fs().clone(),
476 )
477 .await;
478 }
479
480 if let Some(connection_options) = &task_definition.tcp_connection {
481 command = None;
482 arguments = vec![];
483 let (host, port, timeout) =
484 crate::configure_tcp_connection(connection_options.clone()).await?;
485 connection = Some(TcpArguments {
486 host,
487 port,
488 timeout,
489 });
490 } else {
491 let minidelve_path = self.install_shim(delegate).await?;
492 let (host, port, _) =
493 crate::configure_tcp_connection(TcpArgumentsTemplate::default()).await?;
494 command = Some(minidelve_path.to_string_lossy().into_owned());
495 connection = None;
496 arguments = if let Some(mut args) = user_args {
497 args.insert(0, delve_path);
498 args
499 } else if cfg!(windows) {
500 vec![
501 delve_path,
502 "dap".into(),
503 "--listen".into(),
504 format!("{}:{}", host, port),
505 "--headless".into(),
506 ]
507 } else {
508 vec![
509 delve_path,
510 "dap".into(),
511 "--listen".into(),
512 format!("{}:{}", host, port),
513 ]
514 };
515 }
516 Ok(DebugAdapterBinary {
517 command,
518 arguments,
519 cwd,
520 envs,
521 connection,
522 request_args: StartDebuggingRequestArguments {
523 configuration,
524 request: self.request_kind(&task_definition.config).await?,
525 },
526 })
527 }
528}
529
530// delve doesn't do anything with the envFile setting, so we intercept it
531async fn handle_envs(
532 config: &mut Map<String, Value>,
533 envs: &mut HashMap<String, String>,
534 cwd: Option<&Path>,
535 fs: Arc<dyn Fs>,
536) -> Option<()> {
537 let env_files = match config.get("envFile")? {
538 Value::Array(arr) => arr.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
539 Value::String(s) => vec![Some(s.as_str())],
540 _ => return None,
541 };
542
543 let rebase_path = |path: PathBuf| {
544 if path.is_absolute() {
545 Some(path)
546 } else {
547 cwd.map(|p| p.join(path))
548 }
549 };
550
551 let mut env_vars = HashMap::default();
552 for path in env_files {
553 let Some(path) = path
554 .and_then(|s| PathBuf::from_str(s).ok())
555 .and_then(rebase_path)
556 else {
557 continue;
558 };
559
560 if let Ok(file) = fs.open_sync(&path).await {
561 let file_envs: HashMap<String, String> = dotenvy::from_read_iter(file)
562 .filter_map(Result::ok)
563 .collect();
564 envs.extend(file_envs.iter().map(|(k, v)| (k.clone(), v.clone())));
565 env_vars.extend(file_envs);
566 } else {
567 warn!("While starting Go debug session: failed to read env file {path:?}");
568 };
569 }
570
571 let mut env_obj: serde_json::Map<String, Value> = serde_json::Map::new();
572
573 for (k, v) in env_vars {
574 env_obj.insert(k, Value::String(v));
575 }
576
577 if let Some(existing_env) = config.get("env").and_then(|v| v.as_object()) {
578 for (k, v) in existing_env {
579 env_obj.insert(k.clone(), v.clone());
580 }
581 }
582
583 if !env_obj.is_empty() {
584 config.insert("env".to_string(), Value::Object(env_obj));
585 }
586
587 // remove envFile now that it's been handled
588 config.remove("envFile");
589 Some(())
590}