1use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
2
3use anyhow::{Context as _, Result, anyhow};
4use async_trait::async_trait;
5use dap::{
6 StartDebuggingRequestArgumentsRequest,
7 adapters::{DebugTaskDefinition, latest_github_release},
8};
9use futures::StreamExt;
10use gpui::AsyncApp;
11use serde_json::Value;
12use task::{DebugRequest, DebugScenario, ZedDebugConfig};
13use util::fs::remove_matching;
14
15use crate::*;
16
17#[derive(Default)]
18pub(crate) struct CodeLldbDebugAdapter {
19 path_to_codelldb: OnceLock<String>,
20}
21
22impl CodeLldbDebugAdapter {
23 const ADAPTER_NAME: &'static str = "CodeLLDB";
24
25 fn request_args(
26 &self,
27 task_definition: &DebugTaskDefinition,
28 ) -> Result<dap::StartDebuggingRequestArguments> {
29 // CodeLLDB uses `name` for a terminal label.
30 let mut configuration = task_definition.config.clone();
31
32 configuration
33 .as_object_mut()
34 .context("CodeLLDB is not a valid json object")?
35 .insert(
36 "name".into(),
37 Value::String(String::from(task_definition.label.as_ref())),
38 );
39
40 let request = self.validate_config(&configuration)?;
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 fn validate_config(
93 &self,
94 config: &serde_json::Value,
95 ) -> Result<StartDebuggingRequestArgumentsRequest> {
96 let map = config
97 .as_object()
98 .ok_or_else(|| anyhow!("Config isn't an object"))?;
99
100 let request_variant = map
101 .get("request")
102 .and_then(|r| r.as_str())
103 .ok_or_else(|| anyhow!("request field is required and must be a string"))?;
104
105 match request_variant {
106 "launch" => {
107 // For launch, verify that one of the required configs exists
108 if !(map.contains_key("program")
109 || map.contains_key("targetCreateCommands")
110 || map.contains_key("cargo"))
111 {
112 return Err(anyhow!(
113 "launch request requires either 'program', 'targetCreateCommands', or 'cargo' field"
114 ));
115 }
116 Ok(StartDebuggingRequestArgumentsRequest::Launch)
117 }
118 "attach" => {
119 // For attach, verify that either pid or program exists
120 if !(map.contains_key("pid") || map.contains_key("program")) {
121 return Err(anyhow!(
122 "attach request requires either 'pid' or 'program' field"
123 ));
124 }
125 Ok(StartDebuggingRequestArgumentsRequest::Attach)
126 }
127 _ => Err(anyhow!(
128 "request must be either 'launch' or 'attach', got '{}'",
129 request_variant
130 )),
131 }
132 }
133
134 fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
135 let mut configuration = json!({
136 "request": match zed_scenario.request {
137 DebugRequest::Launch(_) => "launch",
138 DebugRequest::Attach(_) => "attach",
139 },
140 });
141 let map = configuration.as_object_mut().unwrap();
142 // CodeLLDB uses `name` for a terminal label.
143 map.insert(
144 "name".into(),
145 Value::String(String::from(zed_scenario.label.as_ref())),
146 );
147 match &zed_scenario.request {
148 DebugRequest::Attach(attach) => {
149 map.insert("pid".into(), attach.process_id.into());
150 }
151 DebugRequest::Launch(launch) => {
152 map.insert("program".into(), launch.program.clone().into());
153
154 if !launch.args.is_empty() {
155 map.insert("args".into(), launch.args.clone().into());
156 }
157 if !launch.env.is_empty() {
158 map.insert("env".into(), launch.env_json());
159 }
160 if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
161 map.insert("stopOnEntry".into(), stop_on_entry.into());
162 }
163 if let Some(cwd) = launch.cwd.as_ref() {
164 map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
165 }
166 }
167 }
168
169 Ok(DebugScenario {
170 adapter: zed_scenario.adapter,
171 label: zed_scenario.label,
172 config: configuration,
173 build: None,
174 tcp_connection: None,
175 })
176 }
177
178 async fn dap_schema(&self) -> serde_json::Value {
179 json!({
180 "properties": {
181 "request": {
182 "type": "string",
183 "enum": ["attach", "launch"],
184 "description": "Debug adapter request type"
185 },
186 "program": {
187 "type": "string",
188 "description": "Path to the program to debug or attach to"
189 },
190 "args": {
191 "type": ["array", "string"],
192 "description": "Program arguments"
193 },
194 "cwd": {
195 "type": "string",
196 "description": "Program working directory"
197 },
198 "env": {
199 "type": "object",
200 "description": "Additional environment variables",
201 "patternProperties": {
202 ".*": {
203 "type": "string"
204 }
205 }
206 },
207 "envFile": {
208 "type": "string",
209 "description": "File to read the environment variables from"
210 },
211 "stdio": {
212 "type": ["null", "string", "array", "object"],
213 "description": "Destination for stdio streams: null = send to debugger console or a terminal, \"<path>\" = attach to a file/tty/fifo"
214 },
215 "terminal": {
216 "type": "string",
217 "enum": ["integrated", "console"],
218 "description": "Terminal type to use",
219 "default": "integrated"
220 },
221 "console": {
222 "type": "string",
223 "enum": ["integratedTerminal", "internalConsole"],
224 "description": "Terminal type to use (compatibility alias of 'terminal')"
225 },
226 "stopOnEntry": {
227 "type": "boolean",
228 "description": "Automatically stop debuggee after launch",
229 "default": false
230 },
231 "initCommands": {
232 "type": "array",
233 "description": "Initialization commands executed upon debugger startup",
234 "items": {
235 "type": "string"
236 }
237 },
238 "targetCreateCommands": {
239 "type": "array",
240 "description": "Commands that create the debug target",
241 "items": {
242 "type": "string"
243 }
244 },
245 "preRunCommands": {
246 "type": "array",
247 "description": "Commands executed just before the program is launched",
248 "items": {
249 "type": "string"
250 }
251 },
252 "processCreateCommands": {
253 "type": "array",
254 "description": "Commands that create the debuggee process",
255 "items": {
256 "type": "string"
257 }
258 },
259 "postRunCommands": {
260 "type": "array",
261 "description": "Commands executed just after the program has been launched",
262 "items": {
263 "type": "string"
264 }
265 },
266 "preTerminateCommands": {
267 "type": "array",
268 "description": "Commands executed just before the debuggee is terminated or disconnected from",
269 "items": {
270 "type": "string"
271 }
272 },
273 "exitCommands": {
274 "type": "array",
275 "description": "Commands executed at the end of debugging session",
276 "items": {
277 "type": "string"
278 }
279 },
280 "expressions": {
281 "type": "string",
282 "enum": ["simple", "python", "native"],
283 "description": "The default evaluator type used for expressions"
284 },
285 "sourceMap": {
286 "type": "object",
287 "description": "Source path remapping between the build machine and the local machine",
288 "patternProperties": {
289 ".*": {
290 "type": ["string", "null"]
291 }
292 }
293 },
294 "relativePathBase": {
295 "type": "string",
296 "description": "Base directory used for resolution of relative source paths. Defaults to the workspace folder"
297 },
298 "sourceLanguages": {
299 "type": "array",
300 "description": "A list of source languages to enable language-specific features for",
301 "items": {
302 "type": "string"
303 }
304 },
305 "reverseDebugging": {
306 "type": "boolean",
307 "description": "Enable reverse debugging",
308 "default": false
309 },
310 "breakpointMode": {
311 "type": "string",
312 "enum": ["path", "file"],
313 "description": "Specifies how source breakpoints should be set"
314 },
315 "pid": {
316 "type": ["integer", "string"],
317 "description": "Process id to attach to"
318 },
319 "waitFor": {
320 "type": "boolean",
321 "description": "Wait for the process to launch (MacOS only)",
322 "default": false
323 }
324 },
325 "required": ["request"],
326 "allOf": [
327 {
328 "if": {
329 "properties": {
330 "request": {
331 "enum": ["launch"]
332 }
333 }
334 },
335 "then": {
336 "oneOf": [
337 {
338 "required": ["program"]
339 },
340 {
341 "required": ["targetCreateCommands"]
342 },
343 {
344 "required": ["cargo"]
345 }
346 ]
347 }
348 },
349 {
350 "if": {
351 "properties": {
352 "request": {
353 "enum": ["attach"]
354 }
355 }
356 },
357 "then": {
358 "oneOf": [
359 {
360 "required": ["pid"]
361 },
362 {
363 "required": ["program"]
364 }
365 ]
366 }
367 }
368 ]
369 })
370 }
371
372 async fn get_binary(
373 &self,
374 delegate: &Arc<dyn DapDelegate>,
375 config: &DebugTaskDefinition,
376 user_installed_path: Option<PathBuf>,
377 _: &mut AsyncApp,
378 ) -> Result<DebugAdapterBinary> {
379 let mut command = user_installed_path
380 .map(|p| p.to_string_lossy().to_string())
381 .or(self.path_to_codelldb.get().cloned());
382
383 if command.is_none() {
384 delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
385 let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
386 let version_path =
387 if let Ok(version) = self.fetch_latest_adapter_version(delegate).await {
388 adapters::download_adapter_from_github(
389 self.name(),
390 version.clone(),
391 adapters::DownloadedFileType::Vsix,
392 delegate.as_ref(),
393 )
394 .await?;
395 let version_path =
396 adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version.tag_name));
397 remove_matching(&adapter_path, |entry| entry != version_path).await;
398 version_path
399 } else {
400 let mut paths = delegate.fs().read_dir(&adapter_path).await?;
401 paths.next().await.context("No adapter found")??
402 };
403 let adapter_dir = version_path.join("extension").join("adapter");
404 let path = adapter_dir.join("codelldb").to_string_lossy().to_string();
405 // todo("windows")
406 #[cfg(not(windows))]
407 {
408 use smol::fs;
409
410 fs::set_permissions(
411 &path,
412 <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
413 )
414 .await
415 .with_context(|| format!("Settings executable permissions to {path:?}"))?;
416
417 let lldb_binaries_dir = version_path.join("extension").join("lldb").join("bin");
418 let mut lldb_binaries =
419 fs::read_dir(&lldb_binaries_dir).await.with_context(|| {
420 format!("reading lldb binaries dir contents {lldb_binaries_dir:?}")
421 })?;
422 while let Some(binary) = lldb_binaries.next().await {
423 let binary_entry = binary?;
424 let path = binary_entry.path();
425 fs::set_permissions(
426 &path,
427 <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
428 )
429 .await
430 .with_context(|| format!("Settings executable permissions to {path:?}"))?;
431 }
432 }
433 self.path_to_codelldb.set(path.clone()).ok();
434 command = Some(path);
435 };
436
437 Ok(DebugAdapterBinary {
438 command: command.unwrap(),
439 cwd: Some(delegate.worktree_root_path().to_path_buf()),
440 arguments: vec![
441 "--settings".into(),
442 json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
443 ],
444 request_args: self.request_args(&config)?,
445 envs: HashMap::default(),
446 connection: None,
447 })
448 }
449}