go.rs

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