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}