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