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    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        _cx: &mut AsyncApp,
413    ) -> Result<DebugAdapterBinary> {
414        let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
415        let dlv_path = adapter_path.join("dlv");
416
417        let delve_path = if let Some(path) = user_installed_path {
418            path.to_string_lossy().into_owned()
419        } else if let Some(path) = delegate.which(OsStr::new("dlv")).await {
420            path.to_string_lossy().into_owned()
421        } else if delegate.fs().is_file(&dlv_path).await {
422            dlv_path.to_string_lossy().into_owned()
423        } else {
424            let go = delegate
425                .which(OsStr::new("go"))
426                .await
427                .context("Go not found in path. Please install Go first, then Dlv will be installed automatically.")?;
428
429            let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
430
431            let install_output = util::command::new_smol_command(&go)
432                .env("GO111MODULE", "on")
433                .env("GOBIN", &adapter_path)
434                .args(&["install", "github.com/go-delve/delve/cmd/dlv@latest"])
435                .output()
436                .await?;
437
438            if !install_output.status.success() {
439                bail!(
440                    "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'",
441                    String::from_utf8_lossy(&install_output.stdout),
442                    String::from_utf8_lossy(&install_output.stderr)
443                );
444            }
445
446            adapter_path.join("dlv").to_string_lossy().into_owned()
447        };
448
449        let cwd = Some(
450            task_definition
451                .config
452                .get("cwd")
453                .and_then(|s| s.as_str())
454                .map(PathBuf::from)
455                .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()),
456        );
457
458        let arguments;
459        let command;
460        let connection;
461
462        let mut configuration = task_definition.config.clone();
463        let mut envs = HashMap::default();
464
465        if let Some(configuration) = configuration.as_object_mut() {
466            configuration
467                .entry("cwd")
468                .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
469
470            handle_envs(
471                configuration,
472                &mut envs,
473                cwd.as_deref(),
474                delegate.fs().clone(),
475            )
476            .await;
477        }
478
479        if let Some(connection_options) = &task_definition.tcp_connection {
480            command = None;
481            arguments = vec![];
482            let (host, port, timeout) =
483                crate::configure_tcp_connection(connection_options.clone()).await?;
484            connection = Some(TcpArguments {
485                host,
486                port,
487                timeout,
488            });
489        } else {
490            let minidelve_path = self.install_shim(delegate).await?;
491            let (host, port, _) =
492                crate::configure_tcp_connection(TcpArgumentsTemplate::default()).await?;
493            command = Some(minidelve_path.to_string_lossy().into_owned());
494            connection = None;
495            arguments = if let Some(mut args) = user_args {
496                args.insert(0, delve_path);
497                args
498            } else if cfg!(windows) {
499                vec![
500                    delve_path,
501                    "dap".into(),
502                    "--listen".into(),
503                    format!("{}:{}", host, port),
504                    "--headless".into(),
505                ]
506            } else {
507                vec![
508                    delve_path,
509                    "dap".into(),
510                    "--listen".into(),
511                    format!("{}:{}", host, port),
512                ]
513            };
514        }
515        Ok(DebugAdapterBinary {
516            command,
517            arguments,
518            cwd,
519            envs,
520            connection,
521            request_args: StartDebuggingRequestArguments {
522                configuration,
523                request: self.request_kind(&task_definition.config).await?,
524            },
525        })
526    }
527}
528
529// delve doesn't do anything with the envFile setting, so we intercept it
530async fn handle_envs(
531    config: &mut Map<String, Value>,
532    envs: &mut HashMap<String, String>,
533    cwd: Option<&Path>,
534    fs: Arc<dyn Fs>,
535) -> Option<()> {
536    let env_files = match config.get("envFile")? {
537        Value::Array(arr) => arr.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
538        Value::String(s) => vec![Some(s.as_str())],
539        _ => return None,
540    };
541
542    let rebase_path = |path: PathBuf| {
543        if path.is_absolute() {
544            Some(path)
545        } else {
546            cwd.map(|p| p.join(path))
547        }
548    };
549
550    let mut env_vars = HashMap::default();
551    for path in env_files {
552        let Some(path) = path
553            .and_then(|s| PathBuf::from_str(s).ok())
554            .and_then(rebase_path)
555        else {
556            continue;
557        };
558
559        if let Ok(file) = fs.open_sync(&path).await {
560            let file_envs: HashMap<String, String> = dotenvy::from_read_iter(file)
561                .filter_map(Result::ok)
562                .collect();
563            envs.extend(file_envs.iter().map(|(k, v)| (k.clone(), v.clone())));
564            env_vars.extend(file_envs);
565        } else {
566            warn!("While starting Go debug session: failed to read env file {path:?}");
567        };
568    }
569
570    let mut env_obj: serde_json::Map<String, Value> = serde_json::Map::new();
571
572    for (k, v) in env_vars {
573        env_obj.insert(k, Value::String(v));
574    }
575
576    if let Some(existing_env) = config.get("env").and_then(|v| v.as_object()) {
577        for (k, v) in existing_env {
578            env_obj.insert(k.clone(), v.clone());
579        }
580    }
581
582    if !env_obj.is_empty() {
583        config.insert("env".to_string(), Value::Object(env_obj));
584    }
585
586    // remove envFile now that it's been handled
587    config.remove("envFile");
588    Some(())
589}