1use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
2
3use anyhow::{Context as _, Result};
4use async_trait::async_trait;
5use dap::adapters::{DebugTaskDefinition, latest_github_release};
6use futures::StreamExt;
7use gpui::AsyncApp;
8use serde_json::Value;
9use task::{DebugRequest, DebugScenario, ZedDebugConfig};
10use util::fs::remove_matching;
11
12use crate::*;
13
14#[derive(Default)]
15pub(crate) struct CodeLldbDebugAdapter {
16 path_to_codelldb: OnceLock<String>,
17}
18
19impl CodeLldbDebugAdapter {
20 const ADAPTER_NAME: &'static str = "CodeLLDB";
21
22 async fn request_args(
23 &self,
24 delegate: &Arc<dyn DapDelegate>,
25 task_definition: &DebugTaskDefinition,
26 ) -> Result<dap::StartDebuggingRequestArguments> {
27 // CodeLLDB uses `name` for a terminal label.
28 let mut configuration = task_definition.config.clone();
29
30 let obj = configuration
31 .as_object_mut()
32 .context("CodeLLDB is not a valid json object")?;
33
34 obj.entry("name")
35 .or_insert(Value::String(String::from(task_definition.label.as_ref())));
36
37 obj.entry("cwd")
38 .or_insert(delegate.worktree_root_path().to_string_lossy().into());
39
40 let request = self.request_kind(&configuration).await?;
41
42 Ok(dap::StartDebuggingRequestArguments {
43 request,
44 configuration,
45 })
46 }
47
48 async fn fetch_latest_adapter_version(
49 &self,
50 delegate: &Arc<dyn DapDelegate>,
51 ) -> Result<AdapterVersion> {
52 let release =
53 latest_github_release("vadimcn/codelldb", true, false, delegate.http_client()).await?;
54
55 let arch = match std::env::consts::ARCH {
56 "aarch64" => "arm64",
57 "x86_64" => "x64",
58 unsupported => {
59 anyhow::bail!("unsupported architecture {unsupported}");
60 }
61 };
62 let platform = match std::env::consts::OS {
63 "macos" => "darwin",
64 "linux" => "linux",
65 "windows" => "win32",
66 unsupported => {
67 anyhow::bail!("unsupported operating system {unsupported}");
68 }
69 };
70 let asset_name = format!("codelldb-{platform}-{arch}.vsix");
71 let ret = AdapterVersion {
72 tag_name: release.tag_name,
73 url: release
74 .assets
75 .iter()
76 .find(|asset| asset.name == asset_name)
77 .with_context(|| format!("no asset found matching {asset_name:?}"))?
78 .browser_download_url
79 .clone(),
80 };
81
82 Ok(ret)
83 }
84}
85
86#[async_trait(?Send)]
87impl DebugAdapter for CodeLldbDebugAdapter {
88 fn name(&self) -> DebugAdapterName {
89 DebugAdapterName(Self::ADAPTER_NAME.into())
90 }
91
92 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
93 let mut configuration = json!({
94 "request": match zed_scenario.request {
95 DebugRequest::Launch(_) => "launch",
96 DebugRequest::Attach(_) => "attach",
97 },
98 });
99 let map = configuration.as_object_mut().unwrap();
100 // CodeLLDB uses `name` for a terminal label.
101 map.insert(
102 "name".into(),
103 Value::String(String::from(zed_scenario.label.as_ref())),
104 );
105 match &zed_scenario.request {
106 DebugRequest::Attach(attach) => {
107 map.insert("pid".into(), attach.process_id.into());
108 }
109 DebugRequest::Launch(launch) => {
110 map.insert("program".into(), launch.program.clone().into());
111
112 if !launch.args.is_empty() {
113 map.insert("args".into(), launch.args.clone().into());
114 }
115 if !launch.env.is_empty() {
116 map.insert("env".into(), launch.env_json());
117 }
118 if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
119 map.insert("stopOnEntry".into(), stop_on_entry.into());
120 }
121 if let Some(cwd) = launch.cwd.as_ref() {
122 map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
123 }
124 }
125 }
126
127 Ok(DebugScenario {
128 adapter: zed_scenario.adapter,
129 label: zed_scenario.label,
130 config: configuration,
131 build: None,
132 tcp_connection: None,
133 })
134 }
135
136 fn dap_schema(&self) -> serde_json::Value {
137 json!({
138 "properties": {
139 "request": {
140 "type": "string",
141 "enum": ["attach", "launch"],
142 "description": "Debug adapter request type"
143 },
144 "program": {
145 "type": "string",
146 "description": "Path to the program to debug or attach to"
147 },
148 "args": {
149 "type": ["array", "string"],
150 "description": "Program arguments"
151 },
152 "cwd": {
153 "type": "string",
154 "description": "Program working directory"
155 },
156 "env": {
157 "type": "object",
158 "description": "Additional environment variables",
159 "patternProperties": {
160 ".*": {
161 "type": "string"
162 }
163 }
164 },
165 "envFile": {
166 "type": "string",
167 "description": "File to read the environment variables from"
168 },
169 "stdio": {
170 "type": ["null", "string", "array", "object"],
171 "description": "Destination for stdio streams: null = send to debugger console or a terminal, \"<path>\" = attach to a file/tty/fifo"
172 },
173 "terminal": {
174 "type": "string",
175 "enum": ["integrated", "console"],
176 "description": "Terminal type to use",
177 "default": "integrated"
178 },
179 "console": {
180 "type": "string",
181 "enum": ["integratedTerminal", "internalConsole"],
182 "description": "Terminal type to use (compatibility alias of 'terminal')"
183 },
184 "stopOnEntry": {
185 "type": "boolean",
186 "description": "Automatically stop debuggee after launch",
187 "default": false
188 },
189 "initCommands": {
190 "type": "array",
191 "description": "Initialization commands executed upon debugger startup",
192 "items": {
193 "type": "string"
194 }
195 },
196 "targetCreateCommands": {
197 "type": "array",
198 "description": "Commands that create the debug target",
199 "items": {
200 "type": "string"
201 }
202 },
203 "preRunCommands": {
204 "type": "array",
205 "description": "Commands executed just before the program is launched",
206 "items": {
207 "type": "string"
208 }
209 },
210 "processCreateCommands": {
211 "type": "array",
212 "description": "Commands that create the debuggee process",
213 "items": {
214 "type": "string"
215 }
216 },
217 "postRunCommands": {
218 "type": "array",
219 "description": "Commands executed just after the program has been launched",
220 "items": {
221 "type": "string"
222 }
223 },
224 "preTerminateCommands": {
225 "type": "array",
226 "description": "Commands executed just before the debuggee is terminated or disconnected from",
227 "items": {
228 "type": "string"
229 }
230 },
231 "exitCommands": {
232 "type": "array",
233 "description": "Commands executed at the end of debugging session",
234 "items": {
235 "type": "string"
236 }
237 },
238 "expressions": {
239 "type": "string",
240 "enum": ["simple", "python", "native"],
241 "description": "The default evaluator type used for expressions"
242 },
243 "sourceMap": {
244 "type": "object",
245 "description": "Source path remapping between the build machine and the local machine",
246 "patternProperties": {
247 ".*": {
248 "type": ["string", "null"]
249 }
250 }
251 },
252 "relativePathBase": {
253 "type": "string",
254 "description": "Base directory used for resolution of relative source paths. Defaults to the workspace folder"
255 },
256 "sourceLanguages": {
257 "type": "array",
258 "description": "A list of source languages to enable language-specific features for",
259 "items": {
260 "type": "string"
261 }
262 },
263 "reverseDebugging": {
264 "type": "boolean",
265 "description": "Enable reverse debugging",
266 "default": false
267 },
268 "breakpointMode": {
269 "type": "string",
270 "enum": ["path", "file"],
271 "description": "Specifies how source breakpoints should be set"
272 },
273 "pid": {
274 "type": ["integer", "string"],
275 "description": "Process id to attach to"
276 },
277 "waitFor": {
278 "type": "boolean",
279 "description": "Wait for the process to launch (MacOS only)",
280 "default": false
281 }
282 },
283 "required": ["request"],
284 "allOf": [
285 {
286 "if": {
287 "properties": {
288 "request": {
289 "enum": ["launch"]
290 }
291 }
292 },
293 "then": {
294 "oneOf": [
295 {
296 "required": ["program"]
297 },
298 {
299 "required": ["targetCreateCommands"]
300 }
301 ]
302 }
303 },
304 {
305 "if": {
306 "properties": {
307 "request": {
308 "enum": ["attach"]
309 }
310 }
311 },
312 "then": {
313 "oneOf": [
314 {
315 "required": ["pid"]
316 },
317 {
318 "required": ["program"]
319 }
320 ]
321 }
322 }
323 ]
324 })
325 }
326
327 async fn get_binary(
328 &self,
329 delegate: &Arc<dyn DapDelegate>,
330 config: &DebugTaskDefinition,
331 user_installed_path: Option<PathBuf>,
332 user_args: Option<Vec<String>>,
333 _: &mut AsyncApp,
334 ) -> Result<DebugAdapterBinary> {
335 let mut command = user_installed_path
336 .map(|p| p.to_string_lossy().to_string())
337 .or(self.path_to_codelldb.get().cloned());
338
339 if command.is_none() {
340 delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
341 let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
342 let version_path =
343 if let Ok(version) = self.fetch_latest_adapter_version(delegate).await {
344 adapters::download_adapter_from_github(
345 self.name(),
346 version.clone(),
347 adapters::DownloadedFileType::Vsix,
348 delegate.as_ref(),
349 )
350 .await?;
351 let version_path =
352 adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version.tag_name));
353 remove_matching(&adapter_path, |entry| entry != version_path).await;
354 version_path
355 } else {
356 let mut paths = delegate.fs().read_dir(&adapter_path).await?;
357 paths.next().await.context("No adapter found")??
358 };
359 let adapter_dir = version_path.join("extension").join("adapter");
360 let path = adapter_dir.join("codelldb").to_string_lossy().to_string();
361 self.path_to_codelldb.set(path.clone()).ok();
362 command = Some(path);
363 };
364
365 Ok(DebugAdapterBinary {
366 command: Some(command.unwrap()),
367 cwd: Some(delegate.worktree_root_path().to_path_buf()),
368 arguments: user_args.unwrap_or_else(|| {
369 vec![
370 "--settings".into(),
371 json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
372 ]
373 }),
374 request_args: self.request_args(delegate, &config).await?,
375 envs: HashMap::default(),
376 connection: None,
377 })
378 }
379}