go.rs

  1use anyhow::{Context as _, bail};
  2use dap::{
  3    StartDebuggingRequestArguments,
  4    adapters::{
  5        DebugTaskDefinition, DownloadedFileType, download_adapter_from_github,
  6        latest_github_release,
  7    },
  8};
  9
 10use gpui::{AsyncApp, SharedString};
 11use language::LanguageName;
 12use std::{collections::HashMap, env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
 13use util;
 14
 15use crate::*;
 16
 17#[derive(Default, Debug)]
 18pub(crate) struct GoDebugAdapter {
 19    shim_path: OnceLock<PathBuf>,
 20}
 21
 22impl GoDebugAdapter {
 23    const ADAPTER_NAME: &'static str = "Delve";
 24    async fn fetch_latest_adapter_version(
 25        delegate: &Arc<dyn DapDelegate>,
 26    ) -> Result<AdapterVersion> {
 27        let release = latest_github_release(
 28            &"zed-industries/delve-shim-dap",
 29            true,
 30            false,
 31            delegate.http_client(),
 32        )
 33        .await?;
 34
 35        let os = match consts::OS {
 36            "macos" => "apple-darwin",
 37            "linux" => "unknown-linux-gnu",
 38            "windows" => "pc-windows-msvc",
 39            other => bail!("Running on unsupported os: {other}"),
 40        };
 41        let suffix = if consts::OS == "windows" {
 42            ".zip"
 43        } else {
 44            ".tar.gz"
 45        };
 46        let asset_name = format!("delve-shim-dap-{}-{os}{suffix}", consts::ARCH);
 47        let asset = release
 48            .assets
 49            .iter()
 50            .find(|asset| asset.name == asset_name)
 51            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
 52
 53        Ok(AdapterVersion {
 54            tag_name: release.tag_name,
 55            url: asset.browser_download_url.clone(),
 56        })
 57    }
 58    async fn install_shim(&self, delegate: &Arc<dyn DapDelegate>) -> anyhow::Result<PathBuf> {
 59        if let Some(path) = self.shim_path.get().cloned() {
 60            return Ok(path);
 61        }
 62
 63        let asset = Self::fetch_latest_adapter_version(delegate).await?;
 64        let ty = if consts::OS == "windows" {
 65            DownloadedFileType::Zip
 66        } else {
 67            DownloadedFileType::GzipTar
 68        };
 69        download_adapter_from_github(
 70            "delve-shim-dap".into(),
 71            asset.clone(),
 72            ty,
 73            delegate.as_ref(),
 74        )
 75        .await?;
 76
 77        let path = paths::debug_adapters_dir()
 78            .join("delve-shim-dap")
 79            .join(format!("delve-shim-dap_{}", asset.tag_name))
 80            .join(format!("delve-shim-dap{}", std::env::consts::EXE_SUFFIX));
 81        self.shim_path.set(path.clone()).ok();
 82
 83        Ok(path)
 84    }
 85}
 86
 87#[async_trait(?Send)]
 88impl DebugAdapter for GoDebugAdapter {
 89    fn name(&self) -> DebugAdapterName {
 90        DebugAdapterName(Self::ADAPTER_NAME.into())
 91    }
 92
 93    fn adapter_language_name(&self) -> Option<LanguageName> {
 94        Some(SharedString::new_static("Go").into())
 95    }
 96
 97    async fn dap_schema(&self) -> serde_json::Value {
 98        // Create common properties shared between launch and attach
 99        let common_properties = json!({
100            "debugAdapter": {
101                "enum": ["legacy", "dlv-dap"],
102                "description": "Select which debug adapter to use with this configuration.",
103                "default": "dlv-dap"
104            },
105            "stopOnEntry": {
106                "type": "boolean",
107                "description": "Automatically stop program after launch or attach.",
108                "default": false
109            },
110            "showLog": {
111                "type": "boolean",
112                "description": "Show log output from the delve debugger. Maps to dlv's `--log` flag.",
113                "default": false
114            },
115            "cwd": {
116                "type": "string",
117                "description": "Workspace relative or absolute path to the working directory of the program being debugged.",
118                "default": "${ZED_WORKTREE_ROOT}"
119            },
120            "dlvFlags": {
121                "type": "array",
122                "description": "Extra flags for `dlv`. See `dlv help` for the full list of supported flags.",
123                "items": {
124                    "type": "string"
125                },
126                "default": []
127            },
128            "port": {
129                "type": "number",
130                "description": "Debug server port. For remote configurations, this is where to connect.",
131                "default": 2345
132            },
133            "host": {
134                "type": "string",
135                "description": "Debug server host. For remote configurations, this is where to connect.",
136                "default": "127.0.0.1"
137            },
138            "substitutePath": {
139                "type": "array",
140                "items": {
141                    "type": "object",
142                    "properties": {
143                        "from": {
144                            "type": "string",
145                            "description": "The absolute local path to be replaced."
146                        },
147                        "to": {
148                            "type": "string",
149                            "description": "The absolute remote path to replace with."
150                        }
151                    }
152                },
153                "description": "Mappings from local to remote paths for debugging.",
154                "default": []
155            },
156            "trace": {
157                "type": "string",
158                "enum": ["verbose", "trace", "log", "info", "warn", "error"],
159                "default": "error",
160                "description": "Debug logging level."
161            },
162            "backend": {
163                "type": "string",
164                "enum": ["default", "native", "lldb", "rr"],
165                "description": "Backend used by delve. Maps to `dlv`'s `--backend` flag."
166            },
167            "logOutput": {
168                "type": "string",
169                "enum": ["debugger", "gdbwire", "lldbout", "debuglineerr", "rpc", "dap"],
170                "description": "Components that should produce debug output.",
171                "default": "debugger"
172            },
173            "logDest": {
174                "type": "string",
175                "description": "Log destination for delve."
176            },
177            "stackTraceDepth": {
178                "type": "number",
179                "description": "Maximum depth of stack traces.",
180                "default": 50
181            },
182            "showGlobalVariables": {
183                "type": "boolean",
184                "default": false,
185                "description": "Show global package variables in variables pane."
186            },
187            "showRegisters": {
188                "type": "boolean",
189                "default": false,
190                "description": "Show register variables in variables pane."
191            },
192            "hideSystemGoroutines": {
193                "type": "boolean",
194                "default": false,
195                "description": "Hide system goroutines from call stack view."
196            },
197            "console": {
198                "default": "internalConsole",
199                "description": "Where to launch the debugger.",
200                "enum": ["internalConsole", "integratedTerminal"]
201            },
202            "asRoot": {
203                "default": false,
204                "description": "Debug with elevated permissions (on Unix).",
205                "type": "boolean"
206            }
207        });
208
209        // Create launch-specific properties
210        let launch_properties = json!({
211            "program": {
212                "type": "string",
213                "description": "Path to the program folder or file to debug.",
214                "default": "${ZED_WORKTREE_ROOT}"
215            },
216            "args": {
217                "type": ["array", "string"],
218                "description": "Command line arguments for the program.",
219                "items": {
220                    "type": "string"
221                },
222                "default": []
223            },
224            "env": {
225                "type": "object",
226                "description": "Environment variables for the debugged program.",
227                "default": {}
228            },
229            "envFile": {
230                "type": ["string", "array"],
231                "items": {
232                    "type": "string"
233                },
234                "description": "Path(s) to files with environment variables.",
235                "default": ""
236            },
237            "buildFlags": {
238                "type": ["string", "array"],
239                "items": {
240                    "type": "string"
241                },
242                "description": "Flags for the Go compiler.",
243                "default": []
244            },
245            "output": {
246                "type": "string",
247                "description": "Output path for the binary.",
248                "default": "debug"
249            },
250            "mode": {
251                "enum": [ "debug", "test", "exec", "replay", "core"],
252                "description": "Debug mode for launch configuration.",
253            },
254            "traceDirPath": {
255                "type": "string",
256                "description": "Directory for record trace (for 'replay' mode).",
257                "default": ""
258            },
259            "coreFilePath": {
260                "type": "string",
261                "description": "Path to core dump file (for 'core' mode).",
262                "default": ""
263            }
264        });
265
266        // Create attach-specific properties
267        let attach_properties = json!({
268            "processId": {
269                "anyOf": [
270                    {
271                        "enum": ["${command:pickProcess}", "${command:pickGoProcess}"],
272                        "description": "Use process picker to select a process."
273                    },
274                    {
275                        "type": "string",
276                        "description": "Process name to attach to."
277                    },
278                    {
279                        "type": "number",
280                        "description": "Process ID to attach to."
281                    }
282                ],
283                "default": 0
284            },
285            "mode": {
286                "enum": ["local", "remote"],
287                "description": "Local or remote debugging.",
288                "default": "local"
289            },
290            "remotePath": {
291                "type": "string",
292                "description": "Path to source on remote machine.",
293                "markdownDeprecationMessage": "Use `substitutePath` instead.",
294                "default": ""
295            }
296        });
297
298        // Create the final schema
299        json!({
300            "oneOf": [
301                {
302                    "allOf": [
303                        {
304                            "type": "object",
305                            "required": ["request"],
306                            "properties": {
307                                "request": {
308                                    "type": "string",
309                                    "enum": ["launch"],
310                                    "description": "Request to launch a new process"
311                                }
312                            }
313                        },
314                        {
315                            "type": "object",
316                            "properties": common_properties
317                        },
318                        {
319                            "type": "object",
320                            "required": ["program", "mode"],
321                            "properties": launch_properties
322                        }
323                    ]
324                },
325                {
326                    "allOf": [
327                        {
328                            "type": "object",
329                            "required": ["request"],
330                            "properties": {
331                                "request": {
332                                    "type": "string",
333                                    "enum": ["attach"],
334                                    "description": "Request to attach to an existing process"
335                                }
336                            }
337                        },
338                        {
339                            "type": "object",
340                            "properties": common_properties
341                        },
342                        {
343                            "type": "object",
344                            "required": ["processId", "mode"],
345                            "properties": attach_properties
346                        }
347                    ]
348                }
349            ]
350        })
351    }
352
353    fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
354        let mut args = match &zed_scenario.request {
355            dap::DebugRequest::Attach(attach_config) => {
356                json!({
357                    "request": "attach",
358                    "mode": "debug",
359                    "processId": attach_config.process_id,
360                })
361            }
362            dap::DebugRequest::Launch(launch_config) => {
363                let mode = if launch_config.program != "." {
364                    "exec"
365                } else {
366                    "debug"
367                };
368
369                json!({
370                    "request": "launch",
371                    "mode": mode,
372                    "program": launch_config.program,
373                    "cwd": launch_config.cwd,
374                    "args": launch_config.args,
375                    "env": launch_config.env_json()
376                })
377            }
378        };
379
380        let map = args.as_object_mut().unwrap();
381
382        if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
383            map.insert("stopOnEntry".into(), stop_on_entry.into());
384        }
385
386        Ok(DebugScenario {
387            adapter: zed_scenario.adapter,
388            label: zed_scenario.label,
389            build: None,
390            config: args,
391            tcp_connection: None,
392        })
393    }
394
395    async fn get_binary(
396        &self,
397        delegate: &Arc<dyn DapDelegate>,
398        task_definition: &DebugTaskDefinition,
399        user_installed_path: Option<PathBuf>,
400        _cx: &mut AsyncApp,
401    ) -> Result<DebugAdapterBinary> {
402        let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
403        let dlv_path = adapter_path.join("dlv");
404
405        let delve_path = if let Some(path) = user_installed_path {
406            path.to_string_lossy().to_string()
407        } else if let Some(path) = delegate.which(OsStr::new("dlv")).await {
408            path.to_string_lossy().to_string()
409        } else if delegate.fs().is_file(&dlv_path).await {
410            dlv_path.to_string_lossy().to_string()
411        } else {
412            let go = delegate
413                .which(OsStr::new("go"))
414                .await
415                .context("Go not found in path. Please install Go first, then Dlv will be installed automatically.")?;
416
417            let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
418
419            let install_output = util::command::new_smol_command(&go)
420                .env("GO111MODULE", "on")
421                .env("GOBIN", &adapter_path)
422                .args(&["install", "github.com/go-delve/delve/cmd/dlv@latest"])
423                .output()
424                .await?;
425
426            if !install_output.status.success() {
427                bail!(
428                    "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'",
429                    String::from_utf8_lossy(&install_output.stdout),
430                    String::from_utf8_lossy(&install_output.stderr)
431                );
432            }
433
434            adapter_path.join("dlv").to_string_lossy().to_string()
435        };
436        let minidelve_path = self.install_shim(delegate).await?;
437        let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
438
439        let (host, port, _) = crate::configure_tcp_connection(tcp_connection).await?;
440
441        let cwd = task_definition
442            .config
443            .get("cwd")
444            .and_then(|s| s.as_str())
445            .map(PathBuf::from)
446            .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf());
447
448        let arguments = if cfg!(windows) {
449            vec![
450                delve_path,
451                "dap".into(),
452                "--listen".into(),
453                format!("{}:{}", host, port),
454                "--headless".into(),
455            ]
456        } else {
457            vec![
458                delve_path,
459                "dap".into(),
460                "--listen".into(),
461                format!("{}:{}", host, port),
462            ]
463        };
464
465        Ok(DebugAdapterBinary {
466            command: minidelve_path.to_string_lossy().into_owned(),
467            arguments,
468            cwd: Some(cwd),
469            envs: HashMap::default(),
470            connection: None,
471            request_args: StartDebuggingRequestArguments {
472                configuration: task_definition.config.clone(),
473                request: self.request_kind(&task_definition.config)?,
474            },
475        })
476    }
477}