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