go.rs

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