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