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 borrow::Cow,
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 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 paths::debug_adapters_dir(),
86 delegate.as_ref(),
87 )
88 .await?;
89
90 let path = paths::debug_adapters_dir()
91 .join("delve-shim-dap")
92 .join(format!("delve-shim-dap_{}", asset.tag_name))
93 .join(format!("delve-shim-dap{}", std::env::consts::EXE_SUFFIX));
94 self.shim_path.set(path.clone()).ok();
95
96 Ok(path)
97 }
98}
99
100#[async_trait(?Send)]
101impl DebugAdapter for GoDebugAdapter {
102 fn name(&self) -> DebugAdapterName {
103 DebugAdapterName(Self::ADAPTER_NAME.into())
104 }
105
106 fn adapter_language_name(&self) -> Option<LanguageName> {
107 Some(SharedString::new_static("Go").into())
108 }
109
110 fn dap_schema(&self) -> Cow<'static, serde_json::Value> {
111 // Create common properties shared between launch and attach
112 let common_properties = json!({
113 "debugAdapter": {
114 "enum": ["legacy", "dlv-dap"],
115 "description": "Select which debug adapter to use with this configuration.",
116 "default": "dlv-dap"
117 },
118 "stopOnEntry": {
119 "type": "boolean",
120 "description": "Automatically stop program after launch or attach.",
121 "default": false
122 },
123 "showLog": {
124 "type": "boolean",
125 "description": "Show log output from the delve debugger. Maps to dlv's `--log` flag.",
126 "default": false
127 },
128 "cwd": {
129 "type": "string",
130 "description": "Workspace relative or absolute path to the working directory of the program being debugged.",
131 "default": "${ZED_WORKTREE_ROOT}"
132 },
133 "dlvFlags": {
134 "type": "array",
135 "description": "Extra flags for `dlv`. See `dlv help` for the full list of supported flags.",
136 "items": {
137 "type": "string"
138 },
139 "default": []
140 },
141 "port": {
142 "type": "number",
143 "description": "Debug server port. For remote configurations, this is where to connect.",
144 "default": 2345
145 },
146 "host": {
147 "type": "string",
148 "description": "Debug server host. For remote configurations, this is where to connect.",
149 "default": "127.0.0.1"
150 },
151 "substitutePath": {
152 "type": "array",
153 "items": {
154 "type": "object",
155 "properties": {
156 "from": {
157 "type": "string",
158 "description": "The absolute local path to be replaced."
159 },
160 "to": {
161 "type": "string",
162 "description": "The absolute remote path to replace with."
163 }
164 }
165 },
166 "description": "Mappings from local to remote paths for debugging.",
167 "default": []
168 },
169 "trace": {
170 "type": "string",
171 "enum": ["verbose", "trace", "log", "info", "warn", "error"],
172 "default": "error",
173 "description": "Debug logging level."
174 },
175 "backend": {
176 "type": "string",
177 "enum": ["default", "native", "lldb", "rr"],
178 "description": "Backend used by delve. Maps to `dlv`'s `--backend` flag."
179 },
180 "logOutput": {
181 "type": "string",
182 "enum": ["debugger", "gdbwire", "lldbout", "debuglineerr", "rpc", "dap"],
183 "description": "Components that should produce debug output.",
184 "default": "debugger"
185 },
186 "logDest": {
187 "type": "string",
188 "description": "Log destination for delve."
189 },
190 "stackTraceDepth": {
191 "type": "number",
192 "description": "Maximum depth of stack traces.",
193 "default": 50
194 },
195 "showGlobalVariables": {
196 "type": "boolean",
197 "default": false,
198 "description": "Show global package variables in variables pane."
199 },
200 "showRegisters": {
201 "type": "boolean",
202 "default": false,
203 "description": "Show register variables in variables pane."
204 },
205 "hideSystemGoroutines": {
206 "type": "boolean",
207 "default": false,
208 "description": "Hide system goroutines from call stack view."
209 },
210 "console": {
211 "default": "internalConsole",
212 "description": "Where to launch the debugger.",
213 "enum": ["internalConsole", "integratedTerminal"]
214 },
215 "asRoot": {
216 "default": false,
217 "description": "Debug with elevated permissions (on Unix).",
218 "type": "boolean"
219 }
220 });
221
222 // Create launch-specific properties
223 let launch_properties = json!({
224 "program": {
225 "type": "string",
226 "description": "Path to the program folder or file to debug.",
227 "default": "${ZED_WORKTREE_ROOT}"
228 },
229 "args": {
230 "type": ["array", "string"],
231 "description": "Command line arguments for the program.",
232 "items": {
233 "type": "string"
234 },
235 "default": []
236 },
237 "env": {
238 "type": "object",
239 "description": "Environment variables for the debugged program.",
240 "default": {}
241 },
242 "envFile": {
243 "type": ["string", "array"],
244 "items": {
245 "type": "string"
246 },
247 "description": "Path(s) to files with environment variables.",
248 "default": ""
249 },
250 "buildFlags": {
251 "type": ["string", "array"],
252 "items": {
253 "type": "string"
254 },
255 "description": "Flags for the Go compiler.",
256 "default": []
257 },
258 "output": {
259 "type": "string",
260 "description": "Output path for the binary.",
261 "default": "debug"
262 },
263 "mode": {
264 "enum": [ "debug", "test", "exec", "replay", "core"],
265 "description": "Debug mode for launch configuration.",
266 },
267 "traceDirPath": {
268 "type": "string",
269 "description": "Directory for record trace (for 'replay' mode).",
270 "default": ""
271 },
272 "coreFilePath": {
273 "type": "string",
274 "description": "Path to core dump file (for 'core' mode).",
275 "default": ""
276 }
277 });
278
279 // Create attach-specific properties
280 let attach_properties = json!({
281 "processId": {
282 "anyOf": [
283 {
284 "enum": ["${command:pickProcess}", "${command:pickGoProcess}"],
285 "description": "Use process picker to select a process."
286 },
287 {
288 "type": "string",
289 "description": "Process name to attach to."
290 },
291 {
292 "type": "number",
293 "description": "Process ID to attach to."
294 }
295 ],
296 "default": 0
297 },
298 "mode": {
299 "enum": ["local", "remote"],
300 "description": "Local or remote debugging.",
301 "default": "local"
302 },
303 "remotePath": {
304 "type": "string",
305 "description": "Path to source on remote machine.",
306 "markdownDeprecationMessage": "Use `substitutePath` instead.",
307 "default": ""
308 }
309 });
310
311 // Create the final schema
312 Cow::Owned(json!({
313 "oneOf": [
314 {
315 "allOf": [
316 {
317 "type": "object",
318 "required": ["request"],
319 "properties": {
320 "request": {
321 "type": "string",
322 "enum": ["launch"],
323 "description": "Request to launch a new process"
324 }
325 }
326 },
327 {
328 "type": "object",
329 "properties": common_properties
330 },
331 {
332 "type": "object",
333 "required": ["program", "mode"],
334 "properties": launch_properties
335 }
336 ]
337 },
338 {
339 "allOf": [
340 {
341 "type": "object",
342 "required": ["request"],
343 "properties": {
344 "request": {
345 "type": "string",
346 "enum": ["attach"],
347 "description": "Request to attach to an existing process"
348 }
349 }
350 },
351 {
352 "type": "object",
353 "properties": common_properties
354 },
355 {
356 "type": "object",
357 "required": ["mode"],
358 "properties": attach_properties
359 }
360 ]
361 }
362 ]
363 }))
364 }
365
366 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
367 let mut args = match &zed_scenario.request {
368 dap::DebugRequest::Attach(attach_config) => {
369 json!({
370 "request": "attach",
371 "mode": "debug",
372 "processId": attach_config.process_id,
373 })
374 }
375 dap::DebugRequest::Launch(launch_config) => {
376 let mode = if launch_config.program != "." {
377 "exec"
378 } else {
379 "debug"
380 };
381
382 json!({
383 "request": "launch",
384 "mode": mode,
385 "program": launch_config.program,
386 "cwd": launch_config.cwd,
387 "args": launch_config.args,
388 "env": launch_config.env_json()
389 })
390 }
391 };
392
393 let map = args.as_object_mut().unwrap();
394
395 if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
396 map.insert("stopOnEntry".into(), stop_on_entry.into());
397 }
398
399 Ok(DebugScenario {
400 adapter: zed_scenario.adapter,
401 label: zed_scenario.label,
402 build: None,
403 config: args,
404 tcp_connection: None,
405 })
406 }
407
408 async fn get_binary(
409 &self,
410 delegate: &Arc<dyn DapDelegate>,
411 task_definition: &DebugTaskDefinition,
412 user_installed_path: Option<PathBuf>,
413 user_args: Option<Vec<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().to_string()
421 } else if let Some(path) = delegate.which(OsStr::new("dlv")).await {
422 path.to_string_lossy().to_string()
423 } else if delegate.fs().is_file(&dlv_path).await {
424 dlv_path.to_string_lossy().to_string()
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().to_string()
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 = HashMap::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 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 envs.extend(dotenvy::from_read_iter(file).filter_map(Result::ok))
562 } else {
563 warn!("While starting Go debug session: failed to read env file {path:?}");
564 };
565 }
566
567 // remove envFile now that it's been handled
568 config.remove("entry");
569 Some(())
570}