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 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}