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 "request": "attach",
311 "mode": "debug",
312 "processId": attach_config.process_id,
313 })
314 }
315 dap::DebugRequest::Launch(launch_config) => {
316 let mode = if launch_config.program != "." {
317 "exec"
318 } else {
319 "debug"
320 };
321
322 json!({
323 "request": "launch",
324 "mode": mode,
325 "program": launch_config.program,
326 "cwd": launch_config.cwd,
327 "args": launch_config.args,
328 "env": launch_config.env_json()
329 })
330 }
331 };
332
333 let map = args.as_object_mut().unwrap();
334
335 if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
336 map.insert("stopOnEntry".into(), stop_on_entry.into());
337 }
338
339 Ok(DebugScenario {
340 adapter: zed_scenario.adapter,
341 label: zed_scenario.label,
342 build: None,
343 config: args,
344 tcp_connection: None,
345 })
346 }
347
348 async fn get_binary(
349 &self,
350 delegate: &Arc<dyn DapDelegate>,
351 task_definition: &DebugTaskDefinition,
352 _user_installed_path: Option<PathBuf>,
353 _cx: &mut AsyncApp,
354 ) -> Result<DebugAdapterBinary> {
355 let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
356 let dlv_path = adapter_path.join("dlv");
357
358 let delve_path = if let Some(path) = delegate.which(OsStr::new("dlv")).await {
359 path.to_string_lossy().to_string()
360 } else if delegate.fs().is_file(&dlv_path).await {
361 dlv_path.to_string_lossy().to_string()
362 } else {
363 let go = delegate
364 .which(OsStr::new("go"))
365 .await
366 .context("Go not found in path. Please install Go first, then Dlv will be installed automatically.")?;
367
368 let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
369
370 let install_output = util::command::new_smol_command(&go)
371 .env("GO111MODULE", "on")
372 .env("GOBIN", &adapter_path)
373 .args(&["install", "github.com/go-delve/delve/cmd/dlv@latest"])
374 .output()
375 .await?;
376
377 if !install_output.status.success() {
378 bail!(
379 "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'",
380 String::from_utf8_lossy(&install_output.stdout),
381 String::from_utf8_lossy(&install_output.stderr)
382 );
383 }
384
385 adapter_path.join("dlv").to_string_lossy().to_string()
386 };
387
388 let mut tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
389
390 if tcp_connection.timeout.is_none()
391 || tcp_connection.timeout.unwrap_or(0) < Self::DEFAULT_TIMEOUT_MS
392 {
393 tcp_connection.timeout = Some(Self::DEFAULT_TIMEOUT_MS);
394 }
395
396 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
397
398 let cwd = task_definition
399 .config
400 .get("cwd")
401 .and_then(|s| s.as_str())
402 .map(PathBuf::from)
403 .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf());
404
405 let arguments = if cfg!(windows) {
406 vec![
407 "dap".into(),
408 "--listen".into(),
409 format!("{}:{}", host, port),
410 "--headless".into(),
411 ]
412 } else {
413 vec![
414 "dap".into(),
415 "--listen".into(),
416 format!("{}:{}", host, port),
417 ]
418 };
419
420 Ok(DebugAdapterBinary {
421 command: delve_path,
422 arguments,
423 cwd: Some(cwd),
424 envs: HashMap::default(),
425 connection: Some(adapters::TcpArguments {
426 host,
427 port,
428 timeout,
429 }),
430 request_args: StartDebuggingRequestArguments {
431 configuration: task_definition.config.clone(),
432 request: self.validate_config(&task_definition.config)?,
433 },
434 })
435 }
436}