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}