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}