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