1use anyhow::{Context as _, Result, bail};
2use async_trait::async_trait;
3use collections::HashMap;
4use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
5use gpui::AsyncApp;
6use std::ffi::OsStr;
7use task::{DebugScenario, ZedDebugConfig};
8
9use crate::*;
10
11#[derive(Default)]
12pub(crate) struct GdbDebugAdapter;
13
14impl GdbDebugAdapter {
15 const ADAPTER_NAME: &'static str = "GDB";
16}
17
18/// Ensures that "-i=dap" is present in the GDB argument list.
19fn ensure_dap_interface(mut gdb_args: Vec<String>) -> Vec<String> {
20 if !gdb_args.iter().any(|arg| arg.trim() == "-i=dap") {
21 gdb_args.insert(0, "-i=dap".to_string());
22 }
23 gdb_args
24}
25
26#[async_trait(?Send)]
27impl DebugAdapter for GdbDebugAdapter {
28 fn name(&self) -> DebugAdapterName {
29 DebugAdapterName(Self::ADAPTER_NAME.into())
30 }
31
32 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
33 let mut obj = serde_json::Map::default();
34
35 match &zed_scenario.request {
36 dap::DebugRequest::Attach(attach) => {
37 obj.insert("request".into(), "attach".into());
38 obj.insert("pid".into(), attach.process_id.into());
39 }
40
41 dap::DebugRequest::Launch(launch) => {
42 obj.insert("request".into(), "launch".into());
43 obj.insert("program".into(), launch.program.clone().into());
44
45 if !launch.args.is_empty() {
46 obj.insert("args".into(), launch.args.clone().into());
47 }
48
49 if !launch.env.is_empty() {
50 obj.insert("env".into(), launch.env_json());
51 }
52
53 if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
54 obj.insert(
55 "stopAtBeginningOfMainSubprogram".into(),
56 stop_on_entry.into(),
57 );
58 }
59 if let Some(cwd) = launch.cwd.as_ref() {
60 obj.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
61 }
62 }
63 }
64
65 Ok(DebugScenario {
66 adapter: zed_scenario.adapter,
67 label: zed_scenario.label,
68 build: None,
69 config: serde_json::Value::Object(obj),
70 tcp_connection: None,
71 })
72 }
73
74 fn dap_schema(&self) -> serde_json::Value {
75 json!({
76 "oneOf": [
77 {
78 "allOf": [
79 {
80 "type": "object",
81 "required": ["request"],
82 "properties": {
83 "request": {
84 "type": "string",
85 "enum": ["launch"],
86 "description": "Request to launch a new process"
87 }
88 }
89 },
90 {
91 "type": "object",
92 "properties": {
93 "program": {
94 "type": "string",
95 "description": "The program to debug. This corresponds to the GDB 'file' command."
96 },
97 "args": {
98 "type": "array",
99 "items": {
100 "type": "string"
101 },
102 "description": "Command line arguments passed to the program. These strings are provided as command-line arguments to the inferior.",
103 "default": []
104 },
105 "cwd": {
106 "type": "string",
107 "description": "Working directory for the debugged program. GDB will change its working directory to this directory."
108 },
109 "gdb_path": {
110 "type": "string",
111 "description": "Alternative path to the GDB executable, if the one in standard path is not desirable"
112 },
113 "gdb_args": {
114 "type": "array",
115 "items": {
116 "type":"string"
117 },
118 "description": "additional arguments given to GDB at startup, not the program debugged",
119 "default": []
120 },
121 "env": {
122 "type": "object",
123 "description": "Environment variables for the debugged program. Each key is the name of an environment variable; each value is the value of that variable."
124 },
125 "stopAtBeginningOfMainSubprogram": {
126 "type": "boolean",
127 "description": "When true, GDB will set a temporary breakpoint at the program's main procedure, like the 'start' command.",
128 "default": false
129 },
130 "stopOnEntry": {
131 "type": "boolean",
132 "description": "When true, GDB will set a temporary breakpoint at the program's first instruction, like the 'starti' command.",
133 "default": false
134 }
135 },
136 "required": ["program"]
137 }
138 ]
139 },
140 {
141 "allOf": [
142 {
143 "type": "object",
144 "required": ["request"],
145 "properties": {
146 "request": {
147 "type": "string",
148 "enum": ["attach"],
149 "description": "Request to attach to an existing process"
150 }
151 }
152 },
153 {
154 "type": "object",
155 "properties": {
156 "pid": {
157 "type": "number",
158 "description": "The process ID to which GDB should attach."
159 },
160 "program": {
161 "type": "string",
162 "description": "The program to debug (optional). This corresponds to the GDB 'file' command. In many cases, GDB can determine which program is running automatically."
163 },
164 "target": {
165 "type": "string",
166 "description": "The target to which GDB should connect. This is passed to the 'target remote' command."
167 }
168 },
169 "required": ["pid"]
170 }
171 ]
172 }
173 ]
174 })
175 }
176
177 async fn get_binary(
178 &self,
179 delegate: &Arc<dyn DapDelegate>,
180 config: &DebugTaskDefinition,
181 user_installed_path: Option<std::path::PathBuf>,
182 user_args: Option<Vec<String>>,
183 user_env: Option<HashMap<String, String>>,
184 _: &mut AsyncApp,
185 ) -> Result<DebugAdapterBinary> {
186 // Try to get gdb_path from config
187 let gdb_path_from_config = config
188 .config
189 .get("gdb_path")
190 .and_then(|v| v.as_str())
191 .map(|s| s.to_string());
192
193 let gdb_path = if let Some(path) = gdb_path_from_config {
194 path
195 } else {
196 // Original logic: use user_installed_path or search in system path
197 let user_setting_path = user_installed_path
198 .filter(|p| p.exists())
199 .and_then(|p| p.to_str().map(|s| s.to_string()));
200
201 let gdb_path_result = delegate
202 .which(OsStr::new("gdb"))
203 .await
204 .and_then(|p| p.to_str().map(|s| s.to_string()))
205 .context("Could not find gdb in path");
206
207 if gdb_path_result.is_err() && user_setting_path.is_none() {
208 bail!("Could not find gdb path or it's not installed");
209 }
210
211 user_setting_path.unwrap_or_else(|| gdb_path_result.unwrap())
212 };
213
214 // Arguments: use gdb_args from config if present, else user_args, else default
215 let gdb_args = {
216 let args = config
217 .config
218 .get("gdb_args")
219 .and_then(|v| v.as_array())
220 .map(|arr| {
221 arr.iter()
222 .filter_map(|v| v.as_str().map(|s| s.to_string()))
223 .collect::<Vec<_>>()
224 })
225 .or(user_args.clone())
226 .unwrap_or_else(|| vec!["-i=dap".into()]);
227 ensure_dap_interface(args)
228 };
229
230 let mut configuration = config.config.clone();
231 if let Some(configuration) = configuration.as_object_mut() {
232 configuration
233 .entry("cwd")
234 .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
235 }
236
237 let mut base_env = delegate.shell_env().await;
238 base_env.extend(user_env.unwrap_or_default());
239
240 let config_env: HashMap<String, String> = config
241 .config
242 .get("env")
243 .and_then(|v| v.as_object())
244 .map(|obj| {
245 obj.iter()
246 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
247 .collect::<HashMap<String, String>>()
248 })
249 .unwrap_or_else(HashMap::default);
250
251 base_env.extend(config_env);
252
253 Ok(DebugAdapterBinary {
254 command: Some(gdb_path),
255 arguments: gdb_args,
256 envs: base_env,
257 cwd: Some(delegate.worktree_root_path().to_path_buf()),
258 connection: None,
259 request_args: StartDebuggingRequestArguments {
260 request: self.request_kind(&config.config).await?,
261 configuration,
262 },
263 })
264 }
265}