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