go.rs

  1use anyhow::{Context as _, anyhow, bail};
  2use dap::{
  3    StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
  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("delve-shim-dap");
 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 validate_config(
354        &self,
355        config: &serde_json::Value,
356    ) -> Result<StartDebuggingRequestArgumentsRequest> {
357        let map = config.as_object().context("Config isn't an object")?;
358
359        let request_variant = map
360            .get("request")
361            .and_then(|val| val.as_str())
362            .context("request argument is not found or invalid")?;
363
364        match request_variant {
365            "launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
366            "attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
367            _ => Err(anyhow!("request must be either 'launch' or 'attach'")),
368        }
369    }
370
371    fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
372        let mut args = match &zed_scenario.request {
373            dap::DebugRequest::Attach(attach_config) => {
374                json!({
375                    "request": "attach",
376                    "mode": "debug",
377                    "processId": attach_config.process_id,
378                })
379            }
380            dap::DebugRequest::Launch(launch_config) => {
381                let mode = if launch_config.program != "." {
382                    "exec"
383                } else {
384                    "debug"
385                };
386
387                json!({
388                    "request": "launch",
389                    "mode": mode,
390                    "program": launch_config.program,
391                    "cwd": launch_config.cwd,
392                    "args": launch_config.args,
393                    "env": launch_config.env_json()
394                })
395            }
396        };
397
398        let map = args.as_object_mut().unwrap();
399
400        if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
401            map.insert("stopOnEntry".into(), stop_on_entry.into());
402        }
403
404        Ok(DebugScenario {
405            adapter: zed_scenario.adapter,
406            label: zed_scenario.label,
407            build: None,
408            config: args,
409            tcp_connection: None,
410        })
411    }
412
413    async fn get_binary(
414        &self,
415        delegate: &Arc<dyn DapDelegate>,
416        task_definition: &DebugTaskDefinition,
417        _user_installed_path: Option<PathBuf>,
418        _cx: &mut AsyncApp,
419    ) -> Result<DebugAdapterBinary> {
420        let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
421        let dlv_path = adapter_path.join("dlv");
422
423        let delve_path = if let Some(path) = delegate.which(OsStr::new("dlv")).await {
424            path.to_string_lossy().to_string()
425        } else if delegate.fs().is_file(&dlv_path).await {
426            dlv_path.to_string_lossy().to_string()
427        } else {
428            let go = delegate
429                .which(OsStr::new("go"))
430                .await
431                .context("Go not found in path. Please install Go first, then Dlv will be installed automatically.")?;
432
433            let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
434
435            let install_output = util::command::new_smol_command(&go)
436                .env("GO111MODULE", "on")
437                .env("GOBIN", &adapter_path)
438                .args(&["install", "github.com/go-delve/delve/cmd/dlv@latest"])
439                .output()
440                .await?;
441
442            if !install_output.status.success() {
443                bail!(
444                    "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'",
445                    String::from_utf8_lossy(&install_output.stdout),
446                    String::from_utf8_lossy(&install_output.stderr)
447                );
448            }
449
450            adapter_path.join("dlv").to_string_lossy().to_string()
451        };
452        let minidelve_path = self.install_shim(delegate).await?;
453        let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
454
455        let (host, port, _) = crate::configure_tcp_connection(tcp_connection).await?;
456
457        let cwd = task_definition
458            .config
459            .get("cwd")
460            .and_then(|s| s.as_str())
461            .map(PathBuf::from)
462            .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf());
463
464        let arguments = if cfg!(windows) {
465            vec![
466                delve_path,
467                "dap".into(),
468                "--listen".into(),
469                format!("{}:{}", host, port),
470                "--headless".into(),
471            ]
472        } else {
473            vec![
474                delve_path,
475                "dap".into(),
476                "--listen".into(),
477                format!("{}:{}", host, port),
478            ]
479        };
480
481        Ok(DebugAdapterBinary {
482            command: minidelve_path.to_string_lossy().into_owned(),
483            arguments,
484            cwd: Some(cwd),
485            envs: HashMap::default(),
486            connection: None,
487            request_args: StartDebuggingRequestArguments {
488                configuration: task_definition.config.clone(),
489                request: self.validate_config(&task_definition.config)?,
490            },
491        })
492    }
493}