go.rs

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